@orkestrel/scaffold 0.0.108 → 0.0.109

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 (157) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
  9. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +290 -69
  10. package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
  11. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
  12. package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
  13. package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
  14. package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
  15. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
  16. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
  17. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
  18. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
  19. package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
  20. package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
  21. package/dist/agents/templates/brief.md +1 -1
  22. package/dist/bin/main.js +51 -46
  23. package/dist/bin/main.js.map +1 -1
  24. package/dist/host/AGENTS.md +23 -21
  25. package/dist/host/agents/orchestration.md +43 -18
  26. package/dist/host/agents/platforms/windows.md +3 -3
  27. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  28. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  29. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  30. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  31. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  32. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  33. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  34. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  35. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  36. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
  37. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
  38. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
  48. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  49. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  50. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  51. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  52. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  53. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  54. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  55. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  56. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  58. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  59. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  60. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  63. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  64. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  65. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  66. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  67. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  68. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
  69. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
  70. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
  71. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
  72. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  73. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  74. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  75. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  76. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  77. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  78. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  79. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
  80. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  81. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  82. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  83. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  86. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  87. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  88. package/dist/host/agents/templates/brief.md +1 -1
  89. package/dist/host/agents/transports/claude-cli.md +3 -3
  90. package/dist/host/agents/transports/codex.md +2 -7
  91. package/dist/host/agents/transports/cursor.md +2 -3
  92. package/dist/host/claude/AGENTS.md +2 -2
  93. package/dist/host/claude/agents/analyst.md +3 -3
  94. package/dist/host/claude/agents/astra.md +3 -3
  95. package/dist/host/claude/agents/builder.md +1 -1
  96. package/dist/host/claude/agents/checker.md +1 -1
  97. package/dist/host/claude/agents/distiller.md +1 -1
  98. package/dist/host/claude/agents/grok.md +1 -1
  99. package/dist/host/claude/agents/opus.md +2 -2
  100. package/dist/host/claude/agents/orkestrel.md +57 -90
  101. package/dist/host/claude/agents/researcher.md +1 -1
  102. package/dist/host/claude/agents/reviewer.md +4 -0
  103. package/dist/host/claude/agents/scout.md +1 -1
  104. package/dist/host/claude/agents/verifier.md +1 -1
  105. package/dist/host/claude/rules/application.md +14 -21
  106. package/dist/host/claude/rules/architecture.md +34 -44
  107. package/dist/host/claude/rules/browser.md +3 -4
  108. package/dist/host/claude/rules/campaign.md +2 -2
  109. package/dist/host/claude/rules/documentation.md +25 -12
  110. package/dist/host/claude/rules/names.md +9 -4
  111. package/dist/host/claude/rules/patterns.md +6 -7
  112. package/dist/host/claude/rules/portability.md +1 -1
  113. package/dist/host/claude/rules/quality.md +4 -5
  114. package/dist/host/claude/rules/styles.md +2 -2
  115. package/dist/host/claude/rules/tests.md +57 -114
  116. package/dist/host/claude/rules/typescript.md +7 -9
  117. package/dist/host/claude/rules/workspace.md +21 -19
  118. package/dist/host/claude/rules/writing.md +2 -5
  119. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  120. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  121. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  122. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  123. package/dist/host/codex/agents/analyst.toml +9 -3
  124. package/dist/host/codex/agents/astra.toml +3 -4
  125. package/dist/host/codex/agents/builder.toml +1 -1
  126. package/dist/host/codex/agents/opus.toml +3 -4
  127. package/dist/host/codex/agents/orkestrel.toml +1 -1
  128. package/dist/host/codex/agents/researcher.toml +2 -2
  129. package/dist/host/codex/agents/verifier.toml +1 -1
  130. package/dist/host/configs/helpers.ts +21 -1
  131. package/dist/host/configs/policy.ts +299 -15
  132. package/dist/host/dotfiles/oxlintrc.json +137 -3
  133. package/dist/host/guides/agent.md +111 -134
  134. package/dist/host/guides/capture.md +801 -0
  135. package/dist/host/guides/probe.md +27 -8
  136. package/dist/host/guides/scaffold.md +96 -18
  137. package/dist/host/guides/study.md +534 -440
  138. package/dist/host/guides/test.md +111 -44
  139. package/dist/host/manifest.json +160 -240
  140. package/dist/host/tests/config.test.ts +971 -187
  141. package/dist/host/tests/policy.test.ts +127 -1
  142. package/dist/host/tests/setupPolicy.ts +893 -33
  143. package/dist/src/core/index.cjs +214 -86
  144. package/dist/src/core/index.cjs.map +1 -1
  145. package/dist/src/core/index.d.cts +122 -38
  146. package/dist/src/core/index.d.ts +122 -38
  147. package/dist/src/core/index.js +209 -85
  148. package/dist/src/core/index.js.map +1 -1
  149. package/dist/src/server/index.cjs +20 -27
  150. package/dist/src/server/index.cjs.map +1 -1
  151. package/dist/src/server/index.js +21 -28
  152. package/dist/src/server/index.js.map +1 -1
  153. package/package.json +11 -11
  154. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  155. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  156. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  157. package/dist/host/guides/supervisor.md +0 -4890
