@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.
- package/bin/__tests__/codex-hooks.test.js +245 -0
- package/bin/__tests__/codex-native.test.js +463 -20
- package/bin/__tests__/package-inventory.test.js +484 -6
- package/package.json +1 -1
- package/src/claude/agents/spec-maker.md +14 -8
- package/src/claude/hooks/__tests__/spec-gate.test.js +77 -1
- package/src/claude/hooks/__tests__/state.test.js +249 -3
- package/src/claude/hooks/spec-state.cjs +30 -3
- package/src/claude/scripts/spec-receipt.cjs +76 -21
- package/src/claude/scripts/spec-resolver.cjs +144 -4
- package/src/claude/skills/specs/SKILL.md +17 -7
- package/src/claude/skills/specs/references/review.md +15 -0
- package/src/claude/skills/specs/references/templates.md +49 -20
- package/src/codex/hooks/spec-state.cjs +32 -4
|
@@ -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
|
|
76
|
-
|
|
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(
|
|
258
|
+
status: workflowTaskStatus(taskContent, taskName),
|
|
259
|
+
dependencies: workflowTaskDependencies(taskContent, taskName, taskNames, queueReady),
|
|
135
260
|
};
|
|
136
261
|
});
|
|
137
|
-
const
|
|
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
|
|
93
|
-
|
|
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
|
|
99
|
-
the task's verification, and appends a real inline `## Receipt`
|
|
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 —
|
|
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` | - |
|
|
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:
|
|
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
|
|
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
|
-
-
|
|
76
|
-
- Reachability: <entrypoint
|
|
77
|
-
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
86
|
-
&& (task?.
|
|
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
|
|
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.`,
|