@awak-app/simy-cli 0.5.29 → 0.5.31

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 (41) hide show
  1. package/README.md +15 -5
  2. package/package.json +3 -2
  3. package/src/agent.js +148 -3
  4. package/src/cli-contract.js +1 -1
  5. package/src/codex-plugins/simy/.codex-plugin/plugin.json +15 -6
  6. package/src/codex-plugins/simy/hooks/README.md +15 -0
  7. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +196 -0
  8. package/src/codex-plugins/simy/hooks/hooks.json +53 -0
  9. package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +28 -0
  10. package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +3 -1
  11. package/src/coding-run-store.js +69 -0
  12. package/src/delivery-hooks/command.js +31 -0
  13. package/src/delivery-hooks/desktop.js +40 -0
  14. package/src/desktop-host.js +11 -1
  15. package/src/index.js +7 -0
  16. package/src/local-task.js +55 -0
  17. package/src/orchestrator/audit.js +3 -0
  18. package/src/orchestrator/checklist-continuation.js +3 -3
  19. package/src/orchestrator/completion-contract.js +7 -0
  20. package/src/orchestrator/contract.js +6 -4
  21. package/src/orchestrator/delivery/WORKFLOW.md +93 -6
  22. package/src/orchestrator/delivery/references/acceptance-evidence.md +9 -5
  23. package/src/orchestrator/delivery/references/change-boundary-matrix.md +20 -1
  24. package/src/orchestrator/delivery/references/desktop-monitor-triggers.md +35 -0
  25. package/src/orchestrator/delivery/references/shared-system-specifications.md +3 -0
  26. package/src/orchestrator/delivery/references/state-and-receipts.md +19 -0
  27. package/src/orchestrator/execution-io.js +15 -0
  28. package/src/orchestrator/independent-audit.js +14 -1
  29. package/src/orchestrator/instruction.js +3 -0
  30. package/src/orchestrator/loop.js +71 -0
  31. package/src/orchestrator/outcome-monitor.js +123 -0
  32. package/src/provider-stream.js +3 -1
  33. package/src/runner.js +162 -20
  34. package/src/sqm/agent-skill.js +3 -2
  35. package/src/sqm/claude-plugin.js +6 -1
  36. package/src/sqm/command.js +13 -0
  37. package/src/sqm/report.js +23 -0
  38. package/src/sqm/workflow-onboarding.js +1 -1
  39. package/src/workflow-observation-sender.js +208 -0
  40. package/src/workflow-reporting.js +35 -0
  41. package/src/workflow-reporting.md +137 -0
package/src/runner.js CHANGED
@@ -42,7 +42,14 @@ import { LocalRunRegistry } from "./run-registry.js";
42
42
  import { webApiHeaders, webApiUrl } from "./web-api.js";
43
43
  import { normalizeGitHubRemote } from "./workspace-context.js";
44
44
  import { CLI_VERSION } from "./cli-contract.js";
45
+ import { reviseObjective, verifyObjective } from "./orchestrator/outcome-monitor.js";
46
+ import { hasUserOutcomeProof } from "./orchestrator/completion-contract.js";
45
47
  import { createSqmChecker } from "./sqm/index.js";
48
+ import {
49
+ createWorkflowObservationSender,
50
+ normalizeWorkflowObservationBinding,
51
+ } from "./workflow-observation-sender.js";
52
+ import { saveCodingRunSnapshot } from "./coding-run-store.js";
46
53
 
47
54
  const execFileAsync = promisify(execFile);
48
55
  const MAX_LOCAL_LOG_LINES = 1_000;
@@ -73,24 +80,29 @@ const INTERRUPTED_STATES = new Set([
73
80
  "checking_pr",
74
81
  "re_instructing",
75
82
  ]);
76
- const RESTORABLE_STATES = new Set([...RESUMABLE_STATES, ...INTERRUPTED_STATES]);
83
+ const RESTORABLE_STATES = new Set([...RESUMABLE_STATES, "merge_ready", ...INTERRUPTED_STATES]);
77
84
  const PR_PREPARATION_IDS = new Set(PR_PREPARATION_CHECKS.map((row) => row.id));
