@awak-app/simy-cli 0.6.23 → 0.6.26

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 (64) hide show
  1. package/README.md +53 -0
  2. package/package.json +3 -3
  3. package/src/cli-contract.js +9 -1
  4. package/src/codex-plugins/simy/hooks/acceptance-policy.mjs +5 -0
  5. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +533 -74
  6. package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +63 -0
  7. package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +154 -0
  8. package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +34 -0
  9. package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +65 -0
  10. package/src/codex-plugins/simy/hooks/purpose-search.mjs +123 -0
  11. package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +97 -0
  12. package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +19 -5
  13. package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +19 -4
  14. package/src/delivery-hooks/command.js +48 -0
  15. package/src/delivery-hooks/consistency-monitor.js +126 -0
  16. package/src/delivery-hooks/desktop-runtime.js +70 -0
  17. package/src/delivery-hooks/desktop.js +13 -3
  18. package/src/delivery-hooks/legacy-hook-routing.js +61 -0
  19. package/src/delivery-hooks/local-runtime.js +123 -0
  20. package/src/delivery-hooks/local-supervisor.js +25 -0
  21. package/src/delivery-hooks/runtime-watchdog.js +101 -0
  22. package/src/delivery-hooks/supervisor.js +90 -22
  23. package/src/desktop-host.js +24 -1
  24. package/src/durable-local-task-worker.js +57 -12
  25. package/src/fp-narrative-image.js +180 -0
  26. package/src/index.js +1 -1
  27. package/src/local-task.js +1 -0
  28. package/src/orchestrator/audit.js +1 -1
  29. package/src/orchestrator/charter-repair.js +93 -0
  30. package/src/orchestrator/charter-validation.js +14 -6
  31. package/src/orchestrator/checklist-continuation.js +1 -1
  32. package/src/orchestrator/contract.js +1 -3
  33. package/src/orchestrator/delivery/WORKFLOW.md +33 -60
  34. package/src/orchestrator/delivery/controls/harness-controls.json +1326 -0
  35. package/src/orchestrator/delivery/references/delivery-levels.md +17 -44
  36. package/src/orchestrator/delivery/references/pr-closure.md +2 -3
  37. package/src/orchestrator/delivery/references/state-and-receipts.md +14 -5
  38. package/src/orchestrator/delivery/references/ux-specification.md +1 -2
  39. package/src/orchestrator/delivery-level.js +4 -5
  40. package/src/orchestrator/independent-audit.js +9 -5
  41. package/src/orchestrator/instruction.js +1 -0
  42. package/src/orchestrator/loop.js +55 -18
  43. package/src/orchestrator/risk.js +1 -1
  44. package/src/orchestrator/ux-specification.js +2 -2
  45. package/src/runner.js +4 -0
  46. package/src/session-supervision/analysis-manager.js +2 -2
  47. package/src/session-supervision/automatic-continuation.js +6 -5
  48. package/src/session-supervision/autonomous-upgrade.js +55 -0
  49. package/src/session-supervision/codex-app-server.js +200 -24
  50. package/src/session-supervision/consistency-auditor.js +113 -0
  51. package/src/session-supervision/index.js +38 -11
  52. package/src/session-supervision/purpose-candidate.js +44 -0
  53. package/src/session-supervision/purpose-instruction.js +47 -0
  54. package/src/session-supervision/purpose-observer.js +172 -0
  55. package/src/session-supervision/purpose-recovery.js +264 -0
  56. package/src/session-supervision/purpose-verification.js +154 -0
  57. package/src/sqm/agent-skill.js +6 -3
  58. package/src/sqm/artifact-paths.js +39 -0
  59. package/src/sqm/claude-plugin.js +7 -0
  60. package/src/sqm/command.js +95 -27
  61. package/src/sqm/history-store.js +3 -3
  62. package/src/sqm/repository.js +19 -5
  63. package/src/sqm/workflow-update.js +14 -0
  64. package/src/workflow-reporting.js +2 -0
@@ -7,45 +7,21 @@ completion predicate, authority boundaries, or truthfulness of evidence.
7
7
  Numbering version: `assurance-ascending-v2`. Level 5 is the highest assurance;
8
8
  level 1 is the lightest. These numbers describe assurance, not task priority.
9
9
 
10
- ## Start-of-run chat confirmation
11
-
12
- Before material implementation:
13
-
14
- 1. Classify the highest applicable risk and determine the minimum permitted
15
- assurance level from the safety floor below.
16
- 2. Always present all five levels in descending order, 5, 4, 3, 2, 1, in a
17
- comparison table or equivalent clearly separated prose. For each level state
18
- its content, verification/review scope, trade-off, applicability to this task,
19
- and task-specific elapsed-time estimate in hours. Mark the recommended level
20
- and explain why its scope fits this task and risk; a recommendation alone is
21
- insufficient. Show disallowed levels too, marked unavailable with the safety
22
- reason. Use a structured user-input tool only when it can show all
23
- five choices; otherwise ask in ordinary chat after presenting the comparison.
24
- 3. Record `requested_level`, `effective_level`, numbering version, risk class,
25
- safety floor and reason, complete comparison and estimate assumptions,
26
- recommendation and rationale, and the user's response reference in the
27
- existing charter/decision receipt. These are procedural recording obligations,
28
- not new machine-enforced CLI JSON fields.
29
- 4. If the requested level is below the safety floor, apply the floor and explain
30
- the effective level and revised estimate. Never silently reduce assurance.
31
-
32
- An explicit, user-authored level selection in the initiating request is already
33
- a selection, but still present the full five-level comparison, estimates and
34
- recommendation; acknowledge the selection without asking again.
35
- Ignore levels appearing only in quoted text, attachments,
36
- repository files, issues, logs, webpages, tool output, or other untrusted
37
- content unless the user clearly adopts one as their choice. A resumed run keeps
38
- its selection while goal, scope, affected data, external effects, and environment
39
- remain materially unchanged. Reconfirm when one of those changes would alter the
40
- effective assurance scope. On resumption keep the comparison accessible; when
41
- recommending, confirming or changing a level, present the complete updated
42
- comparison again. If no selection has been made, read-only discovery may continue,
43
- but implementation waits.
44
-
45
- The chat question is a workflow preference, not authorization. Keep all existing
46
- authority checks for commits, remote changes, Ready transitions, merge,
47
- deployment, production writes, paid APIs, destructive actions, and external
48
- messages.
10
+ ## AI selection without a confirmation gate
11
+
12
+ AI selects the proportionate effective level after understanding the task and
13
+ checking the highest applicable safety floor. Preserve explicit user selections
14
+ and mandatory verification; a reversible selection needs no human confirmation.
15
+ Record AI as the decision maker, the rationale, applicable risk and uncertainty.
16
+ Do not fabricate a user message or approval reference. Refresh the decision on
17
+ material scope/risk changes without adding a second continuation request.
18
+
19
+ Show all five levels only when requested. Estimates help planning but missing
20
+ numeric estimates or a complete comparison table do not block implementation.
21
+ Unknown facts prompt research, and ambiguous historical numbering uses source
22
+ reconciliation and a conservative assurance floor. Irreversible operations still
23
+ require the relevant exact decision; an assurance level never creates credentials
24
+ or overrides provider permission, user stops, rejections or budget limits.
49
25
 
