@awak-app/simy-cli 0.5.11 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@ import {
13
13
  } from "./durable-local-task-steps-contract.js";
14
14
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
15
15
 
16
- export const CLI_VERSION = "0.5.11";
16
+ export const CLI_VERSION = "0.5.12";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -33,9 +33,20 @@ external-message authority and never weakens repository-required gates.
33
33
 
34
34
  ## Freeze the delivery charter
35
35
 
36
+ Start with the beneficiary's experience, not the proposed solution. Read-only
37
+ discovery may supply facts, but before solution design or coding freeze a compact
38
+ experience contract: who is in what situation, what they are trying to achieve,
39
+ the complete usable outcome, what they should understand and do next at each
40
+ observable moment, recovery and forbidden experiences, and how each moment will
41
+ be observed. Give each moment a stable UX/acceptance ID and carry those same IDs
42
+ through the shared To-Be, implementation mapping, and results matrix. Do not use
43
+ a list of components, endpoints, screens, or tasks as a substitute for the
44
+ end-to-end experience.
45
+
36
46
  Before material action, record:
37
47
 
38
- - objective and user-visible success conditions;
48
+ - objective, beneficiary, and the frozen experience contract with user-visible
49
+ success conditions;
39
50
  - authoritative acceptance sources that are independent of the candidate
40
51
  implementation, plus architecture boundaries and dependency direction;
41
52
  - repositories, latest base branches, dedicated worktrees, environments, and
@@ -58,8 +69,9 @@ changes the requested result.
58
69
 
59
70
  Before implementation, read [shared system specifications](references/shared-system-specifications.md).
60
71
  Find the team's canonical specification for this system; create it once if absent.
61
- Record purpose, beneficiary, current pain, intended user experience and observable
62
- success. Reference the current As-Is revision and update only affected To-Be views.
72
+ Reconcile the frozen experience contract with the current As-Is, then update only
73
+ the affected To-Be views. Preserve the purpose, beneficiary, current pain,
74
+ intended user experience, observable success, and stable UX/acceptance IDs.
63
75
  Maintain the twelve-view inventory, stable IDs and cross-view impact links in the
64
76
  shared source, not twelve new task-local copies. Unaffected views retain their
65
77
  version; unknown As-Is facts remain unknown. Review changes through the team's
@@ -84,6 +96,10 @@ that historical failures cannot recur.
84
96
  Before implementation, read [acceptance evidence](references/acceptance-evidence.md)
85
97
  and persist a criterion-by-criterion checklist. Bind the criteria and both
86
98
  verification layers to independent acceptance sources in a frozen charter.
99
+ Use the experience contract as the user-facing slice of these same criteria, not
100
+ as a parallel checklist. Every observable moment from the real trigger through a
101
+ usable outcome and applicable recovery path must map to at least one criterion;
102
+ working components do not prove that the combined experience is complete.
87
103
  Use a separate current-results matrix for observations, status, evidence,
88
104
  unmet reasons, and next actions. Render a readable table alongside the JSON;
89
105
  checklists held only in chat or the model's memory are insufficient.
@@ -100,6 +116,15 @@ values, ownership, authorization, and duplication. HTTP 500/network failures are
100
116
  failure evidence; absence of a 500 or presence of a 200 does not prove persistence.
101
117
  Record justified non-applicability before execution, not after a failed test.
102
118
 
119
+ After building the exact candidate in the production-like local environment,
120
+ execute the complete experience and regenerate the readable table. It must let a
121
+ reviewer compare, for every stable ID, the persona/context and expected visible
122
+ outcome, the actual user observation, the independent technical postcondition or
123
+ integration result, evidence, status, and the next repair. Inspect the table as a
124
+ whole to catch gaps between individually working parts. Any missing, pending,
125
+ failed, stale, or unverifiable applicable experience row blocks
126
+ `user_like_local_verified` and returns the loop to the smallest affected repair.
127
+
103
128
  Run the packaged acceptance validator before promoting a relevant verification
104
129
  gate and before issuing the completion receipt. A missing, pending, failed, stale,
105
130
  or unverifiable applicable row prevents promotion. Matrix completeness is one
@@ -114,7 +139,7 @@ For persistence, require reload/session return and stored values/ownership. A
114
139
  working control, success message or HTTP 200 alone is insufficient.
115
140
 
