@haposoft/cafekit 0.16.0-rc.7 → 0.16.0-rc.8

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.
@@ -70,16 +70,138 @@ function candidateError(reason, code = 'malformed') {
70
70
 
71
71
  const WORKFLOW_STATUSES = new Set(['pending', 'in_progress', 'paused', 'blocked', 'done']);
72
72
  const WORKFLOW_TASK_NAME = /^task-[A-Za-z0-9][A-Za-z0-9._-]*\.md$/;
73
+ const WORKFLOW_CONTRACT = 'process-first-ready-v1';
74
+
75
+ function annotatedMarkdownLines(content) {
76
+ let fence = null;
77
+ return String(content || '').split(/\r?\n/).map((line, index) => {
78
+ const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
79
+ const outsideFence = fence === null && !fenceMatch;
80
+ let fenceEvent = null;
81
+ if (fenceMatch) {
82
+ const marker = fenceMatch[1];
83
+ if (!fence) {
84
+ fence = { char: marker[0], length: marker.length };
85
+ fenceEvent = 'open';
86
+ } else if (marker[0] === fence.char && marker.length >= fence.length && fenceMatch[2].trim() === '') {
87
+ fence = null;
88
+ fenceEvent = 'close';
89
+ }
90
+ }
91
+ return { index, line, outsideFence, fenceEvent };
92
+ });
93
+ }
94
+
95
+ function workflowPlanContract(content) {
96
+ const markers = annotatedMarkdownLines(content)
97
+ .filter(({ line, outsideFence }) => outsideFence && /^\s*Specs-Contract\s*:/i.test(line))
98
+ .map(({ line }) => line);
99
+ if (markers.length === 0) return { workflowContract: null, queueReady: false };
100
+ if (markers.length !== 1 || markers[0] !== `Specs-Contract: ${WORKFLOW_CONTRACT}`) {
101
+ throw candidateError(`plan.md must contain exactly one Specs-Contract: ${WORKFLOW_CONTRACT} marker`);
102
+ }
103
+ return { workflowContract: WORKFLOW_CONTRACT, queueReady: true };
104
+ }
73
105
 
74
106
  function workflowTaskStatus(content, taskName) {
75
- const matches = String(content || '').matchAll(/^\s*(?:\*\*)?Status(?:\*\*)?\s*:\s*(?:\*\*)?\s*([A-Za-z_-]+)(?:\*\*)?\s*$/gim);
76
- const values = [...matches].map((match) => match[1].toLowerCase().replaceAll('-', '_'));
107
+ const values = annotatedMarkdownLines(content)
108
+ .filter(({ outsideFence }) => outsideFence)
109
+ .map(({ line }) => line.match(/^\s*(?:\*\*)?Status(?:\*\*)?\s*:\s*(?:\*\*)?\s*([A-Za-z_-]+)(?:\*\*)?\s*$/i))
110
+ .filter(Boolean)
111
+ .map((match) => match[1].toLowerCase().replaceAll('-', '_'));
77
112
  if (values.length !== 1 || !WORKFLOW_STATUSES.has(values[0])) {
78
113
  throw candidateError(`${taskName} must contain exactly one supported Status field`);
79
114
  }
80
115
  return values[0];
81
116
  }
82
117
 
118
+ function workflowTaskNumericPrefix(taskName) {
119
+ const match = taskName.match(/^task-(\d+)(?:-|\.md$)/);
120
+ return match ? match[1].replace(/^0+(?=\d)/, '') : null;
121
+ }
122
+
123
+ function workflowTaskDependencies(content, taskName, taskNames, queueReady) {
124
+ const lines = annotatedMarkdownLines(content);
125
+ let sectionStart = -1;
126
+ let sectionEnd = lines.length;
127
+ let sectionCount = 0;
128
+
129
+ for (let index = 0; index < lines.length; index += 1) {
130
+ if (!lines[index].outsideFence) continue;
131
+
132
+ const heading = lines[index].line.match(/^(#{1,6})\s+(.+?)\s*$/);
133
+ if (!heading || heading[1].length > 2) continue;
134
+ const title = heading[2].trim().toLowerCase();
135
+ if (heading[1].length === 2 && title === 'dependencies') {
136
+ sectionCount += 1;
137
+ if (sectionStart < 0) sectionStart = index + 1;
138
+ continue;
139
+ }
140
+ if (sectionStart >= 0 && sectionEnd === lines.length) sectionEnd = index;
141
+ }
142
+
143
+ if (!queueReady && sectionCount === 0) return [];
144
+ if (sectionCount !== 1 || sectionStart < 0) {
145
+ throw candidateError(`${taskName} must contain exactly one ## Dependencies section`);
146
+ }
147
+
148
+ const entries = lines.slice(sectionStart, sectionEnd)
149
+ .filter(({ outsideFence }) => outsideFence)
150
+ .map(({ line }) => line.trim())
151
+ .filter(Boolean)
152
+ .map((line) => {
153
+ const bullet = line.match(/^-\s+(.+?)\s*$/);
154
+ if (!bullet) throw candidateError(`${taskName} Dependencies must contain only bullet entries`);
155
+ const value = bullet[1].replace(/^`([^`]+)`$/, '$1').trim();
156
+ if (!value) throw candidateError(`${taskName} contains an empty dependency`);
157
+ return value;
158
+ });
159
+
160
+ if (entries.length === 1 && entries[0].toLowerCase() === 'none') return [];
161
+ if (entries.length === 0 || entries.some((entry) => entry.toLowerCase() === 'none')) {
162
+ throw candidateError(`${taskName} must use either - none or exact task basenames`);
163
+ }
164
+ const resolved = entries.map((entry) => {
165
+ if (WORKFLOW_TASK_NAME.test(entry)) return entry;
166
+ const legacy = !queueReady && entry.match(/^(?:Task\s+)?(\d+)$/i);
167
+ if (!legacy) throw candidateError(`${taskName} dependencies must be exact flat task basenames`);
168
+ const numericId = legacy[1].replace(/^0+(?=\d)/, '');
169
+ const matches = taskNames.filter((candidate) => workflowTaskNumericPrefix(candidate) === numericId);
170
+ if (matches.length !== 1) {
171
+ throw candidateError(`${taskName} legacy dependency ${entry} maps to ${matches.length} task basenames`);
172
+ }
173
+ return matches[0];
174
+ });
175
+ if (new Set(resolved).size !== resolved.length) {
176
+ throw candidateError(`${taskName} contains duplicate dependencies`);
177
+ }
178
+ return resolved;
179
+ }
180
+
181
+ function assertAcyclicWorkflow(tasks) {
182
+ const dependencies = new Map(tasks.map((task) => [task.path, [...task.dependencies].sort()]));
183
+ const state = new Map();
184
+ const stack = [];
185
+
186
+ function visit(taskPath) {
187
+ state.set(taskPath, 'visiting');
188
+ stack.push(taskPath);
189
+ for (const dependency of dependencies.get(taskPath) || []) {
190
+ if (state.get(dependency) === 'visiting') {
191
+ const start = stack.indexOf(dependency);
192
+ throw candidateError(`dependency cycle detected: ${[...stack.slice(start), dependency].join(' -> ')}`);
193
+ }
194
+ if (state.get(dependency) !== 'visited') visit(dependency);
195
+ }
196
+ stack.pop();
197
+ state.set(taskPath, 'visited');
198
+ }
199
+
200
+ for (const taskPath of [...dependencies.keys()].sort()) {
201
+ if (!state.has(taskPath)) visit(taskPath);
202
+ }
203
+ }
204
+
83
205
  function inspectWorkflowFeature(specsDir, canonicalSpecs, requestedName) {
84
206
  const featureDir = path.join(specsDir, requestedName);
85
207
  let featureLstat;
@@ -114,6 +236,7 @@ function inspectWorkflowFeature(specsDir, canonicalSpecs, requestedName) {
114
236
  if (planLstat.isSymbolicLink() || !planLstat.isFile() || fs.realpathSync(planFile) !== planFile) {
115
237
  throw candidateError('plan.md must be a regular non-symlink file in the workflow feature directory');
116
238
  }
239
+ const { workflowContract, queueReady } = workflowPlanContract(fs.readFileSync(planFile, 'utf8'));
117
240
 
118
241
  const taskNames = fs.readdirSync(canonicalFeature, { withFileTypes: true })
119
242
  .filter((entry) => WORKFLOW_TASK_NAME.test(entry.name))
@@ -129,12 +252,26 @@ function inspectWorkflowFeature(specsDir, canonicalSpecs, requestedName) {
129
252
  if (taskLstat.isSymbolicLink() || !taskLstat.isFile() || fs.realpathSync(taskFile) !== taskFile) {
130
253
  throw candidateError(`${taskName} must be a regular non-symlink file in the workflow feature directory`);
131
254
  }
255
+ const taskContent = fs.readFileSync(taskFile, 'utf8');
132
256
  return {
133
257
  path: taskName,
134
- status: workflowTaskStatus(fs.readFileSync(taskFile, 'utf8'), taskName),
258
+ status: workflowTaskStatus(taskContent, taskName),
259
+ dependencies: workflowTaskDependencies(taskContent, taskName, taskNames, queueReady),
135
260
  };
136
261
  });
137
- const taskRegistry = Object.fromEntries(tasks.map((task) => [task.path, { status: task.status }]));
262
+ const taskNameSet = new Set(taskNames);
263
+ for (const task of tasks) {
264
+ if (task.dependencies.includes(task.path)) {
265
+ throw candidateError(`${task.path} cannot depend on itself`);
266
+ }
267
+ const missing = task.dependencies.find((dependency) => !taskNameSet.has(dependency));
268
+ if (missing) throw candidateError(`${task.path} references missing dependency ${missing}`);
269
+ }
270
+ assertAcyclicWorkflow(tasks);
271
+ const taskRegistry = Object.fromEntries(tasks.map((task) => [task.path, {
272
+ status: task.status,
273
+ dependencies: task.dependencies,
274
+ }]));
138
275
  const allTasksDone = tasks.every((task) => task.status === 'done');
139
276
  return {
140
277
  layoutKind: 'process-v3',
@@ -147,6 +284,8 @@ function inspectWorkflowFeature(specsDir, canonicalSpecs, requestedName) {
147
284
  taskRegistry,
148
285
  tasks,
149
286
  allTasksDone,
287
+ workflowContract,
288
+ queueReady,
150
289
  };
151
290
  }
152
291
 
@@ -778,6 +917,7 @@ function refineWorkflowGateResolution(resolved) {
778
917
  }
779
918
 
780
919
  module.exports = {
920
+ annotatedMarkdownLines,
781
921
  specsDirectory,
782
922
  findAllActiveSpecs,
783
923
  findAllSpecCandidates,
@@ -77,6 +77,13 @@ normally no more than about five owned files, explicit dependencies, measurable
77
77
  acceptance, and a runnable verification command. A task must deliver usable
78
78
  behavior; do not create preparation-only tasks.
79
79
 
80
+ Author exact task state, not prose readiness. `pending` means semantically ready
81
+ for the dependency-aware queue. Use `blocked` while a C1/C2 decision, an
82
+ accepted finding, or `UNKNOWN` closure remains open. A named task dependency
83
+ alone does not change `pending` to `blocked`; the resolver derives which pending
84
+ task is next. Change `blocked` to `pending` only after current evidence closes
85
+ every non-dependency blocker; keep pre-execution Receipts empty.
86
+
80
87
  When an outcome is uncertain, write a question rather than guessing. When a
81
88
  rule remains ambiguous, add two or three concrete examples and resolve the rule
82
89
  at C1 or C2.
@@ -89,15 +96,16 @@ failure scenario. Deduplicate, rank, and cap the list at 15.
89
96
 
90
97
  Ask the user to accept, reject, or revise each finding. Apply only accepted
91
98
  decisions. After every plan edit, run the consistency sweep across all plan and
92
- task files. Stop after two paper-review rounds; later findings require runtime
93
- evidence.
99
+ task files, then rederive every task status rather than only the first candidate.
100
+ Write task dependencies as exact flat task basenames so the queue can derive them.
101
+ Stop after two paper-review rounds; later findings require runtime evidence.
94
102
 
95
103
  ### 4. Execute one task at a time
96
104
 
97
- An implementation workflow owns execution. It selects one unblocked task,
98
- changes its single `Status:` field to `in_progress`, respects owned paths, runs
99
- the task's verification, and appends a real inline `## Receipt` only after the
100
- command finishes.
105
+ An implementation workflow owns execution. It selects one unblocked task with
106
+ `Status: pending`, changes its single `Status:` field to `in_progress`, respects
107
+ owned paths, runs the task's verification, and appends a real inline `## Receipt`
108
+ only after the command finishes.
101
109
 
102
110
  Parallel work is allowed only for tasks with disjoint write ownership and
103
111
  satisfied dependencies. One file has one writer in a wave. The controller is
@@ -129,7 +137,7 @@ it does not invent product approval, review independence, or runtime coverage.
129
137
  ownership, dependencies, criteria IDs, and copied prose across every file.
130
138
  8. **B4 — Stop paper churn.** Two pre-code review rounds maximum; later claims
131
139
  need runtime evidence.
132
- 9. **C1 — Done is derived.** Status follows fresh proof, never confidence.
140
+ 9. **C1 — State is derived.** Status follows fresh blocker and proof evidence.
133
141
  10. **C2 — Rules pay rent.** Add a rule or machine check only when it prevents a
134
142
  cited real incident; reassess rules when the primary model changes.
135
143
 
@@ -141,6 +149,8 @@ Stop gate revalidates every done task's inline Receipt and provenance. The old
141
149
  task-scaffold guard remains limited to the older nested layout; flat v3 tasks
142
150
  do not invoke that kernel path.
143
151
 
152
+ The resolver projects exact `Status:` values; it does not infer blockers from prose.
153
+
144
154
  These checks are a final safety net, not a substitute for C1-C3 judgment. Do
145
155
  not add a new schema, approval field, readiness bit, or review state to make the
146
156
  Markdown look more authoritative.
@@ -15,6 +15,7 @@ evidence.
15
15
  list the first ten plus the total.
16
16
  - Label an unresolved external or runtime claim `[UNVERIFIED]` and name the
17
17
  command, document, or environment that would settle it.
18
+ - Any privacy/security claim names the exact identifier surface at risk, such as an env var, header, path, token class, or field name; generic “sensitive data” is insufficient.
18
19
 
19
20
  ## Reviewer roles
20
21
 
@@ -51,6 +52,7 @@ Use reviewers that did not author the plan and give each one a distinct lens:
51
52
  Prompt them to break the plan. For every finding require: severity, exact plan
52
53
  location, concrete counterexample, repository evidence, and the smallest repair.
53
54
  Ignore style-only feedback.
55
+ Run mutation or destructive negative controls only on disposable copies below a verified temporary root, never tracked worktree or canonical source bytes.
54
56
 
55
57
  ## Four red-team lenses
56
58
 
@@ -81,6 +83,19 @@ Present C2 as a table:
81
83
  The user chooses accept, reject, or revise. Do not apply findings before that
82
84
  decision. Record the result in the plan review log.
83
85
 
86
+ ## Accepted-repair closure
87
+
88
+ After applying an accepted C2 finding, a fresh-context closure pass records and
89
+ freshly replays its original counterexample after the repair under this exact review-log header:
90
+
91
+ | ID | Decision | Original counterexample | Repaired at | Proved at | Replay | Closure |
92
+ |---|---|---|---|---|---|---|
93
+
94
+ `Repaired at` cites the repair edit; `Proved at` must cite distinct evidence from the fresh replay, never the repair-edit citation.
95
+ An accepted finding transitions `accepted → repaired → PASS|FAIL|UNKNOWN`.
96
+ Only `PASS` closes it; `FAIL` remains open for the remaining paper-review round; `UNKNOWN` blocks implementation handoff.
97
+ A repair that adds user semantics or scope returns to C1.
98
+
84
99
  ## B3 — consistency sweep
85
100
 
86
101
  After any accepted finding or user edit:
@@ -16,6 +16,7 @@ Keep the plan short enough to scan as one index, normally under 100 lines.
16
16
 
17
17
  ```markdown
18
18
  # <Feature name>
19
+ Specs-Contract: process-first-ready-v1
19
20
 
20
21
  ## Scope decision (C1 — YYYY-MM-DD)
21
22
  - Existing: <reusable code with path:line>
@@ -34,7 +35,7 @@ Keep the plan short enough to scan as one index, normally under 100 lines.
34
35
  ## Tasks
35
36
  | # | Task | Criteria | Primary ownership | Dependencies | Status |
36
37
  |---|---|---|---|---|---|
37
- | 01 | <one outcome> | AC-01 | `src/example.ts` | - | pending |
38
+ | 01 | <one outcome> | AC-01 | `src/example.ts` | - | blocked |
38
39
 
39
40
  ## Review log
40
41
  - Round 1: <findings accepted/rejected/revised>; sweep <result>.
@@ -50,7 +51,7 @@ Keep one exact `Status:` field. The task file is one owner's packet.
50
51
  ````markdown
51
52
  # Task NN — <one observable outcome>
52
53
 
53
- Status: pending
54
+ Status: blocked
54
55
 
55
56
  ## Outcome
56
57
  <What becomes true when this task succeeds.>
@@ -68,18 +69,36 @@ Status: pending
68
69
  - AC-01: <task-local measurable condition>
69
70
 
70
71
  ## Dependencies
71
- - <task number or `none`>
72
+ - <exact `task-NN-*.md` basename or `none`; one bullet per dependency>
72
73
 
73
74
  ## Verification Plan
74
75
  - Command: `<exact runnable command>`
75
- - Expected: <observable success, including nonzero/negative behavior if relevant>
76
- - Reachability: <entrypoint or consumer>
77
- - Artifacts: <path and digest requirement, or none>
76
+ - Named probe: <existing concrete probe/test/hook ID; never only a suite label>
77
+ - Reachability: <real entrypoint/caller at each `source`/`installed`/`live` level; `UNKNOWN` if unexecuted>
78
+ - Oracle: <externally observable success or failure>
79
+ - Counterexample: <material alternative behavior that must make this proof fail>
80
+ - Artifacts: <required artifact path plus digest algorithm/comparison rule, or explicitly ephemeral with cleanup rule>
78
81
 
79
82
  ## Receipt
80
83
  <!-- Fill only after execution; see canonical form below. -->
81
84
  ````
82
85
 
86
+ Trace `Command → Named probe → Reachability → Oracle`. Aggregate suites
87
+ name the owning concrete probe. Proof at `source`/`installed`/`live` stays
88
+ separate; one level never promotes another. Run mutation or destructive
89
+ negative controls only on disposable copies under a verified temporary root,
90
+ never tracked worktree or canonical source bytes.
91
+
92
+ ## Status matrix
93
+
94
+ | Status condition | Persisted state |
95
+ |---|---|
96
+ | C1/C2 decision open | `blocked` |
97
+ | accepted finding open or `UNKNOWN` | `blocked` |
98
+ | every non-dependency blocker closed | `pending` |
99
+ | named task dependency not done | keep `pending`; queue gates it |
100
+ Use only direct-child task basenames or `none` under `## Dependencies`; keep `## Receipt` empty until execution produces canonical proof.
101
+
83
102
  ## Canonical inline Receipt
84
103
 
85
104
  The completed task keeps this section at the end of the same file. Derive Base
@@ -132,25 +151,35 @@ Question: which environment owns the authoritative integration run?
132
151
 
133
152
  Keep examples only until they clarify a rule or acceptance criterion.
134
153
 
154
+ ## No-invention and conditional boundary contracts
155
+
156
+ Before implementation handoff, apply the **no-invention gate**: if two implementations conform to the packet text yet can produce different externally observable output, state, error, security, or compatibility behavior, surface the missing choice as an explicit C1 or C2 question and block handoff.
157
+
158
+ A boundary is material when the task creates, changes, or depends on it and a different choice changes an external observation, security, durable data, compatibility, or proof reachability. Require only the matching material row; omit nonmaterial categories.
159
+ For every required row, name each listed choice exactly; labels such as “JSON”, “local path”, “locked”, or “timestamped” alone remain unresolved.
160
+
161
+ | Boundary | Required contract when material |
162
+ |---|---|
163
+ | API/CLI | entrypoint/route or command grammar; identity/auth; input/default/normalization; success output; error/status/exit; duplicate/retry/idempotency; compatibility |
164
+ | Schema | version; exact keys/nesting/types; required/optional; enum/format/bounds/cardinality; unknown-field behavior; compatibility/migration |
165
+ | State/concurrency | initial/terminal states; event + guard + effect + next + error; ordering; duplicate/retry; writer/lock acquire/contention/release; rollback/recovery |
166
+ | Filesystem/security | authoritative root; trusted/untrusted segment grammar; lexical + canonical containment and symlink policy; flags/mode; temp/rename/fsync; lock/stale reclaim; crash cleanup |
167
+ | Time/retention | clock source; unit/precision/timezone; endpoints and inclusion/comparator; anomaly behavior; expiry/purge/recovery |
168
+ | Integration/proof | caller; export/registration; packaging/config; native path/consumer; proof level (`source`/`installed`/`live`); observable failure oracle |
169
+
135
170
  ## Twelve edge-case dimensions
136
171
 
137
172
  For each material boundary, select relevant dimensions rather than filling a
138
173
  ceremonial matrix:
139
174
 
140
- | Dimension | Probe |
141
- |---|---|
142
- | Input shape | empty, malformed, duplicate, maximum size |
143
- | Identity | missing, wrong actor, cross-scope, replay |
144
- | State | initial, partial, terminal, stale |
145
- | Order | early, late, repeated, out of order |
146
- | Concurrency | double writer, race, cancellation |
147
- | Dependency | missing, blocked, version drift |
148
- | Failure | timeout, exception, nonzero exit |
149
- | Recovery | retry, resume, rollback, cleanup |
150
- | Persistence | partial write, corruption, migration |
151
- | Integration | caller, registration, packaging, reachability |
152
- | Security/privacy | injection, traversal, secret exposure |
153
- | Observability/proof | logs, artifact, negative path, provenance |
175
+ - Input shape, identity, state, order: malformed/duplicate/bounds, wrong actor,
176
+ partial/stale state, replay/reordering.
177
+ - Concurrency, dependency, failure, recovery: races/cancellation, version drift,
178
+ timeout/nonzero exit, retry/rollback/cleanup.
179
+ - Persistence, integration, security/privacy, observability/proof: partial
180
+ writes, registration/reachability, traversal/secrets, artifacts/provenance.
181
+
182
+ `Crash` means abrupt unhandled termination before the claimed catch point; a catchable failure returns/raises an error or exits nonzero. Never use them interchangeably.
154
183
 
155
184
  Turn a discovered risk into an EARS criterion, a task acceptance item, or a
156
185
  verification probe. Do not duplicate it across all three.
@@ -77,15 +77,33 @@ try {
77
77
  result[status] = (result[status] || 0) + 1;
78
78
  return result;
79
79
  }, {});
80
+ const featureDir = path.join(active.specsDir, active.featureName);
80
81
  const statuses = new Map(tasks.map(([taskPath, task]) => [
81
82
  taskPath,
82
83
  task?.status || 'pending'
83
84
  ]));
85
+ const dependencyProof = processWorkflow
86
+ ? RECEIPT.workflowDependencyProofState(featureDir, taskRegistry, runtimeContext, POLICY)
87
+ : {};
88
+ const taskState = tasks.map(([taskPath, task]) => [
89
+ taskPath,
90
+ task?.status || 'pending',
91
+ [...(Array.isArray(task?.dependencies) ? task.dependencies : [])].sort(),
92
+ ]).sort(([left], [right]) => left.localeCompare(right));
93
+ const proofState = Object.entries(dependencyProof).map(([taskPath, proof]) => [
94
+ taskPath,
95
+ proof.done,
96
+ proof.valid,
97
+ proof.eligible,
98
+ proof.signature,
99
+ ]);
84
100
  const next = tasks.find(([, task]) => (
85
- (task?.status || 'pending') === 'pending'
86
- && (task?.dependencies || []).every((dependency) => statuses.get(dependency) === 'done')
101
+ active.queueReady !== false
102
+ && (task?.status || 'pending') === 'pending'
103
+ && (task?.dependencies || []).every((dependency) => (
104
+ processWorkflow ? dependencyProof[dependency]?.eligible === true : statuses.get(dependency) === 'done'
105
+ ))
87
106
  ));
88
- const featureDir = path.join(active.specsDir, active.featureName);
89
107
  const featureReceiptPresent = !processWorkflow
90
108
  && RECEIPT.safeRead(featureDir, 'feature-receipt.md').status === 'ok';
91
109
  const stateKey = JSON.stringify({
@@ -99,6 +117,10 @@ try {
99
117
  Head: runtimeContext.head,
100
118
  context_id: runtimeContext.context_id,
101
119
  phase,
120
+ workflow_contract: active.workflowContract || null,
121
+ queue_ready: active.queueReady !== false,
122
+ task_state: taskState,
123
+ proof_state: proofState,
102
124
  done: counts.done || 0,
103
125
  total: tasks.length,
104
126
  feature_receipt_present: featureReceiptPresent,
@@ -108,9 +130,12 @@ try {
108
130
  try { previous = fs.readFileSync(cache, 'utf8').trim(); } catch { /* first run */ }
109
131
 
110
132
  if (previous === stateKey) {
133
+ const migration = processWorkflow && active.queueReady === false
134
+ ? ' Add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.'
135
+ : '';
111
136
  process.stdout.write(
112
137
  `> Spec \`${active.featureName}\` @ \`${phase}\` `
113
- + `(${counts.done || 0}/${tasks.length} tasks done). Tollgate active.\n`
138
+ + `(${counts.done || 0}/${tasks.length} tasks done). Tollgate active.${migration}\n`
114
139
  );
115
140
  process.exit(0);
116
141
  }
@@ -125,6 +150,9 @@ try {
125
150
  lines.push(`- Flash verification pending: ${flashTasks.map((taskPath) => `\`${taskPath}\``).join(', ')}. A PASS proof keeps the persisted task in_progress until explicit sync-finalize.`);
126
151
  }
127
152
  if (processWorkflow) {
153
+ if (active.queueReady === false) {
154
+ lines.push('- Migration required: add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.');
155
+ }
128
156
  lines.push(
129
157
  '- Sync each flat task `Status:` only after verified work; task proof belongs in that task\'s inline `## Receipt`.',
130
158
  `- Workflow source: \`specs/${active.featureName}/plan.md\`; Stop revalidates every task currently marked done.`,