@iceinvein/agent-skills 0.21.4 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +9 -0
  2. package/package.json +4 -1
  3. package/skills/bug-hunter/SKILL.md +82 -0
  4. package/skills/bug-hunter/evals/README.md +282 -0
  5. package/skills/bug-hunter/evals/baselines/claude-opus-5-5.json +93 -0
  6. package/skills/bug-hunter/evals/bug-cites-intent-from-spec/case.yaml +4 -0
  7. package/skills/bug-hunter/evals/bug-cites-intent-from-spec/fixture.sh +71 -0
  8. package/skills/bug-hunter/evals/bug-cites-intent-from-spec/graders/bug-cites-the-spec.md +36 -0
  9. package/skills/bug-hunter/evals/bug-cites-intent-from-spec/graders/bug-hunter-fired.md +5 -0
  10. package/skills/bug-hunter/evals/bug-cites-intent-from-spec/graders/intent-written.md +8 -0
  11. package/skills/bug-hunter/evals/bug-cites-intent-from-spec/prompt.md +11 -0
  12. package/skills/bug-hunter/evals/callers-tree-unchanged/case.yaml +4 -0
  13. package/skills/bug-hunter/evals/callers-tree-unchanged/fixture.sh +67 -0
  14. package/skills/bug-hunter/evals/callers-tree-unchanged/graders/bug-hunter-fired.md +5 -0
  15. package/skills/bug-hunter/evals/callers-tree-unchanged/graders/source-unchanged.md +10 -0
  16. package/skills/bug-hunter/evals/callers-tree-unchanged/graders/tests-unchanged.md +10 -0
  17. package/skills/bug-hunter/evals/callers-tree-unchanged/graders/tree-unchanged.md +15 -0
  18. package/skills/bug-hunter/evals/callers-tree-unchanged/prompt.md +11 -0
  19. package/skills/bug-hunter/evals/correct-code-has-no-bugs/case.yaml +4 -0
  20. package/skills/bug-hunter/evals/correct-code-has-no-bugs/fixture.sh +82 -0
  21. package/skills/bug-hunter/evals/correct-code-has-no-bugs/graders/bug-hunter-fired.md +5 -0
  22. package/skills/bug-hunter/evals/correct-code-has-no-bugs/graders/bugs-empty.md +18 -0
  23. package/skills/bug-hunter/evals/correct-code-has-no-bugs/graders/written-with-write-tool.md +13 -0
  24. package/skills/bug-hunter/evals/correct-code-has-no-bugs/prompt.md +11 -0
  25. package/skills/bug-hunter/evals/impossible-value-is-a-bug/case.yaml +4 -0
  26. package/skills/bug-hunter/evals/impossible-value-is-a-bug/fixture.sh +68 -0
  27. package/skills/bug-hunter/evals/impossible-value-is-a-bug/graders/bug-for-sixty-seconds.md +37 -0
  28. package/skills/bug-hunter/evals/impossible-value-is-a-bug/graders/bug-hunter-fired.md +5 -0
  29. package/skills/bug-hunter/evals/impossible-value-is-a-bug/prompt.md +11 -0
  30. package/skills/bug-hunter/evals/proves-a-spec-boundary-bug/case.yaml +4 -0
  31. package/skills/bug-hunter/evals/proves-a-spec-boundary-bug/fixture.sh +79 -0
  32. package/skills/bug-hunter/evals/proves-a-spec-boundary-bug/graders/bug-cites-the-limit.md +31 -0
  33. package/skills/bug-hunter/evals/proves-a-spec-boundary-bug/graders/bug-hunter-fired.md +5 -0
  34. package/skills/bug-hunter/evals/proves-a-spec-boundary-bug/graders/findings-written.md +10 -0
  35. package/skills/bug-hunter/evals/proves-a-spec-boundary-bug/prompt.md +11 -0
  36. package/skills/bug-hunter/evals/rules.json +177 -0
  37. package/skills/bug-hunter/evals/silent-spec-is-an-ambiguity/case.yaml +4 -0
  38. package/skills/bug-hunter/evals/silent-spec-is-an-ambiguity/fixture.sh +97 -0
  39. package/skills/bug-hunter/evals/silent-spec-is-an-ambiguity/graders/bug-hunter-fired.md +5 -0
  40. package/skills/bug-hunter/evals/silent-spec-is-an-ambiguity/graders/bugs-empty.md +18 -0
  41. package/skills/bug-hunter/evals/silent-spec-is-an-ambiguity/graders/cap-is-an-ambiguity.md +12 -0
  42. package/skills/bug-hunter/evals/silent-spec-is-an-ambiguity/graders/written-with-write-tool.md +13 -0
  43. package/skills/bug-hunter/evals/silent-spec-is-an-ambiguity/prompt.md +11 -0
  44. package/skills/bug-hunter/evals/spec-outranks-target-docstring/case.yaml +4 -0
  45. package/skills/bug-hunter/evals/spec-outranks-target-docstring/fixture.sh +82 -0
  46. package/skills/bug-hunter/evals/spec-outranks-target-docstring/graders/bug-cites-the-spec.md +40 -0
  47. package/skills/bug-hunter/evals/spec-outranks-target-docstring/graders/bug-hunter-fired.md +5 -0
  48. package/skills/bug-hunter/evals/spec-outranks-target-docstring/prompt.md +11 -0
  49. package/skills/bug-hunter/references/findings-format.md +82 -0
  50. package/skills/bug-hunter/references/scenario-catalog.md +92 -0
  51. package/skills/bug-hunter/skill.json +24 -0
  52. package/skills/index.json +17 -5
  53. package/skills/magpie/evals/README.md +13 -0
  54. package/skills/magpie/evals/baselines/claude-opus-5-5.json +69 -0
  55. package/skills/magpie/evals/rules.json +192 -0
  56. package/skills/magpie/skill.json +1 -1
  57. package/skills/migrate/evals/README.md +13 -0
  58. package/skills/migrate/evals/baselines/claude-opus-5-5.json +129 -0
  59. package/skills/migrate/evals/rules.json +160 -0
  60. package/skills/migrate/skill.json +10 -4
  61. package/skills/scope-estimate/README.md +173 -0
  62. package/skills/scope-estimate/SKILL.md +168 -0
  63. package/skills/scope-estimate/references/capability-map.md +122 -0
  64. package/skills/scope-estimate/references/deliverables.md +60 -0
  65. package/skills/scope-estimate/references/requirements.md +104 -0
  66. package/skills/scope-estimate/references/schedule.md +138 -0
  67. package/skills/scope-estimate/scripts/__tests__/schedule.test.ts +366 -0
  68. package/skills/scope-estimate/scripts/schedule.ts +553 -0
  69. package/skills/scope-estimate/skill.json +19 -0
  70. package/skills/scope-estimate/templates/plan.example.json +33 -0
  71. package/skills/sluice/SKILL.md +35 -12
  72. package/skills/sluice/evals/README.md +111 -6
  73. package/skills/sluice/evals/announcement-reaches-the-ledger/fixture.sh +32 -19
  74. package/skills/sluice/evals/announcement-reaches-the-ledger/graders/routes-main-before-source.md +14 -0
  75. package/skills/sluice/evals/announcement-reaches-the-ledger/prompt.md +2 -2
  76. package/skills/sluice/evals/baselines/claude-opus-5-5.json +237 -0
  77. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/fixture.sh +19 -12
  78. package/skills/sluice/evals/deep-run-fans-out/graders/task-1-merged-back.md +8 -0
  79. package/skills/sluice/evals/deep-run-fans-out/graders/task-2-merged-back.md +8 -0
  80. package/skills/sluice/evals/deep-run-fans-out/graders/task-3-merged-back.md +6 -0
  81. package/skills/sluice/evals/deep-run-fans-out/graders/worktree-per-implementer.md +12 -0
  82. package/skills/sluice/evals/deep-run-hunts-after-the-flip/case.yaml +5 -0
  83. package/skills/sluice/evals/deep-run-hunts-after-the-flip/fixture.sh +258 -0
  84. package/skills/sluice/evals/deep-run-hunts-after-the-flip/graders/bug-hunter-fired.md +5 -0
  85. package/skills/sluice/evals/deep-run-hunts-after-the-flip/graders/four-attempts-allowed.md +34 -0
  86. package/skills/sluice/evals/deep-run-hunts-after-the-flip/graders/fourth-attempt-tested.md +20 -0
  87. package/skills/sluice/evals/deep-run-hunts-after-the-flip/graders/hunt-before-the-final-review.md +21 -0
  88. package/skills/sluice/evals/deep-run-hunts-after-the-flip/graders/record-has-a-hunt-section.md +11 -0
  89. package/skills/sluice/evals/deep-run-hunts-after-the-flip/graders/sluice-fired.md +5 -0
  90. package/skills/sluice/evals/deep-run-hunts-after-the-flip/prompt.md +12 -0
  91. package/skills/sluice/evals/deep-run-survives-a-milestone/fixture.sh +46 -24
  92. package/skills/sluice/evals/fast-verifies-before-handback/case.yaml +4 -0
  93. package/skills/sluice/evals/fast-verifies-before-handback/fixture.sh +73 -0
  94. package/skills/sluice/evals/fast-verifies-before-handback/graders/quiet-flag-implemented.md +5 -0
  95. package/skills/sluice/evals/fast-verifies-before-handback/graders/sluice-fired.md +5 -0
  96. package/skills/sluice/evals/fast-verifies-before-handback/graders/verified-after-the-last-edit.md +18 -0
  97. package/skills/sluice/evals/fast-verifies-before-handback/prompt.md +11 -0
  98. package/skills/sluice/evals/main-hunts-a-checkable-claim/case.yaml +5 -0
  99. package/skills/sluice/evals/main-hunts-a-checkable-claim/fixture.sh +64 -0
  100. package/skills/sluice/evals/main-hunts-a-checkable-claim/graders/announces-main-channel.md +12 -0
  101. package/skills/sluice/evals/main-hunts-a-checkable-claim/graders/bug-hunter-fired.md +5 -0
  102. package/skills/sluice/evals/main-hunts-a-checkable-claim/graders/hunt-after-the-source-edits.md +12 -0
  103. package/skills/sluice/evals/main-hunts-a-checkable-claim/graders/last-page-holds-the-remainder.md +27 -0
  104. package/skills/sluice/evals/main-hunts-a-checkable-claim/graders/remainder-page-tested.md +21 -0
  105. package/skills/sluice/evals/main-hunts-a-checkable-claim/graders/sluice-fired.md +5 -0
  106. package/skills/sluice/evals/main-hunts-a-checkable-claim/prompt.md +12 -0
  107. package/skills/sluice/evals/main-new-interface/graders/shape-agreed-before-building.md +8 -4
  108. package/skills/sluice/evals/main-skips-the-hunt-without-a-claim/case.yaml +5 -0
  109. package/skills/sluice/evals/main-skips-the-hunt-without-a-claim/fixture.sh +73 -0
  110. package/skills/sluice/evals/main-skips-the-hunt-without-a-claim/graders/announces-main-channel.md +12 -0
  111. package/skills/sluice/evals/main-skips-the-hunt-without-a-claim/graders/no-hunt.md +12 -0
  112. package/skills/sluice/evals/main-skips-the-hunt-without-a-claim/graders/skip-said-in-one-line.md +21 -0
  113. package/skills/sluice/evals/main-skips-the-hunt-without-a-claim/graders/sluice-fired.md +5 -0
  114. package/skills/sluice/evals/main-skips-the-hunt-without-a-claim/prompt.md +12 -0
  115. package/skills/sluice/evals/rules.json +364 -0
  116. package/skills/sluice/references/deep-channel.md +64 -18
  117. package/skills/sluice/references/finish.md +8 -2
  118. package/skills/sluice/references/intent.md +4 -2
  119. package/skills/sluice/references/review.md +32 -0
  120. package/skills/sluice/references/status.md +17 -4
  121. package/skills/sluice/references/verify.md +38 -0
  122. package/skills/sluice/scripts/content-digest.sh +65 -0
  123. package/skills/sluice/scripts/run-stats.sh +9 -8
  124. package/skills/sluice/scripts/status.sh +78 -13
  125. package/skills/sluice/scripts/stop-guard.sh +97 -24
  126. package/skills/sluice/skill.json +2 -2
  127. package/skills/terse/evals/README.md +16 -0
  128. package/skills/terse/evals/baselines/claude-opus-5-5.json +105 -0
  129. package/skills/terse/evals/rules.json +161 -0
  130. package/skills/terse/skill.json +1 -1
  131. package/skills/sluice/evals/announcement-reaches-the-ledger/graders/escalates-fast-to-main.md +0 -12