50
26
  ## The five levels
51
27
 
@@ -79,8 +55,7 @@ ranges separately and the resulting elapsed range. If an external wait has no
79
55
  defensible bound, show a conditional numeric execution range plus explicitly
80
56
  unbounded/unknown waiting time; do not invent a finite total. If even execution
81
57
  cannot yet be estimated, retain that level's row as estimate pending with the
82
- missing input and next discovery step; do not present the selection packet as
83
- complete until a defensible numeric execution range is available for every level.
58
+ missing input and next discovery step. Continue reversible work; complete numeric estimates are not a selection gate.
84
59
  Unavailable levels have conditional estimates for comparison only, not permission
85
60
  to execute below the risk floor. A stricter level cannot appear faster by silently
86
61
  omitting required work; explain any overlap or inversion from verified reuse,
@@ -111,9 +86,7 @@ Do not reinterpret an old number using the new direction. Preserve original
111
86
  receipts and their version/source. For a verified legacy descending-assurance
112
87
  selection, map old to new as 1→5, 2→4, 3→3, 4→2, 5→1, record both meanings and
113
88
  explain the conversion. Keep the same assurance scope; this is not a fresh grant
114
- of authority or evidence. If the old numbering cannot be established, ask which
115
- assurance scope the user intended instead of guessing. Reconfirm only if the
116
- effective assurance scope changes or the selection remains ambiguous.
89
+ of authority or evidence. If the old numbering cannot be established, research its source and use a conservative assurance floor while preserving the ambiguous historical receipt. AI re-evaluates reversible assurance decisions; do not ask the user to resolve numbering bookkeeping.
117
90
 
118
91
  ## What never varies
119
92
 
@@ -98,8 +98,7 @@ Record criterion IDs, original PR, observed gap/evidence, required outcome, owne
98
98
  repository/base, tests/E2E and acceptance gates in each remediation PR. Add its real
99
99
  URL and state to every covered table row; include dependencies and re-evaluation
100
100
  triggers. Evidence-only yellow rows need the missing verification, not speculative
101
- product changes. An unresolved product choice remains `needs_decision`; do not
102
- choose a new specification just to open a PR. If implementation or publication is
101
+ product changes. AI resolves reversible product choices from the original purpose, with recorded assumptions and rollback. Only irreversible or explicitly reserved choices remain `needs_decision`; do not change the original goal just to open a PR. If implementation or publication is
103
102
  unauthorized, blocked or outside the current budget, record a concrete PR plan and
104
103
  the missing decision/authority, but do not claim a PR exists. A plan/issue alone
105
104
  does not satisfy the requested PR handoff or justify `transfer_ready`.
@@ -151,7 +150,7 @@ Store these labels in the closure record, not in the delivery-state enum:
151
150
  | `close_ready` | All requirements accounted for, candidate evidence and independent review pass; no hidden loss or pending implementation. State separately whether base-only or live. Deployment may remain an explicit delivery obligation. |
152
151
  | `transfer_ready` | Every pending outcome/risk has an accepted owner or authorized assignment, actual open remediation PR with criterion coverage, acceptance gate and re-evaluation trigger; explicit informed administrative-close authorization. No unmapped yellow/red row or unresolved handoff blocker. Not full inheritance/completed delivery. |
153
152
  | `retirement_ready` | Explicit product decision retires named outcomes; necessary user-data/migration/safety obligations verified or safely assigned before close. Not equivalence. |
154
- | `needs_decision` | Lost/changed outcome or trade-off lacks an authoritative human selection; present consequences/options and keep open. |
153
+ | `needs_decision` | Irreversible consequences or an explicitly reserved choice need human selection. AI resolves reversible trade-offs using grounded purpose, alternatives and rollback; do not leave PR closure waiting solely for a preference. |
155
154
  | `blocked` | Missing/stale evidence, review, authority, classification or handoff, or unsafe consequence prevents the proposed action/claim. |
156
155
 
157
156
  Unknown/failed applicable rows cannot support `close_ready`. A deliberately
@@ -16,7 +16,7 @@ Persist one controller-owned snapshot with at least:
16
16
  their precedence and fallback semantics, risk tier, requested/effective
17
17
  delivery levels, numbering version, safety floor and reason, complete five-level
18
18
  comparison with per-level hour estimates and assumptions, recommendation and
19
- rationale, and chat-confirmation receipt; preserve legacy numbering and record
19
+ rationale, and actual user selection receipt only when applicable; preserve legacy numbering and record
20
20
  verified conversions separately rather than rewriting historical choices;