@@ -4,19 +4,19 @@ Lanes that Claude Code starts in its own session. `.claude/AGENTS.md` owns loadi
4
4
 
5
5
  ## Mechanics
6
6
 
7
- | Question | Claude Code |
8
- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
- | Start | The Agent tool for one unit or when the next step depends on the result, with a role from `.claude/agents/` as `subagent_type`; never `general-purpose`, `Plan`, `claude`, or `fork`. A Workflow for a fan-out of small units, a staged pipeline, or a loop, its nodes configured per `.claude/AGENTS.md` § Dispatch. The built-in `Explore` agent for an exact bounded lookup, per `.claude/AGENTS.md` § Dispatch; it loads no instruction files. |
10
- | Continue | Resume a custom subagent by its agent id through the installed tool's resume or message interface; a team teammate is not a custom subagent, so verify which one the installed version resumes. Recover an interrupted Workflow with `resumeFromRunId`; its journal replays the finished agents. A surviving transcript file is no continuation route, and a one-shot `Explore` or `Plan` agent cannot continue. |
11
- | Wake versus deliver | A finished subagent, Workflow, or background command wakes the session with a notification. |
12
- | Track | The agent id, not its display name, and the parent session that can recover it. A Workflow's `scriptPath` and `runId`, recorded at launch; its journal writes only at agent start and result, so read its liveness from the newest subagent transcript. A background command's task id and output file. |
13
- | Background | A foreground shell call is capped at 10 minutes. A background command runs to its own timeout, at most 2 hours, then stops. Run anything longer under `.agents/skills/orkestrel-dispatch/references/launch.md` § Launch ownership. |
14
- | Isolation | Agents share the session's checkout; `isolation: worktree` gives one agent its own worktree. |
15
- | Tools | A role's frontmatter lists its tools, and the host can still withhold one: in-process roles in the desktop host ran with no shell although their charter listed one (2026-10-03), and the harness can refuse an in-process agent's file write. |
16
- | Limits | At the account's usage limit, running agents and Workflow agents stop mid-run and keep the edits they made. |
17
- | Host effects | Agents and Workflow nodes run inside the session's process and open no window (2026-10-21). An agent's shell can restart in the session root between calls (2026-10-17). |
18
- | Visibility | Agent tool calls, Workflow runs, and background commands appear in the session the user watches, where the user can stop them; a CLI started from a shell shows only its command line, so run it as a capped command inside such a lane. |
19
- | Capability map | `.agents/orchestration.md` § Roles, Claude column; `.claude/AGENTS.md` § Models owns the aliases. |
7
+ | Question | Claude Code |
8
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | Start | The Agent tool for one unit or when the next step depends on the result, with a role from `.claude/agents/` as `subagent_type`; never `general-purpose`, `Plan`, `claude`, `fork`, or `Explore`. A Workflow for a fan-out of small units, a staged pipeline, or a loop, its nodes configured per `.claude/AGENTS.md` § Dispatch. `scout` for an exact bounded lookup, per `.agents/orchestration.md` § Routing. |
10
+ | Continue | Resume a custom subagent by its agent id through the installed tool's resume or message interface; a team teammate is not a custom subagent, so verify which one the installed version resumes. Recover an interrupted Workflow with `resumeFromRunId`; its journal replays the finished agents. A surviving transcript file is no continuation route, and a one-shot `Explore` or `Plan` agent cannot continue. |
11
+ | Wake versus deliver | A finished subagent, Workflow, or background command wakes the session with a notification. |
12
+ | Track | The agent id, not its display name, and the parent session that can recover it. A Workflow's `scriptPath` and `runId`, recorded at launch; its journal writes only at agent start and result, so read its liveness from the newest subagent transcript. A background command's task id and output file. |
13
+ | Background | A foreground shell call is capped at 10 minutes. A background command runs to its own timeout, at most 2 hours, then stops. Run anything longer under `.agents/skills/orkestrel-dispatch/references/launch.md` § Launch ownership. |
14
+ | Isolation | Agents share the session's checkout; `isolation: worktree` gives one agent its own worktree. |
15
+ | Tools | A role's frontmatter lists its tools, and the host can still withhold one: in-process roles in the desktop host ran with no shell although their charter listed one (2026-10-03), and the harness can refuse an in-process agent's file write. |
16
+ | Limits | At the account's usage limit, running agents and Workflow agents stop mid-run and keep the edits they made. |
17
+ | Host effects | Agents and Workflow nodes run inside the session's process and open no window (2026-10-21). An agent's shell can restart in the session root between calls (2026-10-17). |
18
+ | Visibility | Agent tool calls, Workflow runs, and background commands appear in the session the user watches, where the user can stop them; a CLI started from a shell shows only its command line, so run it as a capped command inside such a lane. |
19
+ | Capability map | `.agents/orchestration.md` § Roles, Claude column; `.claude/AGENTS.md` § Models owns the aliases. |
20
20
 
21
21
  ## Quirks
22
22
 
@@ -29,4 +29,6 @@ Lanes that Claude Code starts in its own session. `.claude/AGENTS.md` owns loadi
29
29
  - When an agent works outside the session root, give it absolute paths, and check the root's `git status` for strays after it returns.
