@ran-sh/dsh-crew 1.9.0 → 1.10.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 (151) hide show
  1. package/.claude-plugin/marketplace.json +16 -16
  2. package/.claude-plugin/plugin.json +14 -14
  3. package/LICENSE +21 -21
  4. package/README.de.md +359 -359
  5. package/README.es.md +359 -359
  6. package/README.fr.md +359 -359
  7. package/README.hi.md +359 -359
  8. package/README.id.md +359 -359
  9. package/README.ja.md +359 -359
  10. package/README.ko.md +359 -359
  11. package/README.md +126 -126
  12. package/README.pt.md +359 -359
  13. package/README.ru.md +359 -359
  14. package/README.th.md +359 -359
  15. package/README.tr.md +359 -359
  16. package/README.vi.md +359 -359
  17. package/README.zh-TW.md +359 -359
  18. package/README.zh.md +116 -116
  19. package/agents/ds-flash.md +24 -24
  20. package/agents/ds-pro.md +24 -24
  21. package/agents/ds-reviewer.md +23 -23
  22. package/agents/ds-worker.md +23 -23
  23. package/bin/dsh-crew.mjs +16 -16
  24. package/codex/agents/ds-flash.toml +34 -34
  25. package/codex/agents/ds-pro.toml +34 -34
  26. package/codex/agents/ds-reviewer.toml +32 -32
  27. package/codex/agents/ds-worker.toml +32 -32
  28. package/codex/prompts/dsh-config.md +19 -19
  29. package/codex/prompts/dsh-status.md +5 -5
  30. package/commands/config.md +23 -23
  31. package/commands/off.md +5 -5
  32. package/commands/on.md +5 -5
  33. package/commands/status.md +9 -9
  34. package/cordis.patch.yml +4 -4
  35. package/docs/gpt-relay-extension.md +103 -103
  36. package/docs/installation.md +138 -138
  37. package/docs/job-contracts.md +103 -103
  38. package/docs/readiness-matrix.md +85 -85
  39. package/docs/ui-surfaces.md +107 -107
  40. package/official-web-bridge/cordis.patch.yml +4 -4
  41. package/official-web-bridge/entry.mjs +1 -1
  42. package/official-web-bridge/overlay-entry.mjs +59 -59
  43. package/official-web-bridge/package.json +25 -25
  44. package/package.json +3 -2
  45. package/scripts/build-client.mjs +49 -49
  46. package/scripts/live-crew-smoke.mjs +39 -39
  47. package/scripts/live-policy-matrix.mjs +177 -177
  48. package/scripts/policy-probe.mjs +101 -101
  49. package/scripts/remove-legacy-official-bridge.ps1 +89 -89
  50. package/scripts/setup.mjs +393 -393
  51. package/scripts/smoke-real.mjs +110 -110
  52. package/scripts/smoke.mjs +78 -78
  53. package/scripts/verify-crew-ui-polish.mjs +145 -145
  54. package/scripts/verify-history-ui.mjs +97 -97
  55. package/scripts/verify-installer-fix.mjs +26 -26
  56. package/scripts/verify-npm-install.mjs +311 -311
  57. package/scripts/verify-official-bridge-e2e.mjs +192 -192
  58. package/src/adaptive-routing.mjs +260 -260
  59. package/src/client/activation-summary.tsx +64 -64
  60. package/src/client/collapsible-sections.mjs +55 -55
  61. package/src/client/history-panel.tsx +108 -108
  62. package/src/client/host-readiness.mjs +71 -71
  63. package/src/client/index.tsx +1711 -1711
  64. package/src/client/model-callability-view.mjs +17 -17
  65. package/src/client/panel-chrome.tsx +40 -40
  66. package/src/client/quick-entry.tsx +10 -10
  67. package/src/client/quick-panel.tsx +275 -275
  68. package/src/client/readiness-envelope.mjs +34 -34
  69. package/src/client/surface-detection.mjs +43 -43
  70. package/src/config-readiness.mjs +226 -226
  71. package/src/credential-reference.mjs +38 -38
  72. package/src/delivery.mjs +205 -205
  73. package/src/dsh-cli-runtime.mjs +1021 -1021
  74. package/src/dsh-cohort.mjs +20 -20
  75. package/src/extension-contract.mjs +104 -104
  76. package/src/failure-classification.mjs +201 -201
  77. package/src/history/admission-gate.mjs +67 -67
  78. package/src/history/archive-store.mjs +272 -272
  79. package/src/history/cleanup-plan.mjs +89 -89
  80. package/src/history/http.mjs +32 -32
  81. package/src/history/operation.mjs +86 -86
  82. package/src/history/runner-detach.mjs +34 -34
  83. package/src/history/runner.mjs +52 -52
  84. package/src/history/runtime.mjs +36 -36
  85. package/src/history/service.mjs +177 -177
  86. package/src/history/state.mjs +27 -27
  87. package/src/hub/entry.mjs +104 -104
  88. package/src/hub/index.mjs +2694 -2694
  89. package/src/hub-client.mjs +154 -154
  90. package/src/hub-compatibility.mjs +42 -42
  91. package/src/i18n.mjs +19 -19
  92. package/src/information-flow.mjs +67 -67
  93. package/src/install/cli.mjs +28 -28
  94. package/src/install/install-legacy.mjs +711 -711
  95. package/src/install/install.mjs +483 -483
  96. package/src/install/npx-lifecycle.mjs +3456 -3456
  97. package/src/install/official-frontend-assets.mjs +78 -78
  98. package/src/install/official-web.mjs +95 -95
  99. package/src/install/payload-content.mjs +88 -88
  100. package/src/install/windows-startup.mjs +236 -236
  101. package/src/install/windows-supervisor-adapter.mjs +443 -443
  102. package/src/install/windows-supervisor-lifecycle.mjs +782 -782
  103. package/src/install/zcode.mjs +397 -397
  104. package/src/job-contracts.mjs +255 -255
  105. package/src/local-request-guard.mjs +60 -60
  106. package/src/mcp-runtime.mjs +340 -340
  107. package/src/model-callability-contract.mjs +79 -79
  108. package/src/model-catalog.mjs +180 -180
  109. package/src/model-routing.mjs +586 -586
  110. package/src/model-schedule.mjs +207 -207
  111. package/src/official-web-bridge.mjs +447 -447
  112. package/src/policy.mjs +235 -235
  113. package/src/provider-delete-adapters.mjs +1934 -1934
  114. package/src/provider-health.mjs +130 -130
  115. package/src/provider-inventory.mjs +182 -182
  116. package/src/provider-layer-migration-adapters.mjs +759 -759
  117. package/src/provider-layer-migration.mjs +198 -198
  118. package/src/provider-lifecycle-state.mjs +103 -103
  119. package/src/provider-lifecycle.mjs +252 -252
  120. package/src/provider-profile-store.mjs +390 -390
  121. package/src/provider-settings-store.mjs +633 -633
  122. package/src/provider-store-lock.mjs +67 -67
  123. package/src/readiness-matrix.mjs +181 -181
  124. package/src/removable-waiter.mjs +29 -29
  125. package/src/role-profiles.mjs +107 -107
  126. package/src/runtime-controls.mjs +84 -84
  127. package/src/runtime-identity-contract.mjs +34 -34
  128. package/src/runtime-identity.mjs +235 -235
  129. package/src/runtime-readiness-snapshot.mjs +285 -285
  130. package/src/server.mjs +581 -581
  131. package/src/session-origins.mjs +60 -60
  132. package/src/standalone-sdk.mjs +23 -23
  133. package/src/status-shard.mjs +63 -63
  134. package/src/structured-error-code.mjs +38 -38
  135. package/src/supervisor/restart-request.mjs +256 -256
  136. package/src/workflow-runtime.mjs +739 -739
  137. package/src/workflow.mjs +155 -155
  138. package/src/workspace-audit.mjs +231 -231
  139. package/src/workspace-context.mjs +146 -146
  140. package/src/workspace-isolation.mjs +463 -455
  141. package/src/workspace-readiness.mjs +32 -32
  142. package/statusline/statusline.sh +14 -14
  143. package/statusline/worker-segment.sh +35 -35
  144. package/windows/start-dsh-crew.cmd +57 -57
  145. package/windows/start-dsh-crew.ps1 +1302 -1302
  146. package/windows/supervisor-control.ps1 +467 -467
  147. package/worker.cordis.yml +67 -67
  148. package/zcode/agents/ds-reviewer.md +31 -31
  149. package/zcode/agents/ds-worker.md +31 -31
  150. package/zcode/commands/dsh-config.md +17 -17
  151. package/zcode/commands/dsh-status.md +5 -5