116
141
  The bundled minimum checker extends the acceptance matrix with structured
117
- observations. Compare the original request to the AC-N list independently so
142
+ observations. Compare the original request to the stable criterion-ID list independently so
118
143
  omitted requirements cannot disappear before validation. A missing, failed or
119
144
  stale floor check prevents verified completion at any level. The CLI now enforces
120
145
  this before implementation/PR promotion and on recheck; the standalone command
@@ -301,6 +326,12 @@ the charter permits, then have Codex operate the user's Chrome through the
301
326
  browser extension as a normal user. Do not replace this gate with direct API
302
327
  calls or a fixed selector-only test.
303
328
 
329
+ Immediately after the journey, refresh and inspect the experience/acceptance table
330
+ for the same candidate revision, environment, fixture, and input. Compare the
331
+ entire trigger-to-usable-outcome path, including what the user understands and can
332
+ do next, with the frozen experience contract. Do not promote the local journey
333
+ because its individual controls, requests, or services worked in isolation.
334
+
304
335
  Read [Codex Chrome user verification](references/chrome-user-verification.md)
305
336
  before running this phase.
306
337
 
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Before implementation
4
4
 
5
+ Freeze the user experience before proposing the solution. Read-only discovery may
6
+ inform it, but the first authored success record states the beneficiary and
7
+ context, their goal, the complete usable outcome, what they should see,
8
+ understand, and be able to do next at each observable moment, applicable recovery
9
+ and forbidden experiences, and how those moments will be observed. Assign stable
10
+ UX/acceptance IDs. A screen, component, endpoint, task, or stored row is an
11
+ implementation detail unless it is tied to this experience.
12
+
5
13
  The controller externalizes every acceptance condition with a stable ID, observable
6
14
  outcome, source independent of the candidate, and required checks. For each
7
15
  verification gate, freeze its applicable criteria in `charter.json` and record the
@@ -9,6 +17,13 @@ SHA-256 of the exact file bytes in the controller-owned snapshot. Reconcile the
9
17
  gate charters against the overall delivery charter so no requirement disappears.
10
18
  Do not choose a convenient subset after observing failures.
11
19
 
20
+ Use these criteria as the structured form of the experience contract rather than
21
+ creating a second UX checklist. Cover the full path from its real trigger to its
22
+ usable outcome and the applicable recovery path. When several parts must work
23
+ together for the beneficiary to succeed, include an end-to-end criterion in
24
+ addition to narrower diagnostic criteria. Independent coverage review compares
25
+ the experience contract and original request to the frozen rows before coding.
26
+
12
27
  Each criterion has two layers:
13
28
 
14
29
  - `user`: a goal-driven user journey with visible interactions and evidence of the
@@ -49,6 +64,15 @@ After every evaluated action (test, repair, review or changed inputs):
49
64
  All rows, including pending/failed ones, stay visible. A green matrix cannot advance
50
65
  an unrelated gate or prove merge/deployment. Validate again for the final environment.
51
66
 
67
+ After the exact candidate is built in a production-like local environment, run
68
+ the complete experience and regenerate `checklist.md` for that same revision,
69
+ environment, fixture, and input. Read the user and system rows together as a
70
+ single experience table: expected outcome, actual user observation, independent
71
+ postcondition/integration result, evidence, status, and next repair. Missing,
72
+ pending, failed, stale, or unverifiable applicable rows prevent
73
+ `user_like_local_verified`. Individually working components cannot override a
74
+ failed or absent end-to-end row.
75
+
52
76
  ## File contract
53
77
 
54
78
  Charter example (illustrative values must be replaced with actual requirements):
@@ -59,7 +83,7 @@ Charter example (illustrative values must be replaced with actual requirements):
59
83
  "run_id": "save-run-42",
