@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.
- package/README.md +15 -5
- package/package.json +3 -2
- package/src/agent.js +148 -3
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +15 -6
- package/src/codex-plugins/simy/hooks/README.md +15 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +196 -0
- package/src/codex-plugins/simy/hooks/hooks.json +53 -0
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +28 -0
- package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +3 -1
- package/src/coding-run-store.js +69 -0
- package/src/delivery-hooks/command.js +31 -0
- package/src/delivery-hooks/desktop.js +40 -0
- package/src/desktop-host.js +11 -1
- package/src/index.js +7 -0
- package/src/local-task.js +55 -0
- package/src/orchestrator/audit.js +3 -0
- package/src/orchestrator/checklist-continuation.js +3 -3
- package/src/orchestrator/completion-contract.js +7 -0
- package/src/orchestrator/contract.js +6 -4
- package/src/orchestrator/delivery/WORKFLOW.md +93 -6
- package/src/orchestrator/delivery/references/acceptance-evidence.md +9 -5
- package/src/orchestrator/delivery/references/change-boundary-matrix.md +20 -1
- package/src/orchestrator/delivery/references/desktop-monitor-triggers.md +35 -0
- package/src/orchestrator/delivery/references/shared-system-specifications.md +3 -0
- package/src/orchestrator/delivery/references/state-and-receipts.md +19 -0
- package/src/orchestrator/execution-io.js +15 -0
- package/src/orchestrator/independent-audit.js +14 -1
- package/src/orchestrator/instruction.js +3 -0
- package/src/orchestrator/loop.js +71 -0
- package/src/orchestrator/outcome-monitor.js +123 -0
- package/src/provider-stream.js +3 -1
- package/src/runner.js +162 -20
- package/src/sqm/agent-skill.js +3 -2
- package/src/sqm/claude-plugin.js +6 -1
- package/src/sqm/command.js +13 -0
- package/src/sqm/report.js +23 -0
- package/src/sqm/workflow-onboarding.js +1 -1
- package/src/workflow-observation-sender.js +208 -0
- package/src/workflow-reporting.js +35 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1102
|
+
emitOutput(run, "SIMY ledger update unavailable; durable local record retained for retry.");
|
|
1103
|
+
return false;
|
|
962
1104
|
}
|
|
963
1105
|
}
|
|
964
1106
|
|
package/src/sqm/agent-skill.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
`;
|
package/src/sqm/claude-plugin.js
CHANGED
|
@@ -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
|
}
|
package/src/sqm/command.js
CHANGED
|
@@ -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)}`);
|