@haposoft/cafekit 0.16.0-rc.6 → 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/README.md +21 -58
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +4 -5
- package/bin/__tests__/codex-hooks.test.js +382 -0
- package/bin/__tests__/codex-native.test.js +579 -80
- package/bin/__tests__/develop-contract.test.js +155 -76
- package/bin/__tests__/installer-safety.test.js +19 -154
- package/bin/__tests__/package-inventory.test.js +494 -26
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +2 -2
- package/bin/__tests__/validator-grounding.test.js +11 -11
- package/bin/install.js +2 -7
- package/bin/lib/context.js +0 -45
- package/bin/lib/copy-utils.js +1 -1
- package/bin/lib/i18n.js +0 -3
- package/bin/lib/manifest.js +1 -1
- package/bin/phases/claude-runtime.js +1 -3
- package/bin/phases/codex-runtime.js +4 -2
- package/bin/phases/copy-payload.js +3 -15
- package/bin/phases/post-install.js +5 -17
- package/bin/phases/root-config.js +2 -3
- package/bin/phases/select-platform.js +2 -5
- package/bin/phases/summary.js +0 -1
- package/package.json +2 -5
- package/src/claude/CLAUDE.md +19 -6
- package/src/claude/agents/project-manager.md +41 -9
- package/src/claude/agents/spec-maker.md +82 -155
- package/src/claude/gitignore +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +238 -0
- package/src/claude/hooks/__tests__/state.test.js +249 -3
- package/src/claude/hooks/lib/context.cjs +1 -1
- package/src/claude/hooks/lib/detect.cjs +1 -1
- package/src/claude/hooks/spec-gate.cjs +44 -9
- package/src/claude/hooks/spec-state.cjs +51 -13
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/migration-manifest.json +17 -3
- package/src/claude/rules/manage-docs.md +43 -24
- package/src/claude/rules/state-sync.md +57 -62
- package/src/claude/rules/workflow.md +30 -13
- package/src/claude/scripts/change-firewall.cjs +2 -2
- package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
- package/src/claude/scripts/spec-receipt.cjs +196 -1
- package/src/claude/scripts/spec-resolver.cjs +428 -0
- package/src/claude/skills/develop/SKILL.md +89 -125
- package/src/claude/skills/develop/references/parallel-waves.md +75 -162
- package/src/claude/skills/develop/references/quality-gate.md +61 -121
- package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
- package/src/claude/skills/question/SKILL.md +1 -1
- package/src/claude/skills/specs/SKILL.md +152 -204
- package/src/claude/skills/specs/references/review.md +124 -243
- package/src/claude/skills/specs/references/templates.md +202 -0
- package/src/claude/skills/sync/SKILL.md +66 -90
- package/src/claude/skills/sync/references/sync-protocols.md +62 -83
- package/src/codex/AGENTS.md +20 -7
- package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
- package/src/codex/hooks/lib/spec-utils.cjs +25 -0
- package/src/codex/hooks/spec-gate.cjs +46 -16
- package/src/codex/hooks/spec-state.cjs +54 -16
- package/src/codex/rules/state-sync.md +57 -61
- package/src/common/AGENTS.md +8 -1
- package/bin/__tests__/opencode-spec-gate.test.js +0 -799
- package/bin/lib/opencode-install.js +0 -899
- package/bin/phases/opencode-runtime.js +0 -38
- package/src/claude/skills/specs/references/archive-workflow.md +0 -74
- package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
- package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
- package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
- package/src/claude/skills/specs/references/research-strategy.md +0 -107
- package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
- package/src/claude/skills/specs/references/translation-mirror.md +0 -83
- package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
- package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
- package/src/claude/skills/specs/rules/design-principles.md +0 -138
- package/src/claude/skills/specs/rules/design-review.md +0 -55
- package/src/claude/skills/specs/rules/ears-format.md +0 -42
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
- package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
- package/src/opencode/AGENTS.md +0 -41
- package/src/opencode/plugins/docs-sync.ts +0 -175
- package/src/opencode/plugins/inspect-block.ts +0 -164
- package/src/opencode/plugins/privacy-block.ts +0 -182
- package/src/opencode/plugins/rules.ts +0 -155
- package/src/opencode/plugins/session.ts +0 -179
- package/src/opencode/plugins/spec-gate.ts +0 -457
- package/src/opencode/plugins/spec-state.ts +0 -311
- package/src/opencode/plugins/state.ts +0 -253
- package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
- package/src/opencode/plugins/usage.ts +0 -178
|
@@ -54,39 +54,58 @@ Use [Keep a Changelog](https://keepachangelog.com/) convention:
|
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|
|
57
|
-
##
|
|
57
|
+
## Process-first specification and execution tracker
|
|
58
58
|
|
|
59
|
-
### Where
|
|
60
|
-
Hapo does not use chaotic plan folders. Requirements and architecture are centralized into a machine-readable Specification structure via `specs`.
|
|
59
|
+
### Where plans live
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
Keep every new feature packet in one direct child of `./specs/`. The Markdown
|
|
62
|
+
files are the durable state and must remain readable without a compiler or a
|
|
63
|
+
generated index.
|
|
64
|
+
|
|
65
|
+
### Primary directory layout
|
|
63
66
|
|
|
64
|
-
### Directory Layout
|
|
65
67
|
```text
|
|
66
68
|
specs/
|
|
67
69
|
└── user-auth/
|
|
68
|
-
├──
|
|
69
|
-
├──
|
|
70
|
-
└──
|
|
71
|
-
├── task-R0-01-setup.md # Actionable granular steps for development
|
|
72
|
-
└── task-R1-01-api.md # Next requirement-driven task
|
|
70
|
+
├── plan.md
|
|
71
|
+
├── task-01-setup.md
|
|
72
|
+
└── task-02-api.md
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
Task files are direct children beside `plan.md`; do not place them in a task
|
|
76
|
+
subdirectory.
|
|
77
|
+
The plan records the C1 scope decision, explicit exclusions, acceptance
|
|
78
|
+
criteria, and task mapping. Each task owns one usable outcome, bounded paths,
|
|
79
|
+
dependencies, acceptance, and a runnable Verification Plan.
|
|
80
|
+
|
|
81
|
+
### Gates and execution handoff
|
|
82
|
+
|
|
83
|
+
- C1: the user chooses EXPAND, KEEP, or CUT before the plan is written.
|
|
84
|
+
- C2: the user accepts, rejects, or revises deduplicated adversarial findings.
|
|
85
|
+
- Specs stops after planning; implementation begins only through a new explicit
|
|
86
|
+
Develop invocation.
|
|
87
|
+
- C3: after execution, current receipts and limitations are shown and the user
|
|
88
|
+
decides whether the feature is complete.
|
|
78
89
|
|
|
79
|
-
###
|
|
80
|
-
This blueprint covers:
|
|
81
|
-
- **Context & Overview:** The "Why" of the feature.
|
|
82
|
-
- **Data Flow:** Mandatory Mermaid Data Flow Diagram detailing state transitions, DB interactions, and API payloads.
|
|
83
|
-
- **Risk Assessment:** Pre-identified failure points and mitigations.
|
|
90
|
+
### Task state and proof
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
Each task contains exactly one `Status:` field. The final inline `## Receipt`
|
|
93
|
+
is canonical proof and must contain the exact command, `Exit: 0`,
|
|
94
|
+
`Verification: PASS`, runtime-derived Base and Head values, and non-empty
|
|
95
|
+
fenced current output before the task becomes done.
|
|
96
|
+
|
|
97
|
+
Sync only observed state with surgical edits. Re-read the changed task after
|
|
98
|
+
every update; never infer missing proof, approval, or readiness. When a done
|
|
99
|
+
task changes user-facing behavior, architecture, API contracts, operations, or
|
|
100
|
+
project status, classify docs impact and update only affected existing docs.
|
|
91
101
|
|
|
92
102
|
Comply with the overarching rules in `./rules/ai-dev-rules.md`.
|
|
103
|
+
|
|
104
|
+
## Legacy specification layout
|
|
105
|
+
|
|
106
|
+
Existing features containing `spec.json`, nested `tasks/task-R*.md`, or other
|
|
107
|
+
legacy kernel artifacts keep their installed adapters and original layout.
|
|
108
|
+
Preserve `task_registry`, `semantic_model`, `planning_depth`, lane,
|
|
109
|
+
`execution_tier`, machine authority, typed topology, separate receipts, and
|
|
110
|
+
feature closeout files. Do not migrate them as part of unrelated documentation
|
|
111
|
+
work.
|
|
@@ -1,62 +1,57 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
Never hand-author the `semantic_model` shape or treat an unsynchronized Markdown
|
|
59
|
-
edit as machine authority.
|
|
60
|
-
|
|
61
|
-
Claude-native plan/task state may mirror physical state when available, but it
|
|
62
|
-
is never a substitute for `spec.json` or authority to advance the lifecycle.
|
|
1
|
+
# Process-first state synchronization
|
|
2
|
+
|
|
3
|
+
## Primary file state
|
|
4
|
+
|
|
5
|
+
For new work, `specs/<feature>/plan.md` and its direct-child
|
|
6
|
+
`task-NN-*.md` files are canonical, hand-editable state. Task files stay flat
|
|
7
|
+
beside the plan; indexes and host task views are disposable projections.
|
|
8
|
+
|
|
9
|
+
Each task has exactly one `Status:` field using `pending`, `in_progress`,
|
|
10
|
+
`paused`, `blocked`, or `done`. The controller is the sole writer of task
|
|
11
|
+
status and proof. A task's final `## Receipt` is its only canonical execution
|
|
12
|
+
proof.
|
|
13
|
+
|
|
14
|
+
## Surgical synchronization
|
|
15
|
+
|
|
16
|
+
Before changing state:
|
|
17
|
+
|
|
18
|
+
1. read the plan row and the task's Outcome, Acceptance, Dependencies,
|
|
19
|
+
Verification Plan, Status, and Receipt;
|
|
20
|
+
2. confirm every dependency and owned path at the requested boundary;
|
|
21
|
+
3. edit only the observed Status, concrete blocker, implemented checkbox, or
|
|
22
|
+
Receipt; and
|
|
23
|
+
4. re-read the file and confirm exactly one Status field and one Receipt
|
|
24
|
+
section.
|
|
25
|
+
|
|
26
|
+
Starting or resuming changes only Status to `in_progress`. Blocking records a
|
|
27
|
+
specific cause without creating proof. Do not replace a whole packet to make a
|
|
28
|
+
state transition.
|
|
29
|
+
|
|
30
|
+
Set `Status: done` only after the inline Receipt passes the
|
|
31
|
+
`validateCanonicalReceipt` contract: exact command, `Exit: 0`,
|
|
32
|
+
`Verification: PASS`, runtime-derived Base and Head values, and a non-empty
|
|
33
|
+
fenced block of current command output. Missing, stale, contradictory,
|
|
34
|
+
placeholder, copied, failure, or zero-test evidence leaves the task unfinished.
|
|
35
|
+
|
|
36
|
+
Synchronization never fabricates commands, output, timestamps, completed
|
|
37
|
+
work, reviewer identity, user approval, or product readiness. A passing command
|
|
38
|
+
proves only the boundary it executed.
|
|
39
|
+
|
|
40
|
+
## Human gates and workflow ownership
|
|
41
|
+
|
|
42
|
+
- C1 records the user's scope decision before the plan is written.
|
|
43
|
+
- C2 records the user's disposition of deduplicated adversarial findings.
|
|
44
|
+
- Develop starts only after a new explicit user invocation and executes one
|
|
45
|
+
unblocked task at a time.
|
|
46
|
+
- Sync changes only observed file state and never starts implementation.
|
|
47
|
+
- C3 occurs after current receipts and limitations are shown; the user decides
|
|
48
|
+
whether the requested feature is complete.
|
|
49
|
+
|
|
50
|
+
## Legacy compatibility
|
|
51
|
+
|
|
52
|
+
Existing packets containing `spec.json`, nested `tasks/task-R*.md`, or other
|
|
53
|
+
legacy kernel artifacts stay on their installed adapters. Preserve their
|
|
54
|
+
`task_registry`, `semantic_model`, `planning_depth`, lane,
|
|
55
|
+
`execution_tier`, typed topology, separate receipts, and feature closeout
|
|
56
|
+
contract. Do not migrate that machine authority or storage shape while syncing
|
|
57
|
+
an unrelated process-first packet.
|
|
@@ -12,14 +12,18 @@ Use the CafeKit loop: **Understand -> Plan -> Execute -> Verify -> Sync**.
|
|
|
12
12
|
|
|
13
13
|
## 2. Plan
|
|
14
14
|
|
|
15
|
-
- For non-trivial features, use `/specs` to
|
|
16
|
-
|
|
15
|
+
- For non-trivial features, use `/hapo:specs` to challenge the minimum scope,
|
|
16
|
+
open C1, and create `specs/<feature>/plan.md` with flat
|
|
17
|
+
`task-NN-*.md` files beside it.
|
|
18
|
+
- After adversarial review, open C2 and apply only the findings the user accepts
|
|
19
|
+
or revises. Specs never starts implementation.
|
|
20
|
+
- Start implementation only after a new explicit `/hapo:develop` invocation,
|
|
21
|
+
then select one unblocked task at a time.
|
|
17
22
|
- Extract from the active task:
|
|
18
23
|
- `Status`
|
|
19
24
|
- `Outcome`
|
|
20
25
|
- `Scope`
|
|
21
|
-
- `
|
|
22
|
-
- `Changes`
|
|
26
|
+
- `Ownership`
|
|
23
27
|
- `Acceptance`
|
|
24
28
|
- `Dependencies`
|
|
25
29
|
- `Verification Plan`
|
|
@@ -37,22 +41,26 @@ Use the CafeKit loop: **Understand -> Plan -> Execute -> Verify -> Sync**.
|
|
|
37
41
|
- Run exact commands from `Verification Plan` first.
|
|
38
42
|
- Then run repo-level lint/test/build as needed for confidence.
|
|
39
43
|
- Use only fresh verification from the current run when claiming completion.
|
|
44
|
+
- Review correctness, security, scope, and reachability without turning the
|
|
45
|
+
review into execution proof.
|
|
40
46
|
- `PRECHECK_FAIL` outranks `NO_TESTS`.
|
|
41
47
|
- `NO_TESTS` or `0 tests + exit 0` is not a pass when automated tests are required.
|
|
42
48
|
- If verification fails, fix root cause and rerun. After 3 failed attempts, escalate with evidence.
|
|
43
49
|
|
|
44
50
|
## 5. Sync
|
|
45
51
|
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
- Use `/hapo:sync` to edit observed state surgically; the controller is the
|
|
53
|
+
sole writer of task Status and proof.
|
|
54
|
+
- Keep exactly one `Status:` field. Write or replace the task's final inline
|
|
55
|
+
`## Receipt` before setting `Status: done`.
|
|
56
|
+
- A canonical Receipt contains the exact command, `Exit: 0`,
|
|
57
|
+
`Verification: PASS`, runtime-derived Base and Head values, and non-empty
|
|
58
|
+
fenced current output. Never invent, copy, or infer missing proof.
|
|
59
|
+
- Re-read each edited task and reconcile its plan row, dependencies, acceptance
|
|
60
|
+
mapping, status, and receipt without rewriting unrelated bytes.
|
|
55
61
|
- Run docs checkpoint when a completed task affects public docs or architecture docs.
|
|
62
|
+
- After all requested work has current proof, show evidence and limitations at
|
|
63
|
+
C3. The user decides whether the feature is complete.
|
|
56
64
|
|
|
57
65
|
## Production Or CI Issues
|
|
58
66
|
|
|
@@ -63,3 +71,12 @@ Use the CafeKit loop: **Understand -> Plan -> Execute -> Verify -> Sync**.
|
|
|
63
71
|
5. Review before syncing or shipping.
|
|
64
72
|
|
|
65
73
|
Do not patch symptoms before diagnosis unless the issue is a trivial syntax/type/lint failure with an obvious local cause.
|
|
74
|
+
|
|
75
|
+
## Legacy compatibility
|
|
76
|
+
|
|
77
|
+
When an existing feature contains `spec.json`, nested `tasks/task-R*.md`, or
|
|
78
|
+
other legacy kernel artifacts, keep using the installed adapter. Preserve its
|
|
79
|
+
`task_registry`, `semantic_model`, `planning_depth`, lane,
|
|
80
|
+
`execution_tier`, typed boundaries, separate receipts, and final feature
|
|
81
|
+
receipt. Do not project that machine authority or storage shape into a new
|
|
82
|
+
process-first packet.
|
|
@@ -37,7 +37,7 @@ const REL = {
|
|
|
37
37
|
ledger: 'packages/spec/benchmarks/benchmark-failure-ledger.json',
|
|
38
38
|
freeze: 'packages/spec/.cafekit-release/change-firewall-freeze.json',
|
|
39
39
|
receipt: 'packages/spec/.cafekit-release/release-receipt.json',
|
|
40
|
-
legacyBridge: 'specs/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json',
|
|
40
|
+
legacyBridge: 'specs/archive/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json',
|
|
41
41
|
cafekitReleaseDir: 'packages/spec/.cafekit-release/',
|
|
42
42
|
packageJson: 'packages/spec/package.json',
|
|
43
43
|
};
|
|
@@ -716,7 +716,7 @@ function readLegacyBridgeArtifact(root) {
|
|
|
716
716
|
// C14 BootstrapAttestation / bootstrapBaseline
|
|
717
717
|
// ---------------------------------------------------------------------------
|
|
718
718
|
|
|
719
|
-
const REL_FEATURE_SPEC = 'specs/cafekit-semantic-eval-firewall/spec.json';
|
|
719
|
+
const REL_FEATURE_SPEC = 'specs/archive/cafekit-semantic-eval-firewall/spec.json';
|
|
720
720
|
|
|
721
721
|
function readFeatureSpecJson(root) {
|
|
722
722
|
const loaded = readJsonFileIfExists(root, REL_FEATURE_SPEC);
|
|
@@ -41,7 +41,6 @@ function defaultSkillsRoot() {
|
|
|
41
41
|
path.resolve(__dirname, '..', 'skills'),
|
|
42
42
|
path.resolve(process.cwd(), '.claude', 'skills'),
|
|
43
43
|
path.resolve(process.cwd(), '.agents', 'skills'),
|
|
44
|
-
path.resolve(process.cwd(), '.opencode', 'skills'),
|
|
45
44
|
path.resolve(__dirname, '..', '..', '.agents', 'skills')
|
|
46
45
|
];
|
|
47
46
|
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { annotatedMarkdownLines } = require('./spec-resolver.cjs');
|
|
6
7
|
|
|
7
8
|
const EVIDENCE_HEADINGS = new Set([
|
|
8
9
|
'Evidence',
|
|
@@ -66,6 +67,49 @@ function evidenceBody(text, policy = {}) {
|
|
|
66
67
|
return lines.slice(start, end).join('\n');
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function workflowReceiptBody(text, policy = {}) {
|
|
71
|
+
const lines = annotatedMarkdownLines(text);
|
|
72
|
+
const starts = [];
|
|
73
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
74
|
+
if (lines[index].outsideFence && /^##\s+Receipt\s*$/.test(lines[index].line)) starts.push(index + 1);
|
|
75
|
+
}
|
|
76
|
+
if (starts.length !== 1) return null;
|
|
77
|
+
|
|
78
|
+
const start = starts[0];
|
|
79
|
+
let end = lines.length;
|
|
80
|
+
for (let index = start; index < lines.length; index += 1) {
|
|
81
|
+
if (!lines[index].outsideFence) continue;
|
|
82
|
+
const heading = lines[index].line.match(/^(#{1,6})\s+/);
|
|
83
|
+
const tap = typeof policy.isTapMetadataHeading === 'function' && policy.isTapMetadataHeading(lines[index].line);
|
|
84
|
+
if (heading && heading[1].length <= 2 && !tap) { end = index; break; }
|
|
85
|
+
}
|
|
86
|
+
return lines.slice(start, end).map(({ line }) => line).join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function workflowCanonicalValidationBody(body) {
|
|
90
|
+
return annotatedMarkdownLines(body)
|
|
91
|
+
.filter(({ outsideFence }) => outsideFence)
|
|
92
|
+
.map(({ line }) => line)
|
|
93
|
+
.join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hasNonEmptyFencedBlock(body) {
|
|
97
|
+
let open = false;
|
|
98
|
+
let nonEmpty = false;
|
|
99
|
+
for (const { line, fenceEvent } of annotatedMarkdownLines(body)) {
|
|
100
|
+
if (fenceEvent === 'open') {
|
|
101
|
+
open = true;
|
|
102
|
+
nonEmpty = false;
|
|
103
|
+
} else if (fenceEvent === 'close') {
|
|
104
|
+
if (open && nonEmpty) return true;
|
|
105
|
+
open = false;
|
|
106
|
+
} else if (open && line.trim() !== '') {
|
|
107
|
+
nonEmpty = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
69
113
|
function fieldValues(body, name) {
|
|
70
114
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
71
115
|
const matcher = new RegExp(`^\\s*${escaped}\\s*:\\s*(.*?)\\s*$`, 'i');
|
|
@@ -76,6 +120,49 @@ function concrete(value) {
|
|
|
76
120
|
return typeof value === 'string' && value.trim() !== '' && !/^(?:N\/A|NONE|PENDING|TBD|UNKNOWN|\{\{.*\}\})$/i.test(value.trim());
|
|
77
121
|
}
|
|
78
122
|
|
|
123
|
+
function normalizedCommand(value) {
|
|
124
|
+
if (typeof value !== 'string') return null;
|
|
125
|
+
let command = value.trim();
|
|
126
|
+
if (/^`[^`\r\n]+`$/.test(command)) command = command.slice(1, -1).trim();
|
|
127
|
+
if (!concrete(command) || /^<[^>]+>$/.test(command)) return null;
|
|
128
|
+
return command;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function workflowVerificationCommand(taskText) {
|
|
132
|
+
const sections = [];
|
|
133
|
+
let current = null;
|
|
134
|
+
for (const { line, outsideFence } of annotatedMarkdownLines(taskText)) {
|
|
135
|
+
if (!outsideFence) continue;
|
|
136
|
+
if (/^##\s+Verification Plan\s*$/.test(line)) {
|
|
137
|
+
current = [];
|
|
138
|
+
sections.push(current);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (current && /^#{1,2}\s+/.test(line)) {
|
|
142
|
+
current = null;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (current) current.push(line);
|
|
146
|
+
}
|
|
147
|
+
if (sections.length !== 1) return null;
|
|
148
|
+
const commands = sections[0]
|
|
149
|
+
.map((line) => line.match(/^\s*-\s+(?:\*\*Command\*\*|Command)\s*:\s*(.*?)\s*$/i))
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
.map((match) => normalizedCommand(match[1]));
|
|
152
|
+
return commands.length === 1 ? commands[0] : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function workflowCommandFailures(taskText, body) {
|
|
156
|
+
const planned = workflowVerificationCommand(taskText);
|
|
157
|
+
const actualValues = [
|
|
158
|
+
...fieldValues(body, 'Command'),
|
|
159
|
+
...fieldValues(body, 'Commands'),
|
|
160
|
+
].map(normalizedCommand);
|
|
161
|
+
return planned && actualValues.length === 1 && actualValues[0] === planned
|
|
162
|
+
? []
|
|
163
|
+
: ['command_identity'];
|
|
164
|
+
}
|
|
165
|
+
|
|
79
166
|
function plannedField(taskText, name) {
|
|
80
167
|
const plan = String(taskText || '').split(/^##\s+Verification Plan\s*$/m)[1];
|
|
81
168
|
if (!plan) return null;
|
|
@@ -149,6 +236,103 @@ function checkTaskReceipt(featureDir, taskPath, task, runtimeContext, policy) {
|
|
|
149
236
|
return { ...proof, failures: [...new Set(failures)] };
|
|
150
237
|
}
|
|
151
238
|
|
|
239
|
+
function checkWorkflowTaskReceipt(featureDir, taskPath, runtimeContext, policy) {
|
|
240
|
+
const safeTaskPath = typeof taskPath === 'string' && /^task-[A-Za-z0-9][A-Za-z0-9._-]*\.md$/.test(taskPath);
|
|
241
|
+
if (!safeTaskPath) return { status: 'unsafe', taskPath, failures: ['unsafe_path'] };
|
|
242
|
+
const taskFile = safeRead(featureDir, taskPath);
|
|
243
|
+
if (taskFile.status !== 'ok') {
|
|
244
|
+
return { ...taskFile, taskPath, failures: [taskFile.status === 'unsafe' ? 'unsafe_path' : 'missing_receipt'] };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const taskText = taskFile.bytes.toString('utf8');
|
|
248
|
+
const statusLines = annotatedMarkdownLines(taskText)
|
|
249
|
+
.filter(({ outsideFence }) => outsideFence)
|
|
250
|
+
.map(({ line }) => line.match(/^\s*(?:\*\*)?Status(?:\*\*)?\s*:\s*(?:\*\*)?\s*([A-Za-z_-]+)(?:\*\*)?\s*$/i))
|
|
251
|
+
.filter(Boolean);
|
|
252
|
+
const body = workflowReceiptBody(taskText, policy);
|
|
253
|
+
const failures = [];
|
|
254
|
+
if (statusLines.length !== 1 || statusLines[0][1].toLowerCase().replaceAll('-', '_') !== 'done') {
|
|
255
|
+
failures.push('task_status');
|
|
256
|
+
}
|
|
257
|
+
if (body === null) {
|
|
258
|
+
failures.push('missing_receipt');
|
|
259
|
+
} else {
|
|
260
|
+
const canonicalBody = workflowCanonicalValidationBody(body);
|
|
261
|
+
const options = typeof policy?.receiptValidatorOptions === 'function'
|
|
262
|
+
? policy.receiptValidatorOptions({}, { runtimeContext, requireProvenanceBinding: true })
|
|
263
|
+
: { requireProvenanceBinding: true };
|
|
264
|
+
failures.push(...canonicalFailures(canonicalBody, {}, runtimeContext, policy));
|
|
265
|
+
failures.push(...canonicalFailures(body, {}, runtimeContext, policy)
|
|
266
|
+
.filter((failure) => ['placeholder', 'verification_state', 'exit_result'].includes(failure)));
|
|
267
|
+
failures.push(...workflowCommandFailures(taskText, canonicalBody));
|
|
268
|
+
if (!hasNonEmptyFencedBlock(body)) failures.push('command_output');
|
|
269
|
+
if (!options.expectedProvenance) failures.push('provenance');
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
status: failures.length === 0 ? 'ok' : 'invalid',
|
|
273
|
+
source: 'inline',
|
|
274
|
+
body,
|
|
275
|
+
receiptBytes: body === null ? null : Buffer.from(body),
|
|
276
|
+
taskFile,
|
|
277
|
+
taskText,
|
|
278
|
+
taskPath,
|
|
279
|
+
failures: [...new Set(failures)],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function workflowProofSignature(taskPath, proof) {
|
|
284
|
+
const hash = crypto.createHash('sha256');
|
|
285
|
+
hash.update('cafekit-workflow-proof-v1\0');
|
|
286
|
+
hash.update(String(taskPath));
|
|
287
|
+
hash.update('\0');
|
|
288
|
+
hash.update(proof.taskFile?.bytes || Buffer.alloc(0));
|
|
289
|
+
hash.update('\0');
|
|
290
|
+
hash.update(proof.receiptBytes || Buffer.alloc(0));
|
|
291
|
+
hash.update('\0');
|
|
292
|
+
hash.update(String(proof.status || 'unknown'));
|
|
293
|
+
return hash.digest('hex');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function workflowDependencyProofState(featureDir, taskRegistry, runtimeContext, policy) {
|
|
297
|
+
const tasks = {};
|
|
298
|
+
for (const taskPath of Object.keys(taskRegistry || {}).sort()) {
|
|
299
|
+
const done = taskRegistry[taskPath]?.status === 'done';
|
|
300
|
+
const proof = checkWorkflowTaskReceipt(featureDir, taskPath, runtimeContext, policy);
|
|
301
|
+
const valid = proof.failures.length === 0;
|
|
302
|
+
tasks[taskPath] = {
|
|
303
|
+
done,
|
|
304
|
+
valid,
|
|
305
|
+
eligible: done && valid,
|
|
306
|
+
signature: workflowProofSignature(taskPath, proof),
|
|
307
|
+
failures: [...proof.failures],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return tasks;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function checkWorkflowReceiptSet(candidates, projectRoot, runtimeSession, policy) {
|
|
314
|
+
const failures = [];
|
|
315
|
+
for (const candidate of Array.isArray(candidates) ? candidates : []) {
|
|
316
|
+
const runtimeContext = policy.deriveRuntimeContext({
|
|
317
|
+
projectRoot,
|
|
318
|
+
specsRoot: candidate.specsDir,
|
|
319
|
+
specFile: candidate.stateFile || candidate.planFile,
|
|
320
|
+
featureName: candidate.featureName,
|
|
321
|
+
runtimeSession,
|
|
322
|
+
});
|
|
323
|
+
const doneTasks = Object.keys(candidate.taskRegistry || {}).filter((taskPath) => (
|
|
324
|
+
candidate.taskRegistry[taskPath]?.status === 'done'
|
|
325
|
+
));
|
|
326
|
+
for (const taskPath of doneTasks) {
|
|
327
|
+
const proof = checkWorkflowTaskReceipt(candidate.featureDir, taskPath, runtimeContext, policy);
|
|
328
|
+
if (proof.failures.length > 0) {
|
|
329
|
+
failures.push({ featureName: candidate.featureName, taskPath, failures: proof.failures });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { failures };
|
|
334
|
+
}
|
|
335
|
+
|
|
152
336
|
function checkFeatureReceipt(featureDir, runtimeContext, policy) {
|
|
153
337
|
const receipt = safeRead(featureDir, 'feature-receipt.md');
|
|
154
338
|
if (receipt.status !== 'ok') return { status: receipt.status, failures: [receipt.status === 'unsafe' ? 'unsafe_path' : 'missing_receipt'] };
|
|
@@ -159,4 +343,15 @@ function checkFeatureReceipt(featureDir, runtimeContext, policy) {
|
|
|
159
343
|
return { status: 'ok', source: 'feature', body, receiptBytes: receipt.bytes, receiptFile: receipt, failures: [...new Set(failures)] };
|
|
160
344
|
}
|
|
161
345
|
|
|
162
|
-
module.exports = {
|
|
346
|
+
module.exports = {
|
|
347
|
+
canonicalTaskReceiptPath,
|
|
348
|
+
checkFeatureReceipt,
|
|
349
|
+
checkTaskReceipt,
|
|
350
|
+
checkWorkflowReceiptSet,
|
|
351
|
+
checkWorkflowTaskReceipt,
|
|
352
|
+
evidenceBody,
|
|
353
|
+
readTaskProof,
|
|
354
|
+
safeRead,
|
|
355
|
+
workflowDependencyProofState,
|
|
356
|
+
workflowReceiptBody,
|
|
357
|
+
};
|