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.
- package/README.md +197 -57
- package/bin/wwa.js +35 -9
- package/docs/internal/doc-tokens.yaml +452 -0
- package/docs/internal/feature-registry.yaml +13 -1
- package/lib/cli/casestudy.js +691 -0
- package/lib/cli/check.js +71 -71
- package/lib/cli/close.js +446 -0
- package/lib/cli/completion.js +639 -0
- package/lib/cli/digest.js +66 -0
- package/lib/cli/docs.js +207 -0
- package/lib/cli/helpers.js +49 -2
- package/lib/cli/implement.js +159 -0
- package/lib/cli/init.js +25 -6
- package/lib/cli/plan.js +128 -0
- package/lib/cli/refine.js +202 -202
- package/lib/cli/review.js +68 -0
- package/lib/cli/scan.js +28 -28
- package/lib/cli/status.js +61 -61
- package/lib/cli/upgrade.js +150 -147
- package/lib/init.js +478 -296
- package/package.json +12 -4
- package/templates/README.md +73 -25
- package/templates/entry-points/.cursorrules +143 -14
- package/templates/entry-points/AGENT.md +143 -14
- package/templates/entry-points/CLAUDE.md +143 -14
- package/templates/extensions/analytical-system.md +1 -1
- package/templates/extensions/code-project.md +1 -1
- package/templates/extensions/data-exploration.md +1 -1
- package/templates/full/.context/BASE.md +33 -0
- package/templates/full/.context/METHODOLOGY.md +62 -5
- package/templates/full/.cursorrules +128 -18
- package/templates/full/AGENT.md +128 -18
- package/templates/full/CLAUDE.md +128 -18
- package/templates/full/Management/DIGEST.md +23 -0
- package/templates/full/Management/STATUS.md +46 -0
- package/templates/full/PROJECT.md +34 -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 +29 -0
- package/templates/full/SUMMARY.md +7 -4
- 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/starter/.context/BASE.md +35 -0
- package/templates/starter/.context/METHODOLOGY.md +59 -5
- package/templates/starter/.cursorrules +135 -13
- package/templates/starter/AGENT.md +135 -13
- package/templates/starter/CLAUDE.md +135 -13
- package/templates/starter/Management/DIGEST.md +23 -0
- package/templates/starter/Management/STATUS.md +46 -0
- package/templates/starter/PROJECT.md +34 -0
- package/templates/starter/Reviews/INDEX.md +75 -0
- package/templates/starter/SESSION-LOG.md +29 -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/full/docs/index.md +0 -46
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** wwa digest — show management digest and project health. */
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { resolve, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export function register(program) {
|
|
7
|
+
program
|
|
8
|
+
.command("digest [directory]")
|
|
9
|
+
.description("Show management digest from Management/DIGEST.md")
|
|
10
|
+
.option("--status", "Show Management/STATUS.md instead of digest")
|
|
11
|
+
.option("--json", "Output as JSON")
|
|
12
|
+
.action(async (directory, opts) => {
|
|
13
|
+
directory = directory || ".";
|
|
14
|
+
const d = resolve(directory);
|
|
15
|
+
|
|
16
|
+
const digestPath = join(d, "Management", "DIGEST.md");
|
|
17
|
+
const statusPath = join(d, "Management", "STATUS.md");
|
|
18
|
+
|
|
19
|
+
const target = opts.status ? statusPath : digestPath;
|
|
20
|
+
const label = opts.status ? "STATUS" : "DIGEST";
|
|
21
|
+
|
|
22
|
+
if (!existsSync(target)) {
|
|
23
|
+
// Try to synthesize from SUMMARY.md or STATE.md
|
|
24
|
+
const summaryPath = join(d, "SUMMARY.md");
|
|
25
|
+
const statePath = join(d, "STATE.md");
|
|
26
|
+
|
|
27
|
+
if (opts.json) {
|
|
28
|
+
console.log(
|
|
29
|
+
JSON.stringify(
|
|
30
|
+
{
|
|
31
|
+
error: `Management/${label}.md not found`,
|
|
32
|
+
hasSummary: existsSync(summaryPath),
|
|
33
|
+
hasState: existsSync(statePath),
|
|
34
|
+
},
|
|
35
|
+
null,
|
|
36
|
+
2
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`\n Management/${label}.md not found.\n`);
|
|
43
|
+
if (existsSync(summaryPath) || existsSync(statePath)) {
|
|
44
|
+
console.log(" Available alternatives:");
|
|
45
|
+
if (existsSync(summaryPath))
|
|
46
|
+
console.log(" - SUMMARY.md (session audit trail)");
|
|
47
|
+
if (existsSync(statePath))
|
|
48
|
+
console.log(" - STATE.md (current position + decisions)");
|
|
49
|
+
}
|
|
50
|
+
console.log(
|
|
51
|
+
"\n To get management reports, use the starter or full template tier:" +
|
|
52
|
+
"\n wwa init <type>\n"
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const content = readFileSync(target, "utf-8");
|
|
58
|
+
|
|
59
|
+
if (opts.json) {
|
|
60
|
+
console.log(JSON.stringify({ file: target, content }, null, 2));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(content);
|
|
65
|
+
});
|
|
66
|
+
}
|
package/lib/cli/docs.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/** wwa docs — show docs coverage and staleness from DOCS-MAP.md. */
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { resolve, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export function register(program) {
|
|
7
|
+
program
|
|
8
|
+
.command("docs [directory]")
|
|
9
|
+
.description("Show docs coverage report from .context/DOCS-MAP.md")
|
|
10
|
+
.option("--json", "Output as JSON")
|
|
11
|
+
.action(async (directory, opts) => {
|
|
12
|
+
directory = directory || ".";
|
|
13
|
+
const d = resolve(directory);
|
|
14
|
+
|
|
15
|
+
const docsMapPath = join(d, ".context", "DOCS-MAP.md");
|
|
16
|
+
|
|
17
|
+
if (!existsSync(docsMapPath)) {
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify({ error: "No .context/DOCS-MAP.md found" }, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log("\n No .context/DOCS-MAP.md found.\n");
|
|
23
|
+
console.log(" This file maps project components to their documentation.");
|
|
24
|
+
console.log(" Create it by running: wwa init <type>");
|
|
25
|
+
console.log(" Or ask your agent to populate it during onboarding.\n");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = readFileSync(docsMapPath, "utf-8");
|
|
30
|
+
const report = parseDocsMap(content, d);
|
|
31
|
+
|
|
32
|
+
if (opts.json) {
|
|
33
|
+
console.log(JSON.stringify(report, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
printReport(report);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// DOCS-MAP.md parsing
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
function parseDocsMap(content, projectDir) {
|
|
46
|
+
const inventory = [];
|
|
47
|
+
const mappings = [];
|
|
48
|
+
const scaffolding = [];
|
|
49
|
+
|
|
50
|
+
// Parse docs inventory table
|
|
51
|
+
const invSection = content.match(
|
|
52
|
+
/## Docs inventory\s*\n[\s\S]*?\n\|[^\n]+\n\|[-| :]+\n((?:\|[^\n]+\n)*)/
|
|
53
|
+
);
|
|
54
|
+
if (invSection) {
|
|
55
|
+
for (const row of invSection[1].trim().split("\n")) {
|
|
56
|
+
const cols = row.split("|").map((c) => c.trim()).filter((c) => c);
|
|
57
|
+
if (cols.length >= 3 && !cols[0].startsWith("<!--")) {
|
|
58
|
+
const path = cols[0];
|
|
59
|
+
const exists = existsSync(join(projectDir, path));
|
|
60
|
+
inventory.push({
|
|
61
|
+
path,
|
|
62
|
+
purpose: cols[1],
|
|
63
|
+
sources: cols[2],
|
|
64
|
+
status: cols[3] || (exists ? "active" : "missing"),
|
|
65
|
+
exists,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Parse component-to-docs mapping table
|
|
72
|
+
const mapSection = content.match(
|
|
73
|
+
/## Component-to-docs mapping\s*\n[\s\S]*?\n\|[^\n]+\n\|[-| :]+\n((?:\|[^\n]+\n)*)/
|
|
74
|
+
);
|
|
75
|
+
if (mapSection) {
|
|
76
|
+
for (const row of mapSection[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
|
+
mappings.push({
|
|
80
|
+
component: cols[0],
|
|
81
|
+
documentedIn: cols[1],
|
|
82
|
+
trigger: cols[2],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Parse scaffolding rules table
|
|
89
|
+
const scaffSection = content.match(
|
|
90
|
+
/## Scaffolding rules\s*\n[\s\S]*?\n\|[^\n]+\n\|[-| :]+\n((?:\|[^\n]+\n)*)/
|
|
91
|
+
);
|
|
92
|
+
if (scaffSection) {
|
|
93
|
+
for (const row of scaffSection[1].trim().split("\n")) {
|
|
94
|
+
const cols = row.split("|").map((c) => c.trim()).filter((c) => c);
|
|
95
|
+
if (cols.length >= 3 && !cols[0].startsWith("<!--")) {
|
|
96
|
+
scaffolding.push({
|
|
97
|
+
condition: cols[0],
|
|
98
|
+
proposedDoc: cols[1],
|
|
99
|
+
seedFrom: cols[2],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check which docs actually exist on disk
|
|
106
|
+
const docsDir = join(projectDir, "docs");
|
|
107
|
+
const existingDocs = [];
|
|
108
|
+
const missingDocs = [];
|
|
109
|
+
const unmappedDocs = [];
|
|
110
|
+
|
|
111
|
+
for (const item of inventory) {
|
|
112
|
+
if (item.exists) {
|
|
113
|
+
existingDocs.push(item.path);
|
|
114
|
+
} else {
|
|
115
|
+
missingDocs.push(item.path);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Find docs/ files that exist but aren't in the inventory
|
|
120
|
+
if (existsSync(docsDir)) {
|
|
121
|
+
const inventoryPaths = new Set(inventory.map((i) => i.path));
|
|
122
|
+
// Check common docs paths
|
|
123
|
+
const candidates = ["docs/index.md"];
|
|
124
|
+
for (const c of candidates) {
|
|
125
|
+
if (existsSync(join(projectDir, c)) && !inventoryPaths.has(c)) {
|
|
126
|
+
unmappedDocs.push(c);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Count mapped vs unmapped components
|
|
132
|
+
const mappedComponents = mappings.length;
|
|
133
|
+
const docsWithMappings = new Set(mappings.map((m) => m.documentedIn));
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
inventory,
|
|
137
|
+
mappings,
|
|
138
|
+
scaffolding,
|
|
139
|
+
summary: {
|
|
140
|
+
totalDocs: inventory.length,
|
|
141
|
+
existingDocs: existingDocs.length,
|
|
142
|
+
missingDocs: missingDocs.length,
|
|
143
|
+
unmappedDocs: unmappedDocs.length,
|
|
144
|
+
mappedComponents,
|
|
145
|
+
scaffoldingRules: scaffolding.length,
|
|
146
|
+
},
|
|
147
|
+
existingDocs,
|
|
148
|
+
missingDocs,
|
|
149
|
+
unmappedDocs,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Report output
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
function printReport(report) {
|
|
158
|
+
const s = report.summary;
|
|
159
|
+
|
|
160
|
+
console.log("\n Docs Coverage Report\n");
|
|
161
|
+
console.log(` Docs inventory: ${s.totalDocs} files (${s.existingDocs} exist, ${s.missingDocs} missing)`);
|
|
162
|
+
console.log(` Component mappings: ${s.mappedComponents}`);
|
|
163
|
+
console.log(` Scaffolding rules: ${s.scaffoldingRules}`);
|
|
164
|
+
|
|
165
|
+
if (report.inventory.length > 0) {
|
|
166
|
+
console.log("\n Docs inventory:");
|
|
167
|
+
for (const item of report.inventory) {
|
|
168
|
+
const icon = item.exists ? "[x]" : "[ ]";
|
|
169
|
+
const status = item.status === "stale" ? " (stale)" : "";
|
|
170
|
+
console.log(` ${icon} ${item.path} — ${item.purpose}${status}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (report.missingDocs.length > 0) {
|
|
175
|
+
console.log("\n Missing docs (in inventory but not on disk):");
|
|
176
|
+
for (const p of report.missingDocs) {
|
|
177
|
+
console.log(` - ${p}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (report.unmappedDocs.length > 0) {
|
|
182
|
+
console.log("\n Unmapped docs (on disk but not in inventory):");
|
|
183
|
+
for (const p of report.unmappedDocs) {
|
|
184
|
+
console.log(` - ${p}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (report.mappings.length > 0) {
|
|
189
|
+
console.log("\n Component mappings:");
|
|
190
|
+
for (const m of report.mappings) {
|
|
191
|
+
console.log(` ${m.component} → ${m.documentedIn}`);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
console.log("\n No component-to-docs mappings defined yet.");
|
|
195
|
+
console.log(" Ask your agent to populate .context/DOCS-MAP.md during onboarding.");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (report.scaffolding.length > 0) {
|
|
199
|
+
console.log("\n Scaffolding rules (auto-propose new docs):");
|
|
200
|
+
for (const r of report.scaffolding) {
|
|
201
|
+
console.log(` When: ${r.condition}`);
|
|
202
|
+
console.log(` Create: ${r.proposedDoc}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log("");
|
|
207
|
+
}
|
package/lib/cli/helpers.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* entry point discovery, output formatting.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
-
import { resolve, join, dirname } from "node:path";
|
|
6
|
+
import { readFileSync, existsSync, writeFileSync, copyFileSync } from "node:fs";
|
|
7
|
+
import { resolve, join, dirname, extname } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -113,6 +113,53 @@ export function basename_of(filepath) {
|
|
|
113
113
|
return filepath.split(/[\\/]/).pop();
|
|
114
114
|
}
|
|
115
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
|
+
|
|
116
163
|
// ---------------------------------------------------------------------------
|
|
117
164
|
// Output formatting
|
|
118
165
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
|
|
7
|
+
export function register(program) {
|
|
8
|
+
program
|
|
9
|
+
.command("implement [directory]")
|
|
10
|
+
.description("Show implementation guidance for the current plan step")
|
|
11
|
+
.option("--json", "Output as JSON")
|
|
12
|
+
.action(async (directory, opts) => {
|
|
13
|
+
directory = directory || ".";
|
|
14
|
+
const d = resolve(directory);
|
|
15
|
+
|
|
16
|
+
const planPath = join(d, "PLAN.md");
|
|
17
|
+
if (!existsSync(planPath)) {
|
|
18
|
+
console.error(
|
|
19
|
+
"No PLAN.md found. Run 'wwa init' to set up methodology files."
|
|
20
|
+
);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const planContent = readFileSync(planPath, "utf-8");
|
|
25
|
+
const steps = parseSteps(planContent);
|
|
26
|
+
const nextStep = steps.find((s) => s.status !== "done");
|
|
27
|
+
|
|
28
|
+
// Read STATE.md for context
|
|
29
|
+
const statePath = join(d, "STATE.md");
|
|
30
|
+
let position = null;
|
|
31
|
+
if (existsSync(statePath)) {
|
|
32
|
+
const stateContent = readFileSync(statePath, "utf-8");
|
|
33
|
+
position = extractPosition(stateContent);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Read entry point for scoping and cascade rules
|
|
37
|
+
const ep = findEntryPoint(directory);
|
|
38
|
+
let scopingRules = [];
|
|
39
|
+
let cascadeRules = [];
|
|
40
|
+
if (ep) {
|
|
41
|
+
const epContent = readFileSync(ep, "utf-8");
|
|
42
|
+
scopingRules = extractTableRows(epContent, "Scoping rules");
|
|
43
|
+
cascadeRules = extractTableRows(epContent, "Dependency cascade");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (opts.json) {
|
|
47
|
+
console.log(
|
|
48
|
+
JSON.stringify(
|
|
49
|
+
{ position, nextStep, totalSteps: steps.length, scopingRules, cascadeRules },
|
|
50
|
+
null,
|
|
51
|
+
2
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!nextStep) {
|
|
58
|
+
console.log("\n All plan steps are complete.");
|
|
59
|
+
if (position) {
|
|
60
|
+
console.log(` Phase: ${position.phase}`);
|
|
61
|
+
console.log(` Status: ${position.status}`);
|
|
62
|
+
}
|
|
63
|
+
console.log(
|
|
64
|
+
"\n Next actions:" +
|
|
65
|
+
"\n - Verify all criteria pass: wwa plan" +
|
|
66
|
+
"\n - Close the phase: update STATE.md, ROADMAP.md, SUMMARY.md" +
|
|
67
|
+
"\n - Run session close: wwa close\n"
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const done = steps.filter((s) => s.status === "done").length;
|
|
73
|
+
console.log(`\n Implementation guidance`);
|
|
74
|
+
console.log(` Progress: ${done}/${steps.length} steps complete\n`);
|
|
75
|
+
|
|
76
|
+
console.log(` Next step: #${nextStep.num}`);
|
|
77
|
+
console.log(` Deliverable: ${nextStep.deliverable}`);
|
|
78
|
+
if (nextStep.read) console.log(` Read first: ${nextStep.read}`);
|
|
79
|
+
if (nextStep.produce) console.log(` Produce: ${nextStep.produce}`);
|
|
80
|
+
|
|
81
|
+
// Show relevant scoping rules
|
|
82
|
+
if (scopingRules.length > 0) {
|
|
83
|
+
console.log(`\n Relevant scoping rules (from entry point):`);
|
|
84
|
+
for (const rule of scopingRules.slice(0, 5)) {
|
|
85
|
+
console.log(` ${rule}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Show cascade reminders
|
|
90
|
+
if (cascadeRules.length > 0) {
|
|
91
|
+
console.log(`\n Cascade rules to remember:`);
|
|
92
|
+
for (const rule of cascadeRules.slice(0, 5)) {
|
|
93
|
+
console.log(` ${rule}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(
|
|
98
|
+
"\n After completing this step:" +
|
|
99
|
+
"\n - Record any decisions in STATE.md" +
|
|
100
|
+
"\n - Check cascade table for dependent updates" +
|
|
101
|
+
"\n - Mark step as 'done' in PLAN.md\n"
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseSteps(content) {
|
|
107
|
+
const steps = [];
|
|
108
|
+
const stepPattern =
|
|
109
|
+
/\|\s*(\d+)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(\w+)\s*\|/g;
|
|
110
|
+
let m;
|
|
111
|
+
while ((m = stepPattern.exec(content)) !== null) {
|
|
112
|
+
steps.push({
|
|
113
|
+
num: parseInt(m[1], 10),
|
|
114
|
+
deliverable: m[2].trim(),
|
|
115
|
+
read: m[3].trim(),
|
|
116
|
+
produce: m[4].trim(),
|
|
117
|
+
status: m[5].trim().toLowerCase(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return steps;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function extractPosition(stateContent) {
|
|
124
|
+
const phase =
|
|
125
|
+
stateContent.match(/\*\*Phase\*\*:\s*(.+)/)?.[1]?.trim() || "Unknown";
|
|
126
|
+
const status =
|
|
127
|
+
stateContent.match(/\*\*Status\*\*:\s*(.+)/)?.[1]?.trim() || "Unknown";
|
|
128
|
+
return { phase, status };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractTableRows(content, sectionName) {
|
|
132
|
+
const rows = [];
|
|
133
|
+
const sectionIdx = content.indexOf(sectionName);
|
|
134
|
+
if (sectionIdx === -1) return rows;
|
|
135
|
+
|
|
136
|
+
const afterSection = content.slice(sectionIdx);
|
|
137
|
+
const nextHeading = afterSection.indexOf("\n## ", 1);
|
|
138
|
+
const block =
|
|
139
|
+
nextHeading > 0 ? afterSection.slice(0, nextHeading) : afterSection;
|
|
140
|
+
|
|
141
|
+
for (const line of block.split("\n")) {
|
|
142
|
+
if (
|
|
143
|
+
line.startsWith("|") &&
|
|
144
|
+
!line.startsWith("| Query") &&
|
|
145
|
+
!line.startsWith("| When") &&
|
|
146
|
+
!line.startsWith("|--") &&
|
|
147
|
+
!line.startsWith("|-")
|
|
148
|
+
) {
|
|
149
|
+
const cols = line
|
|
150
|
+
.split("|")
|
|
151
|
+
.map((c) => c.trim())
|
|
152
|
+
.filter((c) => c);
|
|
153
|
+
if (cols.length >= 2) {
|
|
154
|
+
rows.push(`${cols[0]} → ${cols[1]}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return rows;
|
|
159
|
+
}
|
package/lib/cli/init.js
CHANGED
|
@@ -15,6 +15,7 @@ export function register(program) {
|
|
|
15
15
|
.option("--tier <tier>", "Template tier (starter/full)")
|
|
16
16
|
.option("--runtime <runtime>", "Agent runtime (claude/cursor/all)")
|
|
17
17
|
.option("--profile <profile>", "Integration profile (lite/standard/full)")
|
|
18
|
+
.option("--onboarding <mode>", "Onboarding mode (greenfield/brownfield/auto)")
|
|
18
19
|
.option("--registry <path>", "Path to feature-registry.yaml")
|
|
19
20
|
.option("--json", "Output as JSON")
|
|
20
21
|
.option(
|
|
@@ -89,9 +90,9 @@ export function register(program) {
|
|
|
89
90
|
name: "runtime",
|
|
90
91
|
message: "Agent runtime:",
|
|
91
92
|
choices: [
|
|
92
|
-
{ name: "Claude Code
|
|
93
|
-
{ name: "Cursor
|
|
94
|
-
{ name: "Other
|
|
93
|
+
{ name: "Claude Code — creates CLAUDE.md", value: "claude" },
|
|
94
|
+
{ name: "Cursor / Composer — creates .cursorrules", value: "cursor" },
|
|
95
|
+
{ name: "Other (Gemini, Codex, Cline, Aider, etc.) — creates AGENT.md + all entry points", value: "all" },
|
|
95
96
|
],
|
|
96
97
|
},
|
|
97
98
|
]);
|
|
@@ -120,20 +121,37 @@ export function register(program) {
|
|
|
120
121
|
name: "profile",
|
|
121
122
|
message: "Integration profile:",
|
|
122
123
|
choices: [
|
|
123
|
-
{ name: "Standard (Sonnet)
|
|
124
|
-
{ name: "Lite (Haiku)
|
|
125
|
-
{ name: "Full (Opus)
|
|
124
|
+
{ name: "Standard (Sonnet, GPT-4, Gemini Pro, Codex) — recommended", value: "standard" },
|
|
125
|
+
{ name: "Lite (Haiku, GPT-3.5, Gemini Flash) — minimal rules, simple projects", value: "lite" },
|
|
126
|
+
{ name: "Full (Opus, o1, Gemini Ultra) — all rules inline, complex projects", value: "full" },
|
|
126
127
|
],
|
|
127
128
|
},
|
|
128
129
|
]);
|
|
129
130
|
answers.profile = profile;
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
if (!opts.onboarding) {
|
|
134
|
+
const { onboarding } = await inquirer.prompt([
|
|
135
|
+
{
|
|
136
|
+
type: "list",
|
|
137
|
+
name: "onboarding",
|
|
138
|
+
message: "Project scenario:",
|
|
139
|
+
choices: [
|
|
140
|
+
{ name: "Greenfield — new project, no existing code", value: "greenfield" },
|
|
141
|
+
{ name: "Brownfield — existing codebase, add methodology", value: "brownfield" },
|
|
142
|
+
{ name: "Agent decides — let the agent detect on first session", value: "auto" },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
]);
|
|
146
|
+
answers.onboarding = onboarding;
|
|
147
|
+
}
|
|
148
|
+
|
|
132
149
|
projectTypeArg = projectTypeArg || answers.type;
|
|
133
150
|
directory = directory || answers.dir;
|
|
134
151
|
opts.runtime = opts.runtime || answers.runtime;
|
|
135
152
|
opts.tier = opts.tier || answers.tier;
|
|
136
153
|
opts.profile = opts.profile || answers.profile;
|
|
154
|
+
opts.onboarding = opts.onboarding || answers.onboarding;
|
|
137
155
|
}
|
|
138
156
|
|
|
139
157
|
const projectType = resolveProjectType(projectTypeArg);
|
|
@@ -144,6 +162,7 @@ export function register(program) {
|
|
|
144
162
|
tier: opts.tier || "starter",
|
|
145
163
|
runtime: opts.runtime || "all",
|
|
146
164
|
profile: opts.profile || "standard",
|
|
165
|
+
onboarding: opts.onboarding || "auto",
|
|
147
166
|
registryPath: opts.registry,
|
|
148
167
|
});
|
|
149
168
|
});
|
package/lib/cli/plan.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** wwa plan — display current plan status. */
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { resolve, join } from "node:path";
|
|
5
|
+
import { findEntryPoint } from "./helpers.js";
|
|
6
|
+
|
|
7
|
+
export function register(program) {
|
|
8
|
+
program
|
|
9
|
+
.command("plan [directory]")
|
|
10
|
+
.description("Display current plan status from PLAN.md")
|
|
11
|
+
.option("--json", "Output as JSON")
|
|
12
|
+
.action(async (directory, opts) => {
|
|
13
|
+
directory = directory || ".";
|
|
14
|
+
const d = resolve(directory);
|
|
15
|
+
|
|
16
|
+
const planPath = join(d, "PLAN.md");
|
|
17
|
+
if (!existsSync(planPath)) {
|
|
18
|
+
console.error(
|
|
19
|
+
"No PLAN.md found in current directory.\n" +
|
|
20
|
+
"Run 'wwa init' to set up methodology files, or specify a directory."
|
|
21
|
+
);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const content = readFileSync(planPath, "utf-8");
|
|
26
|
+
const parsed = parsePlan(content);
|
|
27
|
+
|
|
28
|
+
// Also read STATE.md for current position
|
|
29
|
+
const statePath = join(d, "STATE.md");
|
|
30
|
+
let position = null;
|
|
31
|
+
if (existsSync(statePath)) {
|
|
32
|
+
const stateContent = readFileSync(statePath, "utf-8");
|
|
33
|
+
position = extractPosition(stateContent);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (opts.json) {
|
|
37
|
+
console.log(JSON.stringify({ position, plan: parsed }, null, 2));
|
|
38
|
+
} else {
|
|
39
|
+
if (position) {
|
|
40
|
+
console.log(`\n Current position:`);
|
|
41
|
+
console.log(` Phase: ${position.phase}`);
|
|
42
|
+
console.log(` Status: ${position.status}`);
|
|
43
|
+
if (position.next) console.log(` Next: ${position.next}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`\n Plan: ${parsed.title}`);
|
|
47
|
+
|
|
48
|
+
if (parsed.objective) {
|
|
49
|
+
console.log(`\n Objective: ${parsed.objective}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (parsed.steps.length > 0) {
|
|
53
|
+
const done = parsed.steps.filter((s) => s.status === "done").length;
|
|
54
|
+
const total = parsed.steps.length;
|
|
55
|
+
console.log(`\n Progress: ${done}/${total} steps complete`);
|
|
56
|
+
console.log("");
|
|
57
|
+
|
|
58
|
+
for (const step of parsed.steps) {
|
|
59
|
+
const icon = step.status === "done" ? "[x]" : "[ ]";
|
|
60
|
+
console.log(` ${icon} Step ${step.num}: ${step.deliverable}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (parsed.verification.length > 0) {
|
|
65
|
+
const passed = parsed.verification.filter((v) => v.checked).length;
|
|
66
|
+
console.log(
|
|
67
|
+
`\n Verification: ${passed}/${parsed.verification.length} criteria met`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log("");
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parsePlan(content) {
|
|
77
|
+
const lines = content.split("\n");
|
|
78
|
+
|
|
79
|
+
// Extract title from first heading
|
|
80
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
81
|
+
const title = titleMatch ? titleMatch[1].trim() : "Unknown";
|
|
82
|
+
|
|
83
|
+
// Extract phase heading
|
|
84
|
+
const phaseMatch = content.match(/^##\s+(.+)$/m);
|
|
85
|
+
const phase = phaseMatch ? phaseMatch[1].trim() : title;
|
|
86
|
+
|
|
87
|
+
// Extract objective paragraph
|
|
88
|
+
let objective = null;
|
|
89
|
+
const objMatch = content.match(/###\s+Objective\s*\n\s*\n(.+?)(?:\n\n|\n###)/s);
|
|
90
|
+
if (objMatch) {
|
|
91
|
+
objective = objMatch[1].trim().split("\n")[0];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Parse implementation table
|
|
95
|
+
const steps = [];
|
|
96
|
+
const stepPattern =
|
|
97
|
+
/\|\s*(\d+)\s*\|\s*(.+?)\s*\|\s*.+?\s*\|\s*.+?\s*\|\s*(\w+)\s*\|/g;
|
|
98
|
+
let m;
|
|
99
|
+
while ((m = stepPattern.exec(content)) !== null) {
|
|
100
|
+
steps.push({
|
|
101
|
+
num: parseInt(m[1], 10),
|
|
102
|
+
deliverable: m[2].trim(),
|
|
103
|
+
status: m[3].trim().toLowerCase(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Parse verification criteria
|
|
108
|
+
const verification = [];
|
|
109
|
+
const verPattern = /- \[([ x])\]\s*(.+)/g;
|
|
110
|
+
while ((m = verPattern.exec(content)) !== null) {
|
|
111
|
+
verification.push({
|
|
112
|
+
checked: m[1] === "x",
|
|
113
|
+
criterion: m[2].trim(),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { title, phase, objective, steps, verification };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function extractPosition(stateContent) {
|
|
121
|
+
const phase =
|
|
122
|
+
stateContent.match(/\*\*Phase\*\*:\s*(.+)/)?.[1]?.trim() || "Unknown";
|
|
123
|
+
const status =
|
|
124
|
+
stateContent.match(/\*\*Status\*\*:\s*(.+)/)?.[1]?.trim() || "Unknown";
|
|
125
|
+
const next =
|
|
126
|
+
stateContent.match(/\*\*Next\*\*:\s*(.+)/)?.[1]?.trim() || null;
|
|
127
|
+
return { phase, status, next };
|
|
128
|
+
}
|