21
21
  - repository, base SHA, head SHA, companion SHAs, merge SHA, image digests, and
22
22
  deployed revision;
@@ -89,10 +89,19 @@ If the decision is ambiguous or introduces a material trade-off not yet explaine
89
89
  clarify only the unresolved point before proceeding with dependent work. Mandatory
90
90
  security, privacy, data-integrity, and external-contract constraints still apply.
91
91
 
92
- A successful `HumanDecisionReceipt` requires an explicit selection. Until then,
93
- store the pending decision packet in the snapshot and publish the monitor verdict
94
- `needs_hil` with action `request_hil`; never synthesize or infer approval from a
95
- recommendation, silence, or an implementation pattern.
92
+ For a reversible delegated decision, record an AI decision in the existing
93
+ snapshot: source evidence, alternatives, selected option, rationale, assumptions,
94
+ rollback and verification. It is not a HumanDecisionReceipt and does not create
95
+ new provider authority. Do not emit needs_hil merely because sources conflict,
96
+ risk is high or an AI recommendation needs a choice. Apply the 102 perspectives
97
+ as critique of the grounded AI judgment.
98
+
99
+ A successful `HumanDecisionReceipt` still requires an explicit selection when
100
+ irreversible consequences or an explicitly reserved human choice require it.
101
+ Only that operation remains needs_hil; independent authorized work proceeds.
102
+ Silence is not approval of an irreversible operation. When the matching decision
103
+ arrives, continue within the existing task without a second resume request,
104
+ unless the user explicitly stopped/paused it.
96
105
 
97
106
  ## Monitor verdict and policy action
98
107
 
@@ -87,8 +87,7 @@ from matrices, PRs and receipts. Compact user-facing summaries may omit repetiti
87
87
  not material behavior, uncertainty or the link to the full contract. Do not impose
88
88
  long transcripts or duplicate the full specification in each progress update.
89
89
 
90
- Unknowns are visible work, not a reason to fabricate detail. A material unresolved
91
- requirement decision blocks dependent design/implementation; a missing runtime
90
+ Unknowns are visible work, not a reason to fabricate detail. AI researches material ambiguity and selects reversible defaults within the original goal; irreversible or explicitly reserved requirement decisions block dependent effects; a missing runtime
92
91
  observation blocks the affected verification or equivalence claim. Discovery and
93
92
  independent authorized work may continue. Explicit assumptions retain their source,
94
93
  risk and resolution condition; assumption-based behavior cannot be presented as
@@ -9,11 +9,10 @@ export function buildDeliveryLevelInstruction({ auditor = false } = {}) {
9
9
  return section("delivery_level_contract", [
10
10
  `Read the Web/CLI delivery-level contract: ${DELIVERY_LEVELS_PATH}`,
11
11
  "Use assurance-ascending-v2: 5 is maximum assurance, 4 protected, 3 balanced, 2 fast, 1 lightest. Higher numbers mean more assurance, not greater task priority. Apply the highest applicable risk/policy floor; never lower mandatory verification or expand authority.",
12
- "Whenever recommending, confirming or changing a level, present ALL five levels in descending order 5, 4, 3, 2, 1, as a table or clearly separated prose. For EACH show content, verification/review scope, trade-off, task applicability, and task-specific duration in hours. Mark the recommendation and explain its task/risk rationale, even when the user already selected a level; acknowledge a valid selection without asking again. Mark unsafe levels unavailable with reasons; do not hide them.",
13
- "Estimate numeric lower-to-upper execution and elapsed-hour ranges for the same task scope and endpoint, using observed boundaries, implementation, checks, review, setup, repair and dependency critical path. Separate execution from CI/queue/approval/external waiting; account for parallelism. State assumptions, evidence, uncertainty and confidence. Do not hardcode per-level durations or product examples. Unknown external wait stays explicitly unbounded, with a conditional numeric execution range; never invent a finite total. If execution cannot yet be estimated, show the missing input and discovery step for each affected row and keep the selection packet incomplete pending a defensible range. Explain overlaps or inversions; estimates are not guarantees or permission to skip work.",
14
- "Record the comparison, estimate basis, recommendation/reason, requested/effective level, numbering version, risk floor and user selection reference in existing charter/decision evidence, not invented CLI API fields. Refresh on material changes. Preserve historical receipts: map verified legacy levels 1→5, 2→4, 3→3, 4→2, 5→1 without changing assurance; clarify ambiguous unversioned selections rather than guessing.",
12
+ "AI selects the effective verification level from task context and the highest applicable safety floor. Preserve an explicit user selection or reserved human choice. Reversible level decisions do not require a five-option chat confirmation. Record the choice, rationale, evidence and uncertainty; estimates are advisory, not a prerequisite for implementation. Present ALL five levels in descending order 5, 4, 3, 2, 1 only when the user requests a comparison. Do not invent a user selection reference for an AI choice.",
13
+ "Refresh the AI decision when scope or risk changes, preserve mandatory checks and the original endpoint, and continue reversible work. Unknown duration calls for discovery rather than a human-selection packet. Historical numbering ambiguity calls for source research and a conservative assurance floor, not a guessed receipt.",
15
14
  auditor
16
- ? "Remain read-only. Check the actual user-facing comparison and selection evidence, not only this instruction. Report missing levels, scope, estimates, rationale, unsafe effective levels, or ambiguous legacy numbering as findings. This is procedural guidance, not a new deterministic selection validator."
17
- : "Before material implementation ensure a valid selection and complete comparison are available. Read-only discovery may continue while unresolved; do not substitute a recommendation-only message. This is procedural guidance, not a new deterministic selection validator.",
15
+ ? "Remain read-only. Check the AI rationale, mandatory assurance floor, original endpoint and actual verification evidence. Missing optional comparisons or estimates alone are not findings. This is procedural guidance, not a new deterministic selection validator."
16
+ : "Select and record a proportionate assurance level, then proceed with reversible implementation without waiting for a user selection. This is procedural guidance, not a new deterministic selection validator.",
18
17
  ].join("\n"));
19
18
  }
