add skills

This commit is contained in:
YeonGyu-Kim
2026-03-14 12:45:53 +09:00
parent 0dcfcd372b
commit c7518eae2d
81 changed files with 19037 additions and 0 deletions
@@ -0,0 +1,11 @@
{
"run_id": "eval-2-with_skill",
"expectations": [
{"text": "Plan uses git worktree in a sibling directory", "passed": true, "evidence": "../omo-wt/fix-atlas-worktree-path-crash"},
{"text": "Fix is minimal — adds null check, doesn't refactor unrelated code", "passed": true, "evidence": "3 targeted changes: readBoulderState sanitization, idle-event guard, tests"},
{"text": "Test case added for the missing worktree_path scenario", "passed": true, "evidence": "Tests for missing and null worktree_path"},
{"text": "Verification loop includes all 3 gates", "passed": true, "evidence": "Gate A (CI), Gate B (review-work), Gate C (Cubic)"},
{"text": "References actual atlas hook files", "passed": true, "evidence": "src/hooks/atlas/idle-event.ts, src/features/boulder-state/storage.ts"},
{"text": "Branch name follows fix/ prefix convention", "passed": true, "evidence": "fix/atlas-worktree-path-crash"}
]
}
@@ -0,0 +1,205 @@
# Code Changes
## File 1: `src/features/boulder-state/storage.ts`
**Change**: Add `worktree_path` sanitization in `readBoulderState()`
```typescript
// BEFORE (lines 29-32):
if (!Array.isArray(parsed.session_ids)) {
parsed.session_ids = []
}
return parsed as BoulderState
// AFTER:
if (!Array.isArray(parsed.session_ids)) {
parsed.session_ids = []
}
if (parsed.worktree_path !== undefined && typeof parsed.worktree_path !== "string") {
parsed.worktree_path = undefined
}
return parsed as BoulderState
```
**Rationale**: `readBoulderState` casts raw `JSON.parse()` output as `BoulderState` without validating individual fields. When boulder.json has `"worktree_path": null` (valid JSON from manual edits, corrupted state, or external tools), the runtime type is `null` but TypeScript type says `string | undefined`. This sanitization ensures downstream code always gets the correct type.
---
## File 2: `src/hooks/atlas/idle-event.ts`
**Change**: Add defensive string type guard before passing `worktree_path` to continuation functions.
```typescript
// BEFORE (lines 83-88 in scheduleRetry):
await injectContinuation({
ctx,
sessionID,
sessionState,
options,
planName: currentBoulder.plan_name,
progress: currentProgress,
agent: currentBoulder.agent,
worktreePath: currentBoulder.worktree_path,
})
// AFTER:
await injectContinuation({
ctx,
sessionID,
sessionState,
options,
planName: currentBoulder.plan_name,
progress: currentProgress,
agent: currentBoulder.agent,
worktreePath: typeof currentBoulder.worktree_path === "string" ? currentBoulder.worktree_path : undefined,
})
```
```typescript
// BEFORE (lines 184-188 in handleAtlasSessionIdle):
await injectContinuation({
ctx,
sessionID,
sessionState,
options,
planName: boulderState.plan_name,
progress,
agent: boulderState.agent,
worktreePath: boulderState.worktree_path,
})
// AFTER:
await injectContinuation({
ctx,
sessionID,
sessionState,
options,
planName: boulderState.plan_name,
progress,
agent: boulderState.agent,
worktreePath: typeof boulderState.worktree_path === "string" ? boulderState.worktree_path : undefined,
})
```
**Rationale**: Belt-and-suspenders defense. Even though `readBoulderState` now sanitizes, direct `writeBoulderState` calls elsewhere could still produce invalid state. The `typeof` check is zero-cost and prevents any possibility of `null` or non-string values leaking through.
---
## File 3: `src/hooks/atlas/index.test.ts`
**Change**: Add test cases for missing `worktree_path` scenarios within the existing `session.idle handler` describe block.
```typescript
test("should inject continuation when boulder.json has no worktree_path field", async () => {
// given - boulder state WITHOUT worktree_path
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2")
const state: BoulderState = {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "test-plan",
}
writeBoulderState(TEST_DIR, state)
const readState = readBoulderState(TEST_DIR)
expect(readState?.worktree_path).toBeUndefined()
const mockInput = createMockPluginInput()
const hook = createAtlasHook(mockInput)
// when
await hook.handler({
event: {
type: "session.idle",
properties: { sessionID: MAIN_SESSION_ID },
},
})
// then - continuation injected, no worktree context in prompt
expect(mockInput._promptMock).toHaveBeenCalled()
const callArgs = mockInput._promptMock.mock.calls[0][0]
expect(callArgs.body.parts[0].text).not.toContain("[Worktree:")
expect(callArgs.body.parts[0].text).toContain("1 remaining")
})
test("should handle boulder.json with worktree_path: null without crashing", async () => {
// given - manually write boulder.json with worktree_path: null (corrupted state)
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2")
const boulderPath = join(SISYPHUS_DIR, "boulder.json")
writeFileSync(boulderPath, JSON.stringify({
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "test-plan",
worktree_path: null,
}, null, 2))
const mockInput = createMockPluginInput()
const hook = createAtlasHook(mockInput)
// when
await hook.handler({
event: {
type: "session.idle",
properties: { sessionID: MAIN_SESSION_ID },
},
})
// then - should inject continuation without crash, no "[Worktree: null]"
expect(mockInput._promptMock).toHaveBeenCalled()
const callArgs = mockInput._promptMock.mock.calls[0][0]
expect(callArgs.body.parts[0].text).not.toContain("[Worktree: null]")
expect(callArgs.body.parts[0].text).not.toContain("[Worktree: undefined]")
})
```
---
## File 4: `src/features/boulder-state/storage.test.ts` (addition to existing)
**Change**: Add `readBoulderState` sanitization test.
```typescript
describe("#given boulder.json with worktree_path: null", () => {
test("#then readBoulderState should sanitize null to undefined", () => {
// given
const boulderPath = join(TEST_DIR, ".sisyphus", "boulder.json")
writeFileSync(boulderPath, JSON.stringify({
active_plan: "/path/to/plan.md",
started_at: "2026-01-02T10:00:00Z",
session_ids: ["session-1"],
plan_name: "test-plan",
worktree_path: null,
}, null, 2))
// when
const state = readBoulderState(TEST_DIR)
// then
expect(state).not.toBeNull()
expect(state!.worktree_path).toBeUndefined()
})
test("#then readBoulderState should preserve valid worktree_path string", () => {
// given
const boulderPath = join(TEST_DIR, ".sisyphus", "boulder.json")
writeFileSync(boulderPath, JSON.stringify({
active_plan: "/path/to/plan.md",
started_at: "2026-01-02T10:00:00Z",
session_ids: ["session-1"],
plan_name: "test-plan",
worktree_path: "/valid/worktree/path",
}, null, 2))
// when
const state = readBoulderState(TEST_DIR)
// then
expect(state?.worktree_path).toBe("/valid/worktree/path")
})
})
```
@@ -0,0 +1,78 @@
# Execution Plan — Fix atlas hook crash on missing worktree_path
## Phase 0: Setup
1. **Create worktree from origin/dev**:
```bash
git fetch origin dev
git worktree add ../omo-wt/fix-atlas-worktree-path-crash origin/dev
```
2. **Create feature branch**:
```bash
cd ../omo-wt/fix-atlas-worktree-path-crash
git checkout -b fix/atlas-worktree-path-crash
```
## Phase 1: Implement
### Step 1: Fix `readBoulderState()` in `src/features/boulder-state/storage.ts`
- Add `worktree_path` sanitization after JSON parse
- Ensure `worktree_path` is `string | undefined`, never `null` or other types
- This is the root cause: raw `JSON.parse` + `as BoulderState` cast allows type violations at runtime
### Step 2: Add defensive guard in `src/hooks/atlas/idle-event.ts`
- Before passing `boulderState.worktree_path` to `injectContinuation`, validate it's a string
- Apply same guard in the `scheduleRetry` callback (line 86)
- Ensures even if `readBoulderState` is bypassed, the idle handler won't crash
### Step 3: Add test coverage in `src/hooks/atlas/index.test.ts`
- Add test: boulder.json without `worktree_path` field → session.idle works
- Add test: boulder.json with `worktree_path: null` → session.idle works (no `[Worktree: null]` in prompt)
- Add test: `readBoulderState` sanitizes `null` worktree_path to `undefined`
- Follow existing given/when/then test pattern
### Step 4: Local validation
```bash
bun run typecheck
bun test src/hooks/atlas/
bun test src/features/boulder-state/
bun run build
```
### Step 5: Atomic commit
```bash
git add src/features/boulder-state/storage.ts src/hooks/atlas/idle-event.ts src/hooks/atlas/index.test.ts
git commit -m "fix(atlas): prevent crash when boulder.json missing worktree_path field
readBoulderState() performs unsafe cast of parsed JSON as BoulderState.
When worktree_path is absent or null in boulder.json, downstream code
in idle-event.ts could receive null where string|undefined is expected.
- Sanitize worktree_path in readBoulderState (reject non-string values)
- Add defensive typeof check in idle-event before passing to continuation
- Add test coverage for missing and null worktree_path scenarios"
```
## Phase 2: PR Creation
```bash
git push -u origin fix/atlas-worktree-path-crash
gh pr create \
--base dev \
--title "fix(atlas): prevent crash when boulder.json missing worktree_path" \
--body-file /tmp/pull-request-atlas-worktree-fix.md
```
## Phase 3: Verify Loop
- **Gate A (CI)**: `gh pr checks --watch` — wait for all checks green
- **Gate B (review-work)**: Run 5-agent review (Oracle goal, Oracle quality, Oracle security, QA execution, context mining)
- **Gate C (Cubic)**: Wait for cubic-dev-ai[bot] to respond "No issues found"
- On any failure: fix-commit-push, re-enter verify loop
## Phase 4: Merge
```bash
gh pr merge --squash --delete-branch
git worktree remove ../omo-wt/fix-atlas-worktree-path-crash
```
@@ -0,0 +1,42 @@
# PR Title
```
fix(atlas): prevent crash when boulder.json missing worktree_path
```
# PR Body
## Summary
- Fix runtime type violation in atlas hook when `boulder.json` lacks `worktree_path` field
- Add `worktree_path` sanitization in `readBoulderState()` to reject non-string values (e.g., `null` from manual edits)
- Add defensive `typeof` guards in `idle-event.ts` before passing worktree path to continuation injection
- Add test coverage for missing and null `worktree_path` scenarios
## Problem
`readBoulderState()` in `src/features/boulder-state/storage.ts` casts raw `JSON.parse()` output directly as `BoulderState` via `return parsed as BoulderState`. This bypasses TypeScript's type system entirely at runtime.
When `boulder.json` is missing the `worktree_path` field (common for boulders created before worktree support was added, or created without `--worktree` flag), `boulderState.worktree_path` is `undefined` which is handled correctly. However, when boulder.json has `"worktree_path": null` (possible from manual edits, external tooling, or corrupted state), the runtime type becomes `null` which violates the TypeScript type `string | undefined`.
This `null` value propagates through:
1. `idle-event.ts:handleAtlasSessionIdle()``injectContinuation()``injectBoulderContinuation()`
2. `idle-event.ts:scheduleRetry()` callback → same chain
While the `boulder-continuation-injector.ts` handles falsy values via `worktreePath ? ... : ""`, the type mismatch can cause subtle downstream issues and violates the contract of the `BoulderState` interface.
## Changes
| File | Change |
|------|--------|
| `src/features/boulder-state/storage.ts` | Sanitize `worktree_path` in `readBoulderState()` — reject non-string values |
| `src/hooks/atlas/idle-event.ts` | Add `typeof` guards before passing worktree_path to continuation (2 call sites) |
| `src/hooks/atlas/index.test.ts` | Add 2 tests: missing worktree_path + null worktree_path in session.idle |
| `src/features/boulder-state/storage.test.ts` | Add 2 tests: sanitization of null + preservation of valid string |
## Testing
- `bun test src/hooks/atlas/` — all existing + new tests pass
- `bun test src/features/boulder-state/` — all existing + new tests pass
- `bun run typecheck` — clean
- `bun run build` — clean
@@ -0,0 +1,87 @@
# Verification Strategy
## Gate A: CI (`gh pr checks --watch`)
### What CI runs (from `ci.yml`)
1. **Tests (split)**: Mock-heavy tests in isolation + batch tests
2. **Typecheck**: `bun run typecheck` (tsc --noEmit)
3. **Build**: `bun run build` (ESM + declarations + schema)
### Pre-push local validation
Before pushing, run the exact CI steps locally to catch failures early:
```bash
# Targeted test runs first (fast feedback)
bun test src/features/boulder-state/storage.test.ts
bun test src/hooks/atlas/index.test.ts
# Full test suite
bun test
# Type check
bun run typecheck
# Build
bun run build
```
### Failure handling
- **Test failure**: Read test output, fix code, create new commit (never amend pushed commits), push
- **Typecheck failure**: Run `lsp_diagnostics` on changed files, fix type errors, commit, push
- **Build failure**: Check build output for missing exports or circular deps, fix, commit, push
After each fix-commit-push: `gh pr checks --watch` to re-enter gate
## Gate B: review-work (5-agent review)
### The 5 parallel agents
1. **Oracle (goal/constraint verification)**: Checks the fix matches the stated problem — `worktree_path` crash resolved, no scope creep
2. **Oracle (code quality)**: Validates code follows existing patterns — factory pattern, given/when/then tests, < 200 LOC, no catch-all files
3. **Oracle (security)**: Ensures no new security issues — JSON parse injection, path traversal in worktree_path
4. **QA agent (hands-on execution)**: Actually runs the tests, checks `lsp_diagnostics` on changed files, verifies the fix in action
5. **Context mining agent**: Checks GitHub issues, git history, related PRs for context alignment
### Expected focus areas for this PR
- Oracle (goal): Does the sanitization in `readBoulderState` actually prevent the crash? Is the `typeof` guard necessary or redundant?
- Oracle (quality): Are the new tests following the given/when/then pattern? Do they use the same mock setup as existing tests?
- Oracle (security): Is the `worktree_path` value ever used in path operations without sanitization? (Answer: no, it's only used in template strings)
- QA: Run `bun test src/hooks/atlas/index.test.ts` — does the null worktree_path test actually trigger the bug before fix?
### Failure handling
- Each oracle produces a PASS/FAIL verdict with specific issues
- On FAIL: read the specific issue, fix in the worktree, commit, push, re-run review-work
- All 5 agents must PASS
## Gate C: Cubic (`cubic-dev-ai[bot]`)
### What Cubic checks
- Automated code review bot that analyzes the PR diff
- Looks for: type safety issues, missing error handling, test coverage gaps, anti-patterns
### Expected result
- "No issues found" for this small, focused fix
- 3 files changed (storage.ts, idle-event.ts, index.test.ts) + 1 test file
### Failure handling
- If Cubic flags an issue: evaluate if it's a real concern or false positive
- Real concern: fix, commit, push
- False positive: comment explaining why the flagged pattern is intentional
- Wait for Cubic to re-review after push
## Post-verification: Merge
Once all 3 gates pass:
```bash
gh pr merge --squash --delete-branch
git worktree remove ../omo-wt/fix-atlas-worktree-path-crash
```
On merge failure (conflicts):
```bash
cd ../omo-wt/fix-atlas-worktree-path-crash
git fetch origin dev
git rebase origin/dev
# Resolve conflicts if any
git push --force-with-lease
# Re-enter verify loop from Gate A
```
@@ -0,0 +1 @@
{"total_tokens": null, "duration_ms": 506000, "total_duration_seconds": 506}