agent-method 1.5.12
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 +343 -0
- package/bin/wwa.js +115 -0
- package/docs/internal/cli-commands.yaml +259 -0
- package/docs/internal/doc-tokens.yaml +1103 -0
- package/docs/internal/feature-registry.yaml +1643 -0
- package/lib/boundaries.js +247 -0
- package/lib/cli/add.js +170 -0
- package/lib/cli/casestudy.js +1000 -0
- package/lib/cli/check.js +323 -0
- package/lib/cli/close.js +838 -0
- package/lib/cli/completion.js +735 -0
- package/lib/cli/deps.js +234 -0
- package/lib/cli/digest.js +73 -0
- package/lib/cli/doc-review.js +486 -0
- package/lib/cli/docs.js +315 -0
- package/lib/cli/helpers.js +198 -0
- package/lib/cli/implement.js +169 -0
- package/lib/cli/init.js +280 -0
- package/lib/cli/pipeline.js +206 -0
- package/lib/cli/plan.js +140 -0
- package/lib/cli/record.js +98 -0
- package/lib/cli/refine.js +202 -0
- package/lib/cli/report-helpers.js +113 -0
- package/lib/cli/review.js +76 -0
- package/lib/cli/routable.js +109 -0
- package/lib/cli/route.js +101 -0
- package/lib/cli/scan.js +133 -0
- package/lib/cli/serve.js +23 -0
- package/lib/cli/status.js +65 -0
- package/lib/cli/update-docs.js +574 -0
- package/lib/cli/upgrade.js +222 -0
- package/lib/cli/watch.js +32 -0
- package/lib/dependencies.js +196 -0
- package/lib/init.js +692 -0
- package/lib/mcp-server.js +612 -0
- package/lib/pipeline.js +907 -0
- package/lib/registry.js +132 -0
- package/lib/watcher.js +165 -0
- package/package.json +54 -0
- package/templates/README.md +363 -0
- package/templates/entry-points/.cursorrules +90 -0
- package/templates/entry-points/AGENT.md +90 -0
- package/templates/entry-points/CLAUDE.md +88 -0
- package/templates/extensions/MANIFEST.md +110 -0
- package/templates/extensions/analytical-system.md +96 -0
- package/templates/extensions/code-project.md +77 -0
- package/templates/extensions/data-exploration.md +117 -0
- package/templates/full/.context/BASE.md +101 -0
- package/templates/full/.context/COMPOSITION.md +47 -0
- package/templates/full/.context/INDEX.yaml +56 -0
- package/templates/full/.context/METHODOLOGY.md +246 -0
- package/templates/full/.context/PROTOCOL.yaml +169 -0
- package/templates/full/.context/REGISTRY.md +75 -0
- package/templates/full/.cursorrules +90 -0
- package/templates/full/AGENT.md +90 -0
- package/templates/full/CLAUDE.md +90 -0
- package/templates/full/Management/DIGEST.md +23 -0
- package/templates/full/Management/STATUS.md +46 -0
- package/templates/full/PLAN.md +67 -0
- package/templates/full/PROJECT-PROFILE.md +61 -0
- package/templates/full/PROJECT.md +80 -0
- package/templates/full/REQUIREMENTS.md +30 -0
- package/templates/full/ROADMAP.md +39 -0
- package/templates/full/Reviews/INDEX.md +41 -0
- package/templates/full/Reviews/backlog.md +52 -0
- package/templates/full/Reviews/plan.md +43 -0
- package/templates/full/Reviews/project.md +41 -0
- package/templates/full/Reviews/requirements.md +42 -0
- package/templates/full/Reviews/roadmap.md +41 -0
- package/templates/full/Reviews/state.md +56 -0
- package/templates/full/SESSION-LOG.md +102 -0
- package/templates/full/STATE.md +42 -0
- package/templates/full/SUMMARY.md +27 -0
- package/templates/full/agentWorkflows/INDEX.md +42 -0
- package/templates/full/agentWorkflows/observations.md +65 -0
- package/templates/full/agentWorkflows/patterns.md +68 -0
- package/templates/full/agentWorkflows/sessions.md +92 -0
- package/templates/full/intro/README.md +39 -0
- package/templates/full/registry/feature-registry.yaml +25 -0
- package/templates/full/registry/features/catalog.yaml +743 -0
- package/templates/full/registry/features/protocol.yaml +121 -0
- package/templates/full/registry/features/routing.yaml +358 -0
- package/templates/full/registry/features/workflows.yaml +404 -0
- package/templates/full/todos/backlog.md +19 -0
- package/templates/starter/.context/BASE.md +66 -0
- package/templates/starter/.context/INDEX.yaml +51 -0
- package/templates/starter/.context/METHODOLOGY.md +228 -0
- package/templates/starter/.context/PROTOCOL.yaml +165 -0
- package/templates/starter/.cursorrules +90 -0
- package/templates/starter/AGENT.md +90 -0
- package/templates/starter/CLAUDE.md +90 -0
- package/templates/starter/Management/DIGEST.md +23 -0
- package/templates/starter/Management/STATUS.md +46 -0
- package/templates/starter/PLAN.md +67 -0
- package/templates/starter/PROJECT-PROFILE.md +44 -0
- package/templates/starter/PROJECT.md +80 -0
- package/templates/starter/ROADMAP.md +39 -0
- package/templates/starter/Reviews/INDEX.md +75 -0
- package/templates/starter/SESSION-LOG.md +102 -0
- package/templates/starter/STATE.md +42 -0
- package/templates/starter/SUMMARY.md +27 -0
- package/templates/starter/agentWorkflows/INDEX.md +61 -0
- package/templates/starter/intro/README.md +37 -0
- package/templates/starter/registry/feature-registry.yaml +25 -0
- package/templates/starter/registry/features/catalog.yaml +743 -0
- package/templates/starter/registry/features/protocol.yaml +121 -0
- package/templates/starter/registry/features/routing.yaml +358 -0
- package/templates/starter/registry/features/workflows.yaml +404 -0
package/lib/cli/docs.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/** wwa docs — show docs coverage, staleness, and dependency graph from DOCS-MAP.md + doc-tokens.yaml. */
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { resolve, join } from "node:path";
|
|
5
|
+
import { loadDocGraph, resolveDocsMapPath } from "../boundaries.js";
|
|
6
|
+
import { printCliReport, writeLastRunSummary } from "./report-helpers.js";
|
|
7
|
+
|
|
8
|
+
export function register(program) {
|
|
9
|
+
program
|
|
10
|
+
.command("docs [directory]")
|
|
11
|
+
.description("Show docs coverage report from .context/DOCS-MAP.md")
|
|
12
|
+
.option("--json", "Output as JSON")
|
|
13
|
+
.option("--deps", "Show document dependency graph from doc-tokens.yaml")
|
|
14
|
+
.action(async (directory, opts) => {
|
|
15
|
+
directory = directory || ".";
|
|
16
|
+
const d = resolve(directory);
|
|
17
|
+
|
|
18
|
+
const docsMapPath = await resolveDocsMapPath(d);
|
|
19
|
+
|
|
20
|
+
if (!existsSync(docsMapPath)) {
|
|
21
|
+
if (opts.json) {
|
|
22
|
+
console.log(JSON.stringify({ error: "No .context/DOCS-MAP.md found" }, null, 2));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.log("\n No .context/DOCS-MAP.md found.\n");
|
|
26
|
+
console.log(" This file maps project components to their documentation.");
|
|
27
|
+
console.log(" Create it by running: wwa init <type>");
|
|
28
|
+
console.log(" Or ask your agent to populate it during onboarding.\n");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const content = readFileSync(docsMapPath, "utf-8");
|
|
33
|
+
const report = parseDocsMap(content, d);
|
|
34
|
+
|
|
35
|
+
// Load dependency graph if --deps requested or for JSON enrichment
|
|
36
|
+
const docGraph = await loadDocGraph(d);
|
|
37
|
+
if (docGraph) {
|
|
38
|
+
report.dependencyGraph = summarizeDocGraph(docGraph);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (opts.json) {
|
|
42
|
+
console.log(JSON.stringify(report, null, 2));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const s = report.summary;
|
|
47
|
+
const summary = [
|
|
48
|
+
`Docs inventory: ${s.totalDocs} files (${s.existingDocs} exist, ${s.missingDocs} missing).`,
|
|
49
|
+
`${s.mappedComponents} component mappings, ${s.scaffoldingRules} scaffolding rules.`,
|
|
50
|
+
s.missingDocs > 0 || s.unmappedDocs > 0 ? "Add or map docs in .context/DOCS-MAP.md." : "Run wwa docs --json for full report.",
|
|
51
|
+
].join(" ");
|
|
52
|
+
const inputRef = `directory=${directory}`;
|
|
53
|
+
printCliReport({ command: "docs", inputRef, summary });
|
|
54
|
+
printReport(report);
|
|
55
|
+
if (opts.deps) {
|
|
56
|
+
printDepsReport(docGraph);
|
|
57
|
+
}
|
|
58
|
+
writeLastRunSummary(d, { command: "docs", inputRef, summary });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// DOCS-MAP.md parsing
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
function parseDocsMap(content, projectDir) {
|
|
67
|
+
const inventory = [];
|
|
68
|
+
const mappings = [];
|
|
69
|
+
const scaffolding = [];
|
|
70
|
+
|
|
71
|
+
// Parse docs inventory table
|
|
72
|
+
const invSection = content.match(
|
|
73
|
+
/## Docs inventory\s*\n[\s\S]*?\n\|[^\n]+\n\|[-| :]+\n((?:\|[^\n]+\n)*)/
|
|
74
|
+
);
|
|
75
|
+
if (invSection) {
|
|
76
|
+
for (const row of invSection[1].trim().split("\n")) {
|
|
77
|
+
const cols = row.split("|").map((c) => c.trim()).filter((c) => c);
|
|
78
|
+
if (cols.length >= 3 && !cols[0].startsWith("<!--")) {
|
|
79
|
+
const path = cols[0];
|
|
80
|
+
const exists = existsSync(join(projectDir, path));
|
|
81
|
+
inventory.push({
|
|
82
|
+
path,
|
|
83
|
+
purpose: cols[1],
|
|
84
|
+
sources: cols[2],
|
|
85
|
+
status: cols[3] || (exists ? "active" : "missing"),
|
|
86
|
+
exists,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Parse component-to-docs mapping table
|
|
93
|
+
const mapSection = content.match(
|
|
94
|
+
/## Component-to-docs mapping\s*\n[\s\S]*?\n\|[^\n]+\n\|[-| :]+\n((?:\|[^\n]+\n)*)/
|
|
95
|
+
);
|
|
96
|
+
if (mapSection) {
|
|
97
|
+
for (const row of mapSection[1].trim().split("\n")) {
|
|
98
|
+
const cols = row.split("|").map((c) => c.trim()).filter((c) => c);
|
|
99
|
+
if (cols.length >= 3 && !cols[0].startsWith("<!--")) {
|
|
100
|
+
mappings.push({
|
|
101
|
+
component: cols[0],
|
|
102
|
+
documentedIn: cols[1],
|
|
103
|
+
trigger: cols[2],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parse scaffolding rules table
|
|
110
|
+
const scaffSection = content.match(
|
|
111
|
+
/## Scaffolding rules\s*\n[\s\S]*?\n\|[^\n]+\n\|[-| :]+\n((?:\|[^\n]+\n)*)/
|
|
112
|
+
);
|
|
113
|
+
if (scaffSection) {
|
|
114
|
+
for (const row of scaffSection[1].trim().split("\n")) {
|
|
115
|
+
const cols = row.split("|").map((c) => c.trim()).filter((c) => c);
|
|
116
|
+
if (cols.length >= 3 && !cols[0].startsWith("<!--")) {
|
|
117
|
+
scaffolding.push({
|
|
118
|
+
condition: cols[0],
|
|
119
|
+
proposedDoc: cols[1],
|
|
120
|
+
seedFrom: cols[2],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check which docs actually exist on disk
|
|
127
|
+
const docsDir = join(projectDir, "docs");
|
|
128
|
+
const existingDocs = [];
|
|
129
|
+
const missingDocs = [];
|
|
130
|
+
const unmappedDocs = [];
|
|
131
|
+
|
|
132
|
+
for (const item of inventory) {
|
|
133
|
+
if (item.exists) {
|
|
134
|
+
existingDocs.push(item.path);
|
|
135
|
+
} else {
|
|
136
|
+
missingDocs.push(item.path);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Find docs/ files that exist but aren't in the inventory
|
|
141
|
+
if (existsSync(docsDir)) {
|
|
142
|
+
const inventoryPaths = new Set(inventory.map((i) => i.path));
|
|
143
|
+
// Check common docs paths
|
|
144
|
+
const candidates = ["docs/index.md"];
|
|
145
|
+
for (const c of candidates) {
|
|
146
|
+
if (existsSync(join(projectDir, c)) && !inventoryPaths.has(c)) {
|
|
147
|
+
unmappedDocs.push(c);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Count mapped vs unmapped components
|
|
153
|
+
const mappedComponents = mappings.length;
|
|
154
|
+
const docsWithMappings = new Set(mappings.map((m) => m.documentedIn));
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
inventory,
|
|
158
|
+
mappings,
|
|
159
|
+
scaffolding,
|
|
160
|
+
summary: {
|
|
161
|
+
totalDocs: inventory.length,
|
|
162
|
+
existingDocs: existingDocs.length,
|
|
163
|
+
missingDocs: missingDocs.length,
|
|
164
|
+
unmappedDocs: unmappedDocs.length,
|
|
165
|
+
mappedComponents,
|
|
166
|
+
scaffoldingRules: scaffolding.length,
|
|
167
|
+
},
|
|
168
|
+
existingDocs,
|
|
169
|
+
missingDocs,
|
|
170
|
+
unmappedDocs,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Report output
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
function printReport(report) {
|
|
179
|
+
const s = report.summary;
|
|
180
|
+
|
|
181
|
+
console.log("\n Docs Coverage Report\n");
|
|
182
|
+
console.log(` Docs inventory: ${s.totalDocs} files (${s.existingDocs} exist, ${s.missingDocs} missing)`);
|
|
183
|
+
console.log(` Component mappings: ${s.mappedComponents}`);
|
|
184
|
+
console.log(` Scaffolding rules: ${s.scaffoldingRules}`);
|
|
185
|
+
|
|
186
|
+
if (report.inventory.length > 0) {
|
|
187
|
+
console.log("\n Docs inventory:");
|
|
188
|
+
for (const item of report.inventory) {
|
|
189
|
+
const icon = item.exists ? "[x]" : "[ ]";
|
|
190
|
+
const status = item.status === "stale" ? " (stale)" : "";
|
|
191
|
+
console.log(` ${icon} ${item.path} — ${item.purpose}${status}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (report.missingDocs.length > 0) {
|
|
196
|
+
console.log("\n Missing docs (in inventory but not on disk):");
|
|
197
|
+
for (const p of report.missingDocs) {
|
|
198
|
+
console.log(` - ${p}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (report.unmappedDocs.length > 0) {
|
|
203
|
+
console.log("\n Unmapped docs (on disk but not in inventory):");
|
|
204
|
+
for (const p of report.unmappedDocs) {
|
|
205
|
+
console.log(` - ${p}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (report.mappings.length > 0) {
|
|
210
|
+
console.log("\n Component mappings:");
|
|
211
|
+
for (const m of report.mappings) {
|
|
212
|
+
console.log(` ${m.component} → ${m.documentedIn}`);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
console.log("\n No component-to-docs mappings defined yet.");
|
|
216
|
+
console.log(" Ask your agent to populate .context/DOCS-MAP.md during onboarding.");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (report.scaffolding.length > 0) {
|
|
220
|
+
console.log("\n Scaffolding rules (auto-propose new docs):");
|
|
221
|
+
for (const r of report.scaffolding) {
|
|
222
|
+
console.log(` When: ${r.condition}`);
|
|
223
|
+
console.log(` Create: ${r.proposedDoc}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log("");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Dependency graph from doc-tokens.yaml
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
function summarizeDocGraph(docGraph) {
|
|
235
|
+
const nodes = docGraph.nodes || [];
|
|
236
|
+
const edges = docGraph.edges || [];
|
|
237
|
+
const edgesByType = {};
|
|
238
|
+
for (const e of edges) {
|
|
239
|
+
edgesByType[e.type] = (edgesByType[e.type] || 0) + 1;
|
|
240
|
+
}
|
|
241
|
+
// Find orphans (nodes with no edges)
|
|
242
|
+
const connected = new Set();
|
|
243
|
+
for (const e of edges) {
|
|
244
|
+
connected.add(e.from);
|
|
245
|
+
connected.add(e.to);
|
|
246
|
+
}
|
|
247
|
+
const orphans = nodes.filter((n) => !connected.has(n.path)).map((n) => n.path);
|
|
248
|
+
return {
|
|
249
|
+
nodeCount: nodes.length,
|
|
250
|
+
edgeCount: edges.length,
|
|
251
|
+
edgesByType,
|
|
252
|
+
orphanCount: orphans.length,
|
|
253
|
+
orphans,
|
|
254
|
+
lastScan: docGraph.last_scan || null,
|
|
255
|
+
defaultsApplied: docGraph.defaults_applied || null,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function printDepsReport(docGraph) {
|
|
260
|
+
if (!docGraph) {
|
|
261
|
+
console.log(" No dependency graph found in token registry.");
|
|
262
|
+
console.log(" Run wwa init or complete brownfield onboarding to generate one.\n");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const nodes = docGraph.nodes || [];
|
|
267
|
+
const edges = docGraph.edges || [];
|
|
268
|
+
|
|
269
|
+
console.log(" Document Dependency Graph\n");
|
|
270
|
+
console.log(` Last scan: ${docGraph.last_scan || "unknown"}`);
|
|
271
|
+
console.log(` Defaults: ${docGraph.defaults_applied || "none"}`);
|
|
272
|
+
console.log(` Nodes: ${nodes.length} Edges: ${edges.length}\n`);
|
|
273
|
+
|
|
274
|
+
// Group nodes by category
|
|
275
|
+
const byCategory = {};
|
|
276
|
+
for (const n of nodes) {
|
|
277
|
+
const cat = n.category || "unknown";
|
|
278
|
+
if (!byCategory[cat]) byCategory[cat] = [];
|
|
279
|
+
byCategory[cat].push(n);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const [cat, catNodes] of Object.entries(byCategory)) {
|
|
283
|
+
console.log(` [${cat}]`);
|
|
284
|
+
for (const n of catNodes) {
|
|
285
|
+
const outgoing = edges.filter((e) => e.from === n.path);
|
|
286
|
+
const incoming = edges.filter((e) => e.to === n.path);
|
|
287
|
+
const arrows = outgoing.map((e) => `→ ${e.to} (${e.type})`);
|
|
288
|
+
console.log(` ${n.path} — ${n.role}`);
|
|
289
|
+
if (arrows.length > 0) {
|
|
290
|
+
for (const a of arrows) {
|
|
291
|
+
console.log(` ${a}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (incoming.length > 0 && outgoing.length === 0) {
|
|
295
|
+
console.log(` ← ${incoming.length} incoming edge(s)`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Show orphans
|
|
301
|
+
const connected = new Set();
|
|
302
|
+
for (const e of edges) {
|
|
303
|
+
connected.add(e.from);
|
|
304
|
+
connected.add(e.to);
|
|
305
|
+
}
|
|
306
|
+
const orphans = nodes.filter((n) => !connected.has(n.path));
|
|
307
|
+
if (orphans.length > 0) {
|
|
308
|
+
console.log(`\n Orphan nodes (no edges):`);
|
|
309
|
+
for (const o of orphans) {
|
|
310
|
+
console.log(` - ${o.path}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log("");
|
|
315
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI helpers — project type resolution, registry loading,
|
|
3
|
+
* entry point discovery, output formatting.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync, writeFileSync, copyFileSync } from "node:fs";
|
|
7
|
+
import { resolve, join, dirname, extname } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Package metadata
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const _pkg = JSON.parse(
|
|
18
|
+
readFileSync(resolve(__dirname, "..", "..", "package.json"), "utf-8")
|
|
19
|
+
);
|
|
20
|
+
export const pkg = _pkg;
|
|
21
|
+
|
|
22
|
+
/** Package root directory (two levels up from lib/cli/). */
|
|
23
|
+
export const packageRoot = resolve(__dirname, "..", "..");
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Project type aliases
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export const PROJECT_TYPE_ALIASES = {
|
|
30
|
+
context: "analytical",
|
|
31
|
+
mix: "mixed",
|
|
32
|
+
all: "mixed",
|
|
33
|
+
code: "code",
|
|
34
|
+
data: "data",
|
|
35
|
+
analytical: "analytical",
|
|
36
|
+
mixed: "mixed",
|
|
37
|
+
general: "general",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const VALID_TYPE_NAMES = Object.keys(PROJECT_TYPE_ALIASES).sort();
|
|
41
|
+
|
|
42
|
+
export function resolveProjectType(name) {
|
|
43
|
+
const resolved = PROJECT_TYPE_ALIASES[name.toLowerCase()];
|
|
44
|
+
if (!resolved) {
|
|
45
|
+
console.error(
|
|
46
|
+
`Unknown project type: '${name}'\nValid types: ${VALID_TYPE_NAMES.join(", ")}`
|
|
47
|
+
);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Lazy module loading
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
let _registryModule = null;
|
|
58
|
+
let _pipelineModule = null;
|
|
59
|
+
|
|
60
|
+
export async function getRegistry() {
|
|
61
|
+
if (!_registryModule) {
|
|
62
|
+
_registryModule = await import("../registry.js");
|
|
63
|
+
}
|
|
64
|
+
return _registryModule;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getPipeline() {
|
|
68
|
+
if (!_pipelineModule) {
|
|
69
|
+
_pipelineModule = await import("../pipeline.js");
|
|
70
|
+
}
|
|
71
|
+
return _pipelineModule;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function loadRegistryData(registryPath) {
|
|
75
|
+
const { loadRegistry } = await getRegistry();
|
|
76
|
+
try {
|
|
77
|
+
return loadRegistry(registryPath || undefined);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error(`Error: ${e.message}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// File discovery
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
export function findEntryPoint(directory) {
|
|
89
|
+
const d = resolve(directory);
|
|
90
|
+
for (const name of ["CLAUDE.md", ".cursorrules", "AGENT.md"]) {
|
|
91
|
+
const p = join(d, name);
|
|
92
|
+
if (existsSync(p)) return p;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function findSessionLog(directory = ".") {
|
|
98
|
+
const d = resolve(directory);
|
|
99
|
+
for (const name of ["SESSION-LOG.md", "session-log.md"]) {
|
|
100
|
+
const p = join(d, name);
|
|
101
|
+
if (existsSync(p)) return p;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function readMethodVersion(entryPointPath) {
|
|
107
|
+
const content = readFileSync(entryPointPath, "utf-8");
|
|
108
|
+
const m = content.match(/method_version:\s*(\S+)/);
|
|
109
|
+
return m ? m[1] : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function basename_of(filepath) {
|
|
113
|
+
return filepath.split(/[\\/]/).pop();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Write-safety invariant
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/** Allowed file extensions for CLI write operations. */
|
|
121
|
+
const SAFE_EXTENSIONS = new Set([".md", ".yaml", ".yml"]);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns true if the file path has an allowed extension for writing.
|
|
125
|
+
* The CLI tool must NEVER modify user source code — only methodology
|
|
126
|
+
* files (.md) and agent-understanding files (.yaml/.yml) are writable.
|
|
127
|
+
*/
|
|
128
|
+
export function isSafeToWrite(filePath) {
|
|
129
|
+
const ext = extname(filePath).toLowerCase();
|
|
130
|
+
return SAFE_EXTENSIONS.has(ext);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Write a file, but only if the extension is safe.
|
|
135
|
+
* Throws if the path would modify user source code.
|
|
136
|
+
*/
|
|
137
|
+
export function safeWriteFile(filePath, content, encoding = "utf-8") {
|
|
138
|
+
if (!isSafeToWrite(filePath)) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Safety invariant: refusing to write '${filePath}' — ` +
|
|
141
|
+
`only .md, .yaml, and .yml files are allowed. ` +
|
|
142
|
+
`The CLI tool never modifies user source code.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
writeFileSync(filePath, content, encoding);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Copy a file, but only if the destination extension is safe.
|
|
150
|
+
* Throws if the destination would modify user source code.
|
|
151
|
+
*/
|
|
152
|
+
export function safeCopyFile(src, dst) {
|
|
153
|
+
if (!isSafeToWrite(dst)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Safety invariant: refusing to copy to '${dst}' — ` +
|
|
156
|
+
`only .md, .yaml, and .yml files are allowed. ` +
|
|
157
|
+
`The CLI tool never modifies user source code.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
copyFileSync(src, dst);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Output formatting
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
function formatValue(value) {
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
if (value.length === 0) return "[]";
|
|
170
|
+
if (typeof value[0] === "object") {
|
|
171
|
+
return value.map((v) => JSON.stringify(v)).join(", ");
|
|
172
|
+
}
|
|
173
|
+
return `[${value.join(", ")}]`;
|
|
174
|
+
}
|
|
175
|
+
if (value && typeof value === "object") {
|
|
176
|
+
return JSON.stringify(value);
|
|
177
|
+
}
|
|
178
|
+
return String(value);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function outputData(data, asJson) {
|
|
182
|
+
if (asJson) {
|
|
183
|
+
console.log(JSON.stringify(data, null, 2));
|
|
184
|
+
} else {
|
|
185
|
+
for (const [key, value] of Object.entries(data)) {
|
|
186
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
187
|
+
console.log(` ${key}:`);
|
|
188
|
+
for (const [k2, v2] of Object.entries(value)) {
|
|
189
|
+
console.log(` ${k2}: ${formatValue(v2)}`);
|
|
190
|
+
}
|
|
191
|
+
} else if (Array.isArray(value)) {
|
|
192
|
+
console.log(` ${key}: ${formatValue(value)}`);
|
|
193
|
+
} else {
|
|
194
|
+
console.log(` ${key}: ${value}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** wwa implement — show implementation guidance for the current plan step. */
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { resolve, join } from "node:path";
|
|
5
|
+
import { findEntryPoint } from "./helpers.js";
|
|
6
|
+
import { printCliReport, writeLastRunSummary } from "./report-helpers.js";
|
|
7
|
+
|
|
8
|
+
export function register(program) {
|
|
9
|
+
program
|
|
10
|
+
.command("implement [directory]")
|
|
11
|
+
.description("Show implementation guidance for the current plan step")
|
|
12
|
+
.option("--json", "Output as JSON")
|
|
13
|
+
.action(async (directory, opts) => {
|
|
14
|
+
directory = directory || ".";
|
|
15
|
+
const d = resolve(directory);
|
|
16
|
+
|
|
17
|
+
const planPath = join(d, "PLAN.md");
|
|
18
|
+
if (!existsSync(planPath)) {
|
|
19
|
+
console.error(
|
|
20
|
+
"No PLAN.md found. Run 'wwa init' to set up methodology files."
|
|
21
|
+
);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const planContent = readFileSync(planPath, "utf-8");
|
|
26
|
+
const steps = parseSteps(planContent);
|
|
27
|
+
const nextStep = steps.find((s) => s.status !== "done");
|
|
28
|
+
|
|
29
|
+
// Read STATE.md for context
|
|
30
|
+
const statePath = join(d, "STATE.md");
|
|
31
|
+
let position = null;
|
|
32
|
+
if (existsSync(statePath)) {
|
|
33
|
+
const stateContent = readFileSync(statePath, "utf-8");
|
|
34
|
+
position = extractPosition(stateContent);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Read entry point for scoping and cascade rules
|
|
38
|
+
const ep = findEntryPoint(directory);
|
|
39
|
+
let scopingRules = [];
|
|
40
|
+
let cascadeRules = [];
|
|
41
|
+
if (ep) {
|
|
42
|
+
const epContent = readFileSync(ep, "utf-8");
|
|
43
|
+
scopingRules = extractTableRows(epContent, "Scoping rules");
|
|
44
|
+
cascadeRules = extractTableRows(epContent, "Dependency cascade");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (opts.json) {
|
|
48
|
+
console.log(
|
|
49
|
+
JSON.stringify(
|
|
50
|
+
{ position, nextStep, totalSteps: steps.length, scopingRules, cascadeRules },
|
|
51
|
+
null,
|
|
52
|
+
2
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const done = steps.filter((s) => s.status === "done").length;
|
|
59
|
+
const inputRef = `directory=${directory}`;
|
|
60
|
+
let summary;
|
|
61
|
+
if (!nextStep) {
|
|
62
|
+
summary =
|
|
63
|
+
"All plan steps complete. Verify criteria (wwa plan), update STATE.md/ROADMAP.md/SUMMARY.md, run wwa close.";
|
|
64
|
+
printCliReport({ command: "implement", inputRef, summary });
|
|
65
|
+
console.log("\n All plan steps are complete.");
|
|
66
|
+
if (position) {
|
|
67
|
+
console.log(` Phase: ${position.phase}`);
|
|
68
|
+
console.log(` Status: ${position.status}`);
|
|
69
|
+
}
|
|
70
|
+
console.log(
|
|
71
|
+
"\n Next actions:" +
|
|
72
|
+
"\n - Verify all criteria pass: wwa plan" +
|
|
73
|
+
"\n - Close the phase: update STATE.md, ROADMAP.md, SUMMARY.md" +
|
|
74
|
+
"\n - Run session close: wwa close\n"
|
|
75
|
+
);
|
|
76
|
+
writeLastRunSummary(d, { command: "implement", inputRef, summary });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
summary = `Next step #${nextStep.num}: ${nextStep.deliverable}. ${nextStep.read ? `Read: ${nextStep.read}.` : ""} ${nextStep.produce ? `Produce: ${nextStep.produce}.` : ""} Progress: ${done}/${steps.length} steps.`;
|
|
81
|
+
printCliReport({ command: "implement", inputRef, summary });
|
|
82
|
+
console.log(`\n Implementation guidance`);
|
|
83
|
+
console.log(` Progress: ${done}/${steps.length} steps complete\n`);
|
|
84
|
+
|
|
85
|
+
console.log(` Next step: #${nextStep.num}`);
|
|
86
|
+
console.log(` Deliverable: ${nextStep.deliverable}`);
|
|
87
|
+
if (nextStep.read) console.log(` Read first: ${nextStep.read}`);
|
|
88
|
+
if (nextStep.produce) console.log(` Produce: ${nextStep.produce}`);
|
|
89
|
+
|
|
90
|
+
// Show relevant scoping rules
|
|
91
|
+
if (scopingRules.length > 0) {
|
|
92
|
+
console.log(`\n Relevant scoping rules (from entry point):`);
|
|
93
|
+
for (const rule of scopingRules.slice(0, 5)) {
|
|
94
|
+
console.log(` ${rule}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Show cascade reminders
|
|
99
|
+
if (cascadeRules.length > 0) {
|
|
100
|
+
console.log(`\n Cascade rules to remember:`);
|
|
101
|
+
for (const rule of cascadeRules.slice(0, 5)) {
|
|
102
|
+
console.log(` ${rule}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(
|
|
107
|
+
"\n After completing this step:" +
|
|
108
|
+
"\n - Record any decisions in STATE.md" +
|
|
109
|
+
"\n - Check cascade table for dependent updates" +
|
|
110
|
+
"\n - Mark step as 'done' in PLAN.md\n"
|
|
111
|
+
);
|
|
112
|
+
writeLastRunSummary(d, { command: "implement", inputRef, summary });
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseSteps(content) {
|
|
117
|
+
const steps = [];
|
|
118
|
+
const stepPattern =
|
|
119
|
+
/\|\s*(\d+)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(\w+)\s*\|/g;
|
|
120
|
+
let m;
|
|
121
|
+
while ((m = stepPattern.exec(content)) !== null) {
|
|
122
|
+
steps.push({
|
|
123
|
+
num: parseInt(m[1], 10),
|
|
124
|
+
deliverable: m[2].trim(),
|
|
125
|
+
read: m[3].trim(),
|
|
126
|
+
produce: m[4].trim(),
|
|
127
|
+
status: m[5].trim().toLowerCase(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return steps;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function extractPosition(stateContent) {
|
|
134
|
+
const phase =
|
|
135
|
+
stateContent.match(/\*\*Phase\*\*:\s*(.+)/)?.[1]?.trim() || "Unknown";
|
|
136
|
+
const status =
|
|
137
|
+
stateContent.match(/\*\*Status\*\*:\s*(.+)/)?.[1]?.trim() || "Unknown";
|
|
138
|
+
return { phase, status };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function extractTableRows(content, sectionName) {
|
|
142
|
+
const rows = [];
|
|
143
|
+
const sectionIdx = content.indexOf(sectionName);
|
|
144
|
+
if (sectionIdx === -1) return rows;
|
|
145
|
+
|
|
146
|
+
const afterSection = content.slice(sectionIdx);
|
|
147
|
+
const nextHeading = afterSection.indexOf("\n## ", 1);
|
|
148
|
+
const block =
|
|
149
|
+
nextHeading > 0 ? afterSection.slice(0, nextHeading) : afterSection;
|
|
150
|
+
|
|
151
|
+
for (const line of block.split("\n")) {
|
|
152
|
+
if (
|
|
153
|
+
line.startsWith("|") &&
|
|
154
|
+
!line.startsWith("| Query") &&
|
|
155
|
+
!line.startsWith("| When") &&
|
|
156
|
+
!line.startsWith("|--") &&
|
|
157
|
+
!line.startsWith("|-")
|
|
158
|
+
) {
|
|
159
|
+
const cols = line
|
|
160
|
+
.split("|")
|
|
161
|
+
.map((c) => c.trim())
|
|
162
|
+
.filter((c) => c);
|
|
163
|
+
if (cols.length >= 2) {
|
|
164
|
+
rows.push(`${cols[0]} → ${cols[1]}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return rows;
|
|
169
|
+
}
|