30
30
  - When read-only fan-out runs on a host where child CLIs open windows, prefer in-process agents and Workflow nodes.
31
31
  - When the Edit or Write tool must reproduce a literal Unicode escape in source (a backslash, the letter `u`, and four hexadecimal digits), write that backslash itself as a Unicode escape (code point 5C). The tools decode every such escape you type before matching or writing, so a typed escape for a backtick lands as a backtick (2026-10-27). Never fall back to a shell edit.
32
+ - When a command carries a program, keep it in a TypeScript file run by `node`: a heredoc, `node -e`, an `&&` chain, or a `${...}` argument trips the harness's approval classifier on a Windows host; a one-shot read-only check can run inline.
33
+ - When a `Remove-Item` runs on a Windows host, run it as its own short command with a relative path: the approval classifier blocks a command that pairs it with a path-like literal elsewhere, such as a `'\'` string. Verify with `git status`.
32
34
  - When a claim about caching, resume, or team behavior matters, verify it against the installed version and the official [subagent documentation](https://code.claude.com/docs/en/sub-agents), [team limitations](https://code.claude.com/docs/en/agent-teams), and [prompt-cache documentation](https://code.claude.com/docs/en/prompt-caching).
@@ -24,5 +24,6 @@ Native Codex agents, and Codex lanes another harness hosts. `.codex/config.toml`
24
24
  - When the shell yields before the command exits, start nothing else, and confirm no owned leftover remains before the next lane; a lane that started the next test file broke the one-runner bound (2026-10-22).
25
25
  - When a brief launches a long command, name its terminal record, such as the launcher's `exit=` line; a lane read a finished step as running and polled it for six minutes (2026-10-22).
26
26
  - When a review brief describes risks in offensive-security wording, the provider's filter can end the lane without a verdict (2026-10-20). Brief the review as quality assurance of the user's own code in neutral words, require an early verdict file, retry once, then use a recorded fallback.
27
+ - When a `codex exec` run is killed, treat it as dead only when its `codex-code-mode-host` child is gone too.
27
28
  - When a Codex child runs through a hosting harness, it can reach that harness's messaging tools; probe them before a writer or runner depends on them (`references/t3.md`).
28
29
  - When a launch fails on a tool server's handshake timeout, relaunch that lane alone; a lane beside it started with the same server (2026-10-19). Stagger launches that start the same server.
@@ -4,19 +4,19 @@ Cursor lanes, through the Cursor CLI or a hosting harness. `.cursor/rules/orches
4
4
 
5
5
  ## Mechanics
6
6
 
7
- | Question | Cursor |
8
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
- | Start | The CLI bench per `.agents/transports/cursor.md`, or the hosting harness's child-task channel (`references/t3.md`). |
10
- | Continue | The CLI's `--resume` option with the session id from the `init` event continues the session with its read context intact (2026-10-01); probe it before its first use. |
11
- | Wake versus deliver | No peer-message wake channel is demonstrated. |
12
- | Track | The session id from the `init` event and the journal path. |
13
- | Background | The CLI runs under `.agents/skills/orkestrel-dispatch/scripts/launch.ts` with a cap. A lane outlived its stated 600-second timeout by running 764 seconds (2026-09-30). |
14
- | Isolation | Lanes share the checkout. |
15
- | Tools | The bench runs in ask mode, read-only. It reads images: it read a capture's heading, captions, and fills exactly in 36 seconds (2026-10-16). |
16
- | Limits | At the account's usage limit, the bench reads dark; record it and use the native reader per `.agents/orchestration.md` § Benches. |
17
- | Host effects | A Grok lane that ran shell commands leaked about one orphaned `conhost.exe` a second on a Windows host (838 in 12 minutes, 8.8 GB, 2026-10-19). Briefs that forbade every command ran ten lanes clean (2026-10-21). |
18
- | Visibility | The CLI bench is hidden; run it only as a capped command inside a visible lane. |
19
- | Capability map | Reader only: the `grok` role. |
7
+ | Question | Cursor |
8
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
9
+ | Start | The CLI bench per `.agents/transports/cursor.md`, or the hosting harness's child-task channel (`references/t3.md`). |
10
+ | Continue | The CLI's `--resume` option with the session id from the `init` event continues the session with its read context intact (2026-10-01); probe it before its first use. |
11
+ | Wake versus deliver | No peer-message wake channel is demonstrated. |
12
+ | Track | The session id from the `init` event and the journal path. |
13
+ | Background | The CLI runs under `.agents/skills/orkestrel-dispatch/scripts/launch.ts` with a cap. A lane outlived its stated 600-second timeout by running 764 seconds (2026-09-30). |
14
+ | Isolation | Lanes share the checkout. |
15
+ | Tools | The bench runs in ask mode, read-only. It reads images: it read a capture's heading, captions, and fills exactly (2026-10-16). |
16
+ | Limits | At the account's usage limit, the bench reads dark; record it and use the native reader per `.agents/orchestration.md` § Benches. |
17
+ | Host effects | A Grok lane that ran shell commands leaked `conhost.exe` processes on a Windows host (2026-10-19; see `.agents/platforms/windows.md` § Processes). Briefs that forbade every command ran clean (2026-10-21). |
18
+ | Visibility | The CLI bench is hidden; run it only as a capped command inside a visible lane. |
19
+ | Capability map | Reader only: the `grok` role. |
20
20
 
21
21
  ## Quirks
22
22
 
@@ -0,0 +1,389 @@
1
+ // Check a Vitest JSON report against named red and green cases. Run from the checkout root:
2
+ // node .agents/skills/orkestrel-parallel/scripts/cases.ts --report FILE.json [--passed NAME ...] [--failed NAME ...]
3
+ // The report is Vitest's `--reporter=json` output. A name matches every case whose full name (its
4
+ // `describe` titles and its own title, joined by spaces) or whose own title equals it exactly; a name
5
+ // that opens with `--` cannot be given. Each `--passed NAME` requires every case it matches to have
6
+ // passed and each `--failed NAME` requires every one to have failed; a name that matches no case is
7
+ // missing. A failed case that no name matches is refused, so a run that fails more than the request
8
+ // expected reads as a difference. A test file that failed with no failed case (it did not collect,
9
+ // or a hook threw) is an unnamed failure named by its file, and a report whose `success` is false
10
+ // while no case and no file failed is a difference too. Name no case to require a report with no
11
+ // failure. Both flags repeat and a name may not be given to both. The script prints one summary
12
+ // line, one line per difference, and `ok` or `differences=N`. The script refuses a flag it does not
13
+ // read, the `--flag=value` form included, and names an unknown flag before a flag given with no
14
+ // value. Exit 0 when the report matches the names, 3 when it does not, 2 when the report cannot be
15
+ // read as a Vitest report, 64 on usage.
16
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
17
+ import { relative } from 'node:path'
18
+
19
+ /** Lists the flags that take a value. */
20
+ export const VALUE_FLAGS: readonly string[] = ['--report', '--passed', '--failed']
21
+
22
+ /** Lists every flag the script reads, value flags and switches alike. */
23
+ export const FLAGS: readonly string[] = VALUE_FLAGS
24
+
25
+ /** Holds the usage line printed when a call names no report. */
26
+ export const USAGE =
27
+ 'usage: cases.ts --report FILE.json [--passed NAME ...] [--failed NAME ...]; --passed and --failed repeat'
28
+
29
+ /** Describes one test case a report holds. */
30
+ export interface Case {
31
+ readonly file: string
32
+ readonly fullName: string
33
+ readonly title: string
34
+ readonly status: string
35
+ }
36
+
37
+ /**
38
+ * Names the value flags a caller gave with no value, at any occurrence.
39
+ *
40
+ * @param argv - The arguments after the script path.
41
+ * @param names - The flags that take a value.
42
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
43
+ */
44
+ export function readMissingFlags(
45
+ argv: readonly string[],
46
+ names: readonly string[],
47
+ ): readonly string[] {
48
+ return names.filter((name) =>
49
+ argv.some((argument, index) => argument === name && isMissingValue(argv, index)),
50
+ )
51
+ }
52
+
53
+ /**
54
+ * Checks whether the argument after a flag is absent or is another flag.
55
+ *
56
+ * @param argv - The arguments after the script path.
57
+ * @param index - The position of the flag.
58
+ * @returns True when no value follows the flag; false otherwise.
59
+ */
60
+ export function isMissingValue(argv: readonly string[], index: number): boolean {
61
+ const value = argv[index + 1]
62
+ return value === undefined || value.startsWith('--')
63
+ }
64
+
65
+ /**
66
+ * Names the flags a caller gave that the script does not read.
67
+ *
68
+ * @remarks
69
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
70
+ * scan.
71
+ *
72
+ * @param argv - The arguments after the script path.
73
+ * @param flags - Every flag the script reads, value flags and switches alike.
74
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
75
+ */
76
+ export function readUnknownFlags(
77
+ argv: readonly string[],
78
+ flags: readonly string[],
79
+ ): readonly string[] {
80
+ const end = argv.indexOf('--')
81
+ return (end === -1 ? argv : argv.slice(0, end)).filter(
82
+ (argument) => argument.startsWith('--') && !flags.includes(argument),
83
+ )
84
+ }
85
+
86
+ /**
87
+ * Reads every value that follows a repeatable flag.
88
+ *
89
+ * @param argv - The arguments after the script path.
90
+ * @param name - The flag, such as `--passed`.
91
+ * @returns The values in order, empty when the flag never appears.
92
+ */
93
+ export function readOptions(argv: readonly string[], name: string): readonly string[] {
94
+ return argv.flatMap((argument, index) => {
95
+ const value = argv[index + 1]
96
+ return argument === name && value !== undefined && !value.startsWith('--') ? [value] : []
97
+ })
98
+ }
99
+
100
+ /**
101
+ * Reads the arguments that are neither a flag nor the value of a value flag.
102
+ *
103
+ * @param argv - The arguments after the script path.
104
+ * @param valueFlags - The flags that take a value.
105
+ * @returns The positional arguments in order.
106
+ */
107
+ export function readPositionals(
108
+ argv: readonly string[],
109
+ valueFlags: readonly string[],
110
+ ): readonly string[] {
111
+ return argv.filter(
112
+ (argument, index) => !argument.startsWith('--') && !valueFlags.includes(argv[index - 1] ?? ''),
113
+ )
114
+ }
115
+
116
+ /**
117
+ * Checks whether a value is a non-null, non-array object.
118
+ *
119
+ * @param value - The value to inspect.
120
+ * @returns True if the value is an object other than null or an array; false otherwise.
121
+ */
122
+ export function isRecord(value: unknown): value is Record<string, unknown> {
123
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
124
+ }
125
+
126
+ /**
127
+ * Quotes an untrusted text for a diagnostic, JSON-encoded and cut to a bounded length.
128
+ *
129
+ * @param text - The text to quote.
130
+ * @returns The JSON string literal of the text, ending in an ellipsis inside the quotes when cut.
131
+ */
132
+ export function quote(text: string): string {
133
+ return JSON.stringify(text.length > 120 ? `${text.slice(0, 120)}…` : text)
134
+ }
135
+
136
+ /**
137
+ * Reads one assertion result of a report as a case.
138
+ *
139
+ * @param file - The test file the result belongs to.
140
+ * @param result - The parsed JSON value of one assertion result.
141
+ * @returns The case as a one-member list, or an empty list when the result lacks a title or status.
142
+ */
143
+ export function readCase(file: string, result: unknown): readonly Case[] {
144
+ if (!isRecord(result)) return []
145
+ const { title, status, fullName, ancestorTitles } = result
146
+ if (typeof title !== 'string' || typeof status !== 'string') return []
147
+ const ancestors = Array.isArray(ancestorTitles)
148
+ ? ancestorTitles.filter((member) => typeof member === 'string')
149
+ : []
150
+ return [
151
+ {
152
+ file,
153
+ title,
154
+ status,
155
+ fullName: typeof fullName === 'string' ? fullName : [...ancestors, title].join(' '),
156
+ },
157
+ ]
158
+ }
159
+
160
+ /**
161
+ * Reads the cases of a Vitest JSON report.
162
+ *
163
+ * @param report - The parsed JSON value of the report.
164
+ * @returns Every case in file order, or undefined when the value is not a Vitest report: an object
165
+ * whose `testResults` is an array.
166
+ */
167
+ export function readCases(report: unknown): readonly Case[] | undefined {
168
+ if (!isRecord(report) || !Array.isArray(report.testResults)) return undefined
169
+ return report.testResults.flatMap((suite): readonly Case[] => {
170
+ if (!isRecord(suite) || !Array.isArray(suite.assertionResults)) return []
171
+ const file = typeof suite.name === 'string' ? suite.name : ''
172
+ return suite.assertionResults.flatMap((result) => readCase(file, result))
173
+ })
174
+ }
175
+
176
+ /**
177
+ * Reads the test files of a report that failed with no failed case.
178
+ *
179
+ * @param report - The parsed JSON value of the report.
180
+ * @returns The `name` of each `testResults` entry whose `status` is `failed` and whose assertion
181
+ * results hold no failed case, in file order; an empty text stands for an entry with no name.
182
+ */
183
+ export function readFailedFiles(report: unknown): readonly string[] {
184
+ if (!isRecord(report) || !Array.isArray(report.testResults)) return []
185
+ return report.testResults.flatMap((suite): readonly string[] => {
186
+ if (!isRecord(suite) || suite.status !== 'failed') return []
187
+ const results = Array.isArray(suite.assertionResults) ? suite.assertionResults : []
188
+ if (results.some((result) => isRecord(result) && result.status === 'failed')) return []
189
+ return [typeof suite.name === 'string' ? suite.name : '']
190
+ })
191
+ }
192
+
193
+ /**
194
+ * Checks whether a report states that the run did not succeed.
195
+ *
196
+ * @param report - The parsed JSON value of the report.
197
+ * @returns True when the report is an object whose `success` is `false`; false otherwise.
198
+ */
199
+ export function isUnsuccessful(report: unknown): boolean {
200
+ return isRecord(report) && report.success === false
201
+ }
202
+
203
+ /**
204
+ * Checks whether a name matches a case.
205
+ *
206
+ * @param entry - The case.
207
+ * @param name - The name from `--passed` or `--failed`.
208
+ * @returns True when the name equals the case's full name or its own title; false otherwise.
209
+ */
210
+ export function matchesCase(entry: Case, name: string): boolean {
211
+ return entry.fullName === name || entry.title === name
212
+ }
213
+
214
+ /**
215
+ * Judges the cases of a report against the names a caller gave.
216
+ *
217
+ * @param cases - The report's cases.
218
+ * @param passed - The names that must have passed.
219
+ * @param failed - The names that must have failed.
220
+ * @param base - The directory the file names in each difference are written relative to.
221
+ * @returns Each difference in order: missing names, wrong statuses, then failed cases no name
222
+ * matches; empty when the report matches.
223
+ */
224
+ export function judgeCases(
225
+ cases: readonly Case[],
226
+ passed: readonly string[],
227
+ failed: readonly string[],
228
+ base: string,
229
+ ): readonly string[] {
230
+ const expectations = [
231
+ ...passed.map((name) => ({ name, status: 'passed' })),
232
+ ...failed.map((name) => ({ name, status: 'failed' })),
233
+ ]
234
+ const named = expectations.flatMap((expectation) => {
235
+ const matches = cases.filter((entry) => matchesCase(entry, expectation.name))
236
+ if (matches.length === 0) return [`missing: ${quote(expectation.name)} matches no case`]
237
+ return matches
238
+ .filter((entry) => entry.status !== expectation.status)
239
+ .map(
240
+ (entry) =>
241
+ `status: ${quote(entry.fullName)} is ${entry.status}, expected ${expectation.status}${describeFile(base, entry.file)}`,
242
+ )
243
+ })
244
+ const unnamed = cases
245
+ .filter(
246
+ (entry) =>
247
+ entry.status === 'failed' &&
248
+ !expectations.some((expectation) => matchesCase(entry, expectation.name)),
249
+ )
250
+ .map((entry) => `unnamed failure: ${quote(entry.fullName)}${describeFile(base, entry.file)}`)
251
+ return [...named, ...unnamed]
252
+ }
253
+
254
+ /**
255
+ * Judges the failures a report holds outside its cases.
256
+ *
257
+ * @param cases - The report's cases.
258
+ * @param failedFiles - The test files that failed with no failed case, from {@link readFailedFiles}.
259
+ * @param unsuccessful - Whether the report states that the run did not succeed.
260
+ * @param base - The directory the file names in each difference are written relative to.
261
+ * @returns One unnamed failure per failed file; or, when no file and no case failed yet the report
262
+ * is not successful, one unnamed failure for the report; empty otherwise.
263
+ */
264
+ export function judgeFailures(
265
+ cases: readonly Case[],
266
+ failedFiles: readonly string[],
267
+ unsuccessful: boolean,
268
+ base: string,
269
+ ): readonly string[] {
270
+ if (failedFiles.length > 0) {
271
+ return failedFiles.map(
272
+ (file) =>
273
+ `unnamed failure: ${file === '' ? 'a test file' : quote(relativeFile(base, file))} failed with no failed case`,
274
+ )
275
+ }
276
+ return unsuccessful && cases.every((entry) => entry.status !== 'failed')
277
+ ? ['unnamed failure: the report is not successful, yet no case and no file in it failed']
278
+ : []
279
+ }
280
+
281
+ /**
282
+ * Writes a test file path relative to a directory.
283
+ *
284
+ * @param base - The directory the path is written relative to.
285
+ * @param file - The test file path from the report.
286
+ * @returns The relative path with forward slashes.
287
+ */
288
+ export function relativeFile(base: string, file: string): string {
289
+ return relative(base, file).split('\\').join('/')
290
+ }
291
+
292
+ /**
293
+ * Describes the file a case belongs to, written relative to a directory.
294
+ *
295
+ * @param base - The directory the path is written relative to.
296
+ * @param file - The test file path from the report.
297
+ * @returns ` in <path>` with forward slashes, or an empty text when the report names no file.
298
+ */
299
+ export function describeFile(base: string, file: string): string {
300
+ return file === '' ? '' : ` in ${relativeFile(base, file)}`
301
+ }
302
+
303
+ /**
304
+ * Counts the cases that carry a status.
305
+ *
306
+ * @param cases - The report's cases.
307
+ * @param status - The status, such as `passed`.
308
+ * @returns How many cases have exactly that status.
309
+ */
310
+ export function countStatus(cases: readonly Case[], status: string): number {
311
+ return cases.filter((entry) => entry.status === status).length
312
+ }
313
+
314
+ /**
315
+ * Reads a report file as JSON.
316
+ *
317
+ * @param path - The report file.
318
+ * @returns The parsed value, or undefined when the file is not readable JSON.
319
+ */
320
+ export function readReport(path: string): unknown {
321
+ try {
322
+ return JSON.parse(readFileSync(path, 'utf8'))
323
+ } catch {
324
+ return undefined
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Runs the script on its arguments.
330
+ *
331
+ * @param argv - The arguments after the script path.
332
+ * @returns The exit code: 0 when the report matches the names, 3 when it does not, 2 when the report
333
+ * cannot be read as a Vitest report, 64 on usage.
334
+ */
335
+ export function main(argv: readonly string[]): number {
336
+ const unknown = readUnknownFlags(argv, FLAGS)
337
+ if (unknown.length > 0) {
338
+ console.error(`cases: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`)
339
+ return 64
340
+ }
341
+ const missing = readMissingFlags(argv, VALUE_FLAGS)
342
+ if (missing.length > 0) {
343
+ console.error(`cases: ${missing.join(', ')} given with no value`)
344
+ return 64
345
+ }
346
+ const reports = readOptions(argv, '--report')
347
+ const path = reports[0]
348
+ const passed = readOptions(argv, '--passed')
349
+ const failed = readOptions(argv, '--failed')
350
+ if (path === undefined || readPositionals(argv, VALUE_FLAGS).length > 0) {
351
+ console.error(USAGE)
352
+ return 64
353
+ }
354
+ const both = passed.filter((name) => failed.includes(name))
355
+ if (both.length > 0) {
356
+ console.error(
357
+ `cases: ${both.map(quote).join(', ')} given to both --passed and --failed; ${USAGE}`,
358
+ )
359
+ return 64
360
+ }
361
+ if (!existsSync(path) || !statSync(path).isFile()) {
362
+ console.error(`cases: ${quote(path)} is not a readable file`)
363
+ return 2
364
+ }
365
+ const report = readReport(path)
366
+ const cases = readCases(report)
367
+ if (cases === undefined) {
368
+ console.error(
369
+ `cases: ${quote(path)} is not a Vitest JSON report; it needs a "testResults" array (run vitest with --reporter=json)`,
370
+ )
371
+ return 2
372
+ }
373
+ const differences = [
374
+ ...judgeCases(cases, passed, failed, process.cwd()),
375
+ ...judgeFailures(cases, readFailedFiles(report), isUnsuccessful(report), process.cwd()),
376
+ ]
377
+ const green = countStatus(cases, 'passed')
378
+ const red = countStatus(cases, 'failed')
379
+ console.log(
380
+ `cases total=${String(cases.length)} passed=${String(green)} failed=${String(red)} other=${String(cases.length - green - red)}`,
381
+ )
382
+ for (const difference of differences) console.log(difference)
383
+ console.log(differences.length === 0 ? 'ok' : `differences=${String(differences.length)}`)
384
+ return differences.length === 0 ? 0 : 3
385
+ }
386
+
387
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
388
+ process.exitCode = main(process.argv.slice(2))
389
+ }
@@ -24,8 +24,8 @@ supposed to produce it. Source-reading review passes a component that renders no
24
24
  accessibility snapshot, and an interaction log.
25
25
  - Source is corroboration for a mechanism, never the proof that the surface shows it.
26
26
  - A claim the portfolio cannot show is unproven, not passed. Say so.
27
- - The same law governs an externally driven surface: a compatibility claim is proven by
28
- driving it with a representative real client, not by its own tests.
27
+ - Prove an externally driven surface's compatibility claim per the `orkestrel-harden` skill's
28
+ Production hardening → Use real implementations.
29
29
 
30
30
  ## Select the scope
31
31
 
@@ -36,29 +36,25 @@ Choose the smallest scope that satisfies the request:
36
36
  re-verdict on the repaired items → independent gates.
37
37
 
38
38
  A narrow verdict request does not authorize fixes. A polish request authorizes fixing every
39
- confirmed finding in scope and rebuilding the harness gaps the verdicts expose. A second round
40
- over the same surface needs an instruction from the user.
39
+ confirmed finding in scope and rebuilding the harness gaps the verdicts expose.
41
40
 
42
41
  ## Execute the round
43
42
 
44
43
  1. **Build the portfolio.** Produce the full evidence set with the harness reference, then
45
- eyeball every artifact yourself before spending a verdict round on it. An unexamined
46
- portfolio buys harness bugs at verdict prices.
44
+ eyeball every artifact yourself before spending a verdict round on it.
47
45
  2. **Seed candidates.** Turn your own mid-integration observations into numbered
48
- confirm-or-refute candidates inside the verdict brief. Observations that stay in your
49
- head are neither evidence nor findings.
46
+ confirm-or-refute candidates inside the verdict brief.
50
47
  3. **Take independent verdicts** on the SAME portfolio, in the shape
51
- § "Return the fixed verdict shape" fixes. The
52
- lanes are subjective design fit; objective state truth; and mechanical inventory of
53
- copy, classes, icons, and accessibility attributes. This is the surface variant of the
54
- review in `.agents/orchestration.md` § Size gate, so its rules bind: each lane is a fresh
55
- subagent with a clean context, blind to the others until reconciliation, run once, and no
56
- lane is dropped because an engine is dark. A narrow verdict request runs the selected lane once
57
- and stops.
48
+ § "Return the fixed verdict shape" fixes. Judgment runs as the two lanes
49
+ `.agents/orchestration.md` § Parallelism names: subjective design fit and objective state
50
+ truth. The mechanical inventory of copy, classes, icons, and accessibility attributes runs as a
51
+ `checker` lane. This is the surface variant of the review in `.agents/orchestration.md`
52
+ § Size gate, so its rules bind: each judgment lane is a fresh subagent with a clean context,
53
+ blind to the other until reconciliation, run once, and no lane is dropped because an engine is
54
+ dark. A narrow verdict request runs the selected lane once and stops.
58
55
  4. **Reconcile.** Confirmed findings get carriers; refuted findings close on the record with
59
56
  the evidence that refuted them; conflicts get a dated ruling. Then walk the reconciled
60
- list once and name the fix-brief item carrying each finding — a finding with no carrier
61
- is a dropped finding, and it will cost the next round.
57
+ list once and name the fix-brief item carrying each finding.
62
58
  5. **Dispatch serialized fix units,** objective lane first because it changes the ground
63
59
  truth the subjective lane reads. Each unit records the failing proof before the
64
60
  repair — the exact command and its red count — and the same command's green count
@@ -87,7 +83,7 @@ over the same surface needs an instruction from the user.
87
83
 
88
84
  ## Return the fixed verdict shape
89
85
 
90
- Free-form verdicts cost one reconciliation pass each. Every verdict returns:
86
+ Every verdict returns:
91
87
 
92
88
  - one line per item, in the brief's numbering:
93
89
  - `RENDERED-PROVEN(<capture>)` — the artifact shows it;
@@ -101,8 +97,8 @@ Triage it against the capture reference before it becomes a finding.
101
97
 
102
98
  ## Accept the result
103
99
 
104
- Do not accept a source-only verdict, a self-audited fix unit, a round without a terminal
105
- line, or a green claim from the engine that wrote the code. Completion requires:
100
+ Accept per `.agents/orchestration.md` § Acceptance. Refuse a source-only verdict, a self-audited
101
+ fix unit, and a round without a terminal line. Completion requires:
106
102
 
107
103
  - every confirmed finding fixed or explicitly ruled out of scope on the record;
108
104
  - a final portfolio whose captures show the accepted state;
@@ -17,9 +17,7 @@ journey-generated and part spawned.
17
17
  Apply this section to a spawned harness only.
18
18
 
19
19
  - Write the harness as one self-contained script that spawns its own children, waits for readiness,
20
- does the capture, and kills them before returning.
21
- - Never leave a child running across calls or expect one to survive its parent. A process started
22
- inside one tool call dies with that call's process group.
20
+ does the capture, and kills them before returning. Spawn, capture, and tear down in one call.
23
21
  - Give every child a pinned working directory. A process that resolves assets, configuration, or
24
22
  fixtures relative to the current directory dies silently when launched from elsewhere.
25
23
  - Pipe child standard error somewhere readable and print it on failure.
@@ -43,33 +41,26 @@ and `orkestrel-journey` fixes how it enters and what it may reach past.
43
41
 
44
42
  ## Capture the full portfolio
45
43
 
46
- Produce every artifact in the following table each round, for every scenario in scope. The Source
47
- column names what produces the artifact in the journey run; a spawned harness produces each one
48
- itself.
49
-
50
- | Artifact | Source in the journey run | What the artifact must show |
51
- | ---------------------- | ------------------------------------ | ------------------------------------------------------------------------------------- |
52
- | Viewport captures | `place` across the declared variants | Every breakpoint the surface declares, never one convenient size |
53
- | Theme captures | `place` across the declared variants | Every theme the surface ships, at every declared viewport |
54
- | Accessibility snapshot | `describeTree` and `describeFocus` | The rendered roles, names, and states, and the focus order the walk took |
55
- | Interaction log | The journal's `steps` | Each interaction, its trigger, and the result observed after it |
56
- | Console and error log | The journal's `output` | Everything the page emitted, including an uncaught error |
57
- | Statechart outcome | The harness after a play-all run | The terminal status, the passed, failed, and total tallies, and each row's own result |
58
-
59
- - Require the statechart outcome of every surface that declares the statechart family, and omit the
60
- row only where the surface declares none.
61
- - Read the journey run's per-variant written artifact for the accessibility snapshot and the logs,
62
- and cite the statechart harness by its deep link where a lane must watch the widget move rather
63
- than read a still of it. `orkestrel-journey` fixes what each holds and what each is named
64
- for.
44
+ Produce each of the following every round, for every scenario in scope:
45
+
46
+ - Viewport captures at every breakpoint the surface declares, and theme captures for every theme it ships at every declared viewport. In the journey run, `place` across the declared variants produces them.
47
+ - The rendered artifact per variant, composed as the `orkestrel-journey` skill's Routing a question → The rendered artifact fixes: the accessibility snapshot, the focus order, the interaction log, and the console and error log.
48
+ - The statechart outcome of every surface that declares the statechart family: the terminal status, the passed, failed, and total tallies, and each row's own result. The harness produces it after a play-all run; cite its deep link where a lane must watch the widget move rather than read a still of it.
49
+
50
+ A spawned harness produces each artifact itself.
51
+
65
52
  - Shoot the whole surface before selecting or focusing anything inside it. A capture taken after a
66
53
  selection reports a duplicate or highlighted artifact that does not exist.
67
54
  - Start a keyboard walk from a neutral state, never from an already-focused control, or the log will
68
55
  report a broken order no person meets.
69
56
  - Name a spawned harness's artifacts so a verdict cites one exactly: scenario, viewport, theme,
70
57
  step. The journey run's filename law already does this.
71
- - Keep the artifacts of each round beside its verdicts. A round judged against the previous round's
72
- captures is not a round.
58
+ - Keep the artifacts of each round beside its verdicts.
59
+ - For a journey-generated portfolio, record the capture manifest snapshot the round judges beside
60
+ its verdicts. Require every judged frame to be current, with its recorded source digests equal
61
+ to the tree's; recapture every stale judged frame before dispatch. Apply this rule to sittings
62
+ and checkpoints alike. Admit
63
+ frames assembled from several runs only when every judged frame is current.
73
64
 
74
65
  ## Preflight before spending a round
75
66
 
@@ -84,8 +75,7 @@ Confirm each of the following:
84
75
  to the total;
85
76
  - nothing in the console log indicates the harness, rather than the surface, failed.
86
77
 
87
- Repair a portfolio that fails preflight before dispatching it. Never spend a verdict round
88
- discovering a harness defect.
78
+ Repair a portfolio that fails preflight before dispatching it.
89
79
 
90
80
  ## Triage missing evidence to the harness first
91
81