wogiflow 2.26.2 → 2.29.0
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/.claude/commands/wogi-bug.md +30 -0
- package/.claude/commands/wogi-debug-hypothesis.md +33 -0
- package/.claude/commands/wogi-morning.md +1 -2
- package/.claude/commands/wogi-review.md +31 -2
- package/.claude/commands/wogi-start.md +32 -0
- package/.claude/commands/wogi-statusline-setup.md +12 -0
- package/.claude/commands/wogi-story.md +3 -2
- package/.claude/docs/claude-code-compatibility.md +40 -0
- package/.claude/docs/phases/01-explore.md +2 -1
- package/.claude/docs/phases/03-implement.md +4 -0
- package/.claude/docs/phases/04-verify.md +45 -0
- package/.claude/rules/README.md +36 -0
- package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
- package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
- package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
- package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
- package/.claude/rules/alternative-short-name.md +12 -0
- package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
- package/.claude/rules/architecture/hook-three-layer.md +68 -0
- package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
- package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
- package/.claude/settings.json +1 -1
- package/.workflow/agents/logic-adversary.md +2 -1
- package/.workflow/agents/personas/README.md +48 -0
- package/.workflow/agents/personas/platform-rigor.md +38 -0
- package/.workflow/agents/personas/scale-skeptic.md +28 -0
- package/.workflow/agents/personas/security-hawk.md +34 -0
- package/.workflow/agents/personas/simplicity-champion.md +37 -0
- package/.workflow/agents/personas/user-advocate.md +36 -0
- package/.workflow/bridges/base-bridge.js +46 -23
- package/.workflow/templates/claude-md.hbs +44 -122
- package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
- package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
- package/.workflow/templates/partials/methodology-rules.hbs +85 -79
- package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
- package/lib/fuzzy-patch.js +251 -0
- package/lib/installer.js +8 -0
- package/lib/memory-proposal-store.js +458 -0
- package/lib/mode-schema.js +255 -0
- package/lib/skill-proposal-store.js +432 -0
- package/lib/skill-registry.js +1 -1
- package/lib/wogi-claude +84 -9
- package/lib/wogi-claude-expect.exp +113 -76
- package/lib/workspace-channel-server.js +19 -0
- package/lib/workspace-contracts.js +1 -1
- package/lib/workspace-dispatch-tracking.js +144 -0
- package/lib/workspace-gates.js +1 -1
- package/lib/workspace-ipc-sqlite.js +550 -0
- package/lib/workspace-messages.js +92 -0
- package/lib/workspace-routing.js +1 -1
- package/lib/workspace-task-injector.js +223 -0
- package/lib/workspace.js +23 -0
- package/lib/worktree-review.js +315 -0
- package/package.json +2 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +28 -4
- package/scripts/flow-ac-scope-preservation.js +238 -0
- package/scripts/flow-auto-review-worker.js +75 -0
- package/scripts/flow-auto-review.js +102 -0
- package/scripts/flow-autonomous-detector.js +118 -0
- package/scripts/flow-autonomous-mode.js +153 -0
- package/scripts/flow-best-of-n.js +1 -1
- package/scripts/flow-bulk-loop.js +1 -1
- package/scripts/flow-checkpoint.js +2 -6
- package/scripts/flow-community-sync.js +1 -1
- package/scripts/flow-completion-summary.js +176 -0
- package/scripts/flow-completion-truth-gate.js +343 -4
- package/scripts/flow-config-defaults.js +52 -5
- package/scripts/flow-context-compact/expander.js +1 -1
- package/scripts/flow-context-compact/section-extractor.js +2 -2
- package/scripts/flow-context-gatherer.js +1 -1
- package/scripts/flow-context-generator.js +1 -1
- package/scripts/flow-context-scoring.js +1 -1
- package/scripts/flow-correct.js +1 -1
- package/scripts/flow-decision-authority.js +66 -15
- package/scripts/flow-done.js +33 -1
- package/scripts/flow-epic-cascade.js +171 -0
- package/scripts/flow-epics.js +2 -7
- package/scripts/flow-eval-judge.js +1 -1
- package/scripts/flow-eval.js +1 -1
- package/scripts/flow-export-scanner.js +2 -6
- package/scripts/flow-failure-learning.js +1 -1
- package/scripts/flow-feature-dossier.js +787 -0
- package/scripts/flow-figma-extract.js +2 -2
- package/scripts/flow-figma-generate.js +1 -1
- package/scripts/flow-gate-confidence.js +1 -1
- package/scripts/flow-health.js +52 -1
- package/scripts/flow-hooks.js +1 -1
- package/scripts/flow-id.js +19 -3
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-knowledge-router.js +1 -1
- package/scripts/flow-knowledge-sync.js +1 -1
- package/scripts/flow-logic-adversary.js +76 -1
- package/scripts/flow-logic-rules.js +380 -0
- package/scripts/flow-long-input.js +5 -5
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +78 -7
- package/scripts/flow-migrate.js +1 -1
- package/scripts/flow-model-caller.js +1 -1
- package/scripts/flow-models.js +2 -2
- package/scripts/flow-morning.js +0 -17
- package/scripts/flow-multi-approach.js +1 -1
- package/scripts/flow-orchestrate-context.js +4 -4
- package/scripts/flow-orchestrate-templates.js +1 -1
- package/scripts/flow-orchestrate.js +8 -8
- package/scripts/flow-peer-review.js +1 -1
- package/scripts/flow-phase.js +9 -0
- package/scripts/flow-proactive-compact.js +1 -1
- package/scripts/flow-providers.js +1 -1
- package/scripts/flow-question-queue.js +255 -0
- package/scripts/flow-repo-map.js +312 -0
- package/scripts/flow-review-passes/index.js +1 -1
- package/scripts/flow-review-passes/integration.js +1 -1
- package/scripts/flow-review-passes/structure.js +1 -1
- package/scripts/flow-revision-tracker.js +1 -1
- package/scripts/flow-section-resolver.js +1 -1
- package/scripts/flow-session-end.js +74 -5
- package/scripts/flow-session-state.js +103 -1
- package/scripts/flow-setup-hooks.js +1 -1
- package/scripts/flow-skeptical-evaluator.js +274 -0
- package/scripts/flow-skill-generator.js +3 -3
- package/scripts/flow-skill-learn.js +3 -6
- package/scripts/flow-skill-manage.js +248 -0
- package/scripts/flow-spec-verifier.js +1 -1
- package/scripts/flow-standards-checker.js +75 -0
- package/scripts/flow-standards-gate.js +1 -1
- package/scripts/flow-statusline-setup.js +8 -2
- package/scripts/flow-step-changelog.js +2 -2
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-knowledge.js +1 -1
- package/scripts/flow-step-regression.js +1 -1
- package/scripts/flow-step-simplifier.js +1 -1
- package/scripts/flow-task-analyzer.js +1 -1
- package/scripts/flow-task-classifier.js +1 -1
- package/scripts/flow-task-enforcer.js +1 -1
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-trap-zone.js +1 -1
- package/scripts/flow-utils.js +4 -0
- package/scripts/flow-worker-question-classifier.js +51 -5
- package/scripts/flow-workspace-migrate-ipc.js +216 -0
- package/scripts/flow-workspace-summary.js +256 -0
- package/scripts/hooks/adapters/base-adapter.js +2 -2
- package/scripts/hooks/core/feature-dossier-gate.js +194 -0
- package/scripts/hooks/core/observation-capture.js +24 -0
- package/scripts/hooks/core/overdue-dispatches.js +20 -1
- package/scripts/hooks/core/phase-gate.js +15 -1
- package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
- package/scripts/hooks/core/post-compact.js +5 -2
- package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
- package/scripts/hooks/core/routing-gate.js +58 -0
- package/scripts/hooks/core/session-context.js +108 -0
- package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
- package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
- package/scripts/hooks/core/session-end.js +25 -0
- package/scripts/hooks/core/setup-handler.js +1 -1
- package/scripts/hooks/core/task-boundary-reset.js +110 -4
- package/scripts/hooks/core/worker-boundary-gate.js +71 -0
- package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
- package/scripts/hooks/entry/claude-code/session-start.js +74 -30
- package/scripts/hooks/entry/claude-code/stop.js +47 -1
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
- package/.workflow/templates/partials/user-commands.hbs +0 -20
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Feature Dossier System
|
|
5
|
+
*
|
|
6
|
+
* Per-feature canonical knowledge docs with mechanical auto-injection.
|
|
7
|
+
*
|
|
8
|
+
* Problem solved (from the 2026-04-24 workspace failure catalog):
|
|
9
|
+
* - Feature/workflow knowledge gets lost between sessions
|
|
10
|
+
* - Claude doesn't proactively fetch context — even when told to look, it
|
|
11
|
+
* grabs one thing and ignores the rest
|
|
12
|
+
* - Owner corrections ("remove the contact block") stop being remembered
|
|
13
|
+
* - Small stuff has maps (app-map, function-map); multi-part logic has nothing
|
|
14
|
+
*
|
|
15
|
+
* How it works:
|
|
16
|
+
* - Each user-facing feature has a dossier at <dossierDir>/<slug>.md
|
|
17
|
+
* - Dossiers capture: canonical summary, match patterns, contracts,
|
|
18
|
+
* rejected alternatives, removed elements, known bugs, append-only log
|
|
19
|
+
* - .workflow/dossiers/index.json maps {route, file, component, keyword}
|
|
20
|
+
* patterns to dossier slugs
|
|
21
|
+
* - At phase transitions, matchFeatures() scans the active task for
|
|
22
|
+
* touched features and injects their canonical headers into phase context
|
|
23
|
+
* - validateSpecAgainstDossier() greps spec text for contradictions with
|
|
24
|
+
* canonical claims (Rejected Alternatives, Removed Elements)
|
|
25
|
+
* - detectDrift() greps the codebase for things dossier claims were
|
|
26
|
+
* removed (the contact-person case)
|
|
27
|
+
*
|
|
28
|
+
* Workspace-mode path resolution:
|
|
29
|
+
* - Cross-repo features: WOGI_WORKSPACE_ROOT/.workspace/dossiers/
|
|
30
|
+
* - Per-repo features: <repo>/.workflow/dossiers/
|
|
31
|
+
* - At match time, both roots are scanned; workspace dossiers shadow per-repo
|
|
32
|
+
* on slug collision (workspace is the shared truth).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const fs = require('node:fs');
|
|
36
|
+
const path = require('node:path');
|
|
37
|
+
const { execSync } = require('node:child_process');
|
|
38
|
+
const { PATHS, safeJsonParse } = require('./flow-utils');
|
|
39
|
+
|
|
40
|
+
const DOSSIER_DIRNAME = 'dossiers';
|
|
41
|
+
const INDEX_FILENAME = 'index.json';
|
|
42
|
+
|
|
43
|
+
const RESERVED_SLUGS = new Set(['_template', '_logic-rules', 'README', 'index']);
|
|
44
|
+
|
|
45
|
+
function getDossierRoots() {
|
|
46
|
+
const roots = [];
|
|
47
|
+
if (process.env.WOGI_WORKSPACE_ROOT) {
|
|
48
|
+
const wsRoot = path.join(process.env.WOGI_WORKSPACE_ROOT, '.workspace', DOSSIER_DIRNAME);
|
|
49
|
+
roots.push({ kind: 'workspace', dir: wsRoot });
|
|
50
|
+
}
|
|
51
|
+
roots.push({ kind: 'repo', dir: path.join(PATHS.workflow, DOSSIER_DIRNAME) });
|
|
52
|
+
return roots;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getPrimaryDossierDir() {
|
|
56
|
+
return path.join(PATHS.workflow, DOSSIER_DIRNAME);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ensureDir(p) {
|
|
60
|
+
try { fs.mkdirSync(p, { recursive: true }); } catch (_err) { /* noop */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function loadIndex() {
|
|
64
|
+
const merged = { patterns: [], slugs: {}, version: '1.0.0' };
|
|
65
|
+
for (const root of getDossierRoots()) {
|
|
66
|
+
const idxPath = path.join(root.dir, INDEX_FILENAME);
|
|
67
|
+
if (!fs.existsSync(idxPath)) continue;
|
|
68
|
+
const idx = safeJsonParse(idxPath, null);
|
|
69
|
+
if (!idx) continue;
|
|
70
|
+
if (Array.isArray(idx.patterns)) {
|
|
71
|
+
for (const p of idx.patterns) merged.patterns.push({ ...p, _root: root.kind });
|
|
72
|
+
}
|
|
73
|
+
if (idx.slugs && typeof idx.slugs === 'object') {
|
|
74
|
+
for (const [slug, meta] of Object.entries(idx.slugs)) {
|
|
75
|
+
if (!merged.slugs[slug] || root.kind === 'workspace') {
|
|
76
|
+
merged.slugs[slug] = { ...meta, _root: root.kind };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return merged;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function saveIndex(index, rootKind = 'repo') {
|
|
85
|
+
const target = rootKind === 'workspace' && process.env.WOGI_WORKSPACE_ROOT
|
|
86
|
+
? path.join(process.env.WOGI_WORKSPACE_ROOT, '.workspace', DOSSIER_DIRNAME)
|
|
87
|
+
: getPrimaryDossierDir();
|
|
88
|
+
ensureDir(target);
|
|
89
|
+
const idxPath = path.join(target, INDEX_FILENAME);
|
|
90
|
+
const out = { version: index.version || '1.0.0', patterns: [], slugs: {} };
|
|
91
|
+
for (const p of index.patterns || []) {
|
|
92
|
+
const { _root: _ignored, ...rest } = p;
|
|
93
|
+
out.patterns.push(rest);
|
|
94
|
+
}
|
|
95
|
+
for (const [slug, meta] of Object.entries(index.slugs || {})) {
|
|
96
|
+
const { _root: _ignored, ...rest } = meta;
|
|
97
|
+
out.slugs[slug] = rest;
|
|
98
|
+
}
|
|
99
|
+
out.lastUpdated = new Date().toISOString();
|
|
100
|
+
fs.writeFileSync(idxPath, JSON.stringify(out, null, 2));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveDossierPath(slug) {
|
|
104
|
+
for (const root of getDossierRoots()) {
|
|
105
|
+
const candidate = path.join(root.dir, `${slug}.md`);
|
|
106
|
+
if (fs.existsSync(candidate)) return { path: candidate, root: root.kind };
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function listFeatures() {
|
|
112
|
+
const index = loadIndex();
|
|
113
|
+
const slugs = new Set(Object.keys(index.slugs));
|
|
114
|
+
for (const root of getDossierRoots()) {
|
|
115
|
+
if (!fs.existsSync(root.dir)) continue;
|
|
116
|
+
for (const f of fs.readdirSync(root.dir)) {
|
|
117
|
+
if (!f.endsWith('.md')) continue;
|
|
118
|
+
const slug = f.replace(/\.md$/, '');
|
|
119
|
+
if (RESERVED_SLUGS.has(slug)) continue;
|
|
120
|
+
slugs.add(slug);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return Array.from(slugs).sort();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseDossier(raw) {
|
|
127
|
+
const result = {
|
|
128
|
+
slug: null, status: null, owners: [], created: null, title: null,
|
|
129
|
+
sections: {}, rawContent: raw
|
|
130
|
+
};
|
|
131
|
+
const slugMatch = raw.match(/<!--\s*slug:\s*([^\s-]+[^>]*)-->/);
|
|
132
|
+
if (slugMatch) result.slug = slugMatch[1].trim();
|
|
133
|
+
const statusMatch = raw.match(/<!--\s*status:\s*([^>]+)-->/);
|
|
134
|
+
if (statusMatch) result.status = statusMatch[1].trim();
|
|
135
|
+
const ownersMatch = raw.match(/<!--\s*owners:\s*([^>]+)-->/);
|
|
136
|
+
if (ownersMatch) result.owners = ownersMatch[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
137
|
+
const createdMatch = raw.match(/<!--\s*created:\s*([^>]+)-->/);
|
|
138
|
+
if (createdMatch) result.created = createdMatch[1].trim();
|
|
139
|
+
const titleMatch = raw.match(/^#\s+(.+)$/m);
|
|
140
|
+
if (titleMatch) result.title = titleMatch[1].trim();
|
|
141
|
+
|
|
142
|
+
const sectionRegex = /\n##\s+([^\n]+)\n([\s\S]*?)(?=\n##\s+|$)/g;
|
|
143
|
+
let m;
|
|
144
|
+
while ((m = sectionRegex.exec(raw)) !== null) {
|
|
145
|
+
const name = m[1].trim();
|
|
146
|
+
result.sections[name] = m[2].trim();
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function loadDossier(slug) {
|
|
152
|
+
const found = resolveDossierPath(slug);
|
|
153
|
+
if (!found) return null;
|
|
154
|
+
let raw;
|
|
155
|
+
try { raw = fs.readFileSync(found.path, 'utf-8'); } catch (_err) { return null; }
|
|
156
|
+
const parsed = parseDossier(raw);
|
|
157
|
+
parsed.path = found.path;
|
|
158
|
+
parsed.root = found.root;
|
|
159
|
+
if (!parsed.slug) parsed.slug = slug;
|
|
160
|
+
return parsed;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalize(s) {
|
|
164
|
+
return String(s || '').toLowerCase();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Match candidate features for a task.
|
|
169
|
+
* Scans both the registered index.json patterns and every dossier's
|
|
170
|
+
* "Match Patterns" section (patterns listed inline in the dossier itself).
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} input
|
|
173
|
+
* @param {string} [input.title]
|
|
174
|
+
* @param {string} [input.description]
|
|
175
|
+
* @param {string[]} [input.files]
|
|
176
|
+
* @param {string[]} [input.keywords]
|
|
177
|
+
* @returns {Array<{slug: string, score: number, reasons: string[]}>}
|
|
178
|
+
*/
|
|
179
|
+
function matchFeatures(input = {}) {
|
|
180
|
+
const title = normalize(input.title);
|
|
181
|
+
const description = normalize(input.description);
|
|
182
|
+
const haystack = `${title}\n${description}`;
|
|
183
|
+
const files = (input.files || []).map(f => f.toLowerCase());
|
|
184
|
+
const extras = (input.keywords || []).map(normalize);
|
|
185
|
+
|
|
186
|
+
const scores = {};
|
|
187
|
+
const addScore = (slug, amount, reason) => {
|
|
188
|
+
if (!slug) return;
|
|
189
|
+
if (!scores[slug]) scores[slug] = { slug, score: 0, reasons: [] };
|
|
190
|
+
scores[slug].score += amount;
|
|
191
|
+
if (reason && !scores[slug].reasons.includes(reason)) {
|
|
192
|
+
scores[slug].reasons.push(reason);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const index = loadIndex();
|
|
197
|
+
|
|
198
|
+
for (const entry of index.patterns || []) {
|
|
199
|
+
if (!entry || !entry.slug) continue;
|
|
200
|
+
const slug = entry.slug;
|
|
201
|
+
if (entry.keyword && haystack.includes(normalize(entry.keyword))) {
|
|
202
|
+
addScore(slug, 1, `keyword: ${entry.keyword}`);
|
|
203
|
+
}
|
|
204
|
+
if (entry.route && haystack.includes(normalize(entry.route))) {
|
|
205
|
+
addScore(slug, 1.5, `route: ${entry.route}`);
|
|
206
|
+
}
|
|
207
|
+
if (entry.component && haystack.includes(normalize(entry.component))) {
|
|
208
|
+
addScore(slug, 1.5, `component: ${entry.component}`);
|
|
209
|
+
}
|
|
210
|
+
if (entry.filePattern && files.length > 0) {
|
|
211
|
+
const re = globToRegex(entry.filePattern);
|
|
212
|
+
for (const f of files) {
|
|
213
|
+
if (re.test(f)) {
|
|
214
|
+
addScore(slug, 2, `file: ${entry.filePattern}`);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const kw of extras) {
|
|
220
|
+
if (entry.keyword && kw === normalize(entry.keyword)) addScore(slug, 0.5, `kw-extra:${kw}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const slug of listFeatures()) {
|
|
225
|
+
const dossier = loadDossier(slug);
|
|
226
|
+
if (!dossier) continue;
|
|
227
|
+
const patternsSection = dossier.sections['Match Patterns'] || '';
|
|
228
|
+
if (!patternsSection) continue;
|
|
229
|
+
const lines = patternsSection.split('\n').map(l => l.trim()).filter(l => l.startsWith('-'));
|
|
230
|
+
for (const line of lines) {
|
|
231
|
+
const kvMatch = line.match(/^-\s*([a-zA-Z-]+):\s*(.+)$/);
|
|
232
|
+
if (!kvMatch) continue;
|
|
233
|
+
const kind = kvMatch[1].toLowerCase();
|
|
234
|
+
const value = kvMatch[2].trim();
|
|
235
|
+
if (!value) continue;
|
|
236
|
+
if (kind === 'keyword' && haystack.includes(normalize(value))) {
|
|
237
|
+
addScore(slug, 1, `keyword: ${value}`);
|
|
238
|
+
} else if (kind === 'route' && haystack.includes(normalize(value))) {
|
|
239
|
+
addScore(slug, 1.5, `route: ${value}`);
|
|
240
|
+
} else if (kind === 'component' && haystack.includes(normalize(value))) {
|
|
241
|
+
addScore(slug, 1.5, `component: ${value}`);
|
|
242
|
+
} else if ((kind === 'file' || kind === 'filepattern' || kind === 'file-pattern') && files.length > 0) {
|
|
243
|
+
const re = globToRegex(value);
|
|
244
|
+
for (const f of files) {
|
|
245
|
+
if (re.test(f)) {
|
|
246
|
+
addScore(slug, 2, `file: ${value}`);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return Object.values(scores).sort((a, b) => b.score - a.score);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function globToRegex(pat) {
|
|
258
|
+
const escaped = pat
|
|
259
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
260
|
+
.replace(/\*\*/g, '.__DBLSTAR__')
|
|
261
|
+
.replace(/\*/g, '[^/]*')
|
|
262
|
+
.replace(/\.__DBLSTAR__/g, '.*')
|
|
263
|
+
.replace(/\?/g, '[^/]');
|
|
264
|
+
return new RegExp(`^${escaped}$`, 'i');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function scaffoldDossier(slug, meta = {}) {
|
|
268
|
+
if (RESERVED_SLUGS.has(slug)) {
|
|
269
|
+
throw new Error(`Slug "${slug}" is reserved`);
|
|
270
|
+
}
|
|
271
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
|
|
272
|
+
throw new Error(`Slug "${slug}" must be kebab-case (lowercase + hyphens)`);
|
|
273
|
+
}
|
|
274
|
+
const existing = resolveDossierPath(slug);
|
|
275
|
+
if (existing) {
|
|
276
|
+
throw new Error(`Dossier already exists at ${existing.path}`);
|
|
277
|
+
}
|
|
278
|
+
const target = meta.root === 'workspace' && process.env.WOGI_WORKSPACE_ROOT
|
|
279
|
+
? path.join(process.env.WOGI_WORKSPACE_ROOT, '.workspace', DOSSIER_DIRNAME)
|
|
280
|
+
: getPrimaryDossierDir();
|
|
281
|
+
ensureDir(target);
|
|
282
|
+
const now = new Date().toISOString().slice(0, 10);
|
|
283
|
+
const title = meta.title || slug.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join(' ');
|
|
284
|
+
const owners = (meta.owners || []).join(', ') || 'unset';
|
|
285
|
+
const patterns = (meta.patterns || []).map(p => `- ${p}`).join('\n') || '- keyword: <add match keyword>';
|
|
286
|
+
const body = `# ${title}
|
|
287
|
+
|
|
288
|
+
<!-- slug: ${slug} -->
|
|
289
|
+
<!-- status: active -->
|
|
290
|
+
<!-- owners: ${owners} -->
|
|
291
|
+
<!-- created: ${now} -->
|
|
292
|
+
|
|
293
|
+
## Canonical Summary
|
|
294
|
+
|
|
295
|
+
${meta.summary || '<One-paragraph description of what this feature IS today. Replace any time the owner revises scope.>'}
|
|
296
|
+
|
|
297
|
+
## Match Patterns
|
|
298
|
+
|
|
299
|
+
<!-- Auto-match patterns for loadOnMatch. Any task whose title/description/files match will auto-load this dossier. -->
|
|
300
|
+
${patterns}
|
|
301
|
+
|
|
302
|
+
## Contracts
|
|
303
|
+
|
|
304
|
+
<!-- DTO/API contracts, state-flow expectations, cross-repo agreements. One bullet per contract. -->
|
|
305
|
+
- <describe contract>
|
|
306
|
+
|
|
307
|
+
## Logic Rules
|
|
308
|
+
|
|
309
|
+
<!-- Cross-cutting rules scoped to this feature. For rules that span features, use .workflow/dossiers/_logic-rules.md -->
|
|
310
|
+
- <describe rule>
|
|
311
|
+
|
|
312
|
+
## Rejected Alternatives
|
|
313
|
+
|
|
314
|
+
<!-- Owner-rejected designs, with date + reason. Any future spec that matches these is a contradiction and will be blocked at spec phase. -->
|
|
315
|
+
- <date>: <alternative name> → REJECTED, reason: <why>
|
|
316
|
+
|
|
317
|
+
## Removed Elements
|
|
318
|
+
|
|
319
|
+
<!-- Things the owner told us to remove. The drift detector greps the codebase for these — if they reappear, you see drift. -->
|
|
320
|
+
- <date>: <element> → removed, reason: <why>, enforcement-grep: \`<regex>\`
|
|
321
|
+
|
|
322
|
+
## Known Bugs / Tech Debt
|
|
323
|
+
|
|
324
|
+
<!-- Active bugs or deferred fixes. Link to task IDs. -->
|
|
325
|
+
- <describe bug> — task: wf-xxxxxxxx
|
|
326
|
+
|
|
327
|
+
## Change Log
|
|
328
|
+
|
|
329
|
+
<!-- Append-only. One row per task that touched this feature. Populated by appendEvent(). -->
|
|
330
|
+
|
|
331
|
+
| Date | Task ID | Event | Note |
|
|
332
|
+
|------|---------|-------|------|
|
|
333
|
+
`;
|
|
334
|
+
const outPath = path.join(target, `${slug}.md`);
|
|
335
|
+
fs.writeFileSync(outPath, body);
|
|
336
|
+
|
|
337
|
+
const index = loadIndex();
|
|
338
|
+
if (!index.slugs[slug]) {
|
|
339
|
+
index.slugs[slug] = { title, created: now, owners: meta.owners || [] };
|
|
340
|
+
saveIndex(index, meta.root === 'workspace' ? 'workspace' : 'repo');
|
|
341
|
+
}
|
|
342
|
+
return outPath;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function appendEvent(slug, event = {}) {
|
|
346
|
+
const dossier = loadDossier(slug);
|
|
347
|
+
if (!dossier) throw new Error(`Dossier not found: ${slug}`);
|
|
348
|
+
const date = event.date || new Date().toISOString().slice(0, 10);
|
|
349
|
+
const taskId = event.taskId || '-';
|
|
350
|
+
const type = event.type || 'touched';
|
|
351
|
+
const note = (event.note || '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
352
|
+
const row = `| ${date} | ${taskId} | ${type} | ${note} |\n`;
|
|
353
|
+
|
|
354
|
+
let raw = fs.readFileSync(dossier.path, 'utf-8');
|
|
355
|
+
const header = '| Date | Task ID | Event | Note |';
|
|
356
|
+
const sep = '|------|---------|-------|------|';
|
|
357
|
+
const idx = raw.indexOf(sep);
|
|
358
|
+
if (idx === -1) {
|
|
359
|
+
raw = raw.trimEnd() + `\n\n${header}\n${sep}\n${row}`;
|
|
360
|
+
} else {
|
|
361
|
+
const insertAt = raw.indexOf('\n', idx) + 1;
|
|
362
|
+
raw = raw.slice(0, insertAt) + row + raw.slice(insertAt);
|
|
363
|
+
}
|
|
364
|
+
fs.writeFileSync(dossier.path, raw);
|
|
365
|
+
return { dossierPath: dossier.path, row: row.trim() };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Auto-append a Change Log row to every dossier matching a completed task.
|
|
370
|
+
* Called by flow-done.js after a task moves to recentlyCompleted.
|
|
371
|
+
* Fail-safe: never throws — returns {touched:[], skipped?, error?} on any failure.
|
|
372
|
+
* Guards: duplicate-row check (by taskId) and per-file lockfile to prevent
|
|
373
|
+
* concurrent /wogi-done corruption.
|
|
374
|
+
*/
|
|
375
|
+
function autoTouchFromTask(taskMeta = {}) {
|
|
376
|
+
try {
|
|
377
|
+
let config = {};
|
|
378
|
+
try {
|
|
379
|
+
const { getConfig } = require('./flow-utils');
|
|
380
|
+
config = getConfig() || {};
|
|
381
|
+
} catch (_err) { /* fail-open */ }
|
|
382
|
+
|
|
383
|
+
const fd = config.featureDossier || {};
|
|
384
|
+
if (fd.enabled === false) return { touched: [], skipped: 'dossier-disabled' };
|
|
385
|
+
if (fd.autoTouchOnDone === false) return { touched: [], skipped: 'auto-touch-disabled' };
|
|
386
|
+
const threshold = fd.autoMatchConfidence ?? 1;
|
|
387
|
+
|
|
388
|
+
const matches = matchFeatures({
|
|
389
|
+
title: taskMeta.title || '',
|
|
390
|
+
description: taskMeta.description || '',
|
|
391
|
+
files: taskMeta.files || []
|
|
392
|
+
});
|
|
393
|
+
const qualifying = matches.filter(m => m.score >= threshold);
|
|
394
|
+
if (qualifying.length === 0) return { touched: [], skipped: 'no-match' };
|
|
395
|
+
|
|
396
|
+
const rawNote = String(taskMeta.title || '').trim();
|
|
397
|
+
const note = rawNote.length > 80 ? rawNote.slice(0, 77) + '...' : rawNote;
|
|
398
|
+
const taskId = taskMeta.taskId || '-';
|
|
399
|
+
const event = {
|
|
400
|
+
taskId,
|
|
401
|
+
type: taskMeta.type || 'feat',
|
|
402
|
+
note,
|
|
403
|
+
date: taskMeta.date
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const touched = [];
|
|
407
|
+
const skipped = [];
|
|
408
|
+
for (const m of qualifying) {
|
|
409
|
+
const dossier = loadDossier(m.slug);
|
|
410
|
+
if (!dossier) continue;
|
|
411
|
+
|
|
412
|
+
// Duplicate-row guard: skip if any existing row already references this taskId.
|
|
413
|
+
if (taskId !== '-') {
|
|
414
|
+
try {
|
|
415
|
+
const existing = fs.readFileSync(dossier.path, 'utf-8');
|
|
416
|
+
if (existing.includes(`| ${taskId} |`)) {
|
|
417
|
+
skipped.push({ slug: m.slug, reason: 'already-touched' });
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
} catch (_err) { /* fall through to attempt append */ }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Per-file lockfile (O_EXCL atomic create) — prevents concurrent writers
|
|
424
|
+
// from clobbering each other's rows on same-second task completions.
|
|
425
|
+
const lockPath = `${dossier.path}.lock`;
|
|
426
|
+
let lockFd;
|
|
427
|
+
try {
|
|
428
|
+
lockFd = fs.openSync(lockPath, 'wx');
|
|
429
|
+
} catch (err) {
|
|
430
|
+
if (err.code === 'EEXIST') {
|
|
431
|
+
skipped.push({ slug: m.slug, reason: 'locked' });
|
|
432
|
+
if (process.env.DEBUG) console.error(`[auto-touch] ${m.slug}: locked by another writer, skipping`);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (process.env.DEBUG) console.error(`[auto-touch] ${m.slug} lock: ${err.message}`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
appendEvent(m.slug, event);
|
|
441
|
+
touched.push({ slug: m.slug, score: m.score });
|
|
442
|
+
} catch (err) {
|
|
443
|
+
if (process.env.DEBUG) console.error(`[auto-touch] ${m.slug}: ${err.message}`);
|
|
444
|
+
} finally {
|
|
445
|
+
try { fs.closeSync(lockFd); } catch (_err) { /* noop */ }
|
|
446
|
+
try { fs.unlinkSync(lockPath); } catch (_err) { /* noop */ }
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return { touched, skipped };
|
|
450
|
+
} catch (err) {
|
|
451
|
+
if (process.env.DEBUG) console.error(`[auto-touch] error: ${err.message}`);
|
|
452
|
+
return { touched: [], error: err.message };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Check a spec against a dossier's canonical claims.
|
|
458
|
+
* Flags contradictions: spec mentions something listed in Rejected Alternatives
|
|
459
|
+
* or reintroduces something listed in Removed Elements.
|
|
460
|
+
*/
|
|
461
|
+
function validateSpecAgainstDossier(specContent, dossier) {
|
|
462
|
+
const issues = [];
|
|
463
|
+
const specLower = String(specContent || '').toLowerCase();
|
|
464
|
+
|
|
465
|
+
const rejected = dossier.sections['Rejected Alternatives'] || '';
|
|
466
|
+
for (const line of rejected.split('\n')) {
|
|
467
|
+
const m = line.match(/^-\s*(?:\d{4}-\d{2}-\d{2}:\s*)?([^→\n]+?)(?:\s*→|$)/);
|
|
468
|
+
if (!m) continue;
|
|
469
|
+
const altName = m[1].trim();
|
|
470
|
+
if (!altName || altName.startsWith('<')) continue;
|
|
471
|
+
const cleanAlt = altName.toLowerCase();
|
|
472
|
+
if (cleanAlt.length < 4) continue;
|
|
473
|
+
if (specLower.includes(cleanAlt)) {
|
|
474
|
+
issues.push({
|
|
475
|
+
severity: 'blocker',
|
|
476
|
+
kind: 'rejected-alternative',
|
|
477
|
+
detail: `Spec mentions "${altName}" which was rejected. See dossier § Rejected Alternatives.`
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const removed = dossier.sections['Removed Elements'] || '';
|
|
483
|
+
for (const line of removed.split('\n')) {
|
|
484
|
+
if (!line.trim().startsWith('-')) continue;
|
|
485
|
+
const gm = line.match(/enforcement-grep:\s*`([^`]+)`/);
|
|
486
|
+
const nameMatch = line.match(/^-\s*(?:\d{4}-\d{2}-\d{2}:\s*)?([^→\n]+?)(?:\s*→|$)/);
|
|
487
|
+
if (nameMatch) {
|
|
488
|
+
const name = nameMatch[1].trim().toLowerCase();
|
|
489
|
+
if (name && name.length >= 4 && !name.startsWith('<') && specLower.includes(name)) {
|
|
490
|
+
issues.push({
|
|
491
|
+
severity: 'blocker',
|
|
492
|
+
kind: 'removed-element',
|
|
493
|
+
detail: `Spec reintroduces "${nameMatch[1].trim()}" which was removed. See dossier § Removed Elements.`
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (gm) {
|
|
498
|
+
try {
|
|
499
|
+
const re = new RegExp(gm[1], 'i');
|
|
500
|
+
if (re.test(specContent)) {
|
|
501
|
+
issues.push({
|
|
502
|
+
severity: 'blocker',
|
|
503
|
+
kind: 'removed-element-pattern',
|
|
504
|
+
detail: `Spec matches removed-element enforcement pattern /${gm[1]}/. See dossier § Removed Elements.`
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
} catch (_err) { /* bad regex, skip */ }
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return issues;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Drift detector: grep the codebase for patterns the dossier claims were removed.
|
|
515
|
+
* If a match is found, the dossier is out of sync with reality (the contact-person case).
|
|
516
|
+
*/
|
|
517
|
+
function detectDrift(slug) {
|
|
518
|
+
const dossier = loadDossier(slug);
|
|
519
|
+
if (!dossier) throw new Error(`Dossier not found: ${slug}`);
|
|
520
|
+
const removed = dossier.sections['Removed Elements'] || '';
|
|
521
|
+
const findings = [];
|
|
522
|
+
const patterns = [];
|
|
523
|
+
for (const line of removed.split('\n')) {
|
|
524
|
+
const gm = line.match(/enforcement-grep:\s*`([^`]+)`/);
|
|
525
|
+
if (gm) patterns.push({ pattern: gm[1], source: line.trim() });
|
|
526
|
+
}
|
|
527
|
+
if (patterns.length === 0) {
|
|
528
|
+
return { slug, patterns: 0, findings: [] };
|
|
529
|
+
}
|
|
530
|
+
for (const { pattern, source } of patterns) {
|
|
531
|
+
try {
|
|
532
|
+
const out = execSync(
|
|
533
|
+
`git grep -nE ${JSON.stringify(pattern)} -- . ':(exclude).workflow' ':(exclude)node_modules' ':(exclude).git' 2>/dev/null || true`,
|
|
534
|
+
{ cwd: PATHS.root, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
535
|
+
);
|
|
536
|
+
const lines = out.split('\n').filter(Boolean).slice(0, 40);
|
|
537
|
+
if (lines.length > 0) {
|
|
538
|
+
findings.push({ pattern, source, hits: lines.length, sample: lines.slice(0, 5) });
|
|
539
|
+
}
|
|
540
|
+
} catch (_err) { /* non-blocking */ }
|
|
541
|
+
}
|
|
542
|
+
return { slug, patterns: patterns.length, findings };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function buildPhaseInjection(matches, opts = {}) {
|
|
546
|
+
const max = opts.maxDossiers || 3;
|
|
547
|
+
const top = matches
|
|
548
|
+
.filter(m => m.score >= (opts.minScore || 1))
|
|
549
|
+
.slice(0, max);
|
|
550
|
+
if (top.length === 0) return null;
|
|
551
|
+
|
|
552
|
+
const blocks = [];
|
|
553
|
+
for (const match of top) {
|
|
554
|
+
const dossier = loadDossier(match.slug);
|
|
555
|
+
if (!dossier) continue;
|
|
556
|
+
const title = dossier.title || match.slug;
|
|
557
|
+
const canonical = dossier.sections['Canonical Summary'] || '(no canonical summary)';
|
|
558
|
+
const contracts = dossier.sections['Contracts'] || '';
|
|
559
|
+
const rejected = dossier.sections['Rejected Alternatives'] || '';
|
|
560
|
+
const removed = dossier.sections['Removed Elements'] || '';
|
|
561
|
+
const logicRules = dossier.sections['Logic Rules'] || '';
|
|
562
|
+
|
|
563
|
+
let block = `### Feature Dossier: ${title} (slug: ${match.slug}, score ${match.score.toFixed(1)})\n`;
|
|
564
|
+
block += `**Matched via**: ${match.reasons.join(', ')}\n\n`;
|
|
565
|
+
block += `**Canonical**: ${canonical.split('\n').map(l => l.trim()).filter(Boolean).join(' ').slice(0, 600)}\n`;
|
|
566
|
+
if (contracts && !contracts.startsWith('<')) {
|
|
567
|
+
block += `\n**Contracts**:\n${contracts.split('\n').filter(l => l.trim().startsWith('-')).slice(0, 6).join('\n')}\n`;
|
|
568
|
+
}
|
|
569
|
+
if (logicRules && !logicRules.startsWith('<')) {
|
|
570
|
+
block += `\n**Feature-scoped logic rules**:\n${logicRules.split('\n').filter(l => l.trim().startsWith('-')).slice(0, 6).join('\n')}\n`;
|
|
571
|
+
}
|
|
572
|
+
if (rejected && !rejected.startsWith('<')) {
|
|
573
|
+
block += `\n**Rejected alternatives (do not re-propose)**:\n${rejected.split('\n').filter(l => l.trim().startsWith('-')).slice(0, 6).join('\n')}\n`;
|
|
574
|
+
}
|
|
575
|
+
if (removed && !removed.startsWith('<')) {
|
|
576
|
+
block += `\n**Removed elements (do not reintroduce)**:\n${removed.split('\n').filter(l => l.trim().startsWith('-')).slice(0, 6).join('\n')}\n`;
|
|
577
|
+
}
|
|
578
|
+
block += `\n**Full dossier**: \`${path.relative(PATHS.root, dossier.path)}\`\n`;
|
|
579
|
+
blocks.push(block);
|
|
580
|
+
}
|
|
581
|
+
if (blocks.length === 0) return null;
|
|
582
|
+
return [
|
|
583
|
+
'## Feature Dossier Auto-Load',
|
|
584
|
+
'',
|
|
585
|
+
'The following feature dossiers match the active task. These capture prior owner decisions, rejected alternatives, and removed elements. Your spec/implementation MUST NOT contradict them.',
|
|
586
|
+
'',
|
|
587
|
+
blocks.join('\n---\n\n')
|
|
588
|
+
].join('\n');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ============================================================
|
|
592
|
+
// CLI
|
|
593
|
+
// ============================================================
|
|
594
|
+
|
|
595
|
+
function printHelp() {
|
|
596
|
+
console.log(`Usage: flow feature-dossier <command> [args]
|
|
597
|
+
|
|
598
|
+
Commands:
|
|
599
|
+
list List all known dossier slugs
|
|
600
|
+
show <slug> Print a dossier's parsed content
|
|
601
|
+
scaffold <slug> [options] Create a new dossier
|
|
602
|
+
Options: --title "X" --owners "be,fe" --summary "..." --workspace
|
|
603
|
+
match --title "..." [--files "a,b"] [--description "..."]
|
|
604
|
+
Show candidate dossiers for a task
|
|
605
|
+
touch <slug> --task wf-XXX --type <event> [--note "..."]
|
|
606
|
+
Append a change-log event
|
|
607
|
+
drift <slug> Grep codebase for removed-element patterns
|
|
608
|
+
validate <slug> --spec <file>
|
|
609
|
+
Check a spec file against the dossier for contradictions
|
|
610
|
+
inject --title "..." [--files "a,b"]
|
|
611
|
+
Print phase-injection block (for hook use)
|
|
612
|
+
help Show this help
|
|
613
|
+
|
|
614
|
+
Examples:
|
|
615
|
+
flow feature-dossier scaffold services-integrations --title "Services + Integrations" --owners "fe,be"
|
|
616
|
+
flow feature-dossier match --title "merge services and integrations card" --files "src/pages/Services.tsx"
|
|
617
|
+
flow feature-dossier drift services-integrations
|
|
618
|
+
`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function parseArgs(args) {
|
|
622
|
+
const out = { _: [], flags: {} };
|
|
623
|
+
for (let i = 0; i < args.length; i++) {
|
|
624
|
+
const a = args[i];
|
|
625
|
+
if (a.startsWith('--')) {
|
|
626
|
+
const key = a.slice(2);
|
|
627
|
+
const next = args[i + 1];
|
|
628
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
629
|
+
out.flags[key] = next;
|
|
630
|
+
i++;
|
|
631
|
+
} else {
|
|
632
|
+
out.flags[key] = true;
|
|
633
|
+
}
|
|
634
|
+
} else {
|
|
635
|
+
out._.push(a);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function cliMain(argv) {
|
|
642
|
+
const [cmd, ...rest] = argv;
|
|
643
|
+
const { _: positional, flags } = parseArgs(rest);
|
|
644
|
+
|
|
645
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') return printHelp();
|
|
646
|
+
|
|
647
|
+
if (cmd === 'list') {
|
|
648
|
+
const slugs = listFeatures();
|
|
649
|
+
if (slugs.length === 0) {
|
|
650
|
+
console.log('(no dossiers — run `flow feature-dossier scaffold <slug>`)');
|
|
651
|
+
} else {
|
|
652
|
+
for (const s of slugs) console.log(s);
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (cmd === 'show') {
|
|
658
|
+
const slug = positional[0];
|
|
659
|
+
if (!slug) { console.error('slug required'); process.exit(1); }
|
|
660
|
+
const d = loadDossier(slug);
|
|
661
|
+
if (!d) { console.error(`not found: ${slug}`); process.exit(1); }
|
|
662
|
+
console.log(d.rawContent);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (cmd === 'scaffold') {
|
|
667
|
+
const slug = positional[0];
|
|
668
|
+
if (!slug) { console.error('slug required'); process.exit(1); }
|
|
669
|
+
const meta = {
|
|
670
|
+
title: flags.title,
|
|
671
|
+
owners: flags.owners ? String(flags.owners).split(',').map(s => s.trim()) : [],
|
|
672
|
+
summary: flags.summary,
|
|
673
|
+
patterns: flags.patterns ? String(flags.patterns).split(',').map(s => s.trim()) : [],
|
|
674
|
+
root: flags.workspace ? 'workspace' : 'repo'
|
|
675
|
+
};
|
|
676
|
+
const out = scaffoldDossier(slug, meta);
|
|
677
|
+
console.log(`Created: ${out}`);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (cmd === 'match') {
|
|
682
|
+
const input = {
|
|
683
|
+
title: flags.title || '',
|
|
684
|
+
description: flags.description || '',
|
|
685
|
+
files: flags.files ? String(flags.files).split(',').map(s => s.trim()) : []
|
|
686
|
+
};
|
|
687
|
+
const matches = matchFeatures(input);
|
|
688
|
+
if (matches.length === 0) {
|
|
689
|
+
console.log('(no matches)');
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
for (const m of matches) {
|
|
693
|
+
console.log(`${m.slug} score=${m.score.toFixed(1)} [${m.reasons.join(', ')}]`);
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (cmd === 'touch') {
|
|
699
|
+
const slug = positional[0];
|
|
700
|
+
if (!slug) { console.error('slug required'); process.exit(1); }
|
|
701
|
+
const result = appendEvent(slug, {
|
|
702
|
+
taskId: flags.task || '-',
|
|
703
|
+
type: flags.type || 'touched',
|
|
704
|
+
note: flags.note || '',
|
|
705
|
+
date: flags.date
|
|
706
|
+
});
|
|
707
|
+
console.log(`Appended to ${result.dossierPath}: ${result.row}`);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (cmd === 'drift') {
|
|
712
|
+
const slug = positional[0];
|
|
713
|
+
if (!slug) { console.error('slug required'); process.exit(1); }
|
|
714
|
+
const report = detectDrift(slug);
|
|
715
|
+
if (report.findings.length === 0) {
|
|
716
|
+
console.log(`drift: clean (${report.patterns} pattern(s) checked)`);
|
|
717
|
+
} else {
|
|
718
|
+
console.log(`DRIFT DETECTED in ${slug}:`);
|
|
719
|
+
for (const f of report.findings) {
|
|
720
|
+
console.log(`\n pattern: /${f.pattern}/ hits: ${f.hits}`);
|
|
721
|
+
console.log(` source: ${f.source}`);
|
|
722
|
+
for (const s of f.sample) console.log(` ${s}`);
|
|
723
|
+
}
|
|
724
|
+
process.exit(2);
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (cmd === 'validate') {
|
|
730
|
+
const slug = positional[0];
|
|
731
|
+
if (!slug || !flags.spec) { console.error('usage: validate <slug> --spec <file>'); process.exit(1); }
|
|
732
|
+
const d = loadDossier(slug);
|
|
733
|
+
if (!d) { console.error(`not found: ${slug}`); process.exit(1); }
|
|
734
|
+
let spec;
|
|
735
|
+
try { spec = fs.readFileSync(flags.spec, 'utf-8'); }
|
|
736
|
+
catch (err) { console.error(`cannot read spec: ${err.message}`); process.exit(1); }
|
|
737
|
+
const issues = validateSpecAgainstDossier(spec, d);
|
|
738
|
+
if (issues.length === 0) {
|
|
739
|
+
console.log('spec OK vs dossier');
|
|
740
|
+
} else {
|
|
741
|
+
console.log(`${issues.length} contradiction(s):`);
|
|
742
|
+
for (const i of issues) console.log(` [${i.severity}] ${i.kind}: ${i.detail}`);
|
|
743
|
+
process.exit(2);
|
|
744
|
+
}
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (cmd === 'inject') {
|
|
749
|
+
const input = {
|
|
750
|
+
title: flags.title || '',
|
|
751
|
+
description: flags.description || '',
|
|
752
|
+
files: flags.files ? String(flags.files).split(',').map(s => s.trim()) : []
|
|
753
|
+
};
|
|
754
|
+
const matches = matchFeatures(input);
|
|
755
|
+
const block = buildPhaseInjection(matches, { minScore: Number(flags['min-score'] || 1), maxDossiers: Number(flags.max || 3) });
|
|
756
|
+
if (block) console.log(block);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
console.error(`unknown command: ${cmd}`);
|
|
761
|
+
printHelp();
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (require.main === module) {
|
|
766
|
+
try { cliMain(process.argv.slice(2)); }
|
|
767
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
module.exports = {
|
|
771
|
+
getDossierRoots,
|
|
772
|
+
getPrimaryDossierDir,
|
|
773
|
+
loadIndex,
|
|
774
|
+
saveIndex,
|
|
775
|
+
listFeatures,
|
|
776
|
+
loadDossier,
|
|
777
|
+
parseDossier,
|
|
778
|
+
matchFeatures,
|
|
779
|
+
scaffoldDossier,
|
|
780
|
+
appendEvent,
|
|
781
|
+
autoTouchFromTask,
|
|
782
|
+
validateSpecAgainstDossier,
|
|
783
|
+
detectDrift,
|
|
784
|
+
buildPhaseInjection,
|
|
785
|
+
DOSSIER_DIRNAME,
|
|
786
|
+
RESERVED_SLUGS
|
|
787
|
+
};
|