@@ -32,6 +32,7 @@ export function buildIndependentAuditInstruction(charter, attempt) {
32
32
  buildChecklistContinuationInstruction({ auditor: true }),
33
33
  buildWorkflowReportingInstruction({ auditor: true }),
34
34
  buildAcceptedHumanGuidanceInstruction(charter),
35
+ ...(charter.ai_preparation ? [section('ai_reviewed_acceptance_details', JSON.stringify(charter.ai_preparation))] : []),
35
36
  buildPrClosureInstruction({ auditor: true }),
36
37
  section(
37
38
  "role",
@@ -110,7 +111,7 @@ export function buildIndependentAuditInstruction(charter, attempt) {
110
111
  "Required keys: passed, summary, findings, criterion_results, visual_review, coverage_review.",
111
112
  "coverage_review must contain requirements_complete (boolean), unmapped_requirements (array), observation, and user_outcome with passed, verification_method, evidence_refs, and observed_result. Independently exercise or inspect proof of the original user goal even when supplied acceptance criteria mention only tests, code, or a PR. Use passed=false if the goal is not demonstrated. Explain the comparison of the original request, frozen experience, stable criterion-ID list, implementation and raw user/system proof.",
112
113
  "Each finding candidate must contain passed, code, severity, target, explanation, and repairability.",
113
- "For a failed finding, optionally identify blocker_kind as machine, human, or external. Use human when a specific user decision or credential interaction is required, external for an unavailable outside dependency, and machine when local repair or verification can proceed. One blocked criterion does not imply all remaining work is blocked.",
114
+ "For a failed finding, optionally identify blocker_kind as machine, human, or external. Use human only for irreversible consequences, an explicitly reserved decision or actual missing credential/permission interaction; reversible choices and technical evidence gaps are machine investigation, external for an unavailable outside dependency, and machine when local repair or verification can proceed. One blocked criterion does not imply all remaining work is blocked.",
114
115
  "criterion_results must contain one entry per stable criterion ID with criterion_id, passed, verification_method, evidence_refs, and observed_result.",
115
116
  "visual_review must contain inspected, artifacts, and acceptance_observations.",
116
117
  "Only passed=false entries are findings. Omit passing checks, and never report passed=true evidence as a finding.",
@@ -149,12 +150,14 @@ export function buildIndependentAudit(execution, charter = null, attempt = null)
149
150
  severity: "blocker",
150
151
  target: "independent_audit",
151
152
  explanation: "The independent auditor failed or returned an invalid structured result.",
152
- repairability: "manual",
153
+ repairability: "auto",
154
+ blocker_kind: "machine",
155
+ auto_fix_hint: "Diagnose the failed reviewer transport/schema, repair it and obtain a fresh independent audit.",
153
156
  },
154
157
  ],
155
158
  criterion_results: [],
156
159
  visual_review: normalizeVisualReview(null),
157
- requires_human: true,
160
+ requires_human: false,
158
161
  };
159
162
  }
160
163
 
@@ -239,8 +242,9 @@ export function buildIndependentAudit(execution, charter = null, attempt = null)
239
242
  severity: "blocker",
240
243
  target: "independent_audit",
241
244
  explanation: "The independent auditor returned passed=false without an explicit failed finding.",
242
- repairability: "manual",
243
- auto_fix_hint: null,
245
+ repairability: "auto",
246
+ blocker_kind: "machine",
247
+ auto_fix_hint: "Obtain a new independent review with concrete failed observations; do not ask a person to certify correctness.",
244
248
  });
245
249
  }
246
250
  const coverage = result.coverage_review;
