@awak-app/simy-cli 0.5.10 → 0.5.12

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.
@@ -1,3 +1,4 @@
1
+ import { minimumCharter } from "../codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-minimum-completion.mjs";
1
2
  import { bullets, looksLikeUiTask, section } from "./shared.js";
2
3
  import { problemSolvingResultContract } from "./problem-solving.js";
3
4
  import { acceptanceCriterionRows } from "./completion-contract.js";
@@ -36,6 +37,12 @@ export function buildCodingInstruction(
36
37
  "completion_contract",
37
38
  [
38
39
  `User-visible outcome: ${charter.completion_contract?.user_outcome || charter.thread_state.user_goal}`,
40
+ `Beneficiary: ${charter.completion_contract?.experience?.beneficiary || "not recorded"}`,
41
+ `Situation: ${charter.completion_contract?.experience?.situation || "not recorded"}`,
42
+ `Experience goal: ${charter.completion_contract?.experience?.goal || "not recorded"}`,
43
+ `Usable outcome: ${charter.completion_contract?.experience?.usable_outcome || "not recorded"}`,
44
+ `Recovery experience: ${charter.completion_contract?.experience?.recovery || "not recorded"}`,
45
+ `Forbidden experiences:\n${bullets(charter.completion_contract?.experience?.forbidden_experiences || [])}`,
39
46
  `Minimum scope:\n${bullets(charter.completion_contract?.minimum_scope || charter.acceptance_criteria)}`,
40
47
  `Non-goals:\n${bullets(charter.completion_contract?.non_goals || charter.thread_state.non_goals)}`,
41
48
  `Proof required:\n${bullets(charter.completion_contract?.proof_required || [])}`,
@@ -50,6 +57,17 @@ export function buildCodingInstruction(
50
57
  ),
51
58
  ),
52
59
  ),
60
+ section("minimum_completion_evidence", [
61
+ "All delivery levels require this floor. Before implementation reconcile every original requirement and frozen experience with the stable criterion-ID list; report omissions instead of silently dropping them.",
62
+ "Save a matrix.json under the local evidence root (ui_evidence_root or repository .artifacts). Return delivery_evidence_path in SIMY_RESULT_JSON. A passed flag or tests_run string is not proof.",
63
+ "Use check-minimum-completion.mjs from the installed Delivery Loop skill. Its reference minimum-completion.md defines the matrix and structured observation format.",
64
+ "For each stable criterion ID include real implementation_refs and required user/system evidence. Exercise the actual operation and result, then inspect postcondition and integration independently. For saving, reload/return in a new session and verify stored values and ownership. Document-only work uses the real command/consumer and artifact integrity, not imaginary API/DB checks.",
65
+ "First evidence artifact per layer must be JSON: {criterion_id, context_sha256, checks}. User checks: operation (expected=true, actual=observed boolean) and result; system checks: postcondition and integration. Each check includes subject, expected and actual. Derive expected values from the original requirement, not from the candidate output. Include screenshots/logs as additional evidence.",
66
+ "Freeze and bind observations to the current committed revision and charter before verification. Do not rehash old observations to make changed code pass. Use pending/failed for unavailable or mismatching evidence.",
67
+ `Controller input JSON: ${JSON.stringify({ id: charter.id, requirement: charter.requirement, repository: charter.repository, acceptance_criteria: charter.acceptance_criteria, completion_contract: charter.completion_contract, thread_state: { original_request: charter.thread_state.original_request } })}`,
68
+ `Controller-owned acceptance specification: ${JSON.stringify(minimumCharter(charter))}`,
69
+ `Context: run_id=${charter.id}, stage=locally_verified, environment=local, fixture_id=${charter.id}; input_sha256 is contextDigest(spec.source); source_revisions maps ${charter.repository} to current full HEAD.`,
70
+ ].join("\n")),
53
71
  section("expected_tests", bullets(charter.expected_tests)),
54
72
  section("must_not", bullets(charter.must_not)),
55
73
  ];
