agent-method 1.5.13 → 1.5.15

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.
@@ -30,7 +30,7 @@ tokens:
30
30
  watcher_start: "npx agent-method watch"
31
31
  project_name: "wwa"
32
32
  repo_url: "https://github.com/anthropics/wwa.git"
33
- npm_version: "1.5.1"
33
+ npm_version: "1.5.14"
34
34
  registry_version: "v1.6"
35
35
  feature_count: 36
36
36
  domain_count: 8
package/lib/cli/add.js CHANGED
@@ -24,25 +24,172 @@ function readStdin() {
24
24
  });
25
25
  }
26
26
 
27
+ /**
28
+ * Core implementation for `wwa add` and MCP write-safe tools.
29
+ * Returns a structured result instead of printing or exiting.
30
+ */
31
+ export async function runAdd(type, content, options = {}) {
32
+ const dir = resolve(options.directory || ".");
33
+ const dryRun = options.dryRun || false;
34
+
35
+ if (!ADD_TYPES.includes(type)) {
36
+ const error = new Error(
37
+ `Unknown type: ${type}. Use one of: ${ADD_TYPES.join(", ")}`
38
+ );
39
+ error.code = "INVALID_TYPE";
40
+ throw error;
41
+ }
42
+
43
+ if (!content || !content.trim()) {
44
+ const error = new Error("Content is required for add operation.");
45
+ error.code = "NO_CONTENT";
46
+ throw error;
47
+ }
48
+
49
+ let targetPath;
50
+ let toAppend;
51
+
52
+ switch (type) {
53
+ case "backlog": {
54
+ targetPath = join(dir, "todos", "backlog.md");
55
+ if (!existsSync(join(dir, "todos"))) {
56
+ targetPath = join(dir, "backlog.md");
57
+ }
58
+ if (!existsSync(targetPath)) {
59
+ targetPath = join(dir, "todos", "backlog.md");
60
+ }
61
+ const title = content.includes(" — ") ? content.split(" — ")[0].trim() : content;
62
+ const rest = content.includes(" — ")
63
+ ? content.split(" — ").slice(1).join(" — ").trim()
64
+ : "";
65
+ toAppend = `\n- [ ] **${title}**${rest ? ` — ${rest}` : ""}\n`;
66
+ break;
67
+ }
68
+ case "decision": {
69
+ targetPath = join(dir, "STATE.md");
70
+ const parts = content.includes(" | ")
71
+ ? content.split(" | ").map((s) => s.trim())
72
+ : [content, ""];
73
+ const decision = parts[0];
74
+ const rationale = parts[1] || "";
75
+ toAppend = `\n| ${today()} | ${decision} | ${rationale} |\n`;
76
+ break;
77
+ }
78
+ case "finding": {
79
+ const reviewDir = join(dir, "docs", "internal", "review");
80
+ const findingsPath = join(reviewDir, "findings.md");
81
+ const fallbackPath = join(reviewDir, "installation-improvements.md");
82
+ if (existsSync(findingsPath)) {
83
+ targetPath = findingsPath;
84
+ } else if (existsSync(fallbackPath)) {
85
+ targetPath = fallbackPath;
86
+ } else {
87
+ targetPath = findingsPath;
88
+ if (!existsSync(join(dir, "docs", "internal", "review"))) {
89
+ const alt = join(dir, "agentWorkflows", "observations.md");
90
+ targetPath = existsSync(join(dir, "agentWorkflows")) ? alt : findingsPath;
91
+ }
92
+ }
93
+ toAppend = `\n### ${today()}\n\n- ${content}\n\n`;
94
+ break;
95
+ }
96
+ case "session": {
97
+ const sessionLog = findSessionLog(dir) || join(dir, "SESSION-LOG.md");
98
+ targetPath = sessionLog;
99
+ toAppend = content.endsWith("\n") ? content : content + "\n";
100
+ break;
101
+ }
102
+ case "summary": {
103
+ targetPath = join(dir, "SUMMARY.md");
104
+ toAppend = content.endsWith("\n") ? content : content + "\n";
105
+ break;
106
+ }
107
+ default: {
108
+ const error = new Error(`Unsupported add type: ${type}`);
109
+ error.code = "INVALID_TYPE";
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ if (!isSafeToWrite(targetPath)) {
115
+ const error = new Error(
116
+ `Safety: refusing to write to ${targetPath} (only .md, .yaml, .yml allowed).`
117
+ );
118
+ error.code = "UNSAFE_TARGET";
119
+ throw error;
120
+ }
121
+
122
+ if (dryRun) {
123
+ return {
124
+ directory: dir,
125
+ type,
126
+ target: targetPath,
127
+ content: toAppend,
128
+ dryRun: true,
129
+ wrote: false,
130
+ };
131
+ }
132
+
133
+ const dirToEnsure = resolve(targetPath, "..");
134
+ if (!existsSync(dirToEnsure)) {
135
+ const { mkdirSync } = await import("node:fs");
136
+ mkdirSync(dirToEnsure, { recursive: true });
137
+ }
138
+
139
+ if (!existsSync(targetPath)) {
140
+ if (type !== "finding") {
141
+ const error = new Error(
142
+ `File not found: ${targetPath}. Create it first or run from project root.`
143
+ );
144
+ error.code = "MISSING_TARGET";
145
+ throw error;
146
+ }
147
+ // For findings, create a new file with header.
148
+ safeWriteFile(
149
+ targetPath,
150
+ `# Findings\n\n<!-- Appended by wwa add finding -->\n${toAppend}`
151
+ );
152
+ remindCascade(dir);
153
+ return {
154
+ directory: dir,
155
+ type,
156
+ target: targetPath,
157
+ content: toAppend,
158
+ dryRun: false,
159
+ wrote: true,
160
+ createdFile: true,
161
+ };
162
+ }
163
+
164
+ const existing = readFileSync(targetPath, "utf-8");
165
+ safeWriteFile(targetPath, existing + toAppend);
166
+ remindCascade(dir);
167
+ return {
168
+ directory: dir,
169
+ type,
170
+ target: targetPath,
171
+ content: toAppend,
172
+ dryRun: false,
173
+ wrote: true,
174
+ createdFile: false,
175
+ };
176
+ }
177
+
27
178
  export function register(program) {
28
179
  program
29
180
  .command("add <type> [content...]")
30
- .description("Append content to methodology files (backlog, decision, finding, session, summary)")
181
+ .description(
182
+ "Append content to methodology files (backlog, decision, finding, session, summary)"
183
+ )
31
184
  .option("-d, --directory <path>", "Project directory", ".")
32
185
  .option("--file <path>", "Read content from file (for session/summary)")
33
186
  .option("--dry-run", "Show what would be written without writing")
34
187
  .option("--json", "Output target path and content as JSON")
35
188
  .action(async (type, contentParts, opts) => {
36
189
  const dir = resolve(opts.directory);
37
- const dryRun = opts.dryRun || false;
38
190
  const asJson = opts.json || false;
39
-
40
- if (!ADD_TYPES.includes(type)) {
41
- console.error(`Unknown type: ${type}. Use one of: ${ADD_TYPES.join(", ")}`);
42
- process.exit(1);
43
- }
44
-
45
191
  let content;
192
+
46
193
  if (opts.file) {
47
194
  const fpath = resolve(opts.file);
48
195
  if (!existsSync(fpath)) {
@@ -63,101 +210,28 @@ export function register(program) {
63
210
  process.exit(1);
64
211
  }
65
212
 
66
- let targetPath;
67
- let toAppend;
68
-
69
- switch (type) {
70
- case "backlog": {
71
- targetPath = join(dir, "todos", "backlog.md");
72
- if (!existsSync(join(dir, "todos"))) {
73
- targetPath = join(dir, "backlog.md");
74
- }
75
- if (!existsSync(targetPath)) {
76
- targetPath = join(dir, "todos", "backlog.md");
77
- }
78
- const title = content.includes(" — ") ? content.split(" — ")[0].trim() : content;
79
- const rest = content.includes(" — ") ? content.split(" — ").slice(1).join(" — ").trim() : "";
80
- toAppend = `\n- [ ] **${title}**${rest ? ` — ${rest}` : ""}\n`;
81
- break;
82
- }
83
- case "decision": {
84
- targetPath = join(dir, "STATE.md");
85
- const parts = content.includes(" | ") ? content.split(" | ").map((s) => s.trim()) : [content, ""];
86
- const decision = parts[0];
87
- const rationale = parts[1] || "";
88
- toAppend = `\n| ${today()} | ${decision} | ${rationale} |\n`;
89
- break;
90
- }
91
- case "finding": {
92
- const reviewDir = join(dir, "docs", "internal", "review");
93
- const findingsPath = join(reviewDir, "findings.md");
94
- const fallbackPath = join(reviewDir, "installation-improvements.md");
95
- if (existsSync(findingsPath)) {
96
- targetPath = findingsPath;
97
- } else if (existsSync(fallbackPath)) {
98
- targetPath = fallbackPath;
99
- } else {
100
- targetPath = findingsPath;
101
- if (!existsSync(join(dir, "docs", "internal", "review"))) {
102
- const alt = join(dir, "agentWorkflows", "observations.md");
103
- targetPath = existsSync(join(dir, "agentWorkflows")) ? alt : findingsPath;
104
- }
105
- }
106
- toAppend = `\n### ${today()}\n\n- ${content}\n\n`;
107
- break;
108
- }
109
- case "session": {
110
- const sessionLog = findSessionLog(dir) || join(dir, "SESSION-LOG.md");
111
- targetPath = sessionLog;
112
- toAppend = content.endsWith("\n") ? content : content + "\n";
113
- break;
114
- }
115
- case "summary": {
116
- targetPath = join(dir, "SUMMARY.md");
117
- toAppend = content.endsWith("\n") ? content : content + "\n";
118
- break;
119
- }
120
- default:
121
- process.exit(1);
122
- }
123
-
124
- if (!isSafeToWrite(targetPath)) {
125
- console.error(`Safety: refusing to write to ${targetPath} (only .md, .yaml, .yml allowed).`);
126
- process.exit(1);
127
- }
213
+ try {
214
+ const result = await runAdd(type, content, {
215
+ directory: dir,
216
+ dryRun: opts.dryRun || false,
217
+ });
128
218
 
129
- if (dryRun || asJson) {
130
219
  if (asJson) {
131
- console.log(JSON.stringify({ target: targetPath, content: toAppend, dryRun }, null, 2));
132
- } else {
133
- console.log("Target:", targetPath);
134
- console.log("Content to append:");
135
- console.log(toAppend);
220
+ console.log(JSON.stringify(result, null, 2));
221
+ return;
136
222
  }
137
- return;
138
- }
139
223
 
140
- const dirToEnsure = resolve(targetPath, "..");
141
- if (!existsSync(dirToEnsure)) {
142
- const { mkdirSync } = await import("node:fs");
143
- mkdirSync(dirToEnsure, { recursive: true });
144
- }
145
-
146
- if (!existsSync(targetPath)) {
147
- if (type === "finding") {
148
- safeWriteFile(targetPath, `# Findings\n\n<!-- Appended by wwa add finding -->\n${toAppend}`);
149
- console.log("Appended to", targetPath);
150
- remindCascade(dir);
151
- return;
224
+ if (result.dryRun) {
225
+ console.log("Target:", result.target);
226
+ console.log("Content to append:");
227
+ console.log(result.content);
228
+ } else {
229
+ console.log("Appended to", result.target);
152
230
  }
153
- console.error(`File not found: ${targetPath}. Create it first or run from project root.`);
231
+ } catch (e) {
232
+ console.error(e.message);
154
233
  process.exit(1);
155
234
  }
156
-
157
- const existing = readFileSync(targetPath, "utf-8");
158
- safeWriteFile(targetPath, existing + toAppend);
159
- console.log("Appended to", targetPath);
160
- remindCascade(dir);
161
235
  });
162
236
  }
163
237
 
@@ -15,6 +15,133 @@ import { resolveTokensPath, resolveDocsMapPath } from "../boundaries.js";
15
15
  const __filename = fileURLToPath(import.meta.url);
16
16
  const __dirname = dirname(__filename);
17
17
 
18
+ export async function runCasestudy(directory, opts = {}) {
19
+ const dir = resolve(directory || ".");
20
+ const options = {
21
+ output: opts.output || null,
22
+ name: opts.name || null,
23
+ json: !!opts.json,
24
+ yamlOnly: !!opts.yamlOnly,
25
+ internalRegistry: !!opts.internalRegistry,
26
+ };
27
+
28
+ // Gather all project data
29
+ const data = await gatherProjectData(dir, options.name);
30
+
31
+ // Load internal registry if requested
32
+ if (options.internalRegistry) {
33
+ const { loadInternalRegistry } = await import("./doc-review.js");
34
+ const registry = await loadInternalRegistry(dir);
35
+ if (registry) {
36
+ data.internalRegistry = registry;
37
+ } else {
38
+ data.warnings.push(
39
+ "No docs/internal/doc-registry.yaml found — run `wwa doc-review` to generate it"
40
+ );
41
+ }
42
+ }
43
+
44
+ if (data.errors.length > 0 && data.sessions.length === 0) {
45
+ const error = new Error(
46
+ "Cannot generate case study: " + data.errors.map((e) => `- ${e}`).join(" ")
47
+ );
48
+ error.details = { errors: data.errors };
49
+ throw error;
50
+ }
51
+
52
+ // JSON-only path (no writes unless output is specified)
53
+ if (options.json) {
54
+ const resultJson = JSON.stringify(data, null, 2);
55
+ if (options.output) {
56
+ safeWriteFile(options.output, resultJson);
57
+ return {
58
+ mode: "json_to_file",
59
+ output: options.output,
60
+ sessions: data.sessions.length,
61
+ warnings: data.warnings,
62
+ };
63
+ }
64
+ return {
65
+ mode: "json_inline",
66
+ data,
67
+ warnings: data.warnings,
68
+ };
69
+ }
70
+
71
+ const created = {
72
+ mode: options.yamlOnly ? "yaml_only" : "markdown_plus_yaml",
73
+ markdownPath: null,
74
+ yamlPath: null,
75
+ sessions: data.sessions.length,
76
+ warnings: data.warnings,
77
+ };
78
+
79
+ // YAML-only mode: write a single .yaml artifact with all structured data.
80
+ if (options.yamlOnly) {
81
+ const baseDir = join(dir, "case-studies");
82
+ const baseSlug = slugify(data.projectName);
83
+ let yamlOutputPath = options.output || join(baseDir, `${baseSlug}.yaml`);
84
+
85
+ // Avoid overwriting existing case studies when using the default path.
86
+ if (!options.output) {
87
+ let counter = 1;
88
+ while (existsSync(yamlOutputPath)) {
89
+ yamlOutputPath = join(baseDir, `${baseSlug}-${counter}.yaml`);
90
+ counter += 1;
91
+ }
92
+ }
93
+
94
+ const yamlDir = resolve(yamlOutputPath, "..");
95
+ if (!existsSync(yamlDir)) {
96
+ mkdirSync(yamlDir, { recursive: true });
97
+ }
98
+
99
+ const ok = await writeCaseStudyYaml(yamlOutputPath, data);
100
+ if (!ok) {
101
+ const error = new Error("Failed to write YAML case study.");
102
+ error.details = { target: yamlOutputPath };
103
+ throw error;
104
+ }
105
+
106
+ created.yamlPath = yamlOutputPath;
107
+ return created;
108
+ }
109
+
110
+ // Default: write markdown case study plus companion YAML.
111
+ const baseDir = join(dir, "case-studies");
112
+ const baseSlug = slugify(data.projectName);
113
+ let markdownPath = options.output || join(baseDir, `${baseSlug}.md`);
114
+
115
+ // Avoid overwriting existing case studies when using the default path.
116
+ if (!options.output) {
117
+ let counter = 1;
118
+ while (existsSync(markdownPath)) {
119
+ markdownPath = join(baseDir, `${baseSlug}-${counter}.md`);
120
+ counter += 1;
121
+ }
122
+ }
123
+
124
+ const outputDir = resolve(markdownPath, "..");
125
+ if (!existsSync(outputDir)) {
126
+ mkdirSync(outputDir, { recursive: true });
127
+ }
128
+
129
+ const caseStudy = generateCaseStudy(data);
130
+ safeWriteFile(markdownPath, caseStudy);
131
+
132
+ const yamlPath = markdownPath.replace(/\.md$/, ".yaml");
133
+ const yamlResult = await writeCaseStudyYaml(yamlPath, data);
134
+ if (!yamlResult) {
135
+ const error = new Error("Failed to write companion YAML case study.");
136
+ error.details = { markdownPath, yamlPath };
137
+ throw error;
138
+ }
139
+
140
+ created.markdownPath = markdownPath;
141
+ created.yamlPath = yamlPath;
142
+ return created;
143
+ }
144
+
18
145
  export function register(program) {
19
146
  program
20
147
  .command("casestudy [directory]")
@@ -30,108 +157,41 @@ export function register(program) {
30
157
  )
31
158
  .option("--internal-registry", "Include docs/internal/doc-registry.yaml data in output")
32
159
  .action(async (directory, opts) => {
33
- directory = directory || ".";
34
- const d = resolve(directory);
35
-
36
- // Gather all project data
37
- const data = await gatherProjectData(d, opts.name);
38
-
39
- // Load internal registry if requested
40
- if (opts.internalRegistry) {
41
- const { loadInternalRegistry } = await import("./doc-review.js");
42
- const registry = await loadInternalRegistry(d);
43
- if (registry) {
44
- data.internalRegistry = registry;
45
- } else {
46
- data.warnings.push(
47
- "No docs/internal/doc-registry.yaml found — run `wwa doc-review` to generate it"
48
- );
49
- }
50
- }
51
-
52
- if (data.errors.length > 0 && data.sessions.length === 0) {
53
- console.error("Cannot generate case study:");
54
- for (const e of data.errors) console.error(` - ${e}`);
55
- process.exit(1);
56
- }
57
-
58
- if (opts.json) {
59
- const output = opts.output;
60
- const result = JSON.stringify(data, null, 2);
61
- if (output) {
62
- safeWriteFile(output, result);
63
- console.log(`Case study data written to ${output}`);
64
- } else {
65
- console.log(result);
66
- }
67
- return;
68
- }
69
-
70
- // YAML-only mode: write a single .yaml artifact with all structured data.
71
- if (opts.yamlOnly) {
72
- const baseDir = join(d, "case-studies");
73
- const baseSlug = slugify(data.projectName);
74
- let yamlOutputPath = opts.output || join(baseDir, `${baseSlug}.yaml`);
75
-
76
- // Avoid overwriting existing case studies when using the default path.
77
- if (!opts.output) {
78
- let counter = 1;
79
- while (existsSync(yamlOutputPath)) {
80
- yamlOutputPath = join(baseDir, `${baseSlug}-${counter}.yaml`);
81
- counter += 1;
160
+ try {
161
+ const result = await runCasestudy(directory, opts);
162
+
163
+ if (opts.json) {
164
+ if (result.mode === "json_inline") {
165
+ console.log(JSON.stringify(result.data, null, 2));
166
+ } else {
167
+ console.log(
168
+ `Case study data written to ${result.output} (${result.sessions} sessions)`
169
+ );
82
170
  }
171
+ return;
83
172
  }
84
173
 
85
- const yamlDir = resolve(yamlOutputPath, "..");
86
- if (!existsSync(yamlDir)) {
87
- mkdirSync(yamlDir, { recursive: true });
88
- }
89
-
90
- const ok = await writeCaseStudyYaml(yamlOutputPath, data);
91
- if (ok) {
174
+ if (result.mode === "yaml_only") {
92
175
  console.log(
93
- `Case study YAML written to ${yamlOutputPath} (${data.sessions.length} sessions)`
176
+ `Case study YAML written to ${result.yamlPath} (${result.sessions} sessions)`
94
177
  );
95
178
  } else {
96
- console.error("Failed to write YAML case study.");
97
- process.exit(1);
98
- }
99
- } else {
100
- // Default: write markdown case study plus companion YAML.
101
- const baseDir = join(d, "case-studies");
102
- const baseSlug = slugify(data.projectName);
103
- let markdownPath = opts.output || join(baseDir, `${baseSlug}.md`);
104
-
105
- // Avoid overwriting existing case studies when using the default path.
106
- if (!opts.output) {
107
- let counter = 1;
108
- while (existsSync(markdownPath)) {
109
- markdownPath = join(baseDir, `${baseSlug}-${counter}.md`);
110
- counter += 1;
111
- }
179
+ console.log(
180
+ `Case study written to ${result.markdownPath} (${result.sessions} sessions)`
181
+ );
182
+ console.log(`Case study YAML written to ${result.yamlPath}`);
112
183
  }
113
184
 
114
- const outputDir = resolve(markdownPath, "..");
115
- if (!existsSync(outputDir)) {
116
- mkdirSync(outputDir, { recursive: true });
185
+ if (result.warnings && result.warnings.length > 0) {
186
+ console.log("\nWarnings:");
187
+ for (const w of result.warnings) console.log(` - ${w}`);
117
188
  }
118
-
119
- const caseStudy = generateCaseStudy(data);
120
- safeWriteFile(markdownPath, caseStudy);
121
- console.log(
122
- `Case study written to ${markdownPath} (${data.sessions.length} sessions)`
123
- );
124
-
125
- const yamlPath = markdownPath.replace(/\.md$/, ".yaml");
126
- const yamlResult = await writeCaseStudyYaml(yamlPath, data);
127
- if (yamlResult) {
128
- console.log(`Case study YAML written to ${yamlPath}`);
189
+ } catch (e) {
190
+ console.error(e.message || "Failed to generate case study.");
191
+ if (e.details?.errors) {
192
+ for (const err of e.details.errors) console.error(` - ${err}`);
129
193
  }
130
- }
131
-
132
- if (data.warnings.length > 0) {
133
- console.log("\nWarnings:");
134
- for (const w of data.warnings) console.log(` - ${w}`);
194
+ process.exit(1);
135
195
  }
136
196
  });
137
197
  }