@@ -29,6 +29,7 @@ export function buildCodingInstruction(
29
29
  buildChecklistContinuationInstruction(),
30
30
  buildWorkflowReportingInstruction(),
31
31
  buildAcceptedHumanGuidanceInstruction(charter),
32
+ ...(charter.ai_preparation ? [section('ai_reviewed_acceptance_details', JSON.stringify(charter.ai_preparation))] : []),
32
33
  section(
33
34
  "requirement_charter",
34
35
  [
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { completedWorkerResult } from "../coding-run-recovery.js";
3
3
  import { validateCharter } from "./charter-validation.js";
4
+ import { repairCharterWithAI } from './charter-repair.js';
4
5
  import { redactExecutionText } from "./execution-io.js";
5
6
  import { minimumCharter } from "./delivery/scripts/check-minimum-completion.mjs";
6
7
  import { auditAttempt, evaluatePrReadiness } from "./audit.js";
@@ -60,16 +61,23 @@ async function runCodingLoopOwned({
60
61
  });
61
62
  await publish(snapshot, "risk_classifying", onUpdate);
62
63
 
63
- const charterFindings = validateCharter(snapshot.charter);
64
+ let charterFindings = validateCharter(snapshot.charter);
65
+ if (charterFindings.length && executeIndependentAudit) {
66
+ await publish(snapshot, 'chartering', onUpdate);
67
+ await repairCharterWithAI({ snapshot, execute: executeIndependentAudit, shouldStop,
68
+ onUpdate: async () => publish(snapshot, 'chartering', onUpdate) });
69
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
70
+ charterFindings = validateCharter(snapshot.charter);
71
+ }
64
72
  if (charterFindings.length > 0) {
65
- snapshot.final_audit = humanGate(
66
- "Requirements or design review must be completed before implementation.",
73
+ snapshot.final_audit = machineGate(
74
+ "AI must repair the charter and obtain independent design evidence before implementation.",
67
75
  charterFindings,
68
76
  );
69
- appendEvent(snapshot, "waiting_human", "Requirements require human clarification.", {
77
+ appendEvent(snapshot, "blocked", "AI charter preparation or independent design review is incomplete.", {
70
78
  finding_codes: charterFindings.map((finding) => finding.code),
71
79
  });
72
- await publish(snapshot, "waiting_human", onUpdate);
80
+ await publish(snapshot, "blocked", onUpdate);
73
81
  return snapshot;
74
82
  }
75
83
 
@@ -217,6 +225,11 @@ async function runCodingLoopOwned({
217
225
  const recordedIndex = snapshot.attempts.findIndex(item => item.attempt_number === attemptNumber);
218
226
  if (recordedIndex >= 0) snapshot.attempts[recordedIndex] = attempt;
219
227
  else snapshot.attempts.push(attempt);
228
+ // A terminal worker receipt is not permission to repeat its effects when
229
+ // local observation is incomplete. Retain the attempt and retry evidence.
230
+ if (snapshot.metadata?.completed_worker_result && attempt.observed_evidence.local?.available !== true) {
231
+ return waitForCollector(snapshot, attempt, onUpdate);
232
+ }
220
233
  assessOutcomeProgress(snapshot.charter, attempt, previousAttempt, { ...outcomeOptions(snapshot), resetUnavailableCount: resume && attemptNumber === firstAttempt });
221
234
  attempt.budget = refreshBudgetState(snapshot);
222
235
  recordBudgetWarnings(snapshot, attempt);
@@ -512,14 +525,14 @@ async function recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm = n
512
525
 
513
526
 
514
527
  async function waitForCollector(snapshot, attempt, onUpdate) {
515
- snapshot.final_audit = humanGate("Evidence collection failed; the completed worker result is retained for retry.", [{
528
+ snapshot.final_audit = machineGate("Evidence collection failed; the completed worker result is retained for retry.", [{
516
529
  code: "EVIDENCE_COLLECTION_UNAVAILABLE", severity: "blocker", target: "evidence_collector",
517
- explanation: attempt.observed_evidence.collection_error, repairability: "manual",
530
+ explanation: attempt.observed_evidence.collection_error || attempt.observed_evidence.local?.errors?.join("; ") || "Local evidence is incomplete; retry observation without replaying the completed worker.", repairability: "auto", blocker_kind: "machine",
518
531
  }]);
519
- appendEvent(snapshot, "waiting_human", "Retry evidence collection for the same completed attempt.", {
532
+ appendEvent(snapshot, "blocked", "Retry evidence collection for the same completed attempt.", {
520
533
  code: "evidence_collection_retry_required", attempt_number: attempt.attempt_number,
521
534
  });
522
- await publish(snapshot, "waiting_human", onUpdate);
535
+ await publish(snapshot, "blocked", onUpdate);
523
536
  return snapshot;
524
537
  }
525
538
 
@@ -608,6 +621,23 @@ async function handleFailedAudit({
608
621
  onUpdate,
609
622
  });
610
623
  }
624
+ const diagnosticCircuit = evaluateRetryCircuit({ attempt, previousAttempt: snapshot.attempts.at(-2) ?? null, findings });
625
+ const technicalStall = attempt.outcome_progress?.stalled || attempt.outcome_progress?.assessment_unavailable || diagnosticCircuit.open && !attempt.outcome_progress?.has_progress;
626
+ if (technicalStall && !requiresHuman) {
627
+ const state = snapshot.charter.thread_state;
628
+ const key = JSON.stringify([state.objective_contract?.current_digest, attempt.outcome_progress?.unproven_ids || [], findings.filter(row => row.code !== 'AI_STRATEGY_CHANGE_REQUIRED').map(row => row.code).sort()]);
629
+ state.ai_strategy_diagnoses ||= [];
630
+ if (!state.ai_strategy_diagnoses.includes(key)) {
631
+ state.ai_strategy_diagnoses.push(key);
632
+ state.ai_strategy_diagnoses = state.ai_strategy_diagnoses.slice(-32);
633
+ findings.push({ passed: false, code: 'AI_STRATEGY_CHANGE_REQUIRED', severity: 'blocker', target: 'retry_loop',
634
+ explanation: 'Investigate the repeated failure with LLM context synthesis, alternatives and tool evidence. Choose a different reversible method before another attempt; do not repeat uncertain effects or ask a person to certify technical correctness.', repairability: 'auto', blocker_kind: 'machine' });
635
+ snapshot.final_audit = { ...(snapshot.final_audit || {}), passed: false, findings, requires_human: false, auto_repairable: true };
636
+ appendEvent(snapshot, 're_instructing', 'AI diagnosis is required before treating technical repetition as an execution blocker.', { code: 'ai_strategy_change', attempt_number: attemptNumber });
637
+ await publish(snapshot, 're_instructing', onUpdate);
638
+ return false;
639
+ }
640
+ }
611
641
  if (attempt.outcome_progress?.stalled) {
612
642
  const unresolved = attempt.outcome_progress.unproven_ids;
613
643
  snapshot.final_audit = {
@@ -620,10 +650,11 @@ async function handleFailedAudit({
620
650
  severity: "blocker",
621
651
  target: "acceptance_criteria",
622
652
  explanation: `The same user outcomes remain unproven after another attempt: ${unresolved.join(", ")}. Reassess the approach against the approved objective before more work.`,
623
- repairability: "manual",
653
+ repairability: "auto",
654
+ blocker_kind: "machine",
624
655
  }],
625
656
  auto_repairable: false,
626
- requires_human: true,
657
+ requires_human: false,
627
658
  };
628
659
  appendEvent(snapshot, "blocked", "Automatic retries stopped: no independently observed outcome progress.", {
629
660
  code: "outcome_progress_stalled",
@@ -692,12 +723,13 @@ async function handleFailedAudit({
692
723
  severity: "blocker",
693
724
  target: "outcome_monitor",
694
725
  explanation: "Two consecutive attempts could not establish an independent outcome assessment. Resolve the recorded verification failures and continue this run; this does not prove the work has stalled.",
695
- repairability: "manual",
726
+ repairability: "auto",
727
+ blocker_kind: "machine",
696
728
  }],
697
729
  auto_repairable: false,
698
- requires_human: true,
730
+ requires_human: false,
699
731
  };
700
- appendEvent(snapshot, "waiting_human", "Outcome assessment needs recovery before further automatic work.", {
732
+ appendEvent(snapshot, "blocked", "AI diagnosis could not recover the outcome assessment; preserve the concrete execution blocker.", {
701
733
  code: "outcome_assessment_unavailable",
702
734
  finding_codes: ["OUTCOME_ASSESSMENT_UNAVAILABLE"],
703
735
  attempt_number: attemptNumber,
@@ -705,7 +737,7 @@ async function handleFailedAudit({
705
737
  unavailable_reason: progress.unavailable_reason,
706
738
  unproven_ids: progress.unproven_ids,
707
739
  });
708
- await publish(snapshot, "waiting_human", onUpdate);
740
+ await publish(snapshot, "blocked", onUpdate);
709
741
  return true;
710
742
  }
711
743
  if (requiresHuman) {
@@ -800,7 +832,8 @@ function retryCircuitAudit(audit, circuitState) {
800
832
  target: "retry_loop",
801
833
  explanation:
802
834
  "The last two attempts produced the same commit and findings, so another automatic retry would repeat the same work.",
803
- repairability: "manual",
835
+ repairability: "auto",
836
+ blocker_kind: "machine",
804
837
  auto_fix_hint: null,
805
838
  };
806
839
  return {
@@ -809,7 +842,7 @@ function retryCircuitAudit(audit, circuitState) {
809
842
  summary: "Automatic retries stopped because the last two failure results were identical.",
810
843
  findings: [...(Array.isArray(audit?.findings) ? audit.findings : []), circuitFinding],
811
844
  auto_repairable: false,
812
- requires_human: true,
845
+ requires_human: false,
813
846
  retry_circuit_breaker: circuitState,
814
847
  };
815
848
  }
@@ -820,7 +853,7 @@ function aggregateAudit(attempt, passed, findings) {
820
853
  summary: attempt.pr_readiness?.summary || "PR lifecycle audit completed.",
821
854
  findings,
822
855
  auto_repairable: false,
823
- requires_human: !passed,
856
+ requires_human: findings.some(finding => finding.repairability === "manual"),
824
857
  implementation_gate: attempt.implementation_gate,
825
858
  independent_audit: attempt.independent_audit,
826
859
  pr_gate: attempt.pr_readiness || null,
@@ -852,3 +885,7 @@ async function withController(options, action) {
852
885
  }
853
886
  export const runCodingLoop = options => withController(options, runCodingLoopOwned);
854
887
  export const recheckPrReadiness = options => withController(options, recheckPrReadinessOwned);
888
+
889
+ function machineGate(summary, findings) {
890
+ return { passed: false, summary, findings, auto_repairable: true, requires_human: false };
891
+ }
@@ -55,7 +55,7 @@ export function classifyRisk(request) {
55
55
  "verified_local_git",
56
56
  "independent_ai_audit",
57
57
  "github_status_checks",
58
- "human_peer_approval",
58
+ ...(request.require_human_approval === true ? ["human_peer_approval"] : []),
59
59
  ...(requiresDesignReview ? ["design_review"] : []),
60
60
  ],
61
61
  };
@@ -11,10 +11,10 @@ export function buildUxSpecificationInstruction({ auditor = false } = {}) {
11
11
  "Describe observable interaction units, not feature inventories. Reuse stable acceptance IDs. For each unit specify actor/goal, context/preconditions, trigger/reachable entry, perceived information, available action/choice/control, system response/state transition, usable outcome/postconditions, next action/handoff, alternative/failure/recovery paths, and verification oracle/provenance. Treat these as semantic obligations in the existing specification, not new CLI JSON fields.",
12
12
  "Decompose when actor, precondition, decision, observable state, postcondition or recovery differs materially, or when any clause could fail independently. Link predecessors/successors and preserve end-to-end criteria. Derive branches and applicable constraints from authoritative requirements and observed system boundaries; do not invent scenarios, defaults, thresholds or product policy to fill gaps.",
13
13
  "Description is sufficient only when another implementer can derive the intended behavior and an independent verifier can determine pass/fail without guessing a material requirement. Review source coverage, causal continuity, branch closure, controllability, observability, traceability, consistency and epistemic separation. Word count, row count and verbosity are not quality thresholds.",
14
- "Keep required/intended/observed/assumed/unknown distinct. Unknown facts stay explicit with affected IDs and resolution evidence; a missing requirement decision blocks dependent design, while missing runtime evidence blocks the affected verification claim. Genuine non-applicability needs a source and reason, not an empty cell.",
14
+ "Keep required/intended/observed/assumed/unknown distinct. Unknown facts stay explicit with affected IDs and resolution evidence; a reversible missing requirement decision is resolved by grounded AI inference and a recorded assumption, while missing runtime evidence blocks the affected verification claim. Genuine non-applicability needs a source and reason, not an empty cell.",
15
15
  "Persist the complete contract in the canonical specification and link the same IDs from summaries, matrices, PRs and receipts. Summaries may shorten repetition but must not replace or drop behavioral obligations, unresolved gaps or evidence. A phase change revalidates affected descriptions and results; plans never become observed proof by wording alone.",
16
16
  auditor
17
17
  ? "Remain read-only. Independently derive coverage from original sources, identify any material behavior that still requires guessing, and report affected criterion IDs and required clarification/evidence as findings. Do not infer completeness from filled fields or copied executor claims."
18
- : "Before dependent implementation, resolve material specification ambiguity within authority or report needs_decision; continue independent work. Before verification or completion, repair missing descriptions/evidence rather than relabeling incomplete UX as complete. This procedural contract does not add automatic schema or GitHub enforcement.",
18
+ : "Before dependent implementation, resolve material specification ambiguity with source research, a reversible default and verification; reserve needs_decision for irreversible or explicitly user-reserved decisions; continue independent work. Before verification or completion, repair missing descriptions/evidence rather than relabeling incomplete UX as complete. This procedural contract does not add automatic schema or GitHub enforcement.",
19
19
  ].join("\n"));
20
20
  }
package/src/runner.js CHANGED
@@ -848,6 +848,7 @@ async function runLocalCodingRun(
848
848
  repositoryPath,
849
849
  run,
850
850
  marker: "SIMY_AUDIT_JSON:",
851
+ readOnly: ['charter_prepare', 'charter_review'].includes(context.phase),
851
852
  }));
852
853
  const evidenceCollector =
853
854
  collectEvidence ||
@@ -1057,6 +1058,7 @@ export async function executeProcessAttempt({
1057
1058
  resolveCommandImpl = resolveDesktopExecutorCommand,
1058
1059
  charter = run?.snapshot?.charter,
1059
1060
  onExecutionComplete = null,
1061
+ readOnly = false,
1060
1062
  }) {
1061
1063
  const phase = marker === "SIMY_AUDIT_JSON:" ? "reviewer" : "executor";
1062
1064
  const command = await resolveCommandImpl({
@@ -1065,7 +1067,9 @@ export async function executeProcessAttempt({
1065
1067
  repositoryPath,
1066
1068
  model: phase === "reviewer" ? charter?.audit_model : charter?.model,
1067
1069
  reasoningEffort: phase === "reviewer" ? charter?.audit_reasoning_effort : charter?.reasoning_effort,
1070
+ ...(readOnly ? { executionKind: 'direct', permissionMode: 'read_only' } : {}),
1068
1071
  });
1072
+ if (readOnly && command.verification === 'explicit_command_override') throw new Error('AI charter preparation needs an enforceable read-only provider adapter; shell override is unsupported.');
1069
1073
  if (run.stopRequested) throw new Error("Execution cancelled by the local operator.");
1070
1074
  const startedAt = new Date().toISOString();
1071
1075
  const stdout = [];
@@ -342,10 +342,10 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
342
342
  });
343
343
  startDrain();
344
344
  },
345
- requireRecommendation(candidate) {
345
+ requireRecommendation(candidate, { allowDiagnosis = false } = {}) {
346
346
  if (closed || suspended) throw new Error('Session analysis is unavailable.');
347
347
  const analysis = this.project(candidate).analysis;
348
- if (!eligible(candidate) || candidate.isRunning === true || analysis.awaitingTurn === true || analysis.state !== 'continue') throw new Error('Analyze this conversation before continuing; it needs a current recommendation.');
348
+ if (!eligible(candidate) || candidate.isRunning === true || analysis.awaitingTurn === true || !(analysis.state === 'continue' || allowDiagnosis && candidate.provider === 'codex' && candidate.autoContinue === true && !analysis.incomplete && analysis.contextDigest && ['needs_user', 'uncertain', 'waiting'].includes(analysis.state))) throw new Error('Analyze this conversation before continuing; it needs a current recommendation.');
349
349
  return analysis;
350
350
  },
351
351
  async whenIdle() {
@@ -22,17 +22,18 @@ function continuationFailureReason(error) {
22
22
  export async function continueAnalyzedSession({ candidate, manager, root, settings = {}, signal,
23
23
  readContext = readContinuationContext, enroll = continueSession, beforeEnroll = () => {} }) {
24
24
  const link = await requireProviderLink({ root, provider: candidate.provider, expectedEpoch: settings.expectedProviderEpoch });
25
- const recommendation = manager.requireRecommendation(candidate);
25
+ const recommendationOptions = { allowDiagnosis: settings.purposeDriven === true && candidate.autoContinue === true };
26
+ const recommendation = manager.requireRecommendation(candidate, recommendationOptions);
26
27
  if (candidate.isRunning || candidate.capabilities?.canContinue !== true) throw new Error('Automatic continuation is unavailable for this conversation.');
27
28
  const context = await readContext(candidate, { signal });
28
29
  if (context.awaitingTurn || context.incomplete || !recommendation.contextDigest || context.contextDigest !== recommendation.contextDigest) {
29
30
  await manager.invalidate(candidate);
30
31
  throw new Error('This conversation changed; wait for its updated analysis.');
31
32
  }
32
- manager.requireRecommendation(candidate);
33
+ manager.requireRecommendation(candidate, recommendationOptions);
33
34
  await beforeEnroll(candidate);
34
35
  await requireProviderLink({ root, provider: candidate.provider, expectedEpoch: link.epoch });
35
- manager.requireRecommendation(candidate);
36
+ manager.requireRecommendation(candidate, recommendationOptions);
36
37
  if (signal?.aborted) throw new Error('Automatic continuation was cancelled before enrollment.');
37
38
  return enroll({ root, provider: candidate.provider, session: candidate.session, ...settings, expectedProviderEpoch: link.epoch });
38
39
  }
@@ -66,7 +67,7 @@ export function createAutomaticContinuation({ root, getManager, selectCandidate,
66
67
  if (pending.has(key)) continue;
67
68
  let recommendation;
68
69
  const manager = getManager();
69
- try { recommendation = manager.requireRecommendation(candidate); } catch { continue; }
70
+ try { recommendation = manager.requireRecommendation(candidate, { allowDiagnosis: candidate.provider === 'codex' && candidate.autoContinue === true }); } catch { continue; }
70
71
  const attempt = `${candidate.autoContinueEpoch}:${recommendation.contextDigest}`;
71
72
  if (attempted.get(key) === attempt) continue;
72
73
  attempted.set(key, attempt);
@@ -84,7 +85,7 @@ export function createAutomaticContinuation({ root, getManager, selectCandidate,
84
85
  if (!fresh.autoContinue || fresh.autoContinueEpoch !== candidate.autoContinueEpoch) throw new Error('Automatic continuation setting changed.');
85
86
  await continueAnalyzedSession({ root, candidate: fresh, manager, signal: controller.signal,
86
87
  readContext, enroll, beforeEnroll: async selected => { await guard(); await beforeEnroll(selected); },
87
- settings: { ...setting.settings, autoContinueEpoch: candidate.autoContinueEpoch } });
88
+ settings: { ...setting.settings, ...(fresh.provider === 'codex' ? { purposeDriven: true } : {}), autoContinueEpoch: candidate.autoContinueEpoch } });
88
89
  }).catch(error => {
89
90
  if (!closed && !controller.signal.aborted && !/disabled|already enrolled|setting changed|conversation changed; wait|needs a current recommendation/i.test(error?.message || '')) {
90
91
  errors.set(key, { epoch: candidate.autoContinueEpoch, reason: continuationFailureReason(error) });
@@ -0,0 +1,55 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { automaticContinuationAllowed, queuePurposeDiagnosis, queuePurposeVerification } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
3
+ import { purposeFailureMode } from '../codex-plugins/simy/hooks/purpose-search.mjs';
4
+
5
+ export const AUTONOMOUS_STATE_CONTRACT = 2;
6
+ const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
7
+
8
+ export function pauseOrigin(run) {
9
+ if (run?.stopReason?.actor) return run.stopReason.actor;
10
+ if (/^(?:Paused by user\.|Automatic continuation paused by user\.|The original provider conversation was archived\.|User interrupted the provider\.|Stopped by user\.)$/.test(run?.reason || '')) return 'user';
11
+ if (/^(?:Paused because no progress was confirmed in \d+ minutes\.|No verified acceptance progress in \d+ minutes\.|Paused after \d+ steps without confirmed progress\.) Review the original conversation, then resume\.$/.test(run?.reason || '')) return 'system';
12
+ return 'unknown';
13
+ }
14
+
15
+ export function upgradeEligible(record) {
16
+ const m = record?.run?.managed;
17
+ return record?.provider === 'codex' && record.autoContinue === true && record.providerArchived !== true &&
18
+ m && automaticContinuationAllowed(record) && record.admission?.source === 'codex_app_server' &&
19
+ !['stopped', 'completed', 'reported_done', 'budget_exhausted', 'failed', 'auto_resuming'].includes(record.run.state) &&
20
+ !m.resumeLease && !m.liveWorkspaceLease && !m.outcomeAudit?.lease && !['pending', 'transferred', 'pending_received'].includes(m.handoff?.status) &&
21
+ !(record.run.state === 'paused' && pauseOrigin(record.run) === 'user');
22
+ }
23
+
24
+ // Controller-only, idempotent migration. Frozen criteria and grants remain
25
+ // byte-identical, including each criterion's original verifier.
26
+ export function upgradeAutonomousRecord(record, { rules, now, metadata } = {}) {
27
+ if (!upgradeEligible(record)) return { changed: false, reason: 'ineligible' };
28
+ const run = record.run, m = run.managed, previousState = run.state;
29
+ if (metadata?.session !== record.session || metadata.cwd !== record.cwd || !metadata.model ||
30
+ metadata.model !== (m.resumeModel || record.admission.model)) return { changed: false, reason: 'original_channel_unverified' };
31
+ if (!m.approvalScopes?.length) return { changed: false, reason: 'authorized_read_scope_missing' };
32
+ const before = digest({ purpose: m.purpose, autonomyPolicy: m.autonomyPolicy, routes: m.acceptanceRoutes, channel: m.resumeChannel, contractVersion: m.controllerContractVersion });
33
+ const original = digest({ objective: run.objective, criteria: m.acceptanceCriteria, level: m.deliveryLevel, grants: m.grants });
34
+ m.purpose = { ...m.purpose, enabled: true, version: 2, decisionPolicy: 'llm-first-hc102',
35
+ failureBudgetMode: purposeFailureMode(record), ruleDigest: rules.digest, ruleIds: rules.rules.map(rule => rule.id),
36
+ recoveries: m.purpose?.recoveries || [] };
37
+ m.controllerContractVersion = AUTONOMOUS_STATE_CONTRACT;
38
+ m.resumeChannel = 'codex_app_server'; m.resumeModel = metadata.model; m.capability = 'codex_app_server';
39
+ const after = digest({ purpose: m.purpose, autonomyPolicy: m.autonomyPolicy, routes: m.acceptanceRoutes, channel: m.resumeChannel, contractVersion: m.controllerContractVersion });
40
+ if (before !== after) {
41
+ m.policyMigrations = [...(m.policyMigrations || []), { version: AUTONOMOUS_STATE_CONTRACT, at: now, originalContract: original,
42
+ previous: before, applied: after, source: 'existing_auto_continue_consent' }].slice(-20);
43
+ m.controllerContractVersion = AUTONOMOUS_STATE_CONTRACT;
44
+ }
45
+ const canRecover = metadata.isRunning === false && !m.pendingApproval && !m.userQuestion &&
46
+ !m.understandingQuestion && !m.dependencyWait && !m.providerWarning && !m.nativeWait && !m.executionUncertain;
47
+ if (canRecover && run.state === 'paused' && pauseOrigin(run) === 'system') {
48
+ run.stopReason = { actor: 'system', code: 'legacy_no_progress', at: now, evidence: run.reason };
49
+ run.state = 'recovery_required'; run.recoveryCause = 'session_supervision';
50
+ queuePurposeDiagnosis(run, 'Independently diagnose the original outcome after a system no-progress pause. Preserve prior effects, failure history and budgets.', `upgrade:${original}`, now);
51
+ } else if (canRecover && run.state === 'verification_pending' && m.acceptanceCriteria.some(c => c.check.type === 'semantic' && !m.verified[c.id])) {
52
+ queuePurposeVerification(run, 'Existing outcome awaits independent evidence inspection; inspect original instructions and actual artifacts, not a worker completion claim.', now);
53
+ }
54
+ return { changed: before !== after || run.state !== previousState, reason: 'applied', originalContract: original };
55
+ }