agent-method 1.5.3 → 1.5.6

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 (64) hide show
  1. package/README.md +197 -57
  2. package/bin/wwa.js +35 -9
  3. package/docs/internal/doc-tokens.yaml +452 -0
  4. package/docs/internal/feature-registry.yaml +13 -1
  5. package/lib/cli/casestudy.js +691 -0
  6. package/lib/cli/check.js +71 -71
  7. package/lib/cli/close.js +446 -0
  8. package/lib/cli/completion.js +639 -0
  9. package/lib/cli/digest.js +66 -0
  10. package/lib/cli/docs.js +207 -0
  11. package/lib/cli/helpers.js +49 -2
  12. package/lib/cli/implement.js +159 -0
  13. package/lib/cli/init.js +25 -6
  14. package/lib/cli/plan.js +128 -0
  15. package/lib/cli/refine.js +202 -202
  16. package/lib/cli/review.js +68 -0
  17. package/lib/cli/scan.js +28 -28
  18. package/lib/cli/status.js +61 -61
  19. package/lib/cli/upgrade.js +150 -147
  20. package/lib/init.js +478 -296
  21. package/package.json +12 -4
  22. package/templates/README.md +73 -25
  23. package/templates/entry-points/.cursorrules +143 -14
  24. package/templates/entry-points/AGENT.md +143 -14
  25. package/templates/entry-points/CLAUDE.md +143 -14
  26. package/templates/extensions/analytical-system.md +1 -1
  27. package/templates/extensions/code-project.md +1 -1
  28. package/templates/extensions/data-exploration.md +1 -1
  29. package/templates/full/.context/BASE.md +33 -0
  30. package/templates/full/.context/METHODOLOGY.md +62 -5
  31. package/templates/full/.cursorrules +128 -18
  32. package/templates/full/AGENT.md +128 -18
  33. package/templates/full/CLAUDE.md +128 -18
  34. package/templates/full/Management/DIGEST.md +23 -0
  35. package/templates/full/Management/STATUS.md +46 -0
  36. package/templates/full/PROJECT.md +34 -0
  37. package/templates/full/Reviews/INDEX.md +41 -0
  38. package/templates/full/Reviews/backlog.md +52 -0
  39. package/templates/full/Reviews/plan.md +43 -0
  40. package/templates/full/Reviews/project.md +41 -0
  41. package/templates/full/Reviews/requirements.md +42 -0
  42. package/templates/full/Reviews/roadmap.md +41 -0
  43. package/templates/full/Reviews/state.md +56 -0
  44. package/templates/full/SESSION-LOG.md +29 -0
  45. package/templates/full/SUMMARY.md +7 -4
  46. package/templates/full/agentWorkflows/INDEX.md +42 -0
  47. package/templates/full/agentWorkflows/observations.md +65 -0
  48. package/templates/full/agentWorkflows/patterns.md +68 -0
  49. package/templates/full/agentWorkflows/sessions.md +92 -0
  50. package/templates/full/intro/README.md +39 -0
  51. package/templates/starter/.context/BASE.md +35 -0
  52. package/templates/starter/.context/METHODOLOGY.md +59 -5
  53. package/templates/starter/.cursorrules +135 -13
  54. package/templates/starter/AGENT.md +135 -13
  55. package/templates/starter/CLAUDE.md +135 -13
  56. package/templates/starter/Management/DIGEST.md +23 -0
  57. package/templates/starter/Management/STATUS.md +46 -0
  58. package/templates/starter/PROJECT.md +34 -0
  59. package/templates/starter/Reviews/INDEX.md +75 -0
  60. package/templates/starter/SESSION-LOG.md +29 -0
  61. package/templates/starter/SUMMARY.md +27 -0
  62. package/templates/starter/agentWorkflows/INDEX.md +61 -0
  63. package/templates/starter/intro/README.md +37 -0
  64. package/templates/full/docs/index.md +0 -46
