@haposoft/cafekit 0.16.0-rc.5 → 0.16.0-rc.7
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 +137 -0
- package/bin/__tests__/codex-native.test.js +130 -74
- 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 +10 -20
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +13 -5
- 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 +3 -8
- package/src/claude/CLAUDE.md +19 -6
- package/src/claude/agents/project-manager.md +41 -9
- package/src/claude/agents/spec-maker.md +76 -155
- package/src/claude/gitignore +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +162 -0
- 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 +22 -11
- 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 +141 -1
- package/src/claude/scripts/spec-resolver.cjs +288 -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 +142 -204
- package/src/claude/skills/specs/references/review.md +109 -243
- package/src/claude/skills/specs/references/templates.md +173 -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 +22 -12
- 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
|
@@ -68,6 +68,105 @@ function candidateError(reason, code = 'malformed') {
|
|
|
68
68
|
return error;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
const WORKFLOW_STATUSES = new Set(['pending', 'in_progress', 'paused', 'blocked', 'done']);
|
|
72
|
+
const WORKFLOW_TASK_NAME = /^task-[A-Za-z0-9][A-Za-z0-9._-]*\.md$/;
|
|
73
|
+
|
|
74
|
+
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('-', '_'));
|
|
77
|
+
if (values.length !== 1 || !WORKFLOW_STATUSES.has(values[0])) {
|
|
78
|
+
throw candidateError(`${taskName} must contain exactly one supported Status field`);
|
|
79
|
+
}
|
|
80
|
+
return values[0];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function inspectWorkflowFeature(specsDir, canonicalSpecs, requestedName) {
|
|
84
|
+
const featureDir = path.join(specsDir, requestedName);
|
|
85
|
+
let featureLstat;
|
|
86
|
+
try {
|
|
87
|
+
featureLstat = fs.lstatSync(featureDir);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (isMissingError(error)) return { missing: true };
|
|
90
|
+
throw candidateError(`feature lstat error: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
if (featureLstat.isSymbolicLink() || !featureLstat.isDirectory()) {
|
|
93
|
+
throw candidateError('workflow feature entry must be a regular directory');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const canonicalFeature = fs.realpathSync(featureDir);
|
|
97
|
+
if (!isPathInside(canonicalSpecs, canonicalFeature) || path.dirname(canonicalFeature) !== canonicalSpecs) {
|
|
98
|
+
throw candidateError('workflow feature directory must be a direct child of specs root');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const featureName = path.basename(canonicalFeature);
|
|
102
|
+
if (fs.existsSync(path.join(canonicalFeature, 'spec.json'))) {
|
|
103
|
+
return { legacyPresent: true, featureName, featureDir: canonicalFeature };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const planFile = path.join(canonicalFeature, 'plan.md');
|
|
107
|
+
let planLstat;
|
|
108
|
+
try {
|
|
109
|
+
planLstat = fs.lstatSync(planFile);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (isMissingError(error)) return { missingWorkflow: true, featureName, featureDir: canonicalFeature };
|
|
112
|
+
throw candidateError(`plan.md lstat error: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
if (planLstat.isSymbolicLink() || !planLstat.isFile() || fs.realpathSync(planFile) !== planFile) {
|
|
115
|
+
throw candidateError('plan.md must be a regular non-symlink file in the workflow feature directory');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const taskNames = fs.readdirSync(canonicalFeature, { withFileTypes: true })
|
|
119
|
+
.filter((entry) => WORKFLOW_TASK_NAME.test(entry.name))
|
|
120
|
+
.map((entry) => entry.name)
|
|
121
|
+
.sort();
|
|
122
|
+
if (taskNames.length === 0) {
|
|
123
|
+
return { missingWorkflow: true, featureName, featureDir: canonicalFeature };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tasks = taskNames.map((taskName) => {
|
|
127
|
+
const taskFile = path.join(canonicalFeature, taskName);
|
|
128
|
+
const taskLstat = fs.lstatSync(taskFile);
|
|
129
|
+
if (taskLstat.isSymbolicLink() || !taskLstat.isFile() || fs.realpathSync(taskFile) !== taskFile) {
|
|
130
|
+
throw candidateError(`${taskName} must be a regular non-symlink file in the workflow feature directory`);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
path: taskName,
|
|
134
|
+
status: workflowTaskStatus(fs.readFileSync(taskFile, 'utf8'), taskName),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
const taskRegistry = Object.fromEntries(tasks.map((task) => [task.path, { status: task.status }]));
|
|
138
|
+
const allTasksDone = tasks.every((task) => task.status === 'done');
|
|
139
|
+
return {
|
|
140
|
+
layoutKind: 'process-v3',
|
|
141
|
+
featureName,
|
|
142
|
+
specsDir: canonicalSpecs,
|
|
143
|
+
featureDir: canonicalFeature,
|
|
144
|
+
planFile,
|
|
145
|
+
stateFile: planFile,
|
|
146
|
+
phase: allTasksDone ? 'done' : 'execution',
|
|
147
|
+
taskRegistry,
|
|
148
|
+
tasks,
|
|
149
|
+
allTasksDone,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeLegacyWorkflowCandidate(candidate) {
|
|
154
|
+
const registry = candidate.spec?.task_registry || {};
|
|
155
|
+
const tasks = Object.entries(registry).map(([taskPath, task]) => ({
|
|
156
|
+
path: taskPath,
|
|
157
|
+
status: task?.status || 'pending',
|
|
158
|
+
}));
|
|
159
|
+
return {
|
|
160
|
+
...candidate,
|
|
161
|
+
layoutKind: 'legacy-spec',
|
|
162
|
+
stateFile: candidate.specFile,
|
|
163
|
+
phase: candidate.spec?.current_phase || candidate.spec?.phase || 'unknown',
|
|
164
|
+
taskRegistry: registry,
|
|
165
|
+
tasks,
|
|
166
|
+
allTasksDone: tasks.length > 0 && tasks.every((task) => task.status === 'done'),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
71
170
|
function inspectFeature(specsDir, canonicalSpecs, requestedName) {
|
|
72
171
|
const featureDir = path.join(specsDir, requestedName);
|
|
73
172
|
let featureLstat;
|
|
@@ -491,6 +590,193 @@ function resolvePersistedSpec({ projectRoot, runtime, explicitFeature, explicitP
|
|
|
491
590
|
}
|
|
492
591
|
}
|
|
493
592
|
|
|
593
|
+
function scanWorkflowFeatures(specsDir) {
|
|
594
|
+
let canonicalSpecs;
|
|
595
|
+
try {
|
|
596
|
+
canonicalSpecs = fs.realpathSync(specsDir);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if (isMissingError(error)) return { active: [], candidates: [], invalid: [] };
|
|
599
|
+
return { active: [], candidates: [], invalid: [{ featureName: '<specs>', reason: error.message }] };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const active = [];
|
|
603
|
+
const candidates = [];
|
|
604
|
+
const invalid = [];
|
|
605
|
+
const entries = fs.readdirSync(canonicalSpecs, { withFileTypes: true })
|
|
606
|
+
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
|
607
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
608
|
+
for (const entry of entries) {
|
|
609
|
+
try {
|
|
610
|
+
const candidate = inspectWorkflowFeature(canonicalSpecs, canonicalSpecs, entry.name);
|
|
611
|
+
if (candidate.missing || candidate.missingWorkflow || candidate.legacyPresent) continue;
|
|
612
|
+
candidates.push(candidate);
|
|
613
|
+
if (!candidate.allTasksDone) active.push(candidate);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
invalid.push({ featureName: entry.name, reason: error.message, planFile: path.join(canonicalSpecs, entry.name, 'plan.md') });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return { active, candidates, invalid };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function explicitWorkflowFeatureName({ projectRoot, specsDir, explicitFeature, explicitPath }) {
|
|
622
|
+
if (explicitFeature !== undefined && explicitFeature !== null) {
|
|
623
|
+
if (typeof explicitFeature !== 'string' || explicitFeature.trim() === ''
|
|
624
|
+
|| explicitFeature.includes('/') || explicitFeature.includes('\\') || explicitFeature.includes('..')) {
|
|
625
|
+
return { error: 'explicit_malformed', explicitFeature, reason: `malformed feature name: ${explicitFeature}` };
|
|
626
|
+
}
|
|
627
|
+
return { featureName: explicitFeature.trim() };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (typeof explicitPath !== 'string' || explicitPath.trim() === '') {
|
|
631
|
+
return { error: 'explicit_malformed', explicitPath, reason: 'empty explicit path' };
|
|
632
|
+
}
|
|
633
|
+
const targetPath = path.resolve(projectRoot, explicitPath);
|
|
634
|
+
let targetLstat;
|
|
635
|
+
try {
|
|
636
|
+
targetLstat = fs.lstatSync(targetPath);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
return isMissingError(error)
|
|
639
|
+
? { error: 'explicit_not_found', explicitPath, reason: `workflow target not found at ${explicitPath}` }
|
|
640
|
+
: { error: 'explicit_malformed', explicitPath, reason: `explicit path stat error: ${error.message}` };
|
|
641
|
+
}
|
|
642
|
+
if (targetLstat.isSymbolicLink()) {
|
|
643
|
+
return { error: 'explicit_malformed', explicitPath, reason: 'explicit workflow target must not be a symlink' };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let featureDir;
|
|
647
|
+
if (targetLstat.isDirectory()) {
|
|
648
|
+
featureDir = targetPath;
|
|
649
|
+
} else if (targetLstat.isFile()
|
|
650
|
+
&& (path.basename(targetPath) === 'plan.md' || WORKFLOW_TASK_NAME.test(path.basename(targetPath)))) {
|
|
651
|
+
featureDir = path.dirname(targetPath);
|
|
652
|
+
} else {
|
|
653
|
+
return { error: 'explicit_malformed', explicitPath, reason: 'explicit workflow path must target a feature directory, plan.md, or flat task-*.md' };
|
|
654
|
+
}
|
|
655
|
+
const relative = path.relative(path.resolve(specsDir), featureDir);
|
|
656
|
+
if (!relative || relative.includes(path.sep) || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
657
|
+
return { error: 'explicit_malformed', explicitPath, reason: 'explicit workflow path must target a direct child of specs root' };
|
|
658
|
+
}
|
|
659
|
+
return { featureName: relative };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Resolve the authoring/execution workflow visible to prompt and Stop hooks.
|
|
664
|
+
* Persisted spec resolution remains unchanged; process-v3 is an additive adapter
|
|
665
|
+
* and never becomes completion-authority input by way of findAllSpecCandidates.
|
|
666
|
+
*/
|
|
667
|
+
function resolveWorkflowCandidate({ projectRoot, runtime, explicitFeature, explicitPath, target, includeCompleted = false } = {}) {
|
|
668
|
+
if (!projectRoot) throw new TypeError('projectRoot required');
|
|
669
|
+
const normalized = extractExplicitTarget(
|
|
670
|
+
target,
|
|
671
|
+
explicitFeature !== undefined ? { explicitFeature } : null,
|
|
672
|
+
explicitPath !== undefined ? { explicitPath } : null,
|
|
673
|
+
);
|
|
674
|
+
if (normalized) {
|
|
675
|
+
if (Object.prototype.hasOwnProperty.call(normalized, 'explicitFeature')) explicitFeature = normalized.explicitFeature;
|
|
676
|
+
if (Object.prototype.hasOwnProperty.call(normalized, 'explicitPath')) explicitPath = normalized.explicitPath;
|
|
677
|
+
}
|
|
678
|
+
const hasExplicit = explicitFeature !== undefined && explicitFeature !== null
|
|
679
|
+
|| explicitPath !== undefined && explicitPath !== null;
|
|
680
|
+
let specsDir;
|
|
681
|
+
try {
|
|
682
|
+
specsDir = specsDirectory(projectRoot, runtime || {});
|
|
683
|
+
} catch (error) {
|
|
684
|
+
return { error: hasExplicit ? 'explicit_malformed' : 'invalid_specs', candidates: ['<specs>'], reason: error.message };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (hasExplicit) {
|
|
688
|
+
const legacyDirect = resolveActiveSpec({ projectRoot, runtime, explicitFeature, explicitPath });
|
|
689
|
+
if (legacyDirect && !legacyDirect.error) return normalizeLegacyWorkflowCandidate(legacyDirect);
|
|
690
|
+
const identity = explicitWorkflowFeatureName({ projectRoot, specsDir, explicitFeature, explicitPath });
|
|
691
|
+
if (identity.error) return identity;
|
|
692
|
+
|
|
693
|
+
const legacy = resolveActiveSpec({ projectRoot, runtime, explicitFeature: identity.featureName });
|
|
694
|
+
if (legacy && !legacy.error) return normalizeLegacyWorkflowCandidate(legacy);
|
|
695
|
+
if (legacy && legacy.error !== 'explicit_not_found') return legacy;
|
|
696
|
+
|
|
697
|
+
let canonicalSpecs;
|
|
698
|
+
try {
|
|
699
|
+
canonicalSpecs = fs.realpathSync(specsDir);
|
|
700
|
+
const candidate = inspectWorkflowFeature(specsDir, canonicalSpecs, identity.featureName);
|
|
701
|
+
if (candidate.missing || candidate.missingWorkflow || candidate.legacyPresent) {
|
|
702
|
+
return { error: 'explicit_not_found', explicitFeature: identity.featureName, explicitPath, reason: `workflow not found for feature ${identity.featureName}` };
|
|
703
|
+
}
|
|
704
|
+
return candidate;
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return { error: 'explicit_malformed', explicitFeature: identity.featureName, explicitPath, reason: error.message };
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
let legacyActive;
|
|
711
|
+
try {
|
|
712
|
+
const legacyCandidates = includeCompleted
|
|
713
|
+
? findAllSpecCandidates(projectRoot, runtime || {})
|
|
714
|
+
: findAllActiveSpecs(projectRoot, runtime || {});
|
|
715
|
+
legacyActive = legacyCandidates.map(normalizeLegacyWorkflowCandidate);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
return {
|
|
718
|
+
error: 'invalid_specs',
|
|
719
|
+
candidates: Array.isArray(error.invalid) ? error.invalid.map((entry) => entry.featureName) : ['<specs>'],
|
|
720
|
+
invalid: error.invalid || [],
|
|
721
|
+
reason: error.message,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
const workflow = scanWorkflowFeatures(specsDir);
|
|
725
|
+
if (workflow.invalid.length > 0) {
|
|
726
|
+
return {
|
|
727
|
+
error: 'invalid_specs',
|
|
728
|
+
candidates: workflow.invalid.map((entry) => entry.featureName),
|
|
729
|
+
invalid: workflow.invalid,
|
|
730
|
+
reason: `Invalid workflow candidates: ${workflow.invalid.map((entry) => `${entry.featureName}: ${entry.reason}`).join('; ')}`,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
const workflowCandidates = includeCompleted ? workflow.candidates : workflow.active;
|
|
734
|
+
const active = [...legacyActive, ...workflowCandidates]
|
|
735
|
+
.sort((left, right) => left.featureName.localeCompare(right.featureName));
|
|
736
|
+
if (active.length === 0) return null;
|
|
737
|
+
if (active.length === 1) return active[0];
|
|
738
|
+
return {
|
|
739
|
+
error: includeCompleted ? 'multiple_persisted' : 'multiple_active',
|
|
740
|
+
candidates: active.map((candidate) => candidate.featureName),
|
|
741
|
+
active,
|
|
742
|
+
reason: `Multiple ${includeCompleted ? 'persisted' : 'active'} workflows found: ${active.map((candidate) => candidate.featureName).join(', ')}. Provide explicit feature.`,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Narrow a Stop-gate ambiguity without changing legacy persisted-spec rules.
|
|
748
|
+
* A single unfinished process-v3 packet is the current workflow; completed
|
|
749
|
+
* packets are historical. If every process-v3 packet is complete, return the
|
|
750
|
+
* set so the gate can revalidate all receipts instead of blocking on identity.
|
|
751
|
+
*/
|
|
752
|
+
function refineWorkflowGateResolution(resolved) {
|
|
753
|
+
if (!resolved || resolved.error !== 'multiple_persisted' || !Array.isArray(resolved.active)) {
|
|
754
|
+
return resolved;
|
|
755
|
+
}
|
|
756
|
+
const candidates = resolved.active;
|
|
757
|
+
if (candidates.length === 0 || candidates.some((candidate) => candidate.layoutKind !== 'process-v3')) {
|
|
758
|
+
return resolved;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const unfinished = candidates.filter((candidate) => !candidate.allTasksDone);
|
|
762
|
+
if (unfinished.length === 1) return unfinished[0];
|
|
763
|
+
if (unfinished.length > 1) {
|
|
764
|
+
return {
|
|
765
|
+
...resolved,
|
|
766
|
+
error: 'multiple_active',
|
|
767
|
+
candidates: unfinished.map((candidate) => candidate.featureName),
|
|
768
|
+
active: unfinished,
|
|
769
|
+
reason: `Multiple active workflows found: ${unfinished.map((candidate) => candidate.featureName).join(', ')}. Provide explicit feature.`,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
773
|
+
layoutKind: 'process-v3-completed-set',
|
|
774
|
+
candidates,
|
|
775
|
+
specsDir: candidates[0].specsDir,
|
|
776
|
+
allTasksDone: true,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
494
780
|
module.exports = {
|
|
495
781
|
specsDirectory,
|
|
496
782
|
findAllActiveSpecs,
|
|
@@ -498,4 +784,6 @@ module.exports = {
|
|
|
498
784
|
extractExplicitTarget,
|
|
499
785
|
resolveActiveSpec,
|
|
500
786
|
resolvePersistedSpec,
|
|
787
|
+
resolveWorkflowCandidate,
|
|
788
|
+
refineWorkflowGateResolution,
|
|
501
789
|
};
|
|
@@ -1,187 +1,151 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hapo:develop
|
|
3
|
-
description: "Implement an explicitly
|
|
3
|
+
description: "Implement an explicitly requested process-first feature or task, run its real verification, and close it with one evidence owner. Also supports existing legacy Specs packets."
|
|
4
4
|
user-invocable: true
|
|
5
|
-
when_to_use: "Use to implement a ready
|
|
5
|
+
when_to_use: "Use to implement a ready feature packet, one named task, or a clear low-risk change."
|
|
6
6
|
category: utilities
|
|
7
7
|
keywords: [implementation, specs, verification]
|
|
8
8
|
argument-hint: "[feature-name|specs-directory-path] [task-file] [--flash] [--parallel [N]] [--notes]"
|
|
9
9
|
metadata:
|
|
10
10
|
author: haposoft
|
|
11
|
-
version: "
|
|
11
|
+
version: "3.0.0"
|
|
12
12
|
---
|
|
13
|
-
# Develop —
|
|
13
|
+
# Develop — implement, prove, synchronize
|
|
14
14
|
|
|
15
|
-
Implement the requested scope
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
is `src/claude/scripts/workflow-policy.cjs` (installed as
|
|
20
|
-
`.claude/scripts/workflow-policy.cjs`).
|
|
15
|
+
Implement only the explicitly requested scope. For a process-first feature,
|
|
16
|
+
`plan.md` and flat tasks are the durable packet; one closeout owner coordinates verification, review, sync, and docs impact without borrowing evidence.
|
|
17
|
+
|
|
18
|
+
Develop never starts merely because Specs finished. The user invokes it.
|
|
21
19
|
|
|
22
20
|
## Usage and pre-state guard
|
|
23
21
|
|
|
24
22
|
```text
|
|
25
23
|
/hapo:develop <feature>
|
|
26
24
|
/hapo:develop specs/<feature>
|
|
27
|
-
/hapo:develop <feature>
|
|
25
|
+
/hapo:develop <feature> task-02-<slug>.md
|
|
28
26
|
/hapo:develop <feature> --flash
|
|
29
27
|
/hapo:develop <feature> --parallel [N]
|
|
30
28
|
/hapo:develop <feature> --notes
|
|
31
29
|
```
|
|
32
30
|
|
|
33
|
-
`--notes` is opt-in.
|
|
31
|
+
`--notes` is opt-in. Only then load `references/implementation-notes-template.html`;
|
|
32
|
+
record decisions, scope exceptions, risks, gaps, and verification limits. Never create it by default.
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
Before any state mutation, reject `--flash` combined with `--parallel`:
|
|
36
35
|
|
|
37
36
|
```bash
|
|
38
37
|
node .claude/scripts/workflow-policy.cjs --flash --parallel --json
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
```text
|
|
44
|
-
Unsupported flags: --flash and --parallel are incompatible.
|
|
45
|
-
Remediation: run `/hapo:develop <feature> --flash` or `/hapo:develop <feature> --parallel [N]`.
|
|
46
|
-
No spec state, task receipt, worktree, subagent, or commit was created.
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Policy authority
|
|
50
|
-
|
|
51
|
-
For durable specs, persisted `workflow_policy` authority is the v2.1 minimum.
|
|
52
|
-
Canonical authoring supplies `planning_depth`, `assurance_level`,
|
|
53
|
-
`classified_minimum`, and normalized `risks`; lane and ceremony remain derived
|
|
54
|
-
views.
|
|
55
|
-
|
|
56
|
-
Classify before spec/state mutation:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
node .claude/scripts/workflow-policy.cjs --classify-lane --task-json '<task JSON>' --json
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
- **Direct**: clear, isolated, reversible, low-risk. Skip spec/state/registry ceremony and run targeted verification with proportional evidence.
|
|
63
|
-
- **Standard**: use the bounded spec profile and focused inspection. A small feature needs no task registry/bundle and closes once at the feature boundary.
|
|
64
|
-
- **Critical**: derived Strict view. Add durable task state, research, or review
|
|
65
|
-
only when actual topology, uncertainty, or assurance requires it.
|
|
40
|
+
The pair exits `2` before a task edit, receipt, worktree, subagent, or commit. Do not reproduce it with a looser parser.
|
|
66
41
|
|
|
67
|
-
|
|
68
|
-
persistence, a per-feature baseline is monotonic and no downgrade is supported
|
|
69
|
-
until a trusted issuer exists; the baseline applies only to that feature.
|
|
70
|
-
Legacy approval fields and caller booleans have zero authority.
|
|
42
|
+
## Resolve the work packet
|
|
71
43
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
44
|
+
1. Resolve one `specs/<feature>/plan.md` plus flat `task-NN-*.md`; an explicit task path wins.
|
|
45
|
+
2. Read C1 scope, exclusions, acceptance mapping, ownership, and task order.
|
|
46
|
+
3. A task is unblocked only when every named dependency is done with a valid inline Receipt.
|
|
47
|
+
4. Full-feature mode selects one unblocked task at a time, in plan order.
|
|
48
|
+
Specific-task mode implements only that task and then stops.
|
|
49
|
+
5. Without a packet, work directly only when the change is clear, isolated, reversible, and low-risk.
|
|
75
50
|
|
|
76
|
-
|
|
51
|
+
Ambiguity, overlapping ownership, a dependency cycle, or a missing Verification Plan is a blocker. Do not guess.
|
|
77
52
|
|
|
78
53
|
## Modes
|
|
79
54
|
|
|
80
|
-
###
|
|
55
|
+
### Sequential feature or specific task
|
|
81
56
|
|
|
82
|
-
|
|
57
|
+
Set the selected task's single `Status:` field to `in_progress`. Implement its
|
|
58
|
+
Outcome and Acceptance only. Do not start the next task until proof, review,
|
|
59
|
+
and sync for the current task finish. A specific-task invocation never chains.
|
|
83
60
|
|
|
84
|
-
###
|
|
61
|
+
### Parallel (`--parallel [N]`)
|
|
85
62
|
|
|
86
|
-
|
|
63
|
+
Load `references/parallel-waves.md`. Require disjoint writes, satisfied dependencies,
|
|
64
|
+
isolated worktrees, a bounded wave, and one controller writer; otherwise work sequentially.
|
|
87
65
|
|
|
88
|
-
###
|
|
66
|
+
### Flash (`--flash`)
|
|
89
67
|
|
|
90
|
-
|
|
68
|
+
Flash is an explicit speed trade-off, never completion:
|
|
91
69
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
- skip dedicated test suites, full task evidence execution, and extended manual
|
|
96
|
-
checks;
|
|
97
|
-
- run only a cheap available syntax/typecheck/compile preflight;
|
|
98
|
-
- never weaken or delete tests;
|
|
99
|
-
- synchronize only as `in_progress` with `FLASH_UNVERIFIED`,
|
|
100
|
-
`dependencyBlocked: true`, `unblocks: false`, and blocker
|
|
70
|
+
- run only an available cheap syntax, typecheck, or compile preflight;
|
|
71
|
+
- skip dedicated tests, extended manual checks, and review retry loops;
|
|
72
|
+
- keep `Status: in_progress` and record `FLASH_UNVERIFIED` plus blocker
|
|
101
73
|
`awaiting /hapo:test <feature>`;
|
|
102
|
-
-
|
|
103
|
-
|
|
74
|
+
- do not unblock dependents or report Test PASS, Evidence PASS,
|
|
75
|
+
production-ready, or done;
|
|
76
|
+
- promotion later requires a fresh canonical PASS receipt through the trusted
|
|
77
|
+
sync-finalize path.
|
|
104
78
|
|
|
105
|
-
|
|
106
|
-
## Workflow
|
|
107
|
-
`Classify lane → load lane artifacts → scout obligations → implement current
|
|
108
|
-
scope → one closeout owner → test receipt → review → docs-impact sync.`
|
|
79
|
+
Never weaken, delete, or rewrite tests to make Flash look complete.
|
|
109
80
|
|
|
110
|
-
|
|
81
|
+
## Task cycle
|
|
111
82
|
|
|
112
|
-
|
|
113
|
-
- Standard requires its bounded validated artifacts, but not a
|
|
114
|
-
registry when the scope is small and no task obligation is persisted.
|
|
115
|
-
- Critical requires only the artifacts named by its snapshot obligations.
|
|
116
|
-
- For task-bearing work, load one requested task or one unblocked task and
|
|
117
|
-
extract the exact seven v2.1 sections: `Outcome`, `Scope`, `Anchors and Ownership`,
|
|
118
|
-
`Changes`, `Acceptance`, `Dependencies`, and `Verification Plan`.
|
|
119
|
-
Use typed `coordination.boundaries` for ownership/DAG/proof/parallel authority.
|
|
120
|
-
- Before implementation, transition `pending` to `in_progress` in task Markdown `**Status:**` and `spec.json.task_registry[path]` with synchronized timestamps. Never write execution proof into the task plan.
|
|
121
|
-
- A spec-ready execution-evidence or independent-audit slot may be `PENDING`
|
|
122
|
-
before implementation; pending is not proof and cannot close the feature.
|
|
83
|
+
### 1. Scout
|
|
123
84
|
|
|
124
|
-
|
|
85
|
+
Trace entrypoints, callers, dependents, registration, errors, and owned files.
|
|
86
|
+
Compilation does not prove an unmounted UI, unregistered route, uncalled service, or missing consumer.
|
|
125
87
|
|
|
126
|
-
|
|
127
|
-
entrypoints/callers, integration points, dependents, reachability risks, and
|
|
128
|
-
safe scope. File count is not review depth; stop if a runtime entrypoint cannot
|
|
129
|
-
be grounded.
|
|
88
|
+
### 2. Implement
|
|
130
89
|
|
|
131
|
-
|
|
90
|
+
Honor Scope, Ownership, Acceptance, and Dependencies. Do not silently replace named contracts;
|
|
91
|
+
scope expansion requires evidence and a return to C1, not implementation convenience.
|
|
132
92
|
|
|
133
|
-
|
|
134
|
-
frameworks, auth, transport, storage, or runtime boundaries. Do not mark a
|
|
135
|
-
runtime-facing file complete while it is orphaned, unmounted, unregistered, or
|
|
136
|
-
uncalled. A specific-task request ends after that task; no automatic chain.
|
|
93
|
+
### 3. Verify and review once
|
|
137
94
|
|
|
138
|
-
|
|
139
|
-
The
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
</SCOPE-FIDELITY>
|
|
95
|
+
The test owner executes the exact Verification Plan and records real output.
|
|
96
|
+
The review owner, when risk or repository policy requires review, evaluates
|
|
97
|
+
correctness, security, scope, and reachability but does not manufacture proof.
|
|
98
|
+
Use `PASS | PASS_WITH_WARNINGS | FAIL | BLOCKED`; only literal PASS can close.
|
|
143
99
|
|
|
144
|
-
|
|
100
|
+
Load `references/quality-gate.md` for the closeout contract. Remediate an
|
|
101
|
+
observed failure, then rerun only affected proof and review. Do not blind-retry
|
|
102
|
+
a blocked environment. After three failed repair rounds, stop and ask the user.
|
|
145
103
|
|
|
146
|
-
|
|
104
|
+
### 4. Write the inline Receipt and sync
|
|
147
105
|
|
|
148
|
-
|
|
149
|
-
- The review owner evaluates correctness, security, and spec compliance; it
|
|
150
|
-
consumes existing proof but never creates or claims execution proof.
|
|
151
|
-
- Review depth follows `assurance_level`, risk, and blast radius. Lane is only
|
|
152
|
-
a derived view, and depth does not follow the
|
|
153
|
-
number of files changed.
|
|
154
|
-
- Finding count never selects review depth or overrides missing execution proof.
|
|
155
|
-
- Use the shared adapter surface: `PASS | PASS_WITH_WARNINGS | FAIL | BLOCKED`.
|
|
156
|
-
Warnings do not waive missing proof; `FAIL` is remediable and `BLOCKED` stops.
|
|
106
|
+
After a real pass, append or replace the task's final `## Receipt` with:
|
|
157
107
|
|
|
158
|
-
|
|
159
|
-
|
|
108
|
+
- `Verification: PASS`;
|
|
109
|
+
- the exact `Command:` and `Exit: 0`;
|
|
110
|
+
- runtime-derived `Base:` and `Head:`;
|
|
111
|
+
- a non-empty fenced block containing current command output;
|
|
112
|
+
- required negative, reachability, and artifact proof from the task.
|
|
160
113
|
|
|
161
|
-
|
|
114
|
+
Then set `Status: done`, update only implemented checkboxes, and recompute the
|
|
115
|
+
queue. Missing, stale, contradictory, zero-test, placeholder, or marker-only
|
|
116
|
+
proof leaves the task unfinished.
|
|
162
117
|
|
|
163
|
-
|
|
164
|
-
After verification, synchronize status/timestamps/task state. Then evaluate
|
|
165
|
-
actual docs impact:
|
|
118
|
+
### 5. Finish the requested boundary
|
|
166
119
|
|
|
167
|
-
-
|
|
168
|
-
|
|
169
|
-
|
|
120
|
+
Specific-task mode stops after its sync. Full-feature mode continues one task
|
|
121
|
+
at a time, then runs the feature-level integration command and reachability
|
|
122
|
+
check named by the repository or plan. Show current evidence and limitations at
|
|
123
|
+
C3; the user decides completion.
|
|
170
124
|
|
|
171
|
-
|
|
125
|
+
Evaluate docs impact from the behavior actually changed:
|
|
172
126
|
|
|
173
|
-
|
|
127
|
+
- `none`: report it and make no docs edit;
|
|
128
|
+
- `minor` or `major`: update only affected existing docs through the docs flow.
|
|
174
129
|
|
|
175
|
-
|
|
176
|
-
a receipt bound to expected Base/Head, and every lane obligation. Only literal
|
|
177
|
-
`PASS` can complete; `PASS_WITH_WARNINGS` remains unfinished.
|
|
178
|
-
`--flash` records only `FLASH_UNVERIFIED` and never done.
|
|
130
|
+
## Definition of done
|
|
179
131
|
|
|
180
|
-
|
|
132
|
+
Done requires satisfied acceptance, reachable behavior, current executable
|
|
133
|
+
proof, correct Base/Head binding, resolved blocking review findings, and no
|
|
134
|
+
scope substitution. `PASS_WITH_WARNINGS`, `NO_TESTS`, `FLASH_UNVERIFIED`, or a
|
|
135
|
+
review-only pass cannot close work.
|
|
181
136
|
|
|
182
137
|
## Attached references
|
|
183
138
|
|
|
184
|
-
- `references/quality-gate.md` —
|
|
185
|
-
|
|
186
|
-
- `references/
|
|
187
|
-
|
|
139
|
+
- `references/quality-gate.md` — proof and review ownership.
|
|
140
|
+
- `references/parallel-waves.md` — opt-in isolated worktree waves.
|
|
141
|
+
- `references/implementation-notes-template.html` — only with `--notes`.
|
|
142
|
+
|
|
143
|
+
## Legacy workflow compatibility
|
|
144
|
+
|
|
145
|
+
If the selected feature contains `spec.json`, use its persisted
|
|
146
|
+
`workflow_policy`, `task_registry`, nested task paths, typed boundaries,
|
|
147
|
+
separate task receipts, and feature closeout receipt exactly as the legacy
|
|
148
|
+
adapters require. Preserve `planning_depth`, `assurance_level`, derived lane,
|
|
149
|
+
and read-only `execution_tier`; do not project these fields into a new v3
|
|
150
|
+
packet. Keep JSON and Markdown status synchronized, and use the existing
|
|
151
|
+
completion-authority path for final closeout.
|