package/src/workflow.mjs CHANGED
@@ -1,63 +1,63 @@
1
- // Unified job workflow: the shared state machine + structured outcome that
2
- // every worker job goes through, whether it was started with dsh_run_worker
3
- // (blocking) or dsh_spawn_worker (async). run and spawn differ only in whether
4
- // the caller awaits the job; the business steps — created → running →
5
- // verifying → (escalating | reviewing) → ready/completed/failed — are the
6
- // same, driven by evidence rather than by which transport started the job.
7
- //
8
- // Everything in this module is a pure function (no DSH, hub or worker runtime,
9
- // no I/O), so the workflow rules are unit-testable in isolation. The runtime
10
- // layers stamp phase + outcome through these helpers so server.mjs stays a
11
- // transport adapter.
12
-
13
- import { evaluateAttempt } from './policy.mjs';
14
- import { parseDeliveryReport } from './delivery.mjs';
15
-
16
- export const JOB_PHASES = Object.freeze({
17
- CREATED: 'created',
18
- QUEUED: 'queued',
19
- RUNNING: 'running',
20
- VERIFYING: 'verifying',
21
- ESCALATING: 'escalating',
22
- REVIEWING: 'reviewing',
23
- READY: 'ready',
24
- COMPLETED: 'completed',
25
- FAILED: 'failed',
26
- CANCELLED: 'cancelled',
27
- INTERRUPTED: 'interrupted',
28
- });
29
-
30
- const TERMINAL_PHASES = new Set([JOB_PHASES.COMPLETED, JOB_PHASES.FAILED, JOB_PHASES.CANCELLED, JOB_PHASES.INTERRUPTED]);
31
-
32
- export function isTerminalPhase(phase) {
33
- return TERMINAL_PHASES.has(phase);
34
- }
35
-
36
- // Legal phase transitions (pure guard — the runtime never just assigns a phase).
37
- const ALLOWED_TRANSITIONS = {
38
- [JOB_PHASES.CREATED]: [JOB_PHASES.QUEUED, JOB_PHASES.RUNNING, JOB_PHASES.CANCELLED, JOB_PHASES.FAILED],
39
- [JOB_PHASES.QUEUED]: [JOB_PHASES.RUNNING, JOB_PHASES.CANCELLED],
40
- [JOB_PHASES.RUNNING]: [JOB_PHASES.VERIFYING, JOB_PHASES.REVIEWING, JOB_PHASES.CANCELLED, JOB_PHASES.FAILED],
41
- [JOB_PHASES.VERIFYING]: [JOB_PHASES.ESCALATING, JOB_PHASES.REVIEWING, JOB_PHASES.READY, JOB_PHASES.FAILED, JOB_PHASES.CANCELLED],
42
- [JOB_PHASES.ESCALATING]: [JOB_PHASES.RUNNING, JOB_PHASES.CANCELLED, JOB_PHASES.FAILED],
43
- [JOB_PHASES.REVIEWING]: [JOB_PHASES.READY, JOB_PHASES.FAILED, JOB_PHASES.CANCELLED],
44
- [JOB_PHASES.READY]: [JOB_PHASES.COMPLETED, JOB_PHASES.FAILED, JOB_PHASES.CANCELLED],
45
- };
46
-
47
- /**
48
- * May a workflow transition from one phase to another? Terminal phases never
49
- * leave. A terminal destination is allowed only from a non-terminal phase;
50
- * every non-terminal transition must be listed above.
51
- */
52
- export function canTransition(from, to) {
53
- const fromKey = from ?? JOB_PHASES.CREATED;
54
- if (isTerminalPhase(fromKey)) return false;
55
- if (isTerminalPhase(to)) return true;
56
- const allowed = ALLOWED_TRANSITIONS[fromKey];
57
- if (!allowed) return false;
58
- return allowed.includes(to);
59
- }
60
-
1
+ // Unified job workflow: the shared state machine + structured outcome that
2
+ // every worker job goes through, whether it was started with dsh_run_worker
3
+ // (blocking) or dsh_spawn_worker (async). run and spawn differ only in whether
4
+ // the caller awaits the job; the business steps — created → running →
5
+ // verifying → (escalating | reviewing) → ready/completed/failed — are the
6
+ // same, driven by evidence rather than by which transport started the job.
7
+ //
8
+ // Everything in this module is a pure function (no DSH, hub or worker runtime,
9
+ // no I/O), so the workflow rules are unit-testable in isolation. The runtime
10
+ // layers stamp phase + outcome through these helpers so server.mjs stays a
11
+ // transport adapter.
12
+
13
+ import { evaluateAttempt } from './policy.mjs';
14
+ import { parseDeliveryReport } from './delivery.mjs';
15
+
16
+ export const JOB_PHASES = Object.freeze({
17
+ CREATED: 'created',
18
+ QUEUED: 'queued',
19
+ RUNNING: 'running',
20
+ VERIFYING: 'verifying',
21
+ ESCALATING: 'escalating',
22
+ REVIEWING: 'reviewing',
23
+ READY: 'ready',
24
+ COMPLETED: 'completed',
25
+ FAILED: 'failed',
26
+ CANCELLED: 'cancelled',
27
+ INTERRUPTED: 'interrupted',
28
+ });
29
+
30
+ const TERMINAL_PHASES = new Set([JOB_PHASES.COMPLETED, JOB_PHASES.FAILED, JOB_PHASES.CANCELLED, JOB_PHASES.INTERRUPTED]);
31
+
32
+ export function isTerminalPhase(phase) {
33
+ return TERMINAL_PHASES.has(phase);
34
+ }
35
+
36
+ // Legal phase transitions (pure guard — the runtime never just assigns a phase).
37
+ const ALLOWED_TRANSITIONS = {
38
+ [JOB_PHASES.CREATED]: [JOB_PHASES.QUEUED, JOB_PHASES.RUNNING, JOB_PHASES.CANCELLED, JOB_PHASES.FAILED],
39
+ [JOB_PHASES.QUEUED]: [JOB_PHASES.RUNNING, JOB_PHASES.CANCELLED],
40
+ [JOB_PHASES.RUNNING]: [JOB_PHASES.VERIFYING, JOB_PHASES.REVIEWING, JOB_PHASES.CANCELLED, JOB_PHASES.FAILED],
41
+ [JOB_PHASES.VERIFYING]: [JOB_PHASES.ESCALATING, JOB_PHASES.REVIEWING, JOB_PHASES.READY, JOB_PHASES.FAILED, JOB_PHASES.CANCELLED],
42
+ [JOB_PHASES.ESCALATING]: [JOB_PHASES.RUNNING, JOB_PHASES.CANCELLED, JOB_PHASES.FAILED],
43
+ [JOB_PHASES.REVIEWING]: [JOB_PHASES.READY, JOB_PHASES.FAILED, JOB_PHASES.CANCELLED],
44
+ [JOB_PHASES.READY]: [JOB_PHASES.COMPLETED, JOB_PHASES.FAILED, JOB_PHASES.CANCELLED],
45
+ };
46
+
47
+ /**
48
+ * May a workflow transition from one phase to another? Terminal phases never
49
+ * leave. A terminal destination is allowed only from a non-terminal phase;
50
+ * every non-terminal transition must be listed above.
51
+ */
52
+ export function canTransition(from, to) {
53
+ const fromKey = from ?? JOB_PHASES.CREATED;
54
+ if (isTerminalPhase(fromKey)) return false;
55
+ if (isTerminalPhase(to)) return true;
56
+ const allowed = ALLOWED_TRANSITIONS[fromKey];
57
+ if (!allowed) return false;
58
+ return allowed.includes(to);
59
+ }
60
+
61
61
  function splitSection(value) {
62
62
  if (typeof value !== 'string' || value.trim() === '') return [];
63
63
  return value.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
@@ -120,99 +120,99 @@ function parseChanges(section) {
120
120
  return !NO_CHANGE_SENTINELS.has(normalized);
121
121
  });