package/lib/cli/check.js CHANGED
@@ -1,71 +1,71 @@
1
- /** wwa check — validate entry point and project setup. */
2
-
3
- import { dirname, resolve } from "node:path";
4
- import {
5
- findEntryPoint,
6
- resolveProjectType,
7
- getPipeline,
8
- loadRegistryData,
9
- } from "./helpers.js";
10
-
11
- export function register(program) {
12
- program
13
- .command("check [entry-point]")
14
- .description("Validate your entry point and project setup")
15
- .option(
16
- "-p, --project-type <type>",
17
- "Project type: code, context, data, mix, general (auto-detected if omitted)"
18
- )
19
- .option("--registry <path>", "Path to feature-registry.yaml")
20
- .option("--json", "Output as JSON")
21
- .action(async (entryPoint, opts) => {
22
- const { validateEntryPoint, detectProjectType } = await getPipeline();
23
- const reg = await loadRegistryData(opts.registry);
24
-
25
- if (!entryPoint) {
26
- entryPoint = findEntryPoint(".");
27
- if (!entryPoint) {
28
- console.error(
29
- "No entry point found in current directory " +
30
- "(looked for CLAUDE.md, .cursorrules, AGENT.md).\n" +
31
- "Specify a path: npx wwa check path/to/CLAUDE.md"
32
- );
33
- process.exit(1);
34
- }
35
- }
36
-
37
- let projectType;
38
- if (opts.projectType) {
39
- projectType = resolveProjectType(opts.projectType);
40
- } else {
41
- const detected = detectProjectType(dirname(resolve(entryPoint)));
42
- projectType = detected.project_type || "general";
43
- }
44
-
45
- const result = validateEntryPoint(entryPoint, projectType, reg);
46
- console.log(`Checking: ${entryPoint} (type: ${projectType})`);
47
-
48
- if (opts.json) {
49
- console.log(JSON.stringify(result, null, 2));
50
- } else {
51
- const overall = result.valid ? "PASS" : "FAIL";
52
- console.log(` Overall: ${overall}`);
53
- for (const [checkId, chk] of Object.entries(result.checks)) {
54
- const cStatus = chk.pass ? "PASS" : "FAIL";
55
- let extra = "";
56
- if (chk.missing && chk.missing.length > 0) {
57
- extra = ` (missing: ${chk.missing.join(", ")})`;
58
- }
59
- console.log(` ${checkId}: ${cStatus}${extra}`);
60
- }
61
- if (result.issues && result.issues.length > 0) {
62
- console.log("\n Issues:");
63
- for (const issue of result.issues) {
64
- console.log(
65
- ` [${issue.severity}] ${issue.check}: ${issue.description}`
66
- );
67
- }
68
- }
69
- }
70
- });
71
- }
1
+ /** wwa check — validate entry point and project setup. */
2
+
3
+ import { dirname, resolve } from "node:path";
4
+ import {
5
+ findEntryPoint,
6
+ resolveProjectType,
7
+ getPipeline,
8
+ loadRegistryData,
9
+ } from "./helpers.js";
10
+
11
+ export function register(program) {
12
+ program
13
+ .command("check [entry-point]")
14
+ .description("Validate your entry point and project setup")
15
+ .option(
16
+ "-p, --project-type <type>",
17
+ "Project type: code, context, data, mix, general (auto-detected if omitted)"
18
+ )
19
+ .option("--registry <path>", "Path to feature-registry.yaml")
20
+ .option("--json", "Output as JSON")
21
+ .action(async (entryPoint, opts) => {
22
+ const { validateEntryPoint, detectProjectType } = await getPipeline();
23
+ const reg = await loadRegistryData(opts.registry);
24
+
25
+ if (!entryPoint) {
26
+ entryPoint = findEntryPoint(".");
27
+ if (!entryPoint) {
28
+ console.error(
29
+ "No entry point found in current directory " +
30
+ "(looked for CLAUDE.md, .cursorrules, AGENT.md).\n" +
31
+ "Specify a path: wwa check path/to/CLAUDE.md"
32
+ );
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ let projectType;
38
+ if (opts.projectType) {
39
+ projectType = resolveProjectType(opts.projectType);
40
+ } else {
41
+ const detected = detectProjectType(dirname(resolve(entryPoint)));
42
+ projectType = detected.project_type || "general";
43
+ }
44
+
45
+ const result = validateEntryPoint(entryPoint, projectType, reg);
46
+ console.log(`Checking: ${entryPoint} (type: ${projectType})`);
47
+
48
+ if (opts.json) {
49
+ console.log(JSON.stringify(result, null, 2));
50
+ } else {
51
+ const overall = result.valid ? "PASS" : "FAIL";
52
+ console.log(` Overall: ${overall}`);
53
+ for (const [checkId, chk] of Object.entries(result.checks)) {
54
+ const cStatus = chk.pass ? "PASS" : "FAIL";
55
+ let extra = "";
56
+ if (chk.missing && chk.missing.length > 0) {
57
+ extra = ` (missing: ${chk.missing.join(", ")})`;
58
+ }
59
+ console.log(` ${checkId}: ${cStatus}${extra}`);
60
+ }
61
+ if (result.issues && result.issues.length > 0) {
62
+ console.log("\n Issues:");
63
+ for (const issue of result.issues) {
64
+ console.log(
65
+ ` [${issue.severity}] ${issue.check}: ${issue.description}`
66
+ );
67
+ }
68
+ }
69
+ }
70
+ });
71
+ }
@@ -0,0 +1,446 @@
1
+ /** wwa close — session close checklist, cascade reminders, and project snapshot. */
2
+
3
+ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
4
+ import { resolve, join, dirname, relative } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { findEntryPoint, findSessionLog, readMethodVersion, packageRoot, safeWriteFile } from "./helpers.js";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ export function register(program) {
12
+ program
13
+ .command("close [directory]")
14
+ .description("Show session close checklist, cascade reminders, and project snapshot")
15
+ .option("--json", "Output as JSON")
16
+ .action(async (directory, opts) => {
17
+ directory = directory || ".";
18
+ const d = resolve(directory);
19
+
20
+ const checklist = [];
21
+ const warnings = [];
22
+
23
+ // 1. Check SESSION-LOG.md exists
24
+ const sessionLog = findSessionLog(directory);
25
+ if (sessionLog) {
26
+ checklist.push({ item: "SESSION-LOG.md exists", ok: true });
27
+ } else {
28
+ checklist.push({ item: "SESSION-LOG.md exists", ok: false });
29
+ warnings.push("No SESSION-LOG.md found — create one to track session metrics");
30
+ }
31
+
32
+ // 2. Check STATE.md exists and has current position
33
+ const statePath = join(d, "STATE.md");
34
+ if (existsSync(statePath)) {
35
+ checklist.push({ item: "STATE.md exists", ok: true });
36
+ } else {
37
+ checklist.push({ item: "STATE.md exists", ok: false });
38
+ warnings.push("No STATE.md found — decisions may be lost between sessions");
39
+ }
40
+
41
+ // 3. Check PLAN.md for incomplete steps
42
+ const planPath = join(d, "PLAN.md");
43
+ if (existsSync(planPath)) {
44
+ const planContent = readFileSync(planPath, "utf-8");
45
+ const todoSteps = (planContent.match(/\|\s*todo\s*\|/gi) || []).length;
46
+ if (todoSteps > 0) {
47
+ checklist.push({
48
+ item: `PLAN.md has ${todoSteps} incomplete step(s)`,
49
+ ok: false,
50
+ });
51
+ } else {
52
+ checklist.push({ item: "PLAN.md — all steps complete", ok: true });
53
+ }
54
+ }
55
+
56
+ // 4. Check Management files
57
+ const digestPath = join(d, "Management", "DIGEST.md");
58
+ if (existsSync(digestPath)) {
59
+ checklist.push({ item: "Management/DIGEST.md exists", ok: true });
60
+ }
61
+
62
+ // 5. Check SUMMARY.md
63
+ const summaryPath = join(d, "SUMMARY.md");
64
+ if (existsSync(summaryPath)) {
65
+ checklist.push({ item: "SUMMARY.md exists for audit entry", ok: true });
66
+ }
67
+
68
+ // Build cascade reminders
69
+ const cascadeReminders = [
70
+ "Append metrics entry to SESSION-LOG.md",
71
+ "Update STATE.md current position",
72
+ "For high-effort tasks: append row to Management/DIGEST.md",
73
+ "If milestone: update Management/STATUS.md",
74
+ "If phase complete: mark ROADMAP.md, write SUMMARY.md entry",
75
+ ];
76
+
77
+ // Compute project snapshot — methodology reference data
78
+ const snapshot = await computeProjectSnapshot(d);
79
+
80
+ // Update project doc-tokens.yaml regardless of output mode
81
+ const tokenUpdateResult = await updateProjectTokens(d, snapshot);
82
+
83
+ if (opts.json) {
84
+ console.log(
85
+ JSON.stringify({ checklist, warnings, cascadeReminders, snapshot, tokensUpdated: !!tokenUpdateResult }, null, 2)
86
+ );
87
+ return;
88
+ }
89
+
90
+ console.log("\n Session Close Checklist\n");
91
+
92
+ for (const item of checklist) {
93
+ const icon = item.ok ? "[x]" : "[ ]";
94
+ console.log(` ${icon} ${item.item}`);
95
+ }
96
+
97
+ if (warnings.length > 0) {
98
+ console.log("\n Warnings:");
99
+ for (const w of warnings) {
100
+ console.log(` ! ${w}`);
101
+ }
102
+ }
103
+
104
+ console.log("\n Cascade reminders:");
105
+ for (const r of cascadeReminders) {
106
+ console.log(` - ${r}`);
107
+ }
108
+
109
+ // Print project snapshot
110
+ printSnapshot(snapshot);
111
+
112
+ if (tokenUpdateResult) {
113
+ console.log("\n Updated .context/doc-tokens.yaml with current project metrics.");
114
+ }
115
+
116
+ console.log("");
117
+ });
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Project snapshot — computed at close, chunked for large projects
122
+ // ---------------------------------------------------------------------------
123
+
124
+ async function computeProjectSnapshot(dir) {
125
+ const snapshot = {
126
+ project: {},
127
+ methodologyFiles: { present: [], missing: [] },
128
+ decisions: { count: 0, recent: [] },
129
+ sessions: { count: 0, latestDate: null },
130
+ features: [],
131
+ registry: null,
132
+ docsMap: null,
133
+ fileStats: {},
134
+ };
135
+
136
+ // --- Chunk 1: Project identity ---
137
+ const entryPoint = findEntryPoint(dir);
138
+ if (entryPoint) {
139
+ snapshot.project.entryPoint = relative(dir, entryPoint) || entryPoint;
140
+ snapshot.project.methodVersion = readMethodVersion(entryPoint);
141
+ const epContent = readFileSync(entryPoint, "utf-8");
142
+ const tierMatch = epContent.match(/tier:\s*(\S+)/);
143
+ if (tierMatch) snapshot.project.tier = tierMatch[1];
144
+ const modeMatch = epContent.match(/mode:\s*(\S+)/);
145
+ if (modeMatch) snapshot.project.interactionLevel = modeMatch[1];
146
+ }
147
+
148
+ const projectPath = join(dir, "PROJECT.md");
149
+ if (existsSync(projectPath)) {
150
+ const content = readFileSync(projectPath, "utf-8");
151
+ const titleMatch = content.match(/^#\s+(.+)/m);
152
+ if (titleMatch) snapshot.project.name = titleMatch[1].trim();
153
+ }
154
+
155
+ const profilePath = join(dir, "PROJECT-PROFILE.md");
156
+ if (existsSync(profilePath)) {
157
+ const content = readFileSync(profilePath, "utf-8");
158
+ const typeMatch = content.match(/project.?type[:\s|]+(\S+)/i);
159
+ if (typeMatch) snapshot.project.type = typeMatch[1].toLowerCase();
160
+ const stageMatch = content.match(/lifecycle.?stage[:\s|]+(\S+)/i);
161
+ if (stageMatch) snapshot.project.lifecycleStage = stageMatch[1].toLowerCase();
162
+ }
163
+
164
+ // --- Chunk 2: Methodology file inventory ---
165
+ const expectedFiles = [
166
+ "STATE.md", "PLAN.md", "ROADMAP.md", "SUMMARY.md",
167
+ "PROJECT.md", "PROJECT-PROFILE.md", "SESSION-LOG.md",
168
+ ".context/BASE.md", ".context/METHODOLOGY.md",
169
+ ];
170
+ for (const f of expectedFiles) {
171
+ const p = join(dir, f);
172
+ if (existsSync(p)) {
173
+ const stat = statSync(p);
174
+ const lines = readFileSync(p, "utf-8").split("\n").length;
175
+ snapshot.methodologyFiles.present.push({ file: f, lines, bytes: stat.size });
176
+ // Track files approaching 300-line threshold
177
+ if (lines > 250) {
178
+ snapshot.fileStats[f] = { lines, nearThreshold: lines >= 280 };
179
+ }
180
+ } else {
181
+ snapshot.methodologyFiles.missing.push(f);
182
+ }
183
+ }
184
+
185
+ // --- Chunk 3: Decisions from STATE.md ---
186
+ const statePath = join(dir, "STATE.md");
187
+ if (existsSync(statePath)) {
188
+ const content = readFileSync(statePath, "utf-8");
189
+ const decisionSection = content.match(
190
+ /##\s*Decisions?\s*\n([\s\S]*?)(?=\n##|\n$|$)/i
191
+ );
192
+ if (decisionSection) {
193
+ const rows = decisionSection[1].match(/\|[^|\n]+\|[^|\n]+\|/g) || [];
194
+ const decisions = [];
195
+ for (const row of rows) {
196
+ const cols = row.split("|").map((c) => c.trim()).filter((c) => c && !c.match(/^[-:]+$/));
197
+ if (cols.length >= 2 && !cols[0].toLowerCase().includes("date")) {
198
+ decisions.push({ date: cols[0], decision: cols.slice(1).join(" — ") });
199
+ }
200
+ }
201
+ snapshot.decisions.count = decisions.length;
202
+ snapshot.decisions.recent = decisions.slice(-5); // last 5
203
+ }
204
+ }
205
+
206
+ // --- Chunk 4: Session count from SESSION-LOG.md ---
207
+ const sessionLog = findSessionLog(dir);
208
+ if (sessionLog) {
209
+ const content = readFileSync(sessionLog, "utf-8");
210
+ const entries = content.match(/###\s+S\d+/g) || [];
211
+ snapshot.sessions.count = entries.length;
212
+ // Find the latest date
213
+ const dates = content.match(/###\s+S\d+\s*(?:—|--)\s*(\S+)/g) || [];
214
+ if (dates.length > 0) {
215
+ const lastDate = dates[dates.length - 1].match(/(\d{4}-\d{2}-\d{2})/);
216
+ if (lastDate) snapshot.sessions.latestDate = lastDate[1];
217
+ }
218
+ }
219
+
220
+ // --- Chunk 5: Feature registry (from project .context/ or installed package) ---
221
+ const localRegistry = join(dir, ".context", "feature-registry.yaml");
222
+ const pkgRegistry = join(packageRoot, "docs", "internal", "feature-registry.yaml");
223
+ const registryPath = existsSync(localRegistry) ? localRegistry : (existsSync(pkgRegistry) ? pkgRegistry : null);
224
+
225
+ if (registryPath) {
226
+ try {
227
+ const yaml = (await import("js-yaml")).default;
228
+ const raw = readFileSync(registryPath, "utf-8");
229
+ const parsed = yaml.load(raw);
230
+ if (parsed) {
231
+ snapshot.registry = {
232
+ source: existsSync(localRegistry) ? "project" : "package",
233
+ version: parsed.version || null,
234
+ featureCount: parsed.features ? Object.keys(parsed.features).length : 0,
235
+ workflowCount: parsed.guided_workflows ? parsed.guided_workflows.length : 0,
236
+ };
237
+ // Collect active features from project entry point scoping
238
+ if (entryPoint) {
239
+ const epContent = readFileSync(entryPoint, "utf-8");
240
+ const featureRefs = epContent.match(/[A-Z]{2,5}-\d{2}/g) || [];
241
+ snapshot.features = [...new Set(featureRefs)].sort();
242
+ }
243
+ }
244
+ } catch (_) {
245
+ // Registry parse failed — skip silently
246
+ }
247
+ }
248
+
249
+ // --- Chunk 6: Docs map status ---
250
+ const docsMapPath = join(dir, ".context", "DOCS-MAP.md");
251
+ if (existsSync(docsMapPath)) {
252
+ const content = readFileSync(docsMapPath, "utf-8");
253
+ const inventoryRows = (content.match(/^\|[^|]+\|[^|]+\|[^|]+\|[^|]+\|$/gm) || [])
254
+ .filter((r) => !r.includes("---") && !r.includes("Path") && !r.includes("Purpose"));
255
+ const mappingRows = (content.match(/^\|[^|]+\|[^|]+\|[^|]+\|$/gm) || [])
256
+ .filter((r) => !r.includes("---") && !r.includes("Project component") && !r.includes("When this"));
257
+
258
+ // Check which docs actually exist on disk
259
+ let docsExist = 0;
260
+ let docsTotal = 0;
261
+ for (const row of inventoryRows) {
262
+ const cols = row.split("|").map((c) => c.trim()).filter((c) => c);
263
+ if (cols.length >= 1) {
264
+ docsTotal++;
265
+ if (existsSync(join(dir, cols[0]))) docsExist++;
266
+ }
267
+ }
268
+
269
+ snapshot.docsMap = {
270
+ inventoryCount: docsTotal,
271
+ docsOnDisk: docsExist,
272
+ docsMissing: docsTotal - docsExist,
273
+ componentMappings: mappingRows.length,
274
+ };
275
+ }
276
+
277
+ return snapshot;
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // Display
282
+ // ---------------------------------------------------------------------------
283
+
284
+ function printSnapshot(snapshot) {
285
+ console.log("\n Project Snapshot\n");
286
+
287
+ // Project identity
288
+ if (snapshot.project.name) {
289
+ console.log(` Project: ${snapshot.project.name}`);
290
+ }
291
+ if (snapshot.project.type) {
292
+ console.log(` Type: ${snapshot.project.type} | Stage: ${snapshot.project.lifecycleStage || "unknown"}`);
293
+ }
294
+ if (snapshot.project.methodVersion) {
295
+ console.log(` Method: v${snapshot.project.methodVersion} | Tier: ${snapshot.project.tier || "unknown"}`);
296
+ }
297
+
298
+ // File inventory
299
+ const present = snapshot.methodologyFiles.present.length;
300
+ const missing = snapshot.methodologyFiles.missing.length;
301
+ console.log(`\n Methodology files: ${present} present, ${missing} missing`);
302
+ if (missing > 0) {
303
+ console.log(` Missing: ${snapshot.methodologyFiles.missing.join(", ")}`);
304
+ }
305
+
306
+ // Threshold warnings
307
+ const nearThreshold = Object.entries(snapshot.fileStats)
308
+ .filter(([, s]) => s.nearThreshold);
309
+ if (nearThreshold.length > 0) {
310
+ console.log(` Scale warnings:`);
311
+ for (const [file, stats] of nearThreshold) {
312
+ console.log(` ! ${file}: ${stats.lines} lines (threshold: 300)`);
313
+ }
314
+ }
315
+
316
+ // Decisions
317
+ if (snapshot.decisions.count > 0) {
318
+ console.log(`\n Decisions: ${snapshot.decisions.count} total`);
319
+ if (snapshot.decisions.recent.length > 0) {
320
+ console.log(` Recent:`);
321
+ for (const d of snapshot.decisions.recent) {
322
+ const text = d.decision.length > 60 ? d.decision.slice(0, 57) + "..." : d.decision;
323
+ console.log(` ${d.date}: ${text}`);
324
+ }
325
+ }
326
+ }
327
+
328
+ // Sessions
329
+ if (snapshot.sessions.count > 0) {
330
+ console.log(`\n Sessions: ${snapshot.sessions.count} logged (latest: ${snapshot.sessions.latestDate || "unknown"})`);
331
+ }
332
+
333
+ // Registry
334
+ if (snapshot.registry) {
335
+ console.log(`\n Registry: ${snapshot.registry.source} (${snapshot.registry.featureCount} features, ${snapshot.registry.workflowCount} workflows)`);
336
+ }
337
+ if (snapshot.features.length > 0) {
338
+ console.log(` Features referenced: ${snapshot.features.length} (${snapshot.features.slice(0, 8).join(", ")}${snapshot.features.length > 8 ? "..." : ""})`);
339
+ }
340
+
341
+ // Docs
342
+ if (snapshot.docsMap) {
343
+ const dm = snapshot.docsMap;
344
+ console.log(`\n Docs: ${dm.docsOnDisk}/${dm.inventoryCount} exist | ${dm.componentMappings} component mappings`);
345
+ if (dm.docsMissing > 0) {
346
+ console.log(` Missing docs: ${dm.docsMissing} (run \`wwa docs\` for details)`);
347
+ }
348
+ }
349
+ }
350
+
351
+ // ---------------------------------------------------------------------------
352
+ // Update project doc-tokens.yaml
353
+ // ---------------------------------------------------------------------------
354
+
355
+ async function updateProjectTokens(dir, snapshot) {
356
+ const tokensPath = join(dir, ".context", "doc-tokens.yaml");
357
+ if (!existsSync(tokensPath)) return null;
358
+
359
+ try {
360
+ const yaml = (await import("js-yaml")).default;
361
+ const raw = readFileSync(tokensPath, "utf-8");
362
+ const parsed = yaml.load(raw) || {};
363
+ const tokens = parsed.tokens || {};
364
+
365
+ const today = new Date().toISOString().slice(0, 10);
366
+ tokens.last_close = today;
367
+
368
+ // File counts — count methodology files (.md/.yaml/.yml) in project
369
+ let methodologyCount = 0;
370
+ let docsCount = 0;
371
+ let contextCount = 0;
372
+ const countFiles = (base) => {
373
+ if (!existsSync(base)) return;
374
+ for (const entry of readdirSync(base, { withFileTypes: true })) {
375
+ const p = join(base, entry.name);
376
+ if (entry.isDirectory()) {
377
+ // Track docs/ and .context/ separately
378
+ if (entry.name === "docs" && base === dir) {
379
+ docsCount += countDir(p);
380
+ } else if (entry.name === ".context" && base === dir) {
381
+ contextCount += countDir(p);
382
+ } else {
383
+ countFiles(p);
384
+ }
385
+ } else if (/\.(md|yaml|yml)$/i.test(entry.name)) {
386
+ methodologyCount++;
387
+ }
388
+ }
389
+ };
390
+ const countDir = (d) => {
391
+ let c = 0;
392
+ if (!existsSync(d)) return c;
393
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
394
+ if (entry.isDirectory()) c += countDir(join(d, entry.name));
395
+ else if (/\.(md|yaml|yml)$/i.test(entry.name)) c++;
396
+ }
397
+ return c;
398
+ };
399
+ countFiles(dir);
400
+ tokens.methodology_file_count = methodologyCount;
401
+ tokens.docs_file_count = docsCount;
402
+ tokens.context_file_count = contextCount;
403
+
404
+ // Session metrics
405
+ tokens.session_count = snapshot.sessions.count;
406
+ tokens.decision_count = snapshot.decisions.count;
407
+ tokens.total_features_referenced = snapshot.features.length;
408
+
409
+ // Scale indicators
410
+ let nearThreshold = 0;
411
+ let overThreshold = 0;
412
+ for (const f of snapshot.methodologyFiles.present) {
413
+ if (f.lines >= 300) overThreshold++;
414
+ else if (f.lines >= 250) nearThreshold++;
415
+ }
416
+ tokens.files_near_threshold = nearThreshold;
417
+ tokens.files_over_threshold = overThreshold;
418
+
419
+ // Docs coverage
420
+ if (snapshot.docsMap) {
421
+ tokens.docs_inventory_count = snapshot.docsMap.inventoryCount;
422
+ tokens.docs_on_disk = snapshot.docsMap.docsOnDisk;
423
+ tokens.docs_missing = snapshot.docsMap.docsMissing;
424
+ tokens.component_mappings = snapshot.docsMap.componentMappings;
425
+ }
426
+
427
+ // Rebuild YAML output preserving structure
428
+ parsed.tokens = tokens;
429
+ const output = yaml.dump(parsed, { lineWidth: 120, noRefs: true, quotingType: '"' });
430
+
431
+ // Prepend header comment
432
+ const header = [
433
+ "# Project Token Registry",
434
+ "# Computed by wwa init. Updated by wwa close.",
435
+ "# Used by wwa casestudy for methodology reference data.",
436
+ "#",
437
+ `# Last updated: ${today}`,
438
+ "",
439
+ ].join("\n");
440
+
441
+ safeWriteFile(tokensPath, header + output, "utf-8");
442
+ return tokens;
443
+ } catch (_) {
444
+ return null;
445
+ }
446
+ }