@awak-app/simy-cli 0.5.72 → 0.5.73
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 +84 -0
- package/package.json +2 -2
- package/src/agent.js +191 -11
- package/src/backend-executable.js +21 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/README.md +22 -0
- package/src/codex-plugins/simy/hooks/continuation-policy.mjs +27 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +179 -34
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +129 -0
- package/src/coding-run-recovery.js +88 -0
- package/src/coding-run-store.js +2 -2
- package/src/continuation-policy.js +2 -0
- package/src/delivery-hooks/command.js +11 -5
- package/src/delivery-hooks/desktop.js +12 -3
- package/src/delivery-hooks/supervisor.js +14 -10
- package/src/desktop-host.js +8 -6
- package/src/desktop-sqm-settings.js +24 -7
- package/src/device-signing-identity.js +65 -0
- package/src/diagnostic-log.js +68 -0
- package/src/doctor.js +55 -0
- package/src/index.js +7 -0
- package/src/local-task.js +68 -3
- package/src/orchestrator/budget.js +55 -4
- package/src/orchestrator/charter-validation.js +33 -0
- package/src/orchestrator/completion-contract.js +1 -1
- package/src/orchestrator/contract.js +2 -0
- package/src/orchestrator/delivery/references/state-and-receipts.md +21 -3
- package/src/orchestrator/independent-audit.js +9 -0
- package/src/orchestrator/instruction.js +6 -1
- package/src/orchestrator/loop.js +38 -41
- package/src/orchestrator/outcome-monitor.js +58 -38
- package/src/orchestrator/outcome-progress.js +2 -0
- package/src/purpose-local-proof.js +380 -0
- package/src/runner.js +224 -36
- package/src/session-supervision/codex-app-server.js +3 -2
- package/src/session-supervision/index.js +16 -4
- package/src/sqm/bundle-store.js +11 -7
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/command.js +36 -4
- package/src/sqm/diagnostics.js +1 -1
- package/src/sqm/evidence-client.js +23 -7
- package/src/sqm/history-store.js +4 -0
- package/src/sqm/index.js +104 -14
- package/src/sqm/proof.js +3 -23
- package/src/sqm/request-recovery.js +82 -0
- package/src/sqm/review-worker.js +59 -12
- package/src/sqm/session.js +33 -3
- package/src/startup-preflight.js +44 -0
package/src/runner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { EventEmitter, once } from "node:events";
|
|
3
4
|
import { access } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
} from "./orchestrator/execution-io.js";
|
|
21
22
|
import { summarizeCodingLoopEvent } from "./orchestrator/presentation.js";
|
|
22
23
|
import { PR_PREPARATION_CHECKS } from "./orchestrator/pr-preparation.js";
|
|
23
|
-
import { increaseProviderTokenBudget, refreshBudgetState } from "./orchestrator/budget.js";
|
|
24
|
+
import { budgetState, extendProgressCeilings, increaseProviderTokenBudget, providerBudgetTokens, refreshBudgetState } from "./orchestrator/budget.js";
|
|
24
25
|
import { appendEvent } from "./orchestrator/shared.js";
|
|
25
26
|
import {
|
|
26
27
|
createProviderStreamDecoder,
|
|
@@ -42,7 +43,7 @@ import { LocalRunRegistry } from "./run-registry.js";
|
|
|
42
43
|
import { webApiHeaders, webApiUrl } from "./web-api.js";
|
|
43
44
|
import { normalizeGitHubRemote } from "./workspace-context.js";
|
|
44
45
|
import { CLI_VERSION } from "./cli-contract.js";
|
|
45
|
-
import { reviseObjective, verifyObjective } from "./orchestrator/outcome-monitor.js";
|
|
46
|
+
import { freezeObjective, reviseObjective, verifyObjective } from "./orchestrator/outcome-monitor.js";
|
|
46
47
|
import { hasUserOutcomeProof } from "./orchestrator/completion-contract.js";
|
|
47
48
|
import { createSqmChecker } from "./sqm/index.js";
|
|
48
49
|
import {
|
|
@@ -51,6 +52,10 @@ import {
|
|
|
51
52
|
normalizeWorkflowStartIntent,
|
|
52
53
|
} from "./workflow-observation-sender.js";
|
|
53
54
|
import { saveCodingRunSnapshot } from "./coding-run-store.js";
|
|
55
|
+
import { acquireDesktopProcessLock } from "./desktop-process-lock.js";
|
|
56
|
+
import {
|
|
57
|
+
assertRecoveryActor, recoveryActorBinding, refreshRecoveryAudit, refreshRecoveryBudgetAudit,
|
|
58
|
+
} from "./coding-run-recovery.js";
|
|
54
59
|
|
|
55
60
|
const execFileAsync = promisify(execFile);
|
|
56
61
|
const MAX_LOCAL_LOG_LINES = 1_000;
|
|
@@ -133,6 +138,7 @@ export function createRun({ runId, request, session, apiOrigin, localStateRoot =
|
|
|
133
138
|
metadata: {
|
|
134
139
|
local_orchestrator_version: 1,
|
|
135
140
|
local_output_persisted: false,
|
|
141
|
+
recovery_actor: recoveryActorBinding(session, apiOrigin, normalizedRequest),
|
|
136
142
|
...(normalizedRequest.workflow_observation_binding
|
|
137
143
|
? {
|
|
138
144
|
workflow_observation: {
|
|
@@ -188,6 +194,10 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null, loc
|
|
|
188
194
|
max_attempts: charter.max_attempts,
|
|
189
195
|
retry_budget: charter.retry_budget,
|
|
190
196
|
token_budget: charter.token_budget,
|
|
197
|
+
continuation_policy: charter.continuation_policy,
|
|
198
|
+
provider_token_ceiling: charter.provider_token_ceiling,
|
|
199
|
+
attempt_ceiling: charter.attempt_ceiling,
|
|
200
|
+
max_no_progress_attempts: charter.max_no_progress_attempts,
|
|
191
201
|
ui_evidence_root: String(charter.ui_evidence_root || ""),
|
|
192
202
|
acceptance_criteria: Array.isArray(charter.acceptance_criteria)
|
|
193
203
|
? charter.acceptance_criteria
|
|
@@ -245,6 +255,15 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null, loc
|
|
|
245
255
|
const objectiveUnverified = !objectiveCheck.valid || objectiveCheck.legacy || savedGoalUnproven;
|
|
246
256
|
const restoredState = restoredFromState || objectiveUnverified ? "waiting_human" : persistedState;
|
|
247
257
|
if (restoredFromState) {
|
|
258
|
+
restoredSnapshot.metadata ??= {};
|
|
259
|
+
restoredSnapshot.metadata.recovery = {
|
|
260
|
+
version: 1,
|
|
261
|
+
status: ["dispatching", "coding"].includes(restoredFromState)
|
|
262
|
+
? "needs_reconciliation" : "ready",
|
|
263
|
+
interrupted_state: restoredFromState,
|
|
264
|
+
interrupted_attempt: restoredSnapshot.events?.findLast((item) =>
|
|
265
|
+
Number.isInteger(item?.detail?.attempt_number))?.detail.attempt_number ?? null,
|
|
266
|
+
};
|
|
248
267
|
restoredSnapshot.events = Array.isArray(restoredSnapshot.events)
|
|
249
268
|
? restoredSnapshot.events
|
|
250
269
|
: [];
|
|
@@ -302,6 +321,63 @@ export async function publishRestoredLocalCodingRun(run) {
|
|
|
302
321
|
return run.snapshot;
|
|
303
322
|
}
|
|
304
323
|
|
|
324
|
+
export async function prepareLocalCodingRunRecovery(run) {
|
|
325
|
+
assertRecoveryActor(run);
|
|
326
|
+
if (run.operation || run.child) throw new Error("The selected run is still active.");
|
|
327
|
+
const repositoryPath = await resolveRepositoryPath(run.request);
|
|
328
|
+
const git = async (args) => (await execFileAsync("git", args, {
|
|
329
|
+
cwd: repositoryPath, maxBuffer: 16 * 1024 * 1024,
|
|
330
|
+
})).stdout;
|
|
331
|
+
const head = (await git(["rev-parse", "HEAD"])).trim();
|
|
332
|
+
const status = await git(["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
333
|
+
const diff = await git(["diff", "HEAD", "--binary"]);
|
|
334
|
+
return {
|
|
335
|
+
version: 1, run_id: run.id,
|
|
336
|
+
objective: verifyObjective(run.snapshot.charter),
|
|
337
|
+
recovery: structuredClone(run.snapshot.metadata?.recovery || { status: "ready" }),
|
|
338
|
+
checkout: { head_sha: head, status_sha256: executionTextSha256(status), diff_sha256: executionTextSha256(diff) },
|
|
339
|
+
attempts_recorded: run.snapshot.attempts.length,
|
|
340
|
+
last_pr_url: run.snapshot.attempts.at(-1)?.pr_url || null,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Only the controller's direct human decision or a trusted reconciler may
|
|
345
|
+
// provide this result. A refreshed checkout alone cannot prove an external
|
|
346
|
+
// publication failed or authorize replaying it.
|
|
347
|
+
export async function reconcileLocalCodingRun(run, { operationStatus, observation, evidenceRefs, authorize } = {}) {
|
|
348
|
+
assertRecoveryActor(run);
|
|
349
|
+
if (run.operation || run.child || !RESUMABLE_STATES.has(run.status)) {
|
|
350
|
+
throw new Error("Interrupted operation reconciliation is available only while the run is waiting.");
|
|
351
|
+
}
|
|
352
|
+
if (run.snapshot.metadata?.recovery?.status !== "needs_reconciliation") {
|
|
353
|
+
throw new Error("This run has no interrupted operation requiring reconciliation.");
|
|
354
|
+
}
|
|
355
|
+
if (!["completed", "not_started", "failed", "none"].includes(operationStatus) ||
|
|
356
|
+
!String(observation || "").trim() || !Array.isArray(evidenceRefs) ||
|
|
357
|
+
!evidenceRefs.some((item) => typeof item === "string" && item.trim())) {
|
|
358
|
+
throw new Error("An authoritative operation status, observation and evidence references are required; unknown success cannot be replayed.");
|
|
359
|
+
}
|
|
360
|
+
const descriptor = await prepareLocalCodingRunRecovery(run);
|
|
361
|
+
await authorize?.();
|
|
362
|
+
assertRecoveryActor(run);
|
|
363
|
+
if (run.operation || run.child || !RESUMABLE_STATES.has(run.status) || run.snapshot.metadata?.recovery?.status !== 'needs_reconciliation') {
|
|
364
|
+
throw new Error('The interrupted run changed while reconciliation evidence was inspected.');
|
|
365
|
+
}
|
|
366
|
+
run.snapshot.metadata.recovery = {
|
|
367
|
+
...descriptor.recovery, status: "ready", operation_status: operationStatus,
|
|
368
|
+
reconciled_at: new Date().toISOString(), checkout: descriptor.checkout,
|
|
369
|
+
observation_sha256: executionTextSha256(String(observation).trim()),
|
|
370
|
+
evidence_refs: evidenceRefs.filter((item) => typeof item === "string" && item.trim()),
|
|
371
|
+
actor: recoveryActorBinding(run.session, run.apiOrigin, run.request),
|
|
372
|
+
};
|
|
373
|
+
appendEvent(run.snapshot, "waiting_human", "The interrupted operation was reconciled before continuation.", {
|
|
374
|
+
code: "interrupted_operation_reconciled", operation_status: operationStatus,
|
|
375
|
+
interrupted_attempt: descriptor.recovery.interrupted_attempt,
|
|
376
|
+
});
|
|
377
|
+
await updateRun(run, "waiting_human");
|
|
378
|
+
return run.snapshot.metadata.recovery;
|
|
379
|
+
}
|
|
380
|
+
|
|
305
381
|
export async function startLocalCodingRun(
|
|
306
382
|
run,
|
|
307
383
|
dependencies = {},
|
|
@@ -329,18 +405,29 @@ function assertRunCanContinue(run) {
|
|
|
329
405
|
if (!RESUMABLE_STATES.has(run.status)) {
|
|
330
406
|
throw new Error("Human guidance is available only when the selected run is waiting.");
|
|
331
407
|
}
|
|
408
|
+
assertRecoveryActor(run);
|
|
409
|
+
if (run.snapshot.metadata?.recovery?.status === "needs_reconciliation") {
|
|
410
|
+
const error = new Error("Reconcile the interrupted operation before continuing the same run; its side effects may already exist.");
|
|
411
|
+
error.code = "interrupted_operation_unverified";
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
if (verifyObjective(run.snapshot.charter).legacy) {
|
|
415
|
+
const error = new Error("Explicitly confirm the saved objective before continuing this legacy run.");
|
|
416
|
+
error.code = "objective_baseline_unverified";
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
332
419
|
|
|
333
|
-
const nextAttempt = run.snapshot.attempts.length + 1;
|
|
334
420
|
const budget = refreshBudgetState(run.snapshot);
|
|
335
421
|
if (budget.token_exhausted) {
|
|
336
422
|
throw new Error("The Agentic Loop run used its token budget. Start a new run to continue.");
|
|
337
423
|
}
|
|
338
|
-
if (
|
|
424
|
+
if (budget.retry_exhausted) {
|
|
339
425
|
throw new Error("The Agentic Loop run used its retry budget. Start a new run to continue.");
|
|
340
426
|
}
|
|
341
427
|
}
|
|
342
428
|
|
|
343
429
|
export async function increaseLocalCodingRunProviderTokenBudget(run, value) {
|
|
430
|
+
assertRecoveryActor(run);
|
|
344
431
|
if (run.operation || run.child) {
|
|
345
432
|
throw new Error("Provider token budget can change only while the run is waiting.");
|
|
346
433
|
}
|
|
@@ -352,27 +439,13 @@ export async function increaseLocalCodingRunProviderTokenBudget(run, value) {
|
|
|
352
439
|
throw new Error("The Agentic Loop run also used its retry budget. Start a new run to continue.");
|
|
353
440
|
}
|
|
354
441
|
const previousBudget = currentBudget.token_budget;
|
|
355
|
-
const budget = increaseProviderTokenBudget(run.snapshot, value);
|
|
356
|
-
run.request.token_budget = budget.token_budget;
|
|
357
|
-
// A budget change resolves only this blocker; retain the previous audit as evidence.
|
|
358
442
|
const previousAudit = run.snapshot.final_audit;
|
|
359
443
|
const resolvedBudgetAudit = previousAudit?.findings?.some(
|
|
360
444
|
(finding) => finding.code === "TOKEN_BUDGET_EXHAUSTED",
|
|
361
445
|
) ? structuredClone(previousAudit) : null;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
...previousAudit,
|
|
366
|
-
passed: false,
|
|
367
|
-
summary: findings.length > 0
|
|
368
|
-
? "Provider token budget increased; other verification findings remain."
|
|
369
|
-
: "Provider token budget increased; continue this run to verify completion.",
|
|
370
|
-
findings,
|
|
371
|
-
budget,
|
|
372
|
-
auto_repairable: findings.length > 0 && findings.every((finding) => finding.repairability === "auto"),
|
|
373
|
-
requires_human: findings.some((finding) => finding.repairability !== "auto"),
|
|
374
|
-
};
|
|
375
|
-
}
|
|
446
|
+
const budget = increaseProviderTokenBudget(run.snapshot, value);
|
|
447
|
+
refreshRecoveryBudgetAudit(run.snapshot, budget);
|
|
448
|
+
run.request.token_budget = budget.token_budget;
|
|
376
449
|
appendEvent(
|
|
377
450
|
run.snapshot,
|
|
378
451
|
"waiting_human",
|
|
@@ -389,6 +462,29 @@ export async function increaseLocalCodingRunProviderTokenBudget(run, value) {
|
|
|
389
462
|
return budget;
|
|
390
463
|
}
|
|
391
464
|
|
|
465
|
+
export async function extendLocalCodingRunCeilings(run, changes) {
|
|
466
|
+
assertRecoveryActor(run);
|
|
467
|
+
if (run.operation || run.child || !RESUMABLE_STATES.has(run.status)) {
|
|
468
|
+
throw new Error("Hard ceilings can change only while the selected run is waiting and idle.");
|
|
469
|
+
}
|
|
470
|
+
const previous = budgetState(run.snapshot);
|
|
471
|
+
const budget = extendProgressCeilings(run.snapshot, changes);
|
|
472
|
+
refreshRecoveryBudgetAudit(run.snapshot, budget);
|
|
473
|
+
run.request.provider_token_ceiling = run.snapshot.charter.provider_token_ceiling;
|
|
474
|
+
run.request.attempt_ceiling = run.snapshot.charter.attempt_ceiling;
|
|
475
|
+
appendEvent(run.snapshot, run.status, "The human operator explicitly increased this saved run's hard ceilings.", {
|
|
476
|
+
code: "continuation_ceilings_extended",
|
|
477
|
+
previous_provider_token_ceiling: previous.hard_token_ceiling,
|
|
478
|
+
previous_attempt_ceiling: previous.hard_attempt_ceiling,
|
|
479
|
+
provider_token_ceiling: budget.hard_token_ceiling,
|
|
480
|
+
attempt_ceiling: budget.hard_attempt_ceiling,
|
|
481
|
+
budget,
|
|
482
|
+
});
|
|
483
|
+
// Extending consent is separate from continuing work, including a stopped run.
|
|
484
|
+
await updateRun(run, run.status);
|
|
485
|
+
return budget;
|
|
486
|
+
}
|
|
487
|
+
|
|
392
488
|
export async function continueLocalCodingRunAfterRepositoryApproval(
|
|
393
489
|
run,
|
|
394
490
|
{ repository, localPath } = {},
|
|
@@ -457,11 +553,39 @@ export function queueLocalCodingGuidance(run, guidance) {
|
|
|
457
553
|
|
|
458
554
|
export async function applyLocalHumanDecision(
|
|
459
555
|
run,
|
|
460
|
-
{ message, designApproval = false, requiredChecks = [], acceptanceCriteria = [] } = {},
|
|
556
|
+
{ message, designApproval = false, requiredChecks = [], acceptanceCriteria = [], confirmObjective = false, confirmedObjective = null } = {},
|
|
461
557
|
dependencies = {},
|
|
462
558
|
) {
|
|
463
559
|
const guidance = String(message || "").trim();
|
|
464
560
|
if (!guidance) throw new Error("A human decision note is required.");
|
|
561
|
+
if (confirmObjective) {
|
|
562
|
+
assertRecoveryActor(run);
|
|
563
|
+
if (run.operation || run.child || !RESUMABLE_STATES.has(run.status)) {
|
|
564
|
+
throw new Error("Objective confirmation is available only while the saved run is waiting.");
|
|
565
|
+
}
|
|
566
|
+
const objective = verifyObjective(run.snapshot.charter);
|
|
567
|
+
if (!objective.valid) throw new Error("Restore the saved objective or explicitly revise its changed criteria; an invalid integrity chain cannot be rebaselined.");
|
|
568
|
+
if (objective.legacy) {
|
|
569
|
+
const charter = run.snapshot.charter;
|
|
570
|
+
if (confirmedObjective?.originalRequest !== charter.thread_state.original_request ||
|
|
571
|
+
confirmedObjective?.userGoal !== charter.thread_state.user_goal ||
|
|
572
|
+
JSON.stringify(confirmedObjective?.acceptanceCriteria) !== JSON.stringify(charter.acceptance_criteria)) {
|
|
573
|
+
throw new Error("Confirm the exact saved original request, user goal and acceptance criteria to establish a legacy baseline.");
|
|
574
|
+
}
|
|
575
|
+
freezeObjective(run.snapshot.charter);
|
|
576
|
+
refreshRecoveryAudit(run.snapshot, {
|
|
577
|
+
reason: "objective_baseline_confirmed",
|
|
578
|
+
stale: (finding) => finding.code === "OBJECTIVE_BASELINE_UNVERIFIED",
|
|
579
|
+
summary: "The saved objective was confirmed; remaining verification or approval is still required.",
|
|
580
|
+
});
|
|
581
|
+
run.snapshot.done_gate_passed = false;
|
|
582
|
+
run.snapshot.implementation_gate_passed = false;
|
|
583
|
+
appendEvent(run.snapshot, "waiting_human", "The original saved objective was explicitly confirmed.", {
|
|
584
|
+
code: "objective_baseline_confirmed", note_sha256: executionTextSha256(guidance),
|
|
585
|
+
});
|
|
586
|
+
await updateRun(run, "waiting_human");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
465
589
|
assertRunCanContinue(run);
|
|
466
590
|
const charter = run.snapshot.charter;
|
|
467
591
|
if (acceptanceCriteria.length > 0) {
|
|
@@ -560,10 +684,12 @@ export async function stopLocalCodingRun(run) {
|
|
|
560
684
|
async function signalAndWaitForClose(child, signal, timeoutMs) {
|
|
561
685
|
const close = once(child, "close").then(() => "closed");
|
|
562
686
|
signalExecutorProcess(child, signal);
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
687
|
+
let timer;
|
|
688
|
+
try {
|
|
689
|
+
return await Promise.race([close,
|
|
690
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve("timeout"), timeoutMs); }),
|
|
691
|
+
]);
|
|
692
|
+
} finally { clearTimeout(timer); }
|
|
567
693
|
}
|
|
568
694
|
|
|
569
695
|
async function waitForLocalStop(run) {
|
|
@@ -618,13 +744,17 @@ async function executeLocalCodingRun(
|
|
|
618
744
|
{ executeAttempt, executeIndependentAudit, collectEvidence } = {},
|
|
619
745
|
{ humanGuidance = "", resume = false } = {},
|
|
620
746
|
) {
|
|
621
|
-
const operation =
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
747
|
+
const operation = (async () => {
|
|
748
|
+
assertRecoveryActor(run);
|
|
749
|
+
const lease = run.localStateRoot ? await acquireDesktopProcessLock(
|
|
750
|
+
path.join(run.localStateRoot, ".controllers", executionTextSha256(run.id)),
|
|
751
|
+
) : null;
|
|
752
|
+
try {
|
|
753
|
+
return await runLocalCodingRun(run, {
|
|
754
|
+
executeAttempt, executeIndependentAudit, collectEvidence, humanGuidance, resume,
|
|
755
|
+
});
|
|
756
|
+
} finally { await lease?.release(); }
|
|
757
|
+
})();
|
|
628
758
|
run.operation = operation;
|
|
629
759
|
try {
|
|
630
760
|
return await operation;
|
|
@@ -666,6 +796,16 @@ async function runLocalCodingRun(
|
|
|
666
796
|
await updateRun(run, run.status);
|
|
667
797
|
}
|
|
668
798
|
|
|
799
|
+
const recovery = run.snapshot.metadata?.recovery;
|
|
800
|
+
if (resume && recovery?.checkout) {
|
|
801
|
+
const recoveredGuidance = [
|
|
802
|
+
"Resume the same saved objective and approvals. Reconcile existing work before taking another action.",
|
|
803
|
+
`Interrupted operation status: ${recovery.operation_status}. Checkout at reconciliation: ${JSON.stringify(recovery.checkout)}.`,
|
|
804
|
+
"Never duplicate a completed external operation; inspect its existing identity and evidence. If its current outcome is unknown, stop for reconciliation instead of replaying it.",
|
|
805
|
+
].join("\n");
|
|
806
|
+
humanGuidance = [recoveredGuidance, humanGuidance].filter(Boolean).join("\n\n");
|
|
807
|
+
}
|
|
808
|
+
|
|
669
809
|
const executor =
|
|
670
810
|
executeAttempt ||
|
|
671
811
|
((context) =>
|
|
@@ -707,6 +847,7 @@ async function runLocalCodingRun(
|
|
|
707
847
|
deviceId: run.session?.device_id,
|
|
708
848
|
});
|
|
709
849
|
run.sqmChecker = sqmChecker;
|
|
850
|
+
refreshSqmCheckerSession(run, sqmChecker);
|
|
710
851
|
|
|
711
852
|
try {
|
|
712
853
|
await runCodingLoop({
|
|
@@ -782,6 +923,7 @@ export async function recheckLocalCodingRun(run, { collectEvidence } = {}) {
|
|
|
782
923
|
deviceId: run.session?.device_id,
|
|
783
924
|
});
|
|
784
925
|
run.sqmChecker = sqmChecker;
|
|
926
|
+
refreshSqmCheckerSession(run, sqmChecker);
|
|
785
927
|
await recheckPrReadiness({
|
|
786
928
|
snapshot: run.snapshot,
|
|
787
929
|
collectEvidence: evidenceCollector,
|
|
@@ -820,6 +962,16 @@ function sqmBundleEndpoint(session) {
|
|
|
820
962
|
return new URL("sqm/knowledge-bundle", session.api_base_url).href;
|
|
821
963
|
}
|
|
822
964
|
|
|
965
|
+
function refreshSqmCheckerSession(run, checker) {
|
|
966
|
+
assertRecoveryActor(run);
|
|
967
|
+
if (!run.session?.token || !checker.updateSession) return;
|
|
968
|
+
checker.updateSession({ token: run.session.token,
|
|
969
|
+
accountId: run.session.account_id || run.session.auth_user_id || run.session.device_id,
|
|
970
|
+
organizationId: run.session.organization_id || run.session.org_id,
|
|
971
|
+
deviceId: run.session.device_id, endpoint: sqmBundleEndpoint(run.session), webOrigin: run.apiOrigin,
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
|
|
823
975
|
function sqmEvidenceEndpoint(session) {
|
|
824
976
|
if (process.env.SIMY_SQM_EVIDENCE_URL?.trim()) return process.env.SIMY_SQM_EVIDENCE_URL.trim();
|
|
825
977
|
if (!session?.api_base_url) return undefined;
|
|
@@ -833,8 +985,9 @@ export async function executeProcessAttempt({
|
|
|
833
985
|
run,
|
|
834
986
|
marker = "SIMY_RESULT_JSON:",
|
|
835
987
|
attemptNumber = null,
|
|
988
|
+
resolveCommandImpl = resolveDesktopExecutorCommand,
|
|
836
989
|
}) {
|
|
837
|
-
const command = await
|
|
990
|
+
const command = await resolveCommandImpl({
|
|
838
991
|
backend,
|
|
839
992
|
instruction,
|
|
840
993
|
repositoryPath,
|
|
@@ -844,6 +997,8 @@ export async function executeProcessAttempt({
|
|
|
844
997
|
const stderr = [];
|
|
845
998
|
const tokenUsageRecords = [];
|
|
846
999
|
const phase = marker === "SIMY_AUDIT_JSON:" ? "reviewer" : "executor";
|
|
1000
|
+
const initialBudget = budgetState(run.snapshot);
|
|
1001
|
+
const invocationId = randomUUID();
|
|
847
1002
|
const observationSender =
|
|
848
1003
|
phase === "executor" &&
|
|
849
1004
|
!["accepted", "already_recorded"].includes(run.snapshot.metadata?.workflow_observation?.status)
|
|
@@ -859,6 +1014,8 @@ export async function executeProcessAttempt({
|
|
|
859
1014
|
return new Promise((resolve) => {
|
|
860
1015
|
let settled = false;
|
|
861
1016
|
let terminalError = null;
|
|
1017
|
+
let terminalErrorCode = null;
|
|
1018
|
+
const checkpointWrites = [];
|
|
862
1019
|
const child = command.spawn(command.bin, command.args, {
|
|
863
1020
|
cwd: repositoryPath,
|
|
864
1021
|
env: { ...process.env, ...command.env },
|
|
@@ -873,13 +1030,42 @@ export async function executeProcessAttempt({
|
|
|
873
1030
|
onLine: (line) => emitOutput(run, line, { backend }),
|
|
874
1031
|
onEvent: (event) => observationSender?.observe(event),
|
|
875
1032
|
onTerminalError: (message) => { terminalError ||= message; },
|
|
876
|
-
onUsage: (usage) =>
|
|
1033
|
+
onUsage: (usage) => {
|
|
877
1034
|
tokenUsageRecords.push({
|
|
878
1035
|
...usage,
|
|
879
1036
|
phase,
|
|
880
1037
|
...(Number.isInteger(attemptNumber) ? { attempt_number: attemptNumber } : {}),
|
|
881
1038
|
backend,
|
|
882
|
-
|
|
1039
|
+
...(initialBudget.continuation_policy === "progress" ? { invocation_id: invocationId } : {}),
|
|
1040
|
+
});
|
|
1041
|
+
if (initialBudget.continuation_policy !== "progress") return;
|
|
1042
|
+
const currentUsed = initialBudget.tokens_used + tokenUsageRecords.reduce((sum, record) => sum + providerBudgetTokens(record), 0);
|
|
1043
|
+
run.snapshot.metadata.provider_usage_checkpoints ??= {};
|
|
1044
|
+
run.snapshot.metadata.provider_usage_checkpoints[invocationId] = {
|
|
1045
|
+
invocation_id: invocationId, phase, attempt_number: attemptNumber, started_at: startedAt,
|
|
1046
|
+
token_usage: { records: structuredClone(tokenUsageRecords) },
|
|
1047
|
+
total_provider_tokens: currentUsed,
|
|
1048
|
+
hard_token_ceiling: initialBudget.hard_token_ceiling,
|
|
1049
|
+
};
|
|
1050
|
+
refreshBudgetState(run.snapshot);
|
|
1051
|
+
if (run.localStateRoot) checkpointWrites.push(saveCodingRunSnapshot(run.localStateRoot, run.snapshot).catch(() => {
|
|
1052
|
+
emitOutput(run, "The Provider usage checkpoint could not be persisted; recovery usage remains unverified.", { backend });
|
|
1053
|
+
}));
|
|
1054
|
+
if (currentUsed < initialBudget.hard_token_ceiling || terminalErrorCode || run.stopRequested) return;
|
|
1055
|
+
terminalErrorCode = "TOKEN_BUDGET_EXHAUSTED";
|
|
1056
|
+
terminalError = `Provider token hard ceiling reached (${currentUsed} / ${initialBudget.hard_token_ceiling}). Extend the same run's ceiling explicitly before continuing.`;
|
|
1057
|
+
appendEvent(run.snapshot, run.status, terminalError, {
|
|
1058
|
+
code: "provider_token_ceiling_reached", phase, attempt_number: attemptNumber,
|
|
1059
|
+
provider_tokens_used: currentUsed, provider_token_ceiling: initialBudget.hard_token_ceiling,
|
|
1060
|
+
});
|
|
1061
|
+
if (!settled) {
|
|
1062
|
+
run.controlState = "stopping";
|
|
1063
|
+
void signalAndWaitForClose(child, "SIGINT", 1_500).then(async result => {
|
|
1064
|
+
if (result === "timeout" && !settled && run.child === child) result = await signalAndWaitForClose(child, "SIGTERM", 1_500);
|
|
1065
|
+
if (result === "timeout" && !settled && run.child === child) await signalAndWaitForClose(child, "SIGKILL", 750);
|
|
1066
|
+
}).catch(() => {});
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
883
1069
|
});
|
|
884
1070
|
const stderrDecoder = createProviderStreamDecoder({
|
|
885
1071
|
backend,
|
|
@@ -904,6 +1090,7 @@ export async function executeProcessAttempt({
|
|
|
904
1090
|
stdoutDecoder.flush();
|
|
905
1091
|
stderrDecoder.flush();
|
|
906
1092
|
run.child = null;
|
|
1093
|
+
await Promise.all(checkpointWrites);
|
|
907
1094
|
// The run must always resolve: a WorkFlow observation failure (or a
|
|
908
1095
|
// sender bug) never changes or blocks the Provider result. flush() is
|
|
909
1096
|
// bounded by per-request timeouts and is designed not to reject.
|
|
@@ -940,6 +1127,7 @@ export async function executeProcessAttempt({
|
|
|
940
1127
|
resolve({
|
|
941
1128
|
exitCode: terminalError && exitCode === 0 ? 1 : exitCode,
|
|
942
1129
|
error: error || terminalError,
|
|
1130
|
+
...(terminalErrorCode ? { errorCode: terminalErrorCode } : {}),
|
|
943
1131
|
result: Object.keys(result).length > 0 ? result : parse(rawStdout),
|
|
944
1132
|
tokenUsage:
|
|
945
1133
|
tokenUsageRecords.length > 0 ? { records: tokenUsageRecords } : {},
|
|
@@ -2,7 +2,7 @@ import { associationContract } from '../codex-plugins/simy/hooks/action-associat
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { realpath } from 'node:fs/promises';
|
|
4
4
|
import { inspectBackendExecutable } from '../backend-executable.js';
|
|
5
|
-
import { assessedCheckpointContract, understandingCheckpointContract } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
5
|
+
import { assessedCheckpointContract, deliveryBudgetState, understandingCheckpointContract } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
6
6
|
import { boundConversation, boundProviderGoal, textContent } from './conversation-context.js';
|
|
7
7
|
import { JsonDataUrlFilter } from './json-data-url-filter.js';
|
|
8
8
|
|
|
@@ -422,7 +422,8 @@ export async function executeCodexAppServer(record, { openClient = openCodexAppS
|
|
|
422
422
|
enrolledScopes: run.managed.approvalScopes,
|
|
423
423
|
lastDependencyResolution: run.managed.lastDependencyResolution,
|
|
424
424
|
...(authorizedWait ? { approvedAction: { approvalId: approved.id, action: approved.action, oneShot: true } } : {}),
|
|
425
|
-
budgets: {
|
|
425
|
+
budgets: { ...deliveryBudgetState(run), continuations: run.continuations, failures: run.failures, maxFailures: run.maxFailures,
|
|
426
|
+
softDeadline: run.deadline, softMaxContinuations: run.maxContinuations } };
|
|
426
427
|
const approvalInstruction = authorizedWait ? `SIMY recorded a matching, one-shot scoped user approval ${approved.id} for this exact selected action. The decision outcome "wait" describes its pre-approval assessment; the approved action is authorized now. Execute it once, then report what actually happened. ` : '';
|
|
427
428
|
const prompt = run.managed.phase === 'action_association' ? associationContract(run.managed.actionAssociation.job) : run.managed.phase === 'understanding' ? `Understand the ORIGINAL user request in this exact Codex conversation. This first/clarifying turn is read-only and must not perform task work. Context: ${JSON.stringify({ ...context, priorUnderstanding: run.managed.sessionUnderstanding, userAnswer: run.managed.understandingAnswer })}\n${understandingCheckpointContract(decision)} Do not write SIMY state directly.` : `Continue this existing SIMY managed task in the exact Codex conversation. Context: ${JSON.stringify(context)}\n${approvalInstruction}Execute ONLY the selected decision action. Do not perform any action from the NEXT plan in this turn. For a next local action within the enrolled task use the exact scope "task"; do not relabel external, broad, or destructive work as local. Preserve provider permissions. End with one JSON checkpoint. ${assessedCheckpointContract(decision)} Do not write SIMY state directly.`;
|
|
428
429
|
const initialSandbox = run.managed.providerSandbox || resumed.sandbox;
|
|
@@ -141,7 +141,7 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// Explicit execution authority: legacy monitor/tracked records stay read-only.
|
|
144
|
-
export async function setSessionAutoContinue({ root, provider, session, enabled, ...options }) {
|
|
144
|
+
export async function setSessionAutoContinue({ root, provider, session, enabled, continuationPolicy, hardMaxMinutes, hardMaxContinuations, providerTokenCeiling, ...options }) {
|
|
145
145
|
if (!['codex', 'claude'].includes(provider) || typeof enabled !== 'boolean') throw new Error('Unsupported automatic continuation request.');
|
|
146
146
|
if (enabled) await trackSession({ root, provider, session, tracked: true, ...options });
|
|
147
147
|
return updateRecord(root, provider, session, async previous => {
|
|
@@ -152,8 +152,20 @@ export async function setSessionAutoContinue({ root, provider, session, enabled,
|
|
|
152
152
|
if (enabled && provider === 'claude' && !await liveClaudeHook(previous,
|
|
153
153
|
{ session, cwd: previous.cwd, observation: { source: previous.admission?.source } }, options.inspectProcess || processIdentity)) throw new Error('Claude needs a live matching SIMY SessionStart hook; cold CLI takeover is unavailable.');
|
|
154
154
|
const epoch = previous.autoContinue === enabled && previous.autoContinueEpoch ? previous.autoContinueEpoch : randomUUID();
|
|
155
|
+
const settings = { autonomyPolicy: 'balanced', maxMinutes: 30, maxContinuations: 6, maxFailures: 3,
|
|
156
|
+
...previous.autoContinueSettings,
|
|
157
|
+
...(continuationPolicy === undefined ? {} : { continuationPolicy }),
|
|
158
|
+
...(hardMaxMinutes === undefined ? {} : { hardMaxMinutes }),
|
|
159
|
+
...(hardMaxContinuations === undefined ? {} : { hardMaxContinuations }),
|
|
160
|
+
...(providerTokenCeiling === undefined ? {} : { providerTokenCeiling }) };
|
|
161
|
+
if (settings.continuationPolicy !== undefined && !['bounded', 'progress'].includes(settings.continuationPolicy)) throw new Error('Choose a continuation policy.');
|
|
162
|
+
for (const [key, maximum] of [['hardMaxMinutes', 1440], ['hardMaxContinuations', 50], ['providerTokenCeiling', 15_000_000]]) {
|
|
163
|
+
if (settings[key] !== undefined && (!Number.isSafeInteger(settings[key]) || settings[key] < 1 || settings[key] > maximum)) throw new Error('Invalid continuation ceiling.');
|
|
164
|
+
}
|
|
165
|
+
if (providerTokenCeiling !== undefined && provider !== 'codex') throw new Error('Token ceilings require host-observed Codex usage.');
|
|
166
|
+
if (previous.run && [continuationPolicy, hardMaxMinutes, hardMaxContinuations, providerTokenCeiling].some(value => value !== undefined)) throw new Error('Change an existing task through its explicit continuation controls.');
|
|
155
167
|
const next = { ...previous, autoContinue: enabled, autoContinueEpoch: epoch,
|
|
156
|
-
...(enabled ? { tracked: true, monitor: true, autoContinueSettings:
|
|
168
|
+
...(enabled ? { tracked: true, monitor: true, autoContinueSettings: settings } : { tracked: false, monitor: false }) };
|
|
157
169
|
// Adopt existing supervision without restarting it or renewing its budgets.
|
|
158
170
|
if (next.run?.managed) next.run.managed.autoContinueEpoch = epoch;
|
|
159
171
|
return next;
|
|
@@ -209,7 +221,7 @@ export async function monitorSession({ root, provider, session, monitor = true,
|
|
|
209
221
|
}
|
|
210
222
|
|
|
211
223
|
export async function enrollSession({ root, provider, session, objective, acceptanceCriteria, contextBootstrap = false, autonomyPolicy = 'balanced',
|
|
212
|
-
maxMinutes = 30, maxContinuations = 6, maxFailures = 3, maxNoProgressMinutes, maxTokens, autoContinueEpoch, openClient = openCodexAppServer,
|
|
224
|
+
maxMinutes = 30, maxContinuations = 6, maxFailures = 3, maxNoProgressMinutes, maxTokens, continuationPolicy, hardMaxMinutes, hardMaxContinuations, providerTokenCeiling, autoContinueEpoch, openClient = openCodexAppServer,
|
|
213
225
|
readClaude = readClaudeSession, inspectProcess = processIdentity, now = Date.now() }) {
|
|
214
226
|
if (!['codex', 'claude'].includes(provider)) throw new Error('This provider does not have a verified exact-session enrollment channel.');
|
|
215
227
|
if (maxTokens !== undefined && provider !== 'codex') throw new Error('Token-budgeted supervision requires host-observed Codex usage; Claude live hooks cannot enforce this budget.');
|
|
@@ -235,7 +247,7 @@ export async function enrollSession({ root, provider, session, objective, accept
|
|
|
235
247
|
const admission = provider === 'codex' ? { source: 'codex_app_server', provider, session, cwd, model: metadata.model } : undefined;
|
|
236
248
|
const liveRecord = provider === 'claude' ? await readRecord(root, provider, session) : null;
|
|
237
249
|
const run = await startDelivery({ root, provider, session, cwd, objective: objective.trim(), nextStep: 'Inspect current evidence and propose the next assessed action.',
|
|
238
|
-
maxMinutes, maxContinuations, maxFailures, now, admission, inspectProcess, autoContinueEpoch,
|
|
250
|
+
maxMinutes, maxContinuations, maxFailures, continuationPolicy, hardMaxMinutes, hardMaxContinuations, providerTokenCeiling, now, admission, inspectProcess, autoContinueEpoch,
|
|
239
251
|
liveHookAdmission: liveRecord ? { hookStartedAt: liveRecord.hookStartedAt, pid: liveRecord.process.pid, identity: liveRecord.process.identity } : undefined,
|
|
240
252
|
managed: { acceptanceCriteria, contextBootstrap, autonomyPolicy, maxNoProgressMinutes, maxTokens, approvalScopes: ['task'], approvalPolicy: 'wait',
|
|
241
253
|
initialAction: initialPlan.alternatives[0].action, initialPlan, remainingWork: objective.trim(),
|
package/src/sqm/bundle-store.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
|
|
7
7
|
import { verifyBundleIntegrity } from "./canonical.js";
|
|
8
8
|
import { validateKnowledgeBundle } from "./validation.js";
|
|
9
|
+
import { fetchSqmRead, sqmDiagnostic } from "./request-recovery.js";
|
|
9
10
|
|
|
10
11
|
const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
11
12
|
export const SQM_BUNDLE_FETCH_TIMEOUT_MS = 15_000;
|
|
@@ -36,6 +37,9 @@ export async function loadKnowledgeBundle({
|
|
|
36
37
|
fetchImpl = globalThis.fetch,
|
|
37
38
|
now = Date.now(),
|
|
38
39
|
publicKeys = publicKeysFromEnvironment(),
|
|
40
|
+
signal,
|
|
41
|
+
recoveryOptions,
|
|
42
|
+
onRecoveryEvent,
|
|
39
43
|
}) {
|
|
40
44
|
if (!organizationId) {
|
|
41
45
|
return result(null, ["SQM organization identity is unavailable; shadow checks were skipped."], "unavailable");
|
|
@@ -50,7 +54,7 @@ export async function loadKnowledgeBundle({
|
|
|
50
54
|
Date.parse(cached.bundle.expires_at) > now;
|
|
51
55
|
if (!offline && endpoint && (!fresh || refresh || cached?.bundle.system_scope)) {
|
|
52
56
|
try {
|
|
53
|
-
const fetched = await fetchBundle({ endpoint, repository, cliVersion, token, organizationId, etag: cached?.etag, fetchImpl });
|
|
57
|
+
const fetched = await fetchBundle({ endpoint, repository, cliVersion, token, organizationId, etag: cached?.etag, fetchImpl, signal, recoveryOptions, onRecoveryEvent });
|
|
54
58
|
if (fetched.notModified) {
|
|
55
59
|
if (!cached) throw new Error("SQM Cloud returned 304 without a verified local bundle.");
|
|
56
60
|
if (Date.parse(cached.bundle.expires_at) <= now) throw nonFallbackError("SQM Cloud returned 304 for an expired knowledge bundle.");
|
|
@@ -82,9 +86,10 @@ export async function loadKnowledgeBundle({
|
|
|
82
86
|
});
|
|
83
87
|
return result(bundle, [], "cloud");
|
|
84
88
|
} catch (error) {
|
|
89
|
+
if (signal?.aborted) throw signal.reason;
|
|
85
90
|
if (error?.code === "incompatible_cli") throw error;
|
|
86
91
|
if (error?.fallbackAllowed !== false && cached && !cached.bundle.system_scope && Date.parse(cached.bundle.expires_at) > now) return result(cached.bundle, [`SQM Cloud unavailable; using last verified bundle: ${message(error)}`], "stale_cache");
|
|
87
|
-
return { ...result(null, [`SQM knowledge could not be used; shadow checks were skipped: ${message(error)}`], "unavailable"), diagnostic: safeSqmDiagnostic(error, "knowledge_bundle", "knowledge_bundle_unavailable") };
|
|
92
|
+
return { ...result(null, [`SQM knowledge could not be used; shadow checks were skipped: ${message(error)}`], "unavailable"), diagnostic: { ...safeSqmDiagnostic(error, "knowledge_bundle", "knowledge_bundle_unavailable"), ...sqmDiagnostic(error, "knowledge_bundle") } };
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
if (cached) {
|
|
@@ -167,7 +172,7 @@ function trustedDiscoveryEndpoint(endpoint) {
|
|
|
167
172
|
(url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname));
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
async function fetchBundle({ endpoint, repository, cliVersion, token, organizationId, etag, fetchImpl }) {
|
|
175
|
+
async function fetchBundle({ endpoint, repository, cliVersion, token, organizationId, etag, fetchImpl, signal, recoveryOptions, onRecoveryEvent }) {
|
|
171
176
|
const url = new URL(endpoint);
|
|
172
177
|
url.searchParams.set("repository", repository);
|
|
173
178
|
url.searchParams.set("cli_version", cliVersion);
|
|
@@ -176,10 +181,8 @@ async function fetchBundle({ endpoint, repository, cliVersion, token, organizati
|
|
|
176
181
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
177
182
|
if (organizationId) headers["x-simy-org-id"] = organizationId;
|
|
178
183
|
if (etag) headers["If-None-Match"] = etag;
|
|
179
|
-
const response = await
|
|
180
|
-
|
|
181
|
-
signal: AbortSignal.timeout(SQM_BUNDLE_FETCH_TIMEOUT_MS),
|
|
182
|
-
});
|
|
184
|
+
const response = await fetchSqmRead(url, { ...recoveryOptions, headers, signal, fetchImpl,
|
|
185
|
+
requestTimeoutMs: SQM_BUNDLE_FETCH_TIMEOUT_MS, onRecoveryEvent });
|
|
183
186
|
const responseEtag = response.headers?.get?.("etag") || null;
|
|
184
187
|
const signingKeyId = response.headers?.get?.("x-simy-sqm-key-id") || null;
|
|
185
188
|
const signingPublicKey = response.headers?.get?.("x-simy-sqm-public-key") || null;
|
|
@@ -201,6 +204,7 @@ async function fetchBundle({ endpoint, repository, cliVersion, token, organizati
|
|
|
201
204
|
);
|
|
202
205
|
}
|
|
203
206
|
const error = new Error(`SQM Cloud returned HTTP ${response.status}${body ? `: ${body.slice(0, 300)}` : ""}.`);
|
|
207
|
+
error.status = response.status;
|
|
204
208
|
Object.assign(error, safeSqmDiagnostic({ status: response.status, code: parsed?.error?.code || parsed?.detail?.code,
|
|
205
209
|
requestId: response.headers?.get?.("x-request-id") }, "knowledge_bundle", "knowledge_bundle_http_error"));
|
|
206
210
|
error.fallbackAllowed = response.status >= 500 && response.status <= 599;
|
package/src/sqm/claude-plugin.js
CHANGED
|
@@ -21,6 +21,8 @@ export async function claudePluginFiles() {
|
|
|
21
21
|
files.set(relative, content.replaceAll("current Codex thread", "current Claude Code conversation"));
|
|
22
22
|
}
|
|
23
23
|
files.set("hooks/delivery-loop.mjs", await readFile(path.join(SOURCE, "hooks/delivery-loop.mjs"), "utf8"));
|
|
24
|
+
files.set("hooks/outcome-progress.mjs", await readFile(path.join(SOURCE, "hooks/outcome-progress.mjs"), "utf8"));
|
|
25
|
+
files.set("hooks/continuation-policy.mjs", await readFile(path.join(SOURCE, "hooks/continuation-policy.mjs"), "utf8"));
|
|
24
26
|
files.set("hooks/action-association.mjs", await readFile(path.join(SOURCE, "hooks/action-association.mjs"), "utf8"));
|
|
25
27
|
files.set("hooks/workspace-lease.mjs", await readFile(path.join(SOURCE, "hooks/workspace-lease.mjs"), "utf8"));
|
|
26
28
|
files.set("hooks/connection-reminder.mjs", await readFile(path.join(SOURCE, "hooks/connection-reminder.mjs"), "utf8"));
|