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.
Files changed (108) hide show
  1. package/README.md +343 -0
  2. package/bin/wwa.js +115 -0
  3. package/docs/internal/cli-commands.yaml +259 -0
  4. package/docs/internal/doc-tokens.yaml +1103 -0
  5. package/docs/internal/feature-registry.yaml +1643 -0
  6. package/lib/boundaries.js +247 -0
  7. package/lib/cli/add.js +170 -0
  8. package/lib/cli/casestudy.js +1000 -0
  9. package/lib/cli/check.js +323 -0
  10. package/lib/cli/close.js +838 -0
  11. package/lib/cli/completion.js +735 -0
  12. package/lib/cli/deps.js +234 -0
  13. package/lib/cli/digest.js +73 -0
  14. package/lib/cli/doc-review.js +486 -0
  15. package/lib/cli/docs.js +315 -0
  16. package/lib/cli/helpers.js +198 -0
  17. package/lib/cli/implement.js +169 -0
  18. package/lib/cli/init.js +280 -0
  19. package/lib/cli/pipeline.js +206 -0
  20. package/lib/cli/plan.js +140 -0
  21. package/lib/cli/record.js +98 -0
  22. package/lib/cli/refine.js +202 -0
  23. package/lib/cli/report-helpers.js +113 -0
  24. package/lib/cli/review.js +76 -0
  25. package/lib/cli/routable.js +109 -0
  26. package/lib/cli/route.js +101 -0
  27. package/lib/cli/scan.js +133 -0
  28. package/lib/cli/serve.js +23 -0
  29. package/lib/cli/status.js +65 -0
  30. package/lib/cli/update-docs.js +574 -0
  31. package/lib/cli/upgrade.js +222 -0
  32. package/lib/cli/watch.js +32 -0
  33. package/lib/dependencies.js +196 -0
  34. package/lib/init.js +692 -0
  35. package/lib/mcp-server.js +612 -0
  36. package/lib/pipeline.js +907 -0
  37. package/lib/registry.js +132 -0
  38. package/lib/watcher.js +165 -0
  39. package/package.json +54 -0
  40. package/templates/README.md +363 -0
  41. package/templates/entry-points/.cursorrules +90 -0
  42. package/templates/entry-points/AGENT.md +90 -0
  43. package/templates/entry-points/CLAUDE.md +88 -0
  44. package/templates/extensions/MANIFEST.md +110 -0
  45. package/templates/extensions/analytical-system.md +96 -0
  46. package/templates/extensions/code-project.md +77 -0
  47. package/templates/extensions/data-exploration.md +117 -0
  48. package/templates/full/.context/BASE.md +101 -0
  49. package/templates/full/.context/COMPOSITION.md +47 -0
  50. package/templates/full/.context/INDEX.yaml +56 -0
  51. package/templates/full/.context/METHODOLOGY.md +246 -0
  52. package/templates/full/.context/PROTOCOL.yaml +169 -0
  53. package/templates/full/.context/REGISTRY.md +75 -0
  54. package/templates/full/.cursorrules +90 -0
  55. package/templates/full/AGENT.md +90 -0
  56. package/templates/full/CLAUDE.md +90 -0
  57. package/templates/full/Management/DIGEST.md +23 -0
  58. package/templates/full/Management/STATUS.md +46 -0
  59. package/templates/full/PLAN.md +67 -0
  60. package/templates/full/PROJECT-PROFILE.md +61 -0
  61. package/templates/full/PROJECT.md +80 -0
  62. package/templates/full/REQUIREMENTS.md +30 -0
  63. package/templates/full/ROADMAP.md +39 -0
  64. package/templates/full/Reviews/INDEX.md +41 -0
  65. package/templates/full/Reviews/backlog.md +52 -0
  66. package/templates/full/Reviews/plan.md +43 -0
  67. package/templates/full/Reviews/project.md +41 -0
  68. package/templates/full/Reviews/requirements.md +42 -0
  69. package/templates/full/Reviews/roadmap.md +41 -0
  70. package/templates/full/Reviews/state.md +56 -0
  71. package/templates/full/SESSION-LOG.md +102 -0
  72. package/templates/full/STATE.md +42 -0
  73. package/templates/full/SUMMARY.md +27 -0
  74. package/templates/full/agentWorkflows/INDEX.md +42 -0
  75. package/templates/full/agentWorkflows/observations.md +65 -0
  76. package/templates/full/agentWorkflows/patterns.md +68 -0
  77. package/templates/full/agentWorkflows/sessions.md +92 -0
  78. package/templates/full/intro/README.md +39 -0
  79. package/templates/full/registry/feature-registry.yaml +25 -0
  80. package/templates/full/registry/features/catalog.yaml +743 -0
  81. package/templates/full/registry/features/protocol.yaml +121 -0
  82. package/templates/full/registry/features/routing.yaml +358 -0
  83. package/templates/full/registry/features/workflows.yaml +404 -0
  84. package/templates/full/todos/backlog.md +19 -0
  85. package/templates/starter/.context/BASE.md +66 -0
  86. package/templates/starter/.context/INDEX.yaml +51 -0
  87. package/templates/starter/.context/METHODOLOGY.md +228 -0
  88. package/templates/starter/.context/PROTOCOL.yaml +165 -0
  89. package/templates/starter/.cursorrules +90 -0
  90. package/templates/starter/AGENT.md +90 -0
  91. package/templates/starter/CLAUDE.md +90 -0
  92. package/templates/starter/Management/DIGEST.md +23 -0
  93. package/templates/starter/Management/STATUS.md +46 -0
  94. package/templates/starter/PLAN.md +67 -0
  95. package/templates/starter/PROJECT-PROFILE.md +44 -0
  96. package/templates/starter/PROJECT.md +80 -0
  97. package/templates/starter/ROADMAP.md +39 -0
  98. package/templates/starter/Reviews/INDEX.md +75 -0
  99. package/templates/starter/SESSION-LOG.md +102 -0
  100. package/templates/starter/STATE.md +42 -0
  101. package/templates/starter/SUMMARY.md +27 -0
  102. package/templates/starter/agentWorkflows/INDEX.md +61 -0
  103. package/templates/starter/intro/README.md +37 -0
  104. package/templates/starter/registry/feature-registry.yaml +25 -0
  105. package/templates/starter/registry/features/catalog.yaml +743 -0
  106. package/templates/starter/registry/features/protocol.yaml +121 -0
  107. package/templates/starter/registry/features/routing.yaml +358 -0
  108. package/templates/starter/registry/features/workflows.yaml +404 -0
@@ -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
+ }