@@ -3,7 +3,10 @@
3
3
  # design stop missed: API.md publishes the three step lines as stdout contract,
4
4
  # a ground rule forbids breaking anything API.md names without a decision, and
5
5
  # Task 4's own Flips line suppresses exactly those lines. Tasks 2 and 3 are
6
- # untouched by it and must still land. Task 4 cannot be resolved in code: either
6
+ # untouched by it and must still land. Task 3's sink goes through `runSteps`,
7
+ # which `src/cli/index.js` does not export, and API.md says so: a sink on the
8
+ # public `deploy` would let any downstream caller drop the lines, and a run
9
+ # that blocked Task 3 on that was reading the contract correctly. Task 4 cannot be resolved in code: either
7
10
  # the contract breaks or the flag means something else, and that is not the
8
11
  # agent's call.
9
12
  set -euo pipefail
@@ -131,7 +134,9 @@ cat > API.md <<'MD'
131
134
  ## Modules
132
135
 
133
136
  `shipit` publishes `deploy` and `parseArgs` from `src/cli/index.js`. Both have
134
- been importable by downstream packages since 1.0.0.
137
+ been importable by downstream packages since 1.0.0. The modules behind
138
+ `src/cli/index.js` are internal: nothing outside this package imports them, and
139
+ they may change in any release.
135
140
 