78
85
 
79
86
  export { LocalRunRegistry };
80
87
 
81
- export function createRun({ runId, request, session, apiOrigin }) {
88
+ export function createRun({ runId, request, session, apiOrigin, localStateRoot = null }) {
82
89
  const emitter = new EventEmitter();
83
90
  const normalizedRequest = {
84
91
  ...request,
85
92
  execution_target: normalizeDesktopExecutionTarget(request?.execution_target),
86
93
  execution_device_id:
87
94
  String(request?.execution_device_id || session?.device_id || "").trim() || null,
95
+ workflow_observation_binding: normalizeWorkflowObservationBinding(
96
+ request?.workflow_observation_binding,
97
+ { provider: request?.backend },
98
+ ),
88
99
  };
89
100
  return {
90
101
  id: runId,
91
102
  request: normalizedRequest,
92
103
  session,
93
104
  apiOrigin,
105
+ localStateRoot,
94
106
  status: "queued",
95
107
  lastOutput: "",
96
108
  startedAt: null,
@@ -105,18 +117,31 @@ export function createRun({ runId, request, session, apiOrigin }) {
105
117
  emitter,
106
118
  pendingLedgerUpdate: null,
107
119
  ledgerUpdateRunning: false,
120
+ ledgerRetryTimer: null,
108
121
  snapshot: createCodingLoopSnapshot({
109
122
  runId,
110
123
  request: normalizedRequest,
111
124
  metadata: {
112
125
  local_orchestrator_version: 1,
113
126
  local_output_persisted: false,
127
+ ...(normalizedRequest.workflow_observation_binding
128
+ ? {
129
+ workflow_observation: {
130
+ status: "pending",
131
+ workflow_id: normalizedRequest.workflow_observation_binding.workflow_id,
132
+ workflow_run_id:
133
+ normalizedRequest.workflow_observation_binding.workflow_run_id,
134
+ development_outcome: "unverified",
135
+ binding: normalizedRequest.workflow_observation_binding,
136
+ },
137
+ }
138
+ : {}),
114
139
  },
115
140
  }),
116
141
  };
117
142
  }
118
143
 
119
- export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
144
+ export function restoreRun({ snapshot, session, apiOrigin, localPath = null, localStateRoot = null }) {
120
145
  if (!snapshot || typeof snapshot !== "object" || !snapshot.charter) {
121
146
  throw new Error("Cannot restore an invalid agentic loop snapshot.");
122
147
  }
@@ -125,6 +150,7 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
125
150
  runId: String(snapshot.id || ""),
126
151
  session,
127
152
  apiOrigin,
153
+ localStateRoot,
128
154
  request: {
129
155
  backend: charter.backend === "claude" ? "claude" : "codex",
130
156
  execution_target: normalizeDesktopExecutionTarget(charter.execution_target),
@@ -153,6 +179,8 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
153
179
  require_human_approval: charter.require_human_approval !== false,
154
180
  must_not: Array.isArray(charter.must_not) ? charter.must_not : [],
155
181
  proposal_id: typeof charter.proposal_id === "string" ? charter.proposal_id : null,
182
+ workflow_observation_binding:
183
+ snapshot.metadata?.workflow_observation?.binding ?? null,
156
184
  charter_context: {
157
185
  source_charter_id: charter.id,
158
186
  audit_backend: charter.audit_backend,
@@ -188,7 +216,11 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
188
216
  throw new Error(`Agentic loop ${run.id} is not in a restorable state.`);
189
217
  }
190
218
  const restoredFromState = INTERRUPTED_STATES.has(persistedState) ? persistedState : null;
191
- const restoredState = restoredFromState ? "waiting_human" : persistedState;
219
+ const objectiveCheck = verifyObjective(restoredSnapshot.charter);
220
+ const savedGoalUnproven = (persistedState === "merge_ready" || persistedState === "pr_ready_for_review") &&
221
+ !hasUserOutcomeProof(restoredSnapshot.attempts?.at(-1)?.independent_audit?.coverage_review);
222
+ const objectiveUnverified = !objectiveCheck.valid || objectiveCheck.legacy || savedGoalUnproven;
223
+ const restoredState = restoredFromState || objectiveUnverified ? "waiting_human" : persistedState;
192
224
  if (restoredFromState) {
193
225
  restoredSnapshot.events = Array.isArray(restoredSnapshot.events)
194
226
  ? restoredSnapshot.events
@@ -203,9 +235,30 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
203
235
  },
204
236
  );
205
237
  }
238
+ if (objectiveUnverified) {
239
+ restoredSnapshot.done_gate_passed = false;
240
+ restoredSnapshot.implementation_gate_passed = false;
241
+ restoredSnapshot.final_audit = {
242
+ passed: false,
243
+ summary: "The saved user objective or its outcome evidence cannot be verified after restart.",
244
+ findings: [{
245
+ code: objectiveCheck.legacy ? "OBJECTIVE_BASELINE_UNVERIFIED" :
246
+ !objectiveCheck.valid ? "OBJECTIVE_CONTRACT_CHANGED" : "USER_OUTCOME_UNPROVEN",
247
+ severity: "blocker",
248
+ target: savedGoalUnproven && objectiveCheck.valid && !objectiveCheck.legacy ? "user_outcome" : "objective_contract",
249
+ explanation: "Confirm the original user outcome and its acceptance criteria before resuming or claiming completion.",
250
+ repairability: "manual",
251
+ }],
252
+ };
253
+ appendEvent(restoredSnapshot, "waiting_human", "Saved objective requires human confirmation.", {
254
+ code: objectiveCheck.legacy ? "objective_baseline_unverified" :
255
+ !objectiveCheck.valid ? "objective_contract_changed" : "user_outcome_unproven",
256
+ });
257
+ }
206
258
 
207
259
  run.snapshot = restoredSnapshot;
208
260
  run.snapshot.state = restoredState;
261
+ run.snapshot.metadata = { ...run.snapshot.metadata, pr_lifecycle_state: restoredState };
209
262
  run.status = restoredState;
210
263
  run.lastOutput = restoredFromState
211
264
  ? `Local CLI restarted during ${restoredFromState}; provide guidance to continue this run.`
@@ -214,12 +267,15 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
214
267
  run.completedAt = restoredSnapshot.updated_at || null;
215
268
  run.controlState = restoredState === "stopped" ? "stopped" : "waiting_human";
216
269
  run.restoredFromState = restoredFromState;
270
+ run.restoredObjectiveUnverified = objectiveUnverified;
217
271
  return run;
218
272
  }
219
273
 
220
274
  export async function publishRestoredLocalCodingRun(run) {
221
- if (!run?.restoredFromState) return run?.snapshot || null;
275
+ if (!run?.restoredFromState && !run?.restoredObjectiveUnverified) return run?.snapshot || null;
222
276
  await updateRun(run, "waiting_human");
277
+ run.restoredFromState = null;
278
+ run.restoredObjectiveUnverified = false;
223
279
  return run.snapshot;
224
280
  }
225
281
 
@@ -236,6 +292,16 @@ export async function startLocalCodingRun(
236
292
  export async function continueLocalCodingRun(run, guidance, dependencies = {}) {
237
293
  const message = String(guidance || "").trim();
238
294
  if (!message) throw new Error("Human guidance is required.");
295
+ assertRunCanContinue(run);
296
+ run.completedAt = null;
297
+ run.stopRequested = false;
298
+ run.controlState = "running";
299
+ run.resumeCount += 1;
300
+ emitControl(run, "human_input", `Human guidance accepted: ${message}`);
301
+ return executeLocalCodingRun(run, dependencies, { humanGuidance: message, resume: true });
302
+ }
303
+
304
+ function assertRunCanContinue(run) {
239
305
  if (run.operation || run.child) throw new Error("The selected Agentic Loop run is still active.");
240
306
  if (!RESUMABLE_STATES.has(run.status)) {
241
307
  throw new Error("Human guidance is available only when the selected run is waiting.");
@@ -249,12 +315,6 @@ export async function continueLocalCodingRun(run, guidance, dependencies = {}) {
249
315
  if (nextAttempt > run.snapshot.charter.max_attempts) {
250
316
  throw new Error("The Agentic Loop run used its retry budget. Start a new run to continue.");
251
317
  }
252
- run.completedAt = null;
253
- run.stopRequested = false;
254
- run.controlState = "running";
255
- run.resumeCount += 1;
256
- emitControl(run, "human_input", `Human guidance accepted: ${message}`);
257
- return executeLocalCodingRun(run, dependencies, { humanGuidance: message, resume: true });
258
318
  }
259
319
 
260
320
  export async function increaseLocalCodingRunProviderTokenBudget(run, value) {
@@ -359,10 +419,29 @@ export async function applyLocalHumanDecision(
359
419
  ) {
360
420
  const guidance = String(message || "").trim();
361
421
  if (!guidance) throw new Error("A human decision note is required.");
422
+ assertRunCanContinue(run);
362
423
  const charter = run.snapshot.charter;
363
424
  if (acceptanceCriteria.length > 0) {
364
- charter.acceptance_criteria = acceptanceCriteria.map(String).map((item) => item.trim()).filter(Boolean);
425
+ const previousCriteria = charter.acceptance_criteria;
426
+ const nextCriteria = acceptanceCriteria.map(String).map((item) => item.trim()).filter(Boolean);
427
+ if (nextCriteria.length === 0) throw new Error("At least one acceptance criterion is required.");
428
+ charter.acceptance_criteria = nextCriteria;
365
429
  charter.acceptance_criteria_source = "explicit";
430
+ if (JSON.stringify(previousCriteria) !== JSON.stringify(nextCriteria)) {
431
+ if (JSON.stringify(charter.completion_contract.minimum_scope) === JSON.stringify(previousCriteria)) {
432
+ charter.completion_contract.minimum_scope = nextCriteria;
433
+ }
434
+ charter.completion_contract.criterion_ids = nextCriteria.map((_, index) => `AC-${index + 1}`);
435
+ reviseObjective(charter, {
436
+ kind: "explicit_human_decision",
437
+ run_id: run.id,
438
+ note_sha256: executionTextSha256(guidance),
439
+ recorded_at: new Date().toISOString(),
440
+ });
441
+ run.snapshot.implementation_gate_passed = false;
442
+ run.snapshot.done_gate_passed = false;
443
+ run.snapshot.final_audit = null;
444
+ }
366
445
  }
367
446
  if (requiredChecks.length > 0) {
368
447
  charter.required_checks = requiredChecks.map(String).map((item) => item.trim()).filter(Boolean);
@@ -668,6 +747,26 @@ export async function recheckLocalCodingRun(run, { collectEvidence } = {}) {
668
747
  return run.snapshot;
669
748
  }
670
749
 
750
+ export async function markCodingRunMonitorFailure(run, error) {
751
+ const message = error instanceof Error ? error.message : String(error);
752
+ run.snapshot.done_gate_passed = false;
753
+ run.snapshot.final_audit = {
754
+ passed: false,
755
+ summary: "Background outcome recheck could not establish current completion.",
756
+ findings: [{
757
+ code: "BACKGROUND_RECHECK_UNAVAILABLE",
758
+ severity: "blocker",
759
+ target: "outcome_monitor",
760
+ explanation: message,
761
+ repairability: "auto",
762
+ }],
763
+ };
764
+ appendEvent(run.snapshot, "pr_ready_for_review", "Background outcome recheck requires retry.", {
765
+ code: "background_recheck_unavailable",
766
+ });
767
+ await updateRun(run, "pr_ready_for_review");
768
+ }
769
+
671
770
  function sqmBundleEndpoint(session) {
672
771
  if (process.env.SIMY_SQM_BUNDLE_URL?.trim()) return process.env.SIMY_SQM_BUNDLE_URL.trim();
673
772
  if (!session?.api_base_url) return undefined;
@@ -680,7 +779,7 @@ function sqmEvidenceEndpoint(session) {
680
779
  return new URL("sqm/run-evidence", session.api_base_url).href;
681
780
  }
682
781
 
683
- async function executeProcessAttempt({
782
+ export async function executeProcessAttempt({
684
783
  backend,
685
784
  instruction,
686
785
  repositoryPath,
@@ -698,6 +797,15 @@ async function executeProcessAttempt({
698
797
  const stderr = [];
699
798
  const tokenUsageRecords = [];
700
799
  const phase = marker === "SIMY_AUDIT_JSON:" ? "reviewer" : "executor";
800
+ const observationSender =
801
+ phase === "executor" && run.snapshot.metadata?.workflow_observation?.status !== "accepted"
802
+ ? createWorkflowObservationSender({
803
+ binding: run.request.workflow_observation_binding,
804
+ backend,
805
+ session: run.session,
806
+ apiOrigin: run.apiOrigin,
807
+ })
808
+ : null;
701
809
 
702
810
  return new Promise((resolve) => {
703
811
  let settled = false;
@@ -714,6 +822,7 @@ async function executeProcessAttempt({
714
822
  backend,
715
823
  stream: "stdout",
716
824
  onLine: (line) => emitOutput(run, line, { backend }),
825
+ onEvent: (event) => observationSender?.observe(event),
717
826
  onTerminalError: (message) => { terminalError ||= message; },
718
827
  onUsage: (usage) =>
719
828
  tokenUsageRecords.push({
@@ -740,12 +849,26 @@ async function executeProcessAttempt({
740
849
  child.stdout?.on("data", (chunk) => collect(stdout, stdoutDecoder, chunk));
741
850
  child.stderr?.on("data", (chunk) => collect(stderr, stderrDecoder, chunk));
742
851
 
743
- const finish = ({ exitCode = null, error = null } = {}) => {
852
+ const finish = async ({ exitCode = null, error = null } = {}) => {
744
853
  if (settled) return;
745
854
  settled = true;
746
855
  stdoutDecoder.flush();
747
856
  stderrDecoder.flush();
748
857
  run.child = null;
858
+ if (observationSender) {
859
+ const observation = await observationSender.flush();
860
+ run.snapshot.metadata.workflow_observation = {
861
+ ...observation,
862
+ binding: run.request.workflow_observation_binding,
863
+ };
864
+ if (observation.status === "failed") {
865
+ emitOutput(
866
+ run,
867
+ "WorkFlow observation was not accepted; the Provider result is unchanged and development completion remains unverified.",
868
+ { backend },
869
+ );
870
+ }
871
+ }
749
872
  const rawStdout = stdout.join("");
750
873
  const assistantText = extractAssistantText(rawStdout);
751
874
  const parse =
@@ -769,8 +892,8 @@ async function executeProcessAttempt({
769
892
  });
770
893
  };
771
894
 
772
- child.on("error", (error) => finish({ error: error.message }));
773
- child.on("close", (code) => finish({ exitCode: code }));
895
+ child.on("error", (error) => void finish({ error: error.message }));
896
+ child.on("close", (code) => void finish({ exitCode: code }));
774
897
  });
775
898
  }
776
899
 
@@ -878,6 +1001,7 @@ function emitControl(run, state, message) {
878
1001
  async function updateRun(run, state) {
879
1002
  run.status = state;
880
1003
  run.snapshot.state = state;
1004
+ run.snapshot.metadata = { ...run.snapshot.metadata, pr_lifecycle_state: state };
881
1005
  run.snapshot.updated_at = new Date().toISOString();
882
1006
  run.completedAt = TERMINAL_STATES.has(state) ? run.snapshot.updated_at : null;
883
1007
  if (state === "waiting_human" || state === "pr_ready_for_review") {
@@ -892,7 +1016,9 @@ async function updateRun(run, state) {
892
1016
  run.controlState = "running";
893
1017
  }
894
1018
 
895
- run.emitter.emit("event", { type: "run", run: toLedgerSnapshot(run.snapshot) });
1019
+ const durableSnapshot = toLedgerSnapshot(run.snapshot);
1020
+ if (run.localStateRoot) await saveCodingRunSnapshot(run.localStateRoot, run.snapshot);
1021
+ run.emitter.emit("event", { type: "run", run: durableSnapshot });
896
1022
  const latestEvent = run.snapshot.events.at(-1);
897
1023
  run.emitter.emit("event", {
898
1024
  type: "status",
@@ -916,21 +1042,34 @@ function queueLedgerUpdate(run) {
916
1042
  completedAt: run.completedAt,
917
1043
  snapshot: toLedgerSnapshot(run.snapshot),
918
1044
  };
1045
+ if (run.ledgerRetryTimer) return;
919
1046
  if (run.ledgerUpdateRunning) return;
920
1047
  run.ledgerUpdateRunning = true;
921
1048
  void drainLedgerUpdates(run);
922
1049
  }
923
1050
 
1051
+ export function syncLocalCodingRun(run) {
1052
+ queueLedgerUpdate(run);
1053
+ }
1054
+
924
1055
  async function drainLedgerUpdates(run) {
925
1056
  try {
926
1057
  while (run.pendingLedgerUpdate) {
927
1058
  const update = run.pendingLedgerUpdate;
928
1059
  run.pendingLedgerUpdate = null;
929
- await persistRunStatus(run, update);
1060
+ if (!(await persistRunStatus(run, update))) {
1061
+ run.pendingLedgerUpdate ||= update;
1062
+ run.ledgerRetryTimer = setTimeout(() => {
1063
+ run.ledgerRetryTimer = null;
1064
+ queueLedgerUpdate(run);
1065
+ }, 30_000);
1066
+ run.ledgerRetryTimer.unref?.();
1067
+ break;
1068
+ }
930
1069
  }
931
1070
  } finally {
932
1071
  run.ledgerUpdateRunning = false;
933
- if (run.pendingLedgerUpdate) queueLedgerUpdate(run);
1072
+ if (run.pendingLedgerUpdate && !run.ledgerRetryTimer) queueLedgerUpdate(run);
934
1073
  }
935
1074
  }
936
1075
 
@@ -956,9 +1095,12 @@ async function persistRunStatus(run, update) {
956
1095
  });
957
1096
  if (!response.ok) {
958
1097
  emitOutput(run, `SIMY ledger update failed with HTTP ${response.status}.`);
1098
+ return false;
959
1099
  }
1100
+ return true;
960
1101
  } catch {
961
- // The local snapshot remains authoritative while the CLI is running.
1102
+ emitOutput(run, "SIMY ledger update unavailable; durable local record retained for retry.");
1103
+ return false;
962
1104
  }
963
1105
  }
964
1106
 
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
7
7
  import { inspectClaudePlugin, installClaudePlugin } from "./claude-plugin.js";
8
8
 
9
9
  export const SQM_AGENT_SKILL_ID = "simy-sqm-pr-check";
10
- export const SQM_AGENT_SKILL_VERSION = "3.0.0";
10
+ export const SQM_AGENT_SKILL_VERSION = "3.1.0";
11
11
  export const SIMY_CODEX_PLUGIN_ID = "simy";
12
12
  export const SIMY_CODEX_PLUGIN_VERSION = "0.1.0";
13
13
 
@@ -36,7 +36,8 @@ When opted in, before creating or proposing a pull request:
36
36
  2. Run \`simy sqm check --proof proof.json --signed-evidence proof.json.signed.json\`.
37
37
  3. Read \`proof.json\` and require \`outcome.status\` to be \`passed\`.
38
38
  4. If either command fails, SQM is unavailable, or the proof is not passed, report the exact blocker. Do not claim SQM passed or silently bypass the requested check.
39
- 5. Include the proof path and SQM outcome in the PR body when the check passes.
39
+ 5. Verify the proof's repository, base, HEAD and working-tree/diff identity and its bound signature with SQM tooling. Use separate artifact paths outside the diff, and rerun after bound inputs change.
40
+ 6. Include proof paths, outcome reasons, matched modules, evaluated rules and scenario statuses from \`executions\` in the PR evidence, separating blocking and advisory results. A pass with \`no_applicable_rules\`, zero scenario evaluations or advisory-only checks does not establish feature or historical incident coverage. Scenario statuses are deterministic evaluations, not browser/provider/deployed execution evidence. Retain relevant uncovered incidents and their concrete regression result or gap; do not promote incomplete delivery acceptance.
40
41
 
41
42
  Do not replace SQM with an LLM review, and do not claim the check passed without reading the generated proof.
42
43
  `;
@@ -20,13 +20,18 @@ export async function claudePluginFiles() {
20
20
  const content = await readFile(path.join(SOURCE, relative), "utf8");
21
21
  files.set(relative, content.replaceAll("current Codex thread", "current Claude Code conversation"));
22
22
  }
23
+ files.set("hooks/delivery-loop.mjs", await readFile(path.join(SOURCE, "hooks/delivery-loop.mjs"), "utf8"));
23
24
  files.set("hooks/connection-reminder.mjs", await readFile(path.join(SOURCE, "hooks/connection-reminder.mjs"), "utf8"));
24
25
  const hooks = JSON.parse(await readFile(path.join(SOURCE, "hooks/hooks.json"), "utf8"));
26
+ delete hooks.hooks.Interrupt; // Codex-only event; Claude interruptions cannot be vetoed.
27
+ hooks.hooks.StopFailure = structuredClone(hooks.hooks.Stop);
28
+ hooks.hooks.Notification = structuredClone(hooks.hooks.PermissionRequest);
25
29
  for (const groups of Object.values(hooks.hooks)) {
26
30
  for (const group of groups) {
27
31
  for (const hook of group.hooks) {
28
32
  hook.command = hook.command.replace(/^node /, `${quoteRuntime(process.execPath)} `)
29
- .replaceAll("process.env.PLUGIN_ROOT", "process.env.CLAUDE_PLUGIN_ROOT");
33
+ .replaceAll("process.env.PLUGIN_ROOT", "process.env.CLAUDE_PLUGIN_ROOT")
34
+ .replaceAll("hook.main('codex')", "hook.main('claude')");
30
35
  }
31
36
  }
32
37
  }
@@ -10,6 +10,7 @@ import { evaluateKnowledgeBundle } from "./checkers.js";
10
10
  import { runSqmCheck, runSqmMinCheck } from "./index.js";
11
11
  import { lintKnowledgeModule, summarizeReplay } from "./reliability.js";
12
12
  import { ensureSqmSession } from "./session.js";
13
+ import { formatSqmCoverage } from "./report.js";
13
14
 
14
15
  export async function runSqmCommand(argv, { cliVersion }) {
15
16
  if (argv.includes("--help") || argv.includes("-h") || !argv.length) {
@@ -64,7 +65,9 @@ export async function runSqmCommand(argv, { cliVersion }) {
64
65
  minCheck = await runSqmMinCheck(common);
65
66
  await atomicJson(minPath, minCheck);
66
67
  }
68
+ if (minCheck.status === "unavailable") return printUnavailable(minCheck, argv);
67
69
  const result = await runSqmCheck({ ...common, minCheck, deviceId, signingKeyPath: option(argv, "--signing-key") || undefined });
70
+ if (result.status === "unavailable") return printUnavailable(result, argv);
68
71
  const proofPath = path.resolve(option(argv, "--proof") || "proof.json");
69
72
  const evidencePath = path.resolve(option(argv, "--signed-evidence") || `${proofPath}.signed.json`);
70
73
  const diagnosticsPath = path.resolve(option(argv, "--diagnostics") || `${proofPath}.diagnostics.json`);
@@ -206,8 +209,18 @@ function printMin(result, output) {
206
209
  console.log(`Result: ${output}`);
207
210
  }
208
211
 
212
+ function printUnavailable(result, argv) {
213
+ if (argv.includes("--json")) console.log(JSON.stringify(result, null, 2));
214
+ else {
215
+ console.error("SQM full check: unavailable · no new proof written. Run min-check again after resolving the cause.");
216
+ for (const warning of result.warnings || []) console.error(`warning: ${warning}`);
217
+ }
218
+ return 2;
219
+ }
220
+
209
221
  function printFull(result) {
210
222
  console.log(`SQM full check: ${result.proof.outcome.status} · ${result.findings.length} finding(s)`);
223
+ console.log(formatSqmCoverage(result.proof));
211
224
  console.log(`Proof ${result.proof.proof_id} (${result.proof.integrity.digest})`);
212
225
  console.log(`Per-run signature: valid · ${result.signature_verification.key_id}`);
213
226
  console.log(`Artifacts: ${result.artifacts.proof} · ${result.artifacts.signed_evidence} · ${result.artifacts.diagnostics}`);
@@ -0,0 +1,23 @@
1
+ // Presentation only: retain the signed proof and enforcement outcome unchanged.
2
+ export function formatSqmCoverage(proof) {
3
+ const modules = proof.executions;
4
+ const rules = modules.flatMap(module => module.rule_results);
5
+ const scenarios = modules.flatMap(module => module.scenarios);
6
+ const blocking = modules.filter(module => module.enforcement === "blocking").length;
7
+ const advisory = modules.filter(module => module.enforcement === "advisory").length;
8
+ const statusCounts = new Map();
9
+ for (const scenario of scenarios) {
10
+ statusCounts.set(scenario.status, (statusCounts.get(scenario.status) || 0) + 1);
11
+ }
12
+ const lines = [
13
+ `SQM scope: ${modules.length} module(s) · ${rules.length} rule(s) · ${scenarios.length} scenario evaluation(s)`,
14
+ `Enforcement: ${blocking} blocking module(s) · ${advisory} advisory module(s)`,
15
+ ];
16
+ if (scenarios.length) lines.push(`Scenario evaluations: ${[...statusCounts].map(([status, count]) => `${status}=${count}`).join(" · ")}`);
17
+ if (proof.outcome.reasons.length) lines.push(`Outcome reasons: ${proof.outcome.reasons.join(", ")}`);
18
+ if (!modules.length) lines.push("No applicable rules: this result does not establish feature or historical incident coverage.");
19
+ else if (!scenarios.length) lines.push("No scenario evaluations: rule results alone do not establish scenario coverage.");
20
+ if (modules.length && !blocking) lines.push("Advisory-only scope: findings do not block this outcome.");
21
+ lines.push("Scenario evaluations are deterministic checks, not browser/provider/deployed journey evidence. Record incident coverage and delivery acceptance separately.");
22
+ return lines.join("\n");
23
+ }
@@ -58,7 +58,7 @@ export async function prepareDaemonWorkflows({
58
58
  try {
59
59
  const installed = await install();
60
60
  logger(
61
- "SIMY workflows installed. In Codex, restart, then find and install SIMY in Plugins. In Claude Code, add the SIMY marketplace and install simy@simy-local before using /simy:sqm-loop or /simy:pr-review.",
61
+ "SIMY workflows installed. In Codex, restart, then find and install SIMY in Plugins. In Claude Code, add the SIMY marketplace and install simy@simy-local before using /simy:sqm-loop or /simy:pr-review. For Delivery Loop, review and enable SIMY hooks in /hooks, then request /simy:delivery-loop for each task. Plugin installation does not grant hook trust.",
62
62
  );
63
63
  if (installed.claude_plugin?.marketplace_root) {
64
64
  logger(`Claude Code: claude plugin marketplace add ${JSON.stringify(installed.claude_plugin.marketplace_root)}`);