122
122
  }
123
-
124
- function parseTests(section) {
125
- return splitSection(section).map((line) => {
126
- const m = line.match(/^(?:[-*+]\s+)?(PASS|FAIL|NOT RUN)\s+—\s+(.+?)\s+—\s+(.+)$/);
127
- if (!m) return { line };
128
- const [, status, command, summary] = m;
129
- return { status, command: command.trim(), summary: summary.trim() };
130
- });
131
- }
132
-
133
- /**
134
- * Classify a worker run into a canonical task status. Completion of the
135
- * process is not success: FAIL/not-run tests and missing delivery all downgrade
136
- * the verdict. Returns 'success' | 'partial' | 'blocked' | 'failed'.
137
- */
138
- export function classifyTaskStatus({ executionStatus = 'completed', testsStatus, deliveryComplete = true, deliveryMissing = [] } = {}) {
139
- void deliveryMissing;
140
- if (executionStatus !== 'completed') return 'failed';
141
- if (testsStatus === 'FAIL') return 'partial';
142
- if (!deliveryComplete) return 'blocked';
143
- if (testsStatus === 'NOT RUN') return 'partial';
144
- return 'success';
145
- }
146
-
147
- /**
148
- * Normalize a worker's final message (+ delivery metadata) into the canonical
149
- * structured outcome the workflow consumes. The Markdown Delivery Report stays
150
- * the human-readable layer; this is the runtime's internal standard.
151
- */
152
- export function buildOutcome({ result = '', deliveryMeta, executionStatus, stopReason, deliveryMissing } = {}) {
153
- const parsed = parseDeliveryReport(result);
154
- const testsStatus = parsed.tests_status ?? deliveryMeta?.tests_status;
155
- const tests = parseTests(parsed.sections.Tests);
156
- const execStatus = executionStatus ?? (stopReason === 'completed' ? 'completed' : 'failed');
157
- return {
158
- execution_status: execStatus,
159
- task_status: classifyTaskStatus({
160
- executionStatus: execStatus,
161
- testsStatus,
162
- deliveryComplete: parsed.complete,
163
- deliveryMissing: deliveryMissing ?? parsed.missing,
164
- }),
165
- confidence: null,
166
- needs_escalation: false,
123
+
124
+ function parseTests(section) {
125
+ return splitSection(section).map((line) => {
126
+ const m = line.match(/^(?:[-*+]\s+)?(PASS|FAIL|NOT RUN)\s+—\s+(.+?)\s+—\s+(.+)$/);
127
+ if (!m) return { line };
128
+ const [, status, command, summary] = m;
129
+ return { status, command: command.trim(), summary: summary.trim() };
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Classify a worker run into a canonical task status. Completion of the
135
+ * process is not success: FAIL/not-run tests and missing delivery all downgrade
136
+ * the verdict. Returns 'success' | 'partial' | 'blocked' | 'failed'.
137
+ */
138
+ export function classifyTaskStatus({ executionStatus = 'completed', testsStatus, deliveryComplete = true, deliveryMissing = [] } = {}) {
139
+ void deliveryMissing;
140
+ if (executionStatus !== 'completed') return 'failed';
141
+ if (testsStatus === 'FAIL') return 'partial';
142
+ if (!deliveryComplete) return 'blocked';
143
+ if (testsStatus === 'NOT RUN') return 'partial';
144
+ return 'success';
145
+ }
146
+
147
+ /**
148
+ * Normalize a worker's final message (+ delivery metadata) into the canonical
149
+ * structured outcome the workflow consumes. The Markdown Delivery Report stays
150
+ * the human-readable layer; this is the runtime's internal standard.
151
+ */
152
+ export function buildOutcome({ result = '', deliveryMeta, executionStatus, stopReason, deliveryMissing } = {}) {
153
+ const parsed = parseDeliveryReport(result);
154
+ const testsStatus = parsed.tests_status ?? deliveryMeta?.tests_status;
155
+ const tests = parseTests(parsed.sections.Tests);
156
+ const execStatus = executionStatus ?? (stopReason === 'completed' ? 'completed' : 'failed');
157
+ return {
158
+ execution_status: execStatus,
159
+ task_status: classifyTaskStatus({
160
+ executionStatus: execStatus,
161
+ testsStatus,
162
+ deliveryComplete: parsed.complete,
163
+ deliveryMissing: deliveryMissing ?? parsed.missing,
164
+ }),
165
+ confidence: null,
166
+ needs_escalation: false,
167
167
  changes: parseChanges(parsed.sections.Diff),
168
- tests,
169
- tests_status: testsStatus ?? null,
170
- risks: splitSection(parsed.sections.Risks),
171
- unverified: splitSection(parsed.sections.Unverified),
172
- delivery: {
173
- complete: parsed.complete,
174
- missing: [...(parsed.missing ?? [])],
175
- format: parsed.format,
176
- sections: [...(parsed.present ?? [])],
177
- },
178
- };
179
- }
180
-
181
- /**
182
- * Decide what happens next after a worker attempt completes. Pure: consumes
183
- * the canonical outcome + the role's model policy + the attempt number, and
184
- * returns the workflow step (accept / escalate / review / fail) with a reason.
185
- * This is the single rule for blocking AND async jobs — server.mjs only
186
- * transports the result.
187
- */
188
- export function decideNextStep({ outcome, policy, attempt = 0, reviewRequested = false, reviewerAuto = false } = {}) {
189
- const o = outcome ?? {};
190
- const evaluation = evaluateAttempt({
191
- execution: o.execution_status,
192
- taskStatus: o.task_status,
193
- testsStatus: o.tests_status,
194
- deliveryComplete: o.delivery?.complete,
195
- workspaceEvidenceOK: o.workspace_evidence_ok !== false,
196
- policy,
197
- attempt,
198
- });
199
- if (evaluation.decision === 'escalate') {
200
- return { step: 'escalate', phase: JOB_PHASES.ESCALATING, reason: evaluation.reason, evaluation };
201
- }
202
- if (reviewRequested && reviewerAuto && evaluation.decision === 'accept') {
203
- return { step: 'review', phase: JOB_PHASES.REVIEWING, reason: 'review requested', evaluation };
204
- }
205
- if (evaluation.decision === 'accept') {
206
- return { step: 'accept', phase: JOB_PHASES.READY, reason: 'verified', evaluation };
207
- }
208
- return { step: 'fail', phase: JOB_PHASES.FAILED, reason: evaluation.reason ?? 'no accept path', evaluation };
209
- }
210
-
211
- /**
212
- * Pure run/spawn parity check helper: given the same spec + outcome, blocking
213
- * and async jobs must reach the same next step. Exported for tests so the
214
- * "run and spawn share one workflow" guarantee is asserted directly.
215
- */
216
- export function parityStep(spec) {
217
- return decideNextStep(spec).step;
218
- }
168
+ tests,
169
+ tests_status: testsStatus ?? null,
170
+ risks: splitSection(parsed.sections.Risks),
171
+ unverified: splitSection(parsed.sections.Unverified),
172
+ delivery: {
173
+ complete: parsed.complete,
174
+ missing: [...(parsed.missing ?? [])],
175
+ format: parsed.format,
176
+ sections: [...(parsed.present ?? [])],
177
+ },
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Decide what happens next after a worker attempt completes. Pure: consumes
183
+ * the canonical outcome + the role's model policy + the attempt number, and
184
+ * returns the workflow step (accept / escalate / review / fail) with a reason.
185
+ * This is the single rule for blocking AND async jobs — server.mjs only
186
+ * transports the result.
187
+ */
188
+ export function decideNextStep({ outcome, policy, attempt = 0, reviewRequested = false, reviewerAuto = false } = {}) {
189
+ const o = outcome ?? {};
190
+ const evaluation = evaluateAttempt({
191
+ execution: o.execution_status,
192
+ taskStatus: o.task_status,
193
+ testsStatus: o.tests_status,
194
+ deliveryComplete: o.delivery?.complete,
195
+ workspaceEvidenceOK: o.workspace_evidence_ok !== false,
196
+ policy,
197
+ attempt,
198
+ });
199
+ if (evaluation.decision === 'escalate') {
200
+ return { step: 'escalate', phase: JOB_PHASES.ESCALATING, reason: evaluation.reason, evaluation };
201
+ }
202
+ if (reviewRequested && reviewerAuto && evaluation.decision === 'accept') {
203
+ return { step: 'review', phase: JOB_PHASES.REVIEWING, reason: 'review requested', evaluation };
204
+ }
205
+ if (evaluation.decision === 'accept') {
206
+ return { step: 'accept', phase: JOB_PHASES.READY, reason: 'verified', evaluation };
207
+ }
208
+ return { step: 'fail', phase: JOB_PHASES.FAILED, reason: evaluation.reason ?? 'no accept path', evaluation };
209
+ }
210
+
211
+ /**
212
+ * Pure run/spawn parity check helper: given the same spec + outcome, blocking
213
+ * and async jobs must reach the same next step. Exported for tests so the
214
+ * "run and spawn share one workflow" guarantee is asserted directly.
215
+ */
216
+ export function parityStep(spec) {
217
+ return decideNextStep(spec).step;
218
+ }