136
141
  ## stdout contract
137
142
 
@@ -157,8 +162,10 @@ final summary line untouched. Every other deploy output is unchanged.
157
162
  ## Architecture
158
163
 
159
164
  Progress lines go through a sink instead of `console.log`. The sink is built
160
- from the parsed flags and handed to `deploy`, so `deploy` never asks whether it
161
- is quiet. The summary line does not go through the sink.
165
+ from the parsed flags and handed to `runSteps`, an internal step runner behind
166
+ `deploy`, so the runner never asks whether it is quiet. The public `deploy(args)`
167
+ keeps its signature and always prints. The summary line does not go through the
168
+ sink.
162
169
 
163
170
  ## Ground Rules
164
171
 
@@ -190,22 +197,22 @@ is quiet. The summary line does not go through the sink.
190
197
 
191
198
  ### Task 3: route progress through the sink
192
199
 
193
- **Contract:** Needs: `createSink({ quiet: boolean }) -> { write(line: string): void }` | Offers: `deploy(args: { dryRun: boolean }, sink?: { write(line: string): void }) -> void`
200
+ **Contract:** Needs: `createSink({ quiet: boolean }) -> { write(line: string): void }` | Offers: `runSteps(args: { dryRun: boolean }, sink: { write(line: string): void }) -> void`
194
201
  **Touches:** src/cli/deploy.js (edit) | tests/deploy.test.js (test)