60
84
  "criteria": [{
61
85
  "id": "SAVE-1",
62
- "outcome": "The owner can recover the saved note after returning",
86
+ "outcome": "An owner returning to the task sees the saved note, understands it was retained, and can continue editing",
63
87
  "acceptance_source": "Approved issue acceptance criteria, revision 3",
64
88
  "layers": {
65
89
  "user": {"applicability": "required", "method": "Save in Chrome, reload, sign in again and compare the note"},
@@ -9,19 +9,25 @@ in an executor result or assurance-level selection disables the gate.
9
9
  ## Collect real observations
10
10
 
11
11
  Before implementation, reconcile the original request, acceptance criteria and
12
- planned operations. Preserve every requested outcome; a missing requirement must
12
+ planned operations against the frozen experience contract. Preserve every
13
+ observable moment from the real trigger to a usable result, including applicable
14
+ recovery and forbidden experiences; a missing requirement must
13
15
  be added through the controller's charter process, not omitted from the matrix.
14
16
  The independent auditor compares original-request coverage and checks the oracle
15
17
  against raw evidence. Machine mapping checks cannot discover an omitted idea in
16
18
  natural language or authenticate what an evidence collector observed.
17
19
 
18
- For each AC-N record implementation_refs (repository-relative file paths), then:
20
+ For each stable criterion ID record implementation_refs (repository-relative file
21
+ paths). New UX-first charters preserve the IDs supplied by the experience
22
+ contract; legacy charters without explicit IDs retain deterministic `AC-N` IDs.
23
+ Then:
19
24
 
20
25
  - user/operation: operate the actual UI or CLI/consumer. Expected is true; actual
21
26
  is whether the intended operation really executed, not just whether a click
22
27
  command succeeded. Include screenshots/traces where relevant.
23
28
  - user/result: observe the requested visible outcome, including reload and session
24
- return for a saving feature. Compare expected and actual values.
29
+ return for a saving feature. Compare expected and actual values, user
30
+ understanding, and the next action the experience makes available.
25
31
  - system/postcondition: independently inspect the final state; for persistence,
26
32
  include the saved values and ownership, not a toast or HTTP status.
27
33
  - system/integration: inspect the necessary combined path through APIs, storage,
@@ -33,6 +39,8 @@ For document-only work these mean opening the actual consumer and checking conte
33
39
  links and packaged bytes. No DB/API is required if none participates. Both generic
34
40
  layers remain required; there is no post-failure waiver. Tests, permissions or
35
41
  runtime unavailable means pending/blocked. Never fabricate expected/actual equality.
42
+ Passing narrower rows for controls, APIs, services, or artifacts cannot substitute
43
+ for the end-to-end row when the requested experience depends on their combination.
36
44
 
37
45
  ## Executable contract
38
46
 
@@ -59,7 +67,8 @@ identities in the evidence; the independent reviewer must validate their current
59
67
  identity. Do not claim that this single-checkout gate automatically re-reads a
60
68
  companion service's Git HEAD or production environment.
61
69
 
62
- Every matrix row adds `implementation_refs: ["src/example.js"]`. Each required
70
+ Every matrix row uses the same stable criterion ID and adds
71
+ `implementation_refs: ["src/example.js"]`. Each required
63
72
  user/system cell's **first** evidence file is JSON with this shape (user example):
64
73
 
65
74
  ```json
@@ -128,7 +137,7 @@ external provider calls are introduced.
128
137
 
129
138
  ## Shared specifications and independent review
130
139
 
131
- The floor's AC-N rows trace back to stable requirement IDs in the team's shared
140
+ The floor's stable criterion rows trace back to requirement IDs in the team's shared
132
141
  As-Is/To-Be specification. Follow [shared system specifications](shared-system-specifications.md)
133
142
  for purpose, user experience, cross-view consistency, independent reviews and
134
143
  pre/post-PR SQM. These are controller workflow obligations, not fields magically
@@ -15,23 +15,36 @@ An old design or green PR is not proof of current production behavior.
15
15
 
16
16
  At the start of each run:
17
17
 
18
- 1. Read the existing index and current linked views. Reconcile relevant As-Is with
18
+ 1. Freeze the experience contract from the original request before proposing a
19
+ solution: beneficiary/context, goal, complete usable outcome, observable
20
+ moments with what the person sees, understands, and can do next, recovery and
21
+ forbidden experiences, and the observation method. Read-only discovery may
22
+ inform this record. Give every moment a stable UX/acceptance ID.
23
+ 2. Read the existing index and current linked views. Reconcile relevant As-Is with
19
24
  code and actual behavior; label unknowns, cite evidence, and correct documented
20
25
  drift without claiming that the drift was part of the requested feature.
21
- 2. State why the change matters: beneficiary, current difficulty, what they can
22
- do after the change, observable success, and non-goals. Preserve the user's
23
- original request as an independent acceptance source.
24
- 3. Prepare a proposed To-Be on a branch/change proposal linked from the shared
26
+ 3. Reconcile why the change matters against that contract: current difficulty,
27
+ observable success, and non-goals. Preserve the user's original request as an
28
+ independent acceptance source; do not replace their outcome with a component
29
+ or implementation inventory.
30
+ 4. Prepare a proposed To-Be on a branch/change proposal linked from the shared
25
31
  system index. Record its baseline revision, affected stable IDs, before/after
26
32
  behavior, dependencies and acceptance oracles. Reuse unchanged views by reference;
27
33
  do not copy the whole specification into every run or generate empty diagrams.
28
- 4. Link the requirement to workflow, screen/control, state/sequence, API/event,
34
+ 5. Link each experience ID to workflow, screen/control, state/sequence, API/event,
29
35
  data/relationship, permission and runtime boundaries as applicable. Map these
30
- stable system IDs to the run's AC-N rows and implementation/evidence locations.
31
- 5. Obtain independent design review before implementation. Supply the original
36
+ stable system IDs to the run's criterion rows and implementation/evidence locations.
37
+ 6. Obtain independent design review before implementation. Supply the original
32
38
  request, shared baseline, To-Be and linked unchanged views. Missing evidence or
33
39
  contradictory views remains pending/failed, never silently accepted.
34
40
 
41
+ After the exact local candidate is built, update the existing acceptance matrix
42
+ and its readable table for those same experience IDs. Compare expected experience,
43
+ actual user observation, independent system result, evidence, status, and next
44
+ repair across the whole journey. This is a view of the canonical acceptance data,
45
+ not another specification. A gap between working parts blocks local user-journey
46
+ promotion even if every part passed a narrower test.
47
+
35
48
  The twelve-view index must account for every category: present (with a canonical
36
49
  link), unchanged (with a revision), not applicable (with a reason), or unknown
37
50
  (with an owner/next action). Only create/update a diagram where it describes a
@@ -9,6 +9,8 @@ Persist one controller-owned snapshot with at least:
9
9
 
10
10
  - `run_id`, `state`, monotonic `revision`, and terminal disposition;
11
11
  - immutable explicit conditions and their stable digest;
12
+ - frozen experience-contract identities, their mapping to acceptance rows, and
13
+ the current readable local-experience table path and digest;
12
14
  - acceptance-source identities and digests, architecture boundaries, applicable
13
15
  rule decisions, risk tier, requested/effective delivery levels, the safety
14
16
  ceiling reason, and chat-confirmation receipt;
@@ -156,5 +158,8 @@ with current context and the required user/system layers. After each evaluated
156
158
  action, refresh the JSON matrix and its readable table; append a receipt for the
157
159
  update. Keep the frozen charter distinct from mutable results. A charter patch
158
160
  supersedes the old digest only after the controller verifies its authority.
161
+ For `user_like_local_verified`, the current table must cover the frozen experience
162
+ contract from real trigger to usable outcome for the exact local candidate. A
163
+ collection of narrower passing rows does not waive a missing end-to-end row.
159
164
 
160
165
  The CLI records minimum evidence in observed_evidence.minimum_completion, independently of executor status. The independent audit is bound to its matrix digest. Legacy v2 histories remain readable, but missing minimum evidence cannot be reused as passing evidence on resume/recheck. Missing or unsupported charters require a refresh; they do not disable verification.
@@ -25,7 +25,15 @@ function requireValue(condition, message) {
25
25
  }
26
26
 
27
27
  function validateCharter(charter) {
28
- requireValue(object(charter) && charter.schema_version === 1 && text(charter.run_id), "Invalid charter header");
28
+ requireValue(object(charter) && [1, 2].includes(charter.schema_version) && text(charter.run_id), "Invalid charter header");
29
+ if (charter.schema_version === 2) {
30
+ requireValue(object(charter.experience), "Experience contract required");
31
+ for (const field of ["beneficiary", "situation", "goal", "usable_outcome", "recovery"])
32
+ requireValue(text(charter.experience[field]), `Experience contract needs ${field}`);
33
+ requireValue(Array.isArray(charter.experience.forbidden_experiences) &&
34
+ charter.experience.forbidden_experiences.length > 0 &&
35
+ charter.experience.forbidden_experiences.every(text), "Experience contract needs forbidden experiences");
36
+ }
29
37
  requireValue(Array.isArray(charter.criteria) && charter.criteria.length > 0, "Charter requires criteria");
30
38
  const ids = new Set();
31
39
  for (const criterion of charter.criteria) {
@@ -88,15 +96,24 @@ export async function readEvidenceArtifact(root, artifact) {
88
96
  const tableText = (value) => String(value ?? "").replaceAll("|", "\\|").replace(/[\r\n]+/g, " ");
89
97
 
90
98
  export function renderChecklist(report) {
99
+ const experienceContext = report.experience
100
+ ? `${report.experience.beneficiary} / ${report.experience.situation}`
101
+ : "Legacy charter: persona and situation not recorded";
91
102
  const header = [
92
- "| Criterion / outcome | Layer / method | Status | Observation | Evidence | Unmet reason / next action |",
93
- "| --- | --- | --- | --- | --- | --- |",
103
+ "| Experience criterion / expected outcome | Persona / situation | Layer / method | Status | Actual observation | Evidence | Unmet reason / next repair |",
104
+ "| --- | --- | --- | --- | --- | --- | --- |",
94
105
  ];
95
106
  const rows = report.checks.map((row) => "| " + [
96
- `${row.id}: ${row.outcome}`, `${row.layer}: ${row.method}`, row.status,
107
+ `${row.id}: ${row.outcome}`, experienceContext,
108
+ `${row.layer}: ${row.method}`, row.status,
97
109
  row.observation, row.evidence.join(", "), [row.reason, row.next_action].filter(Boolean).join(" / "),
98
110
  ].map(tableText).join(" | ") + " |");
111
+ const context = report.context;
112
+ const candidate = context
113
+ ? `Candidate context: stage=${tableText(context.stage)}; environment=${tableText(context.environment)}; fixture=${tableText(context.fixture_id)}; input_sha256=${tableText(context.input_sha256)}; source_revisions=${tableText(JSON.stringify(context.source_revisions))}`
114
+ : "Candidate context: unavailable";
99
115
  return [...header, ...rows, "", `Validator: ${report.status}`,
116
+ candidate,
100
117
  ...report.errors.map((error) => `- ${tableText(error)}`)].join("\n") + "\n";
101
118
  }
102
119
 
@@ -111,7 +128,8 @@ export async function checkAcceptance({ charterBytes, matrix, expectedCharterSha
111
128
  requireValue(contextDigest(matrix.context) === expectedContextSha256, "Current context digest mismatch");
112
129
  requireValue(Array.isArray(matrix.results), "Matrix requires results");
113
130
  const root = await realpath(evidenceRoot);
114
- const report = { status: "evidence_incomplete", checks: [], errors: [] };
131
+ const report = { status: "evidence_incomplete", experience: charter.experience ?? null,
132
+ context: matrix.context, checks: [], errors: [] };
115
133
  const results = new Map();
116
134
  const expectedIds = new Set(charter.criteria.map((criterion) => criterion.id));
117
135
  for (const row of matrix.results) {
@@ -35,13 +35,22 @@ export function minimumCharter(charter) {
35
35
  charter.completion_contract.minimum_scope || charter.acceptance_criteria,
36
36
  non_goals: charter.completion_contract.non_goals || [],
37
37
  };
38
+ const ids = criterionIds(
39
+ charter.completion_contract.criterion_ids,
40
+ charter.acceptance_criteria.length,
41
+ );
42
+ const experience = minimumExperience(
43
+ charter.completion_contract.experience,
44
+ charter.completion_contract.user_outcome || charter.requirement,
45
+ );
38
46
  return {
39
- schema_version: 1,
47
+ schema_version: 2,
40
48
  minimum_completion_version: MINIMUM_COMPLETION_VERSION,
41
49
  run_id: charter.id,
42
50
  source,
51
+ experience,
43
52
  criteria: charter.acceptance_criteria.map((outcome, index) => ({
44
- id: `AC-${index + 1}`,
53
+ id: ids[index],
45
54
  outcome,
46
55
  acceptance_source: `request:${contextDigest(source)}`,
47
56
  layers: {
@@ -60,6 +69,49 @@ export function minimumCharter(charter) {
60
69
  };
61
70
  }
62
71
 
72
+ function criterionIds(value, count) {
73
+ if (value === undefined || value === null)
74
+ return Array.from({ length: count }, (_, index) => `AC-${index + 1}`);
75
+ if (!Array.isArray(value)) throw new Error("Invalid acceptance criterion IDs");
76
+ const ids = value.map((id) => (text(id) ? id.trim() : ""));
77
+ if (
78
+ ids.length !== count ||
79
+ new Set(ids).size !== ids.length ||
80
+ ids.some(
81
+ (id) => !/^[A-Za-z][A-Za-z0-9]*(?:[-_.][A-Za-z0-9]+)*$/.test(id),
82
+ )
83
+ )
84
+ throw new Error("Invalid acceptance criterion IDs");
85
+ return ids;
86
+ }
87
+
88
+ function minimumExperience(value, userOutcome) {
89
+ const source = object(value) ? value : {};
90
+ return {
91
+ beneficiary: text(source.beneficiary)
92
+ ? source.beneficiary.trim()
93
+ : "The requester",
94
+ situation: text(source.situation)
95
+ ? source.situation.trim()
96
+ : "Using the requested changed behavior",
97
+ goal: text(source.goal) ? source.goal.trim() : userOutcome,
98
+ usable_outcome: text(source.usable_outcome)
99
+ ? source.usable_outcome.trim()
100
+ : userOutcome,
101
+ recovery: text(source.recovery)
102
+ ? source.recovery.trim()
103
+ : "Unmet acceptance rows remain visible with the next repair.",
104
+ forbidden_experiences:
105
+ Array.isArray(source.forbidden_experiences) &&
106
+ source.forbidden_experiences.length > 0 &&
107
+ source.forbidden_experiences.every(text)
108
+ ? source.forbidden_experiences.map((item) => item.trim())
109
+ : [
110
+ "Reporting completion from components or commands without the end-to-end outcome",
111
+ ],
112
+ };
113
+ }
114
+
63
115
  export function minimumContext(charter, headSha) {
64
116
  const frozen = minimumCharter(charter);
65
117
  return {
@@ -1,6 +1,7 @@
1
1
  import { cleanString, stringArray } from "./shared.js";
2
2
 
3
3
  export const COMPLETION_CONTRACT_VERSION = 2;
4
+ const criterionIdPattern = /^[A-Za-z][A-Za-z0-9]*(?:[-_.][A-Za-z0-9]+)*$/;
4
5
 
5
6
  export function buildCompletionContract({
6
7
  inherited,
@@ -12,11 +13,15 @@ export function buildCompletionContract({
12
13
  const source = record(inherited);
13
14
  const minimumScope = stringArray(source.minimum_scope);
14
15
  const proofRequired = stringArray(source.proof_required);
16
+ const criteria = stringArray(acceptanceCriteria);
17
+ const userOutcome = cleanString(source.user_outcome) || cleanString(requirement);
15
18
  return {
16
19
  version: COMPLETION_CONTRACT_VERSION,
17
- user_outcome: cleanString(source.user_outcome) || cleanString(requirement),
20
+ user_outcome: userOutcome,
18
21
  minimum_scope:
19
- minimumScope.length > 0 ? minimumScope : stringArray(acceptanceCriteria),
22
+ minimumScope.length > 0 ? minimumScope : criteria,
23
+ criterion_ids: criterionIds(source.criterion_ids, criteria.length),
24
+ experience: normalizeExperience(source.experience, userOutcome),
20
25
  non_goals: stringArray(nonGoals),
21
26
  proof_required: uniqueStrings([
22
27
  "criterion_evidence",
@@ -29,12 +34,53 @@ export function buildCompletionContract({
29
34
  }
30
35
 
31
36
  export function acceptanceCriterionRows(charter) {
32
- return stringArray(charter?.acceptance_criteria).map((statement, index) => ({
33
- id: `AC-${index + 1}`,
37
+ const statements = stringArray(charter?.acceptance_criteria);
38
+ const ids = criterionIds(
39
+ record(charter?.completion_contract).criterion_ids,
40
+ statements.length,
41
+ );
42
+ return statements.map((statement, index) => ({
43
+ id: ids[index],
34
44
  statement,
35
45
  }));
36
46
  }
37
47
 
48
+ function criterionIds(value, count) {
49
+ if (value === undefined || value === null) {
50
+ return Array.from({ length: count }, (_, index) => `AC-${index + 1}`);
51
+ }
52
+ if (!Array.isArray(value)) throw new Error("Invalid acceptance criterion IDs");
53
+ const ids = value.map(cleanString);
54
+ if (
55
+ ids.length !== count ||
56
+ new Set(ids).size !== ids.length ||
57
+ ids.some((id) => !criterionIdPattern.test(id))
58
+ ) {
59
+ throw new Error("Invalid acceptance criterion IDs");
60
+ }
61
+ return ids;
62
+ }
63
+
64
+ function normalizeExperience(value, userOutcome) {
65
+ const source = record(value);
66
+ return {
67
+ beneficiary: cleanString(source.beneficiary) || "The requester",
68
+ situation:
69
+ cleanString(source.situation) || "Using the requested changed behavior",
70
+ goal: cleanString(source.goal) || userOutcome,
71
+ usable_outcome: cleanString(source.usable_outcome) || userOutcome,
72
+ recovery:
73
+ cleanString(source.recovery) ||
74
+ "Unmet acceptance rows remain visible with the next repair.",
75
+ forbidden_experiences:
76
+ stringArray(source.forbidden_experiences).length > 0
77
+ ? stringArray(source.forbidden_experiences)
78
+ : [
79
+ "Reporting completion from components or commands without the end-to-end outcome",
80
+ ],
81
+ };
82
+ }
83
+
38
84
  export function normalizeUncertaintyVerification(value) {
39
85
  const source = record(value);
40
86
  return {
@@ -65,6 +65,7 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
65
65
  const inheritedArtifacts = stringArray(charterContext.artifacts_required);
66
66
  const inheritedAssumptions = stringArray(charterContext.assumptions);
67
67
  const inheritedCompletionContract = record(charterContext.completion_contract);
68
+ const inheritedExperienceContract = record(charterContext.experience_contract);
68
69
  const finalAcceptanceCriteria =
69
70
  acceptanceCriteria.length > 0
70
71
  ? acceptanceCriteria
@@ -157,7 +158,17 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
157
158
  visual_review_required: browserEvidenceRequired,
158
159
  },
159
160
  completion_contract: buildCompletionContract({
160
- inherited: inheritedCompletionContract,
161
+ inherited: {
162
+ ...inheritedCompletionContract,
163
+ ...(Object.keys(inheritedExperienceContract).length > 0
164
+ ? {
165
+ experience: inheritedExperienceContract,
166
+ criterion_ids:
167
+ inheritedExperienceContract.criterion_ids ??
168
+ inheritedCompletionContract.criterion_ids,
169
+ }
170
+ : {}),
171
+ },
161
172
  requirement: cleanString(charterContext.user_goal) || cleanString(request.requirement),
162
173
  acceptanceCriteria: finalAcceptanceCriteria,
163
174
  nonGoals: finalNonGoals,
@@ -27,7 +27,8 @@ export function buildIndependentAuditInstruction(charter, attempt) {
27
27
  [
28
28
  `Requirement: ${charter.requirement}`,
29
29
  `Original request: ${charter.thread_state?.original_request || charter.requirement}`,
30
- `Acceptance criteria:\n${bullets(charter.acceptance_criteria)}`,
30
+ `Acceptance criteria:\n${bullets(acceptanceCriterionRows(charter).map(row => `${row.id}: ${row.statement}`))}`,
31
+ `Frozen experience (controller-owned): ${JSON.stringify(charter.completion_contract?.experience ?? null)}`,
31
32
  `User-visible outcome: ${charter.completion_contract?.user_outcome || charter.requirement}`,
32
33
  `Minimum scope:\n${bullets(charter.completion_contract?.minimum_scope || charter.acceptance_criteria)}`,
33
34
  `Risk level: ${charter.risk.level}`,
@@ -62,8 +63,8 @@ export function buildIndependentAuditInstruction(charter, attempt) {
62
63
  "Inspect the final diff and related code directly from the repository.",
63
64
  "Look for unmet acceptance criteria, regressions, security issues, data and concurrency risks, missing tests, and unrelated changes.",
64
65
  "Treat executor claims as untrusted until supported by repository evidence.",
65
- "Compare the original request to the complete AC-N list and implementation mapping. Report forgotten features, dead controls, unsaved results and broken integration. Open the raw minimum evidence, verify its oracle and actual runtime observations; matching expected/actual JSON and hashes alone do not prove truth. Check reload/session return and persisted values/ownership when saving is required.",
66
- "For every AC-N criterion, return a criterion_results entry only after directly checking its cited proof and observed outcome.",
66
+ "Compare the original request and frozen experience to the complete stable criterion-ID list and implementation mapping. Report forgotten features, dead controls, unsaved results and broken integration. Open the raw minimum evidence, verify its oracle and actual runtime observations; matching expected/actual JSON and hashes alone do not prove truth. Check reload/session return and persisted values/ownership when saving is required.",
67
+ "For every stable criterion ID, return a criterion_results entry only after directly checking its cited proof and observed outcome.",
67
68
  charter.evidence_policy?.browser_required
68
69
  ? "Open and inspect the actual local screenshots or videos. Return visual_review.inspected=true only after checking their visible content against the acceptance criteria."
69
70
  : "Do not require visual evidence when the Charter does not require browser evidence.",
@@ -75,9 +76,9 @@ export function buildIndependentAuditInstruction(charter, attempt) {
75
76
  [
76
77
  "Finish with exactly one line beginning SIMY_AUDIT_JSON: followed by one JSON object.",
77
78
  "Required keys: passed, summary, findings, criterion_results, visual_review, coverage_review.",
78
- "coverage_review must contain requirements_complete (boolean), unmapped_requirements (array), and observation explaining the comparison of the original request, AC-N list, implementation and raw user/system proof. Use false and list gaps when anything is missing or unverified.",
79
+ "coverage_review must contain requirements_complete (boolean), unmapped_requirements (array), and observation explaining the comparison of the original request, frozen experience, stable criterion-ID list, implementation and raw user/system proof. Use false and list gaps when anything is missing or unverified.",
79
80
  "Each finding candidate must contain passed, code, severity, target, explanation, and repairability.",
80
- "criterion_results must contain one entry per AC-N with criterion_id, passed, verification_method, evidence_refs, and observed_result.",
81
+ "criterion_results must contain one entry per stable criterion ID with criterion_id, passed, verification_method, evidence_refs, and observed_result.",
81
82
  "visual_review must contain inspected, artifacts, and acceptance_observations.",
82
83
  "Only passed=false entries are findings. Omit passing checks, and never report passed=true evidence as a finding.",
83
84
  "Use passed=false when a blocker or major finding remains, or when evidence is unavailable.",
@@ -37,6 +37,12 @@ export function buildCodingInstruction(
37
37
  "completion_contract",
38
38
  [
39
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 || [])}`,
40
46
  `Minimum scope:\n${bullets(charter.completion_contract?.minimum_scope || charter.acceptance_criteria)}`,
41
47
  `Non-goals:\n${bullets(charter.completion_contract?.non_goals || charter.thread_state.non_goals)}`,
42
48
  `Proof required:\n${bullets(charter.completion_contract?.proof_required || [])}`,
@@ -52,10 +58,10 @@ export function buildCodingInstruction(
52
58
  ),
53
59
  ),
54
60
  section("minimum_completion_evidence", [
55
- "All delivery levels require this floor. Before implementation reconcile every original requirement with the AC-N list; report omissions instead of silently dropping them.",
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.",
56
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.",
57
63
  "Use check-minimum-completion.mjs from the installed Delivery Loop skill. Its reference minimum-completion.md defines the matrix and structured observation format.",
58
- "For each AC-N 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.",
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.",
59
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.",
60
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.",
61
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 } })}`,
@@ -145,7 +151,7 @@ export function buildCodingInstruction(
145
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.",
146
152
  problemSolvingResultContract(),
147
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.",
148
- "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.",
149
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.",
150
156
  "Use null for unavailable scalar values and [] for unavailable arrays. Do not claim evidence that was not observed.",
151
157
  ].join("\n"),
@@ -69,7 +69,7 @@ export async function collectMinimumCompletion({
69
69
  return {
70
70
  status: "evidence_incomplete",
71
71
  errors: [
72
- "Minimum completion evidence missing, invalid or stale; verify the actual operation, result, postcondition and integration for every AC-N.",
72
+ "Minimum completion evidence missing, invalid or stale; verify the actual operation, result, postcondition and integration for every stable criterion ID.",
73
73
  ],
74
74
  };
75
75
  }