@@ -130,10 +148,10 @@ export function buildCodingInstruction(
130
148
  "result_contract",
131
149
  [
132
150
  "Finish with exactly one line beginning SIMY_RESULT_JSON: followed by one JSON object.",
133
- "Required keys: outcome_kind, summary, branch_name, commit_sha, commit_message_headline, pr_url, pr_number, pr_title, pr_base_branch, tests_run, tests_passed, ui_evidence_path, unrelated_changes_detected, secret_scan_passed, changed_files, residual_risks, problem_solving, uncertainty_verification, criterion_evidence, visual_review.",
151
+ "Required keys: outcome_kind, summary, branch_name, commit_sha, commit_message_headline, pr_url, pr_number, pr_title, pr_base_branch, tests_run, tests_passed, ui_evidence_path, unrelated_changes_detected, secret_scan_passed, changed_files, residual_risks, problem_solving, uncertainty_verification, criterion_evidence, visual_review, delivery_evidence_path.",
134
152
  problemSolvingResultContract(),
135
153
  "uncertainty_verification must contain top_uncertainty, first_verification, evidence_checked, observed_result, resolved, and fallback. Only set resolved=true when concrete observed evidence resolves it.",
136
- "criterion_evidence must contain one entry for every AC-N with criterion_id, passed, verification_method, evidence_refs, and observed_result. Only set passed=true when the evidence directly proves that criterion.",
154
+ "criterion_evidence must contain one entry for every stable criterion ID with criterion_id, passed, verification_method, evidence_refs, and observed_result. Only set passed=true when the evidence directly proves that criterion.",
137
155
  "For UI or browser-extension work, visual_review must contain inspected=true, the local screenshot or video paths in artifacts, and concrete acceptance_observations from viewing the actual content. Capturing a file without viewing it does not count.",
138
156
  "Use null for unavailable scalar values and [] for unavailable arrays. Do not claim evidence that was not observed.",
139
157
  ].join("\n"),
@@ -1,3 +1,4 @@
1
+ import { minimumCharter } from "../codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-minimum-completion.mjs";
1
2
  import { auditAttempt, evaluatePrReadiness } from "./audit.js";
2
3
  import {
3
4
  buildIndependentAudit,
@@ -9,7 +10,7 @@ import { buildAttempt, mergeTokenUsage } from "./result.js";
9
10
  import { evaluateRetryCircuit } from "./retry.js";
10
11
  import { appendEvent, publish } from "./shared.js";
11
12
 
12
- export async function runCodingLoop({
13
+ async function runCodingLoopOwned({
13
14
  snapshot,
14
15
  executeAttempt,
15
16
  executeIndependentAudit,
@@ -21,6 +22,19 @@ export async function runCodingLoop({
21
22
  shouldStop = () => false,
22
23
  consumeHumanGuidance = () => "",
23
24
  }) {
25
+ if (!resume && snapshot.attempts.length > 0) {
26
+ return recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm, onUpdate });
27
+ }
28
+ snapshot.implementation_gate_passed = false;
29
+ snapshot.done_gate_passed = false;
30
+ try { minimumCharter(snapshot.charter); } catch {
31
+ snapshot.final_audit = humanGate("Completion charter requires refresh before verification.", [{
32
+ code: "MINIMUM_CHARTER_REQUIRED", severity: "blocker", target: "completion_contract",
33
+ explanation: "Missing or unsupported completion charter. Preserve history, re-charter and collect current minimum evidence.", repairability: "manual",
34
+ }]);
35
+ await publish(snapshot, "waiting_human", onUpdate);
36
+ return snapshot;
37
+ }
24
38
  if (!resume || snapshot.attempts.length === 0) {
25
39
  appendEvent(snapshot, "risk_classifying", "PR risk classification completed.", {
26
40
  risk: snapshot.charter.risk,
@@ -197,6 +211,7 @@ export async function runCodingLoop({
197
211
  await publish(snapshot, "independent_auditing", onUpdate);
198
212
  if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
199
213
  const auditInstruction = buildIndependentAuditInstruction(snapshot.charter, attempt);
214
+ const reviewedMatrix = attempt.observed_evidence?.minimum_completion?.matrix_sha256;
200
215
  const auditExecution = await safeExecution(() =>
201
216
  executeIndependentAudit({
202
217
  attemptNumber,
@@ -215,6 +230,8 @@ export async function runCodingLoop({
215
230
  attempt.token_usage = mergeTokenUsage(attempt.token_usage, auditExecution.tokenUsage);
216
231
  attempt.budget = refreshBudgetState(snapshot);
217
232
 
233
+ attempt.independent_audit.minimum_matrix_sha256 = reviewedMatrix;
234
+
218
235
  // Re-collect after the read-only auditor to catch any mutated HEAD or working tree.
219
236
  attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
220
237
  attempt.sqm = await safeSqm(checkSqm, attempt, attempt.sqm);
@@ -276,9 +293,11 @@ async function stopCodingLoop(snapshot, onUpdate) {
276
293
  return snapshot;
277
294
  }
278
295
 
279
- export async function recheckPrReadiness({ snapshot, collectEvidence, checkSqm = null, onUpdate }) {
296
+ async function recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm = null, onUpdate }) {
280
297
  const attempt = snapshot.attempts.at(-1);
281
- if (!attempt || !attempt.audit?.passed || !attempt.independent_audit?.passed) return snapshot;
298
+ snapshot.implementation_gate_passed = false;
299
+ snapshot.done_gate_passed = false;
300
+ if (!attempt) return snapshot;
282
301
  appendEvent(snapshot, "checking_pr", "Refreshing GitHub review and CI evidence.", {
283
302
  attempt_number: attempt.attempt_number,
284
303
  });
@@ -296,7 +315,7 @@ export async function recheckPrReadiness({ snapshot, collectEvidence, checkSqm =
296
315
  snapshot.final_audit = aggregateAudit(
297
316
  attempt,
298
317
  attempt.pr_readiness.ready,
299
- attempt.independent_audit.findings || [],
318
+ attempt.independent_audit?.findings || [],
300
319
  );
301
320
  appendEvent(snapshot, attempt.pr_readiness.state, attempt.pr_readiness.summary, {
302
321
  pr_url: attempt.pr_url,
@@ -583,3 +602,14 @@ function humanGate(summary, findings) {
583
602
  release_gate: null,
584
603
  };
585
604
  }
605
+
606
+ const activeControllers = new Set();
607
+ async function withController(options, action) {
608
+ const owner = options.snapshot?.id;
609
+ if (typeof owner !== "string" || !owner) throw new Error("Delivery snapshot ID required");
610
+ if (activeControllers.has(owner)) throw new Error("Delivery controller is already active for this snapshot");
611
+ activeControllers.add(owner);
612
+ try { return await action(options); } finally { activeControllers.delete(owner); }
613
+ }
614
+ export const runCodingLoop = options => withController(options, runCodingLoopOwned);
615
+ export const recheckPrReadiness = options => withController(options, recheckPrReadinessOwned);
@@ -0,0 +1,108 @@
1
+ import path from "node:path";
2
+ import { lstat, realpath } from "node:fs/promises";
3
+ import {
4
+ minimumCharter,
5
+ minimumContext,
6
+ checkMinimumCompletion,
7
+ loadMinimumJson,
8
+ } from "../codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-minimum-completion.mjs";
9
+ import {
10
+ contextDigest,
11
+ sha256,
12
+ } from "../codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-acceptance.mjs";
13
+
14
+ export function minimumCharterDigest(charter) {
15
+ return sha256(Buffer.from(JSON.stringify(minimumCharter(charter))));
16
+ }
17
+
18
+ export async function collectMinimumCompletion({
19
+ charter,
20
+ attempt,
21
+ repositoryPath,
22
+ local,
23
+ }) {
24
+ try {
25
+ if (!local.available || !local.working_tree_clean)
26
+ throw new Error("Clean current checkout required");
27
+ const root = await realpath(
28
+ charter.ui_evidence_root || path.join(repositoryPath, ".artifacts"),
29
+ );
30
+ const requested = attempt.raw_output?.delivery_evidence_path;
31
+ if (typeof requested !== "string") throw new Error("Matrix path missing");
32
+ const file = path.resolve(root, requested);
33
+ const relative = path.relative(root, file);
34
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative))
35
+ throw new Error("Outside evidence root");
36
+ let current = root;
37
+ for (const segment of relative.split(path.sep)) {
38
+ current = path.join(current, segment);
39
+ if ((await lstat(current)).isSymbolicLink())
40
+ throw new Error("Symlink evidence path");
41
+ }
42
+ const matrix = await loadMinimumJson(file);
43
+ const report = await checkMinimumCompletion({
44
+ charter,
45
+ matrix,
46
+ context: minimumContext(charter, local.head_sha),
47
+ evidenceRoot: path.dirname(file),
48
+ });
49
+ if (report.status !== "evidence_complete") return report;
50
+ // Check mapping paths without reading source contents or following links outside the checkout.
51
+ const repo = await realpath(repositoryPath);
52
+ for (const row of matrix.results) {
53
+ for (const ref of row.implementation_refs || []) {
54
+ if (typeof ref !== "string") continue;
55
+ const target = await realpath(path.resolve(repo, ref));
56
+ const rel = path.relative(repo, target);
57
+ if (
58
+ rel.startsWith("..") ||
59
+ path.isAbsolute(rel) ||
60
+ !(await lstat(target)).isFile()
61
+ )
62
+ throw new Error("Invalid implementation mapping");
63
+ }
64
+ }
65
+ if (contextDigest(await loadMinimumJson(file)) !== report.matrix_sha256)
66
+ throw new Error("Matrix changed during verification");
67
+ return { ...report, evidence_path: file };
68
+ } catch {
69
+ return {
70
+ status: "evidence_incomplete",
71
+ errors: [
72
+ "Minimum completion evidence missing, invalid or stale; verify the actual operation, result, postcondition and integration for every stable criterion ID.",
73
+ ],
74
+ };
75
+ }
76
+ }
77
+
78
+ export function minimumCompletionCheck(charter, attempt) {
79
+ const report = attempt.observed_evidence?.minimum_completion;
80
+ const head = attempt.observed_evidence?.local?.head_sha;
81
+ let passed = false;
82
+ try {
83
+ passed =
84
+ report?.minimum_completion_version === 1 &&
85
+ report.status === "evidence_complete" &&
86
+ report.errors?.length === 0 &&
87
+ report.charter_sha256 === minimumCharterDigest(charter) &&
88
+ report.context_sha256 === contextDigest(minimumContext(charter, head)) &&
89
+ /^[a-f0-9]{64}$/.test(report.matrix_sha256) &&
90
+ report.head_sha === head &&
91
+ attempt.observed_evidence.local.working_tree_clean === true;
92
+ } catch {
93
+ /* Unsupported/missing charters never waive this gate. */
94
+ }
95
+ return {
96
+ id: "minimum_completion",
97
+ label: "Every requested outcome has current operation and system evidence",
98
+ passed: Boolean(passed),
99
+ detail: passed
100
+ ? "Current minimum evidence verified; independent coverage/oracle review and remaining gates still apply."
101
+ : (
102
+ report?.errors || [
103
+ "Minimum completion evidence has not been verified.",
104
+ ]
105
+ ).join("; "),
106
+ evidence_refs: report?.evidence_path ? [report.evidence_path] : [],
107
+ };
108
+ }
@@ -1,3 +1,6 @@
1
+ import { minimumCharterDigest } from "./minimum-completion.js";
2
+ import { minimumContext } from "../codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-minimum-completion.mjs";
3
+ import { contextDigest } from "../codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-acceptance.mjs";
1
4
  import { cleanString, stringArray } from "./shared.js";
2
5
  import { normalizeProblemSolving } from "./problem-solving.js";
3
6
  import {
@@ -15,6 +18,8 @@ export function buildAttempt({ attemptNumber, charter, instruction, promptInterv
15
18
  const completed = execution.exitCode === 0 && !execution.error;
16
19
  const now = new Date().toISOString();
17
20
  return {
21
+ minimum_charter_sha256: safeDigest(() => minimumCharterDigest(charter)),
22
+ minimum_context_sha256: safeDigest(() => contextDigest(minimumContext(charter, cleanString(result.commit_sha)))),
18
23
  attempt_number: attemptNumber,
19
24
  backend: charter.backend,
20
25
  base_branch: charter.base_branch,
@@ -112,3 +117,5 @@ function buildArtifactPreviews(result) {
112
117
  function isUsageRecord(value) {
113
118
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
114
119
  }
120
+
121
+ function safeDigest(compute) { try { return compute(); } catch { return null; } }