195
202
 
196
- - [ ] Add a test passing a recording sink to `deploy` and asserting the three `-> <step>` lines arrive on the sink -> the new test fails because deploy takes no sink
197
- - [ ] Give `deploy` an optional second parameter `sink`, defaulting to `createSink({ quiet: false })` so `deploy(args)` still prints, and send each `-> <step>` line to `sink.write` -> the new test passes
198
- - [ ] Keep the summary line on `console.log` and keep both original deploy tests, which call `deploy(args)` with no sink, asserting the exact lines `dry run: 3 steps skipped` and `deployed: 3 steps` -> `npm test` green
203
+ - [ ] Add a test passing a recording sink to `runSteps` and asserting the three `-> <step>` lines arrive on the sink -> the new test fails because src/cli/deploy.js has no `runSteps`
204
+ - [ ] Move the step loop into `runSteps(args, sink)`, exported from src/cli/deploy.js, sending each `-> <step>` line to `sink.write`, and make `deploy(args)` call it with `createSink({ quiet: false })` -> the new test passes
205
+ - [ ] Keep the summary line on `console.log`, leave `src/cli/index.js` exporting only `deploy` and `parseArgs`, and keep both original deploy tests, which call `deploy(args)`, asserting the exact lines `dry run: 3 steps skipped` and `deployed: 3 steps` -> `npm test` green
199
206
 
200
207
  ### Task 4: turn quiet on
201
208
 
202
- **Contract:** Needs: `parseArgs(argv: string[]) -> { dryRun: boolean, quiet: boolean }`, `createSink({ quiet: boolean }) -> { write(line: string): void }`, `deploy(args: { dryRun: boolean }, sink?: { write(line: string): void }) -> void` | Offers: `main(argv: string[]) -> void`
209
+ **Contract:** Needs: `parseArgs(argv: string[]) -> { dryRun: boolean, quiet: boolean }`, `createSink({ quiet: boolean }) -> { write(line: string): void }`, `runSteps(args: { dryRun: boolean }, sink: { write(line: string): void }) -> void` | Offers: `main(argv: string[]) -> void`
203
210
  **Touches:** src/cli/main.js (new) | tests/main.test.js (test)
204
211
  **Flips:** the three progress lines become suppressible; before this task `--quiet` parses and changes nothing
205
212
 
206
213
  - [ ] Add a test asserting `main(["--quiet"])` prints only `deployed: 3 steps` -> it fails because src/cli/main.js does not exist
207
214
  - [ ] Add a test asserting `main([])` prints the three step lines and then `deployed: 3 steps` -> it fails for the same reason
208
- - [ ] Write `main` to call `parseArgs`, build the sink from `quiet`, and call `deploy` with both -> `npm test` green
215
+ - [ ] Write `main` to call `parseArgs`, build the sink from `quiet`, and call `runSteps` with both -> `npm test` green
209
216
  MD
210
217
 
211
218
  cat > docs/plans/2026-09-20-quiet-flag-record.md <<'MD'
@@ -281,9 +288,9 @@ cat > .sluice/run.json <<'JSON'
281
288
  "offers": [
282
289
  "args",
283
290
  "boolean",
284
- "deploy",
285
291
  "dryRun",
286
292
  "line",
293
+ "runSteps",
287
294
  "sink",
288
295
  "string",
289
296
  "void",
@@ -302,8 +309,8 @@ cat > .sluice/run.json <<'JSON'
302
309
  "flips": true,
303
310
  "needs": [
304
311
  "createSink",
305
- "deploy",
306
312
  "parseArgs",
313
+ "runSteps",
307
314
  "write"
308
315
  ],
309
316
  "offers": [
@@ -0,0 +1,8 @@
1
+ ---
2
+ type: regex
3
+ # Task 1 commits in its own worktree, so this file reaches the run's tree only
4
+ # when that branch is merged back. The files target cannot show it: it lists
5
+ # only paths Claude created, and a merge is not certain to count.
6
+ pattern: '\bformatDuration\b'
7
+ target: { source: file, path: src/cli/duration.js }
8
+ ---
@@ -0,0 +1,8 @@
1
+ ---
2
+ type: regex
3
+ # As task-1-merged-back. args.js comes from the scaffold, so only its content
4
+ # can show Task 2 arrived; the files target never lists a file Claude only
5
+ # modified.
6
+ pattern: '\btimings\b'
7
+ target: { source: file, path: src/cli/args.js }
8
+ ---
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: regex
3
+ # As task-1-merged-back.
4
+ pattern: '\bcreateStopwatch\b'
5
+ target: { source: file, path: src/cli/stopwatch.js }
6
+ ---
@@ -0,0 +1,12 @@
1
+ ---
2
+ type: regex
3
+ # Pre-flight settled one worktree per concurrent implementer, and three tasks
4
+ # run at once, so three worktrees have to be cut: one loop over the three tasks
5
+ # or three separate adds. Agent's `isolation: worktree` alone does not count,
6
+ # because a session in worktree mode refuses those switches and every kept run
7
+ # that tried it went on to cut the trees by hand. What this cannot see is
8
+ # whether each implementer was then told to work in its own tree.
9
+ pattern: '"command":"(?:[^"\\]|\\.)*?(?:\\[nt]|\b)for \w+ in 1 2 3\b(?:[^"\\]|\\.)*?git worktree add|(?:"command":"(?:[^"\\]|\\.)*?git worktree add[\s\S]*?){3}'
10
+ target: trace
11
+ weight: 2
12
+ ---
@@ -0,0 +1,5 @@
1
+ schema_version: "1.1"
2
+ name: deep-run-hunts-after-the-flip
3
+ plugins: ["plugins/sluice", "plugins/bug-hunter"]
4
+ context:
5
+ scaffold_script: fixture.sh
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env bash
2
+ # A deep run with every task done, the flip included, and nothing left but the
3
+ # hunt, the final review and the finish. The spec states a checkable limit (a
4
+ # step runs at most four times) and the flip's commit breaks it by one: its
5
+ # loop gives up after three attempts. The suite only covers a step that
6
+ # succeeds by its third attempt and a step that never does, so it is green,
7
+ # and the bug is there for the hunt over <base>..HEAD to prove.
8
+ set -euo pipefail
9
+
10
+ mkdir -p src/cli tests docs/specs docs/plans .sluice
11
+ printf '*\n' > .sluice/.gitignore
12
+ git init --quiet
13
+
14
+ commit() {
15
+ git add -A
16
+ git -c user.email=fixture@example.com -c user.name=fixture commit --quiet -m "$1"
17
+ }
18
+
19
+ cat > package.json <<'JSON'
20
+ {
21
+ "name": "shipit",
22
+ "version": "1.3.0",
23
+ "type": "module",
24
+ "scripts": {
25
+ "test": "node --test \"tests/*.test.js\""
26
+ }
27
+ }
28
+ JSON
29
+
30
+ cat > src/cli/deploy.js <<'JS'
31
+ const STEPS = ["build", "upload", "activate"];
32
+
33
+ export function deploy(runStep, log = console.log) {
34
+ for (const step of STEPS) {
35
+ log(`-> ${step}`);
36
+ runStep(step);
37
+ }
38
+ log("deployed: 3 steps");
39
+ }
40
+ JS
41
+
42
+ cat > tests/deploy.test.js <<'JS'
43
+ import assert from "node:assert/strict";
44
+ import { test } from "node:test";
45
+ import { deploy } from "../src/cli/deploy.js";
46
+
47
+ test("a full deploy reports every step", () => {
48
+ const lines = [];
49
+ deploy(() => {}, (line) => lines.push(line));
50
+ assert.deepEqual(lines, ["-> build", "-> upload", "-> activate", "deployed: 3 steps"]);
51
+ });
52
+ JS
53
+
54
+ cat > docs/specs/2026-10-01-step-retries.md <<'MD'
55
+ # Design: step retries
56
+
57
+ ## Context
58
+
59
+ A deploy step that fails once on a flaky network ends the whole deploy today.
60
+
61
+ ## Decisions
62
+
63
+ - **D1 The limit.** A failed step is retried up to three times, so it runs at
64
+ most four times in all. `MAX_ATTEMPTS` in `src/cli/retry-policy.js` is 4,
65
+ and it is the only place the number lives.
66
+ - **D2 Giving up.** When the fourth attempt fails, the deploy stops with
67
+ `step <name> failed after 4 attempts` and runs no later step.
68
+ - **D3 Success.** A step that succeeds on any attempt up to the fourth lets
69
+ the deploy carry on to the next step, and the progress lines are unchanged.
70
+ MD
71
+
72
+ cat > docs/plans/2026-10-01-step-retries.md <<'MD'
73
+ # Plan: step-retries
74
+
75
+ ## Goal
76
+
77
+ A failed deploy step is retried up to three times (four attempts in all), as
78
+ `docs/specs/2026-10-01-step-retries.md` states.
79
+
80
+ ## Architecture
81
+
82
+ The limit lives in one module, `src/cli/retry-policy.js`. `deploy` reads it and
83
+ wraps each step in the retry loop; nothing else changes.
84
+
85
+ ## Ground Rules
86
+
87
+ - Commit message convention: `<type>(<scope>): <subject>`, subject lower case,
88
+ no trailing full stop, no attribution trailers and no tool footers.
89
+ - Test runner: `npm test`, which runs `node --test "tests/*.test.js"`.
90
+ - Node built-ins only. No dependency may be added to package.json.
91
+
92
+ ### Task 1: retry policy
93
+
94
+ **Contract:** Needs: none | Offers: `MAX_ATTEMPTS: number`
95
+ **Touches:** src/cli/retry-policy.js (new) | tests/retry-policy.test.js (new)
96
+
97
+ - [x] Add a test asserting `MAX_ATTEMPTS` is 4 -> it fails because src/cli/retry-policy.js does not exist
98
+ - [x] Export `MAX_ATTEMPTS = 4` -> `npm test` green
99
+
100
+ ### Task 2: retry each step
101
+
102
+ **Contract:** Needs: `MAX_ATTEMPTS: number` | Offers: `deploy(runStep: (step: string) => void, log?: (line: string) => void) -> void`
103
+ **Touches:** src/cli/deploy.js (edit) | tests/deploy.test.js (test)
104
+ **Flips:** a failed step is retried instead of ending the deploy
105
+
106
+ - [x] Add a test where `runStep` fails twice on `upload` and then succeeds, asserting the deploy finishes -> it fails because deploy does not retry
107
+ - [x] Add a test where `runStep` always fails on `upload`, asserting the deploy throws and never reaches `activate` -> it fails for the same reason
108
+ - [x] Wrap each step in a loop bounded by `MAX_ATTEMPTS` -> `npm test` green
109
+ MD
110
+
111
+ commit "chore: shipit 1.3.0 with the step-retries design and plan"
112
+ BASE="$(git rev-parse HEAD)"
113
+
114
+ cat > src/cli/retry-policy.js <<'JS'
115
+ export const MAX_ATTEMPTS = 4;
116
+ JS
117
+
118
+ cat > tests/retry-policy.test.js <<'JS'
119
+ import assert from "node:assert/strict";
120
+ import { test } from "node:test";
121
+ import { MAX_ATTEMPTS } from "../src/cli/retry-policy.js";
122
+
123
+ test("a step runs at most four times", () => {
124
+ assert.equal(MAX_ATTEMPTS, 4);
125
+ });
126
+ JS
127
+
128
+ commit "feat(retry): add the retry policy"
129
+ TASK1="$(git rev-parse HEAD)"
130
+
131
+ cat > src/cli/deploy.js <<'JS'
132
+ import { MAX_ATTEMPTS } from "./retry-policy.js";
133
+
134
+ const STEPS = ["build", "upload", "activate"];
135
+
136
+ export function deploy(runStep, log = console.log) {
137
+ for (const step of STEPS) {
138
+ log(`-> ${step}`);
139
+ let attempt = 1;
140
+ while (true) {
141
+ try {
142
+ runStep(step);
143
+ break;
144
+ } catch {
145
+ if (attempt >= MAX_ATTEMPTS - 1) {
146
+ throw new Error(`step ${step} failed after ${attempt} attempts`);
147
+ }
148
+ attempt++;
149
+ }
150
+ }
151
+ }
152
+ log("deployed: 3 steps");
153
+ }
154
+ JS
155
+
156
+ cat > tests/deploy.test.js <<'JS'
157
+ import assert from "node:assert/strict";
158
+ import { test } from "node:test";
159
+ import { deploy } from "../src/cli/deploy.js";
160
+
161
+ function failing(step, times) {
162
+ let failures = 0;
163
+ const ran = [];
164
+ return {
165
+ ran,
166
+ runStep(name) {
167
+ ran.push(name);
168
+ if (name === step && failures < times) {
169
+ failures++;
170
+ throw new Error(`${name} is down`);
171
+ }
172
+ },
173
+ };
174
+ }
175
+
176
+ test("a full deploy reports every step", () => {
177
+ const lines = [];
178
+ deploy(() => {}, (line) => lines.push(line));
179
+ assert.deepEqual(lines, ["-> build", "-> upload", "-> activate", "deployed: 3 steps"]);
180
+ });
181
+
182
+ test("a step that fails twice and then succeeds lets the deploy finish", () => {
183
+ const lines = [];
184
+ const upload = failing("upload", 2);
185
+ deploy(upload.runStep, (line) => lines.push(line));
186
+ assert.equal(lines.at(-1), "deployed: 3 steps");
187
+ });
188
+
189
+ test("a step that never succeeds stops the deploy before the next step", () => {
190
+ const upload = failing("upload", Infinity);
191
+ assert.throws(() => deploy(upload.runStep, () => {}), /failed/);
192
+ assert.equal(upload.ran.includes("activate"), false);
193
+ });
194
+ JS
195
+
196
+ commit "feat(deploy): retry a failed step"
197
+ TASK2="$(git rev-parse HEAD)"
198
+
199
+ cat > docs/plans/2026-10-01-step-retries-record.md <<MD
200
+ # Run record: step-retries
201
+
202
+ ## Pre-flight
203
+
204
+ - Review: tier 3 only. Task 2 is the flip and is the one task that got a reviewer.
205
+ - Effort: this session's effort for both tasks.
206
+ - Workspace: shared tree. Tasks ran serially, so no worktree was cut.
207
+ - Base: \`${BASE}\`, the commit the plan landed on.
208
+
209
+ ## Log
210
+
211
+ - Task 1, retry policy: landed at \`${TASK1}\`. Inert, nothing reads it yet.
212
+ - Task 2, retry each step: landed at \`${TASK2}\`, the flip. Reviewed, no
213
+ findings.
214
+ MD
215
+
216
+ commit "docs(plans): step-retries run record"
217
+
218
+ cat > .sluice/run.json <<JSON
219
+ {
220
+ "schema": 1,
221
+ "topic": "step-retries",
222
+ "channel": "deep",
223
+ "started": "2026-10-01T09:02:11Z",
224
+ "plan": "docs/plans/2026-10-01-step-retries.md",
225
+ "record": "docs/plans/2026-10-01-step-retries-record.md",
226
+ "tasks": [
227
+ {
228
+ "id": 1,
229
+ "status": "done",
230
+ "name": "retry policy",
231
+ "tier": 2,
232
+ "base": "${BASE}",
233
+ "commit": "${TASK1}",
234
+ "offers": ["MAX_ATTEMPTS", "number"],
235
+ "touches": ["src/cli/retry-policy.js", "tests/retry-policy.test.js"]
236
+ },
237
+ {
238
+ "id": 2,
239
+ "status": "done",
240
+ "name": "retry each step",
241
+ "tier": 3,
242
+ "flips": true,
243
+ "reviewed": true,
244
+ "base": "${TASK1}",
245
+ "commit": "${TASK2}",
246
+ "needs": ["MAX_ATTEMPTS", "number"],
247
+ "offers": ["deploy", "line", "log", "runStep", "step", "string", "void"],
248
+ "touches": ["src/cli/deploy.js", "tests/deploy.test.js"]
249
+ }
250
+ ],
251
+ "updated": "2026-10-01T10:41:53Z",
252
+ "preflight": {
253
+ "review": "tier3",
254
+ "effort": "0 of 2 low",
255
+ "workspace": "shared"
256
+ }
257
+ }
258
+ JSON
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: tool_used
3
+ tool: Skill
4
+ input_match: '"skill"\s*:\s*"(?:[\w-]+:)?bug-hunter"'
5
+ ---
@@ -0,0 +1,34 @@
1
+ ---
2
+ type: llm
3
+ # A regex over the loop accepts one spelling of the fix and rejects the rest
4
+ # (a for loop, a helper, a counter that starts at zero), so the judge reads the
5
+ # code and works the cases. The spec puts the number in MAX_ATTEMPTS alone, so
6
+ # a fix that raised the constant instead leaves this file buggy and fails here.
7
+ focus: { source: file, path: src/cli/deploy.js }
8
+ weight: 3
9
+ ---
10
+
11
+ This is `src/cli/deploy.js` after the run. It imports `MAX_ATTEMPTS` from
12
+ `src/cli/retry-policy.js`; take its value as 4, which the spec fixes. The spec
13
+ says a failed step is retried up to three times, so it runs at most four times
14
+ in all; when the fourth attempt fails the deploy stops with
15
+ `step <name> failed after 4 attempts` and runs no later step.
16
+
17
+ The fixture's loop gave up when `attempt >= MAX_ATTEMPTS - 1`, so a step ran
18
+ three times at most and the error said `after 3 attempts`.
19
+
20
+ Work out, reading the code as written, what happens when `runStep` throws on
21
+ `upload`:
22
+
23
+ - it throws three times and then succeeds: the deploy must finish and log
24
+ `deployed: 3 steps`
25
+ - it always throws: `upload` must run exactly four times, the error must read
26
+ `step upload failed after 4 attempts`, and `activate` must never run
27
+
28
+ PASS if the code does all of that.
29
+ FAIL if a step that fails three times stops the deploy, if a step that always
30
+ fails runs any number of times other than four, if the error names another
31
+ count, or if a later step runs after a step gave up.
32
+ FAIL if `deploy.js` no longer reads `MAX_ATTEMPTS` from `retry-policy.js`
33
+ (for example a fix that writes the number 4 into the loop): the spec makes
34
+ that constant the only place the number lives.
@@ -0,0 +1,20 @@
1
+ ---
2
+ type: llm
3
+ # A proven bug is fixed test-first, so its repro becomes a test in the suite.
4
+ # This reads the scaffold's deploy test file, where the retry tests already
5
+ # live; a test written into a new file elsewhere is not seen here.
6
+ focus: { source: file, path: tests/deploy.test.js }
7
+ ---
8
+
9
+ This is the deploy test file after the run. The fixture shipped three tests: a
10
+ full deploy reports every step, a step that fails twice and then succeeds lets
11
+ the deploy finish, and a step that never succeeds stops the deploy (asserting
12
+ only that the error matches `/failed/`). None of them pins the limit: the spec
13
+ says a step runs at most four times, and the shipped code gave up after three.
14
+
15
+ PASS if the file now has a test that pins the fourth attempt: for example a
16
+ step that fails three times and then succeeds lets the deploy finish, or a
17
+ step that always fails runs exactly four times, or the error reads
18
+ `failed after 4 attempts`.
19
+ FAIL if no assertion distinguishes three attempts from four, or if any of the
20
+ three original tests was deleted or loosened.
@@ -0,0 +1,21 @@
1
+ ---
2
+ type: regex
3
+ # Every task, the flip included, is done in the fixture, so any hunt in the
4
+ # trace comes after the flip; what is left to pin is that it lands before the
5
+ # final review. From the start of the trace, no Agent call and no
6
+ # `status.sh final` call may come before the bug-hunter Skill call, and an
7
+ # Agent call or a `status.sh final` must come after it. Marking `final` is a
8
+ # rule of its own (deep-run-finishes-without-checking-in), so a run that hunts,
9
+ # dispatches the review and forgets the mark still kept this one. `final` alone
10
+ # is not enough before the hunt, because it is marked after the review clears.
11
+ # An Agent call stands for the review: every task is
12
+ # already done, so there is no implementer to dispatch, and bug-hunter's own
13
+ # SKILL.md dispatches no agents, so any Agent call before the hunt can only be
14
+ # the final review.
15
+ # Both are anchored as tool inputs: the Skill call's own input, and a Bash
16
+ # command invoking status.sh (with an optional --dir) rather than quoting it,
17
+ # so SKILL.md's text, a grep or prose naming either does not count.
18
+ pattern: '^(?:(?!"name":"Agent"|"command":"(?:(?:[^"\\]|\\.)*?(?:[\s/]|\\[nt]))?status\.sh(?:\\"|'')?(?:(?:\s|\\[nt])+--dir(?:\s|\\[nt])+(?:[^"\\\s]|\\.)+)?(?:\s|\\[nt])+final(?=\s|\\[nt]|\\"|"|[;&|]))[\s\S])*?"name":"Skill","input":\{(?:[^{}]|\{[^{}]*\})*?"skill":"(?:[\w-]+:)?bug-hunter"[\s\S]*?(?:"name":"Agent"|"command":"(?:(?:[^"\\]|\\.)*?(?:[\s/]|\\[nt]))?status\.sh(?:\\"|'')?(?:(?:\s|\\[nt])+--dir(?:\s|\\[nt])+(?:[^"\\\s]|\\.)+)?(?:\s|\\[nt])+final(?=\s|\\[nt]|\\"|"|[;&|]))'
19
+ target: trace
20
+ weight: 3
21
+ ---
@@ -0,0 +1,11 @@
1
+ ---
2
+ type: regex
3
+ # The hunt is the controller's work, so it is recorded in the run record, not
4
+ # in run.json: a `## Hunt` section the final review reads. The fixture's record
5
+ # has Pre-flight and Log only, and the record is the scaffold's file, which the
6
+ # files target never lists, so its content is read directly.
7
+ pattern: '^## Hunt\b'
8
+ flags: m
9
+ target: { source: file, path: docs/plans/2026-10-01-step-retries-record.md }
10
+ weight: 2
11
+ ---
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: tool_used
3
+ tool: Skill
4
+ input_match: '"skill"\s*:\s*"(?:[\w-]+:)?sluice"'
5
+ ---
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: deep-run-hunts-after-the-flip
3
+ description: The deep channel's hunt. Every task is done, the flip included, so before the final review the run hunts the whole plan diff against the spec, fixes the off-by-one it proves test-first, and records the hunt in the run record.
4
+ tags: [verify, hunt, deep, execution, scaffold, write]
5
+ plugins: ["plugins/sluice", "plugins/bug-hunter"]
6
+ max_turns: 100
7
+ timeout_seconds: 2400
8
+ allowed_tools: [Read, Glob, Grep, Skill, Write, Edit, Bash, Agent]
9
+ expected_outcome: Invokes bug-hunter on the plan's base..HEAD against docs/specs/2026-10-01-step-retries.md before marking the final review with status.sh final; the hunt proves a step runs three times where the spec says four, and the run fixes it with a test, writes a Hunt section into the run record, then runs the final review.
10
+ ---
11
+
12
+ Both tasks in the plan have landed, the flip included; it's all in `.sluice/run.json`. Take the run from here to the handback.