forge-workflow 0.0.9 → 0.0.10

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 (102) hide show
  1. package/.claude/commands/dev.md +2 -2
  2. package/.claude/commands/plan.md +2 -2
  3. package/.claude/commands/ship.md +2 -2
  4. package/.claude/commands/status.md +4 -4
  5. package/.cline/workflows/dev.md +2 -2
  6. package/.cline/workflows/plan.md +2 -2
  7. package/.cline/workflows/ship.md +2 -2
  8. package/.cline/workflows/status.md +4 -4
  9. package/.codex/skills/dev/SKILL.md +2 -2
  10. package/.codex/skills/plan/SKILL.md +2 -2
  11. package/.codex/skills/ship/SKILL.md +2 -2
  12. package/.codex/skills/status/SKILL.md +4 -4
  13. package/.cursor/commands/dev.md +2 -2
  14. package/.cursor/commands/plan.md +2 -2
  15. package/.cursor/commands/ship.md +2 -2
  16. package/.cursor/commands/status.md +4 -4
  17. package/.github/prompts/dev.prompt.md +2 -2
  18. package/.github/prompts/plan.prompt.md +2 -2
  19. package/.github/prompts/ship.prompt.md +2 -2
  20. package/.github/prompts/status.prompt.md +4 -4
  21. package/.github/workflows/beads-to-github.yml +43 -10
  22. package/.github/workflows/github-to-beads.yml +10 -7
  23. package/.kilocode/workflows/dev.md +2 -2
  24. package/.kilocode/workflows/plan.md +2 -2
  25. package/.kilocode/workflows/ship.md +2 -2
  26. package/.kilocode/workflows/status.md +4 -4
  27. package/.opencode/commands/dev.md +2 -2
  28. package/.opencode/commands/plan.md +2 -2
  29. package/.opencode/commands/ship.md +2 -2
  30. package/.opencode/commands/status.md +4 -4
  31. package/.roo/commands/dev.md +2 -2
  32. package/.roo/commands/plan.md +2 -2
  33. package/.roo/commands/ship.md +2 -2
  34. package/.roo/commands/status.md +4 -4
  35. package/AGENTS.md +1 -0
  36. package/CLAUDE.md +12 -0
  37. package/bin/forge.js +10 -5
  38. package/docs/BEADS_GITHUB_SYNC.md +26 -0
  39. package/docs/TOOLCHAIN.md +130 -148
  40. package/lib/beads-bootstrap.js +225 -0
  41. package/lib/beads-health-check.js +55 -10
  42. package/lib/beads-setup.js +104 -28
  43. package/lib/beads-sync-scaffold.js +11 -6
  44. package/lib/commands/_issue.js +11 -1
  45. package/lib/commands/issues.js +49 -0
  46. package/lib/commands/recommend.js +22 -1
  47. package/lib/commands/setup.js +16 -10
  48. package/lib/commands/status.js +181 -0
  49. package/lib/commands/team.js +11 -1
  50. package/lib/commands/test.js +37 -2
  51. package/lib/commands/validate.js +14 -8
  52. package/lib/commands/worktree.js +27 -54
  53. package/lib/dep-guard/keyword-ripple.js +184 -0
  54. package/lib/detect-worktree.js +9 -10
  55. package/lib/forge-issues.js +326 -0
  56. package/lib/issue-sync/authority.js +100 -0
  57. package/lib/issue-sync/github-pull.js +184 -0
  58. package/lib/issue-sync/import-primitives.js +98 -0
  59. package/lib/issue-sync/legacy-link-bridge.js +436 -0
  60. package/lib/issue-sync/link-store.js +292 -0
  61. package/lib/issue-sync/project-github.js +123 -0
  62. package/lib/issue-sync/reconcile.js +195 -0
  63. package/lib/issue-sync/schema.js +126 -0
  64. package/lib/lefthook-check.js +5 -2
  65. package/lib/project-memory.js +564 -0
  66. package/lib/runtime-health.js +100 -12
  67. package/lib/smart-status/conflicts.js +205 -0
  68. package/lib/smart-status/scoring.js +177 -0
  69. package/lib/status/beads-snapshot.js +102 -0
  70. package/lib/status/presenter.js +65 -0
  71. package/lib/workflow/enforce-stage.js +3 -1
  72. package/lib/workflow/state-manager.js +164 -8
  73. package/package.json +12 -4
  74. package/scripts/beads-context.sh +124 -5
  75. package/scripts/beads-context.test.js +21 -4
  76. package/scripts/beads-migrate-to-dolt.sh +7 -0
  77. package/scripts/beads-upgrade-smoke.sh +263 -0
  78. package/scripts/behavioral-judge.sh +115 -11
  79. package/scripts/benchmark.js +349 -63
  80. package/scripts/dep-guard-analyze.js +52 -17
  81. package/scripts/dep-guard-keyword-ripple.js +29 -0
  82. package/scripts/dep-guard-render-review.js +86 -0
  83. package/scripts/dep-guard.sh +45 -232
  84. package/scripts/forge-team/lib/sync-github.sh +160 -28
  85. package/scripts/forge-team/tests/sync-github.test.sh +195 -58
  86. package/scripts/github-beads-sync/index.mjs +122 -98
  87. package/scripts/github-beads-sync/mapping.mjs +54 -0
  88. package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
  89. package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
  90. package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
  91. package/scripts/preflight.sh +181 -0
  92. package/scripts/smart-status-score.js +31 -0
  93. package/scripts/smart-status-sessions.js +51 -0
  94. package/scripts/smart-status.sh +74 -329
  95. package/scripts/sync-agentic-workflow.js +48 -0
  96. package/scripts/test-ci-shard.js +244 -0
  97. package/scripts/test-dashboard.js +188 -52
  98. package/scripts/test-full-suite.js +186 -0
  99. package/scripts/test-profile.js +278 -0
  100. package/scripts/test.js +219 -28
  101. package/scripts/validate.js +143 -0
  102. package/scripts/validate.sh +18 -1
@@ -1,85 +1,371 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * Performance benchmark script for Forge CLI.
5
- * Measures CLI startup time, autoDetect, and detectFramework performance.
6
- *
7
- * Usage:
8
- * node scripts/benchmark.js # Human-readable output
9
- * node scripts/benchmark.js --json # JSON output (for CI/tests)
10
- */
11
-
12
- const { execFileSync } = require('node:child_process');
13
- const { performance } = require('node:perf_hooks');
14
- const path = require('node:path');
3
+ const { spawnSync: defaultSpawnSync } = require('node:child_process');
15
4
  const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { performance } = require('node:perf_hooks');
7
+
8
+ const { buildProfile, parseJUnitFiles, walk } = require('./test-profile');
16
9
 
17
10
  const rootDir = path.join(__dirname, '..');
18
- const ITERATIONS = 3;
11
+ const DEFAULT_OUTPUT = path.join(rootDir, 'test-results', 'benchmark-results.json');
12
+ const DEFAULT_PROFILE_DIR = path.join(rootDir, 'test-results', 'benchmark-profiles');
13
+ const DEFAULT_SAMPLES = 3;
14
+ const DEFAULT_JUNIT_PATH = path.join(rootDir, 'test-results', 'test-results.xml');
19
15
 
20
- function benchmarkCLIStartup() {
21
- const times = [];
22
- for (let i = 0; i < ITERATIONS; i++) {
23
- const start = performance.now();
24
- execFileSync('node', [path.join(rootDir, 'bin', 'forge.js'), '--help'], {
25
- cwd: rootDir,
26
- encoding: 'utf-8',
27
- timeout: 10000,
28
- stdio: 'pipe'
29
- });
30
- times.push(performance.now() - start);
16
+ const BENCHMARK_GROUPS = [
17
+ {
18
+ id: 'full-suite',
19
+ label: 'Whole suite',
20
+ command: ['bun', 'test'],
21
+ },
22
+ {
23
+ id: 'pre-push-runner',
24
+ label: 'Pre-push runner slice',
25
+ command: ['bun', 'test', 'test/scripts/test-runner.test.js'],
26
+ },
27
+ {
28
+ id: 'validation-core',
29
+ label: 'Validation core slice',
30
+ command: [
31
+ 'bun',
32
+ 'test',
33
+ 'test/scripts/test-profile.test.js',
34
+ 'test/scripts/test-ci-shard.test.js',
35
+ 'test/scripts/test-runner.test.js',
36
+ ],
37
+ },
38
+ {
39
+ id: 'hotspot-shell',
40
+ label: 'Hotspot shell slice',
41
+ command: [
42
+ 'bun',
43
+ 'test',
44
+ 'test/scripts/smart-status.conflicts.files.test.js',
45
+ 'test/scripts/smart-status.conflicts.merge-tree.test.js',
46
+ 'test/scripts/dep-guard.check-ripple.basic.test.js',
47
+ ],
48
+ },
49
+ {
50
+ id: 'validate',
51
+ label: 'Validate command',
52
+ command: ['bash', 'scripts/validate.sh'],
53
+ },
54
+ ];
55
+
56
+ function parseArgs(argv) {
57
+ const args = {
58
+ groups: [],
59
+ json: false,
60
+ output: DEFAULT_OUTPUT,
61
+ profileDir: DEFAULT_PROFILE_DIR,
62
+ samples: DEFAULT_SAMPLES,
63
+ };
64
+
65
+ for (let index = 0; index < argv.length; index += 1) {
66
+ const current = argv[index];
67
+ const next = argv[index + 1];
68
+
69
+ if (current === '--json') args.json = true;
70
+ if (current === '--output') args.output = next;
71
+ if (current === '--profile-dir') args.profileDir = next;
72
+ if (current === '--samples') args.samples = Number.parseInt(next, 10) || DEFAULT_SAMPLES;
73
+ if (current === '--group') args.groups.push(next);
31
74
  }
32
- const mean = times.reduce((a, b) => a + b, 0) / times.length;
33
- return { name: 'CLI startup (--help)', mean: Math.round(mean), unit: 'ms', samples: ITERATIONS };
75
+
76
+ return args;
34
77
  }
35
78
 
36
- function benchmarkAutoDetect() {
37
- const { autoDetect } = require(path.join(rootDir, 'lib', 'project-discovery.js'));
38
- const times = [];
39
- for (let i = 0; i < ITERATIONS; i++) {
40
- const start = performance.now();
41
- autoDetect(rootDir);
42
- times.push(performance.now() - start);
79
+ function resolveGroups(groupIds = []) {
80
+ if (groupIds.length === 0) {
81
+ return BENCHMARK_GROUPS.map((group) => ({ ...group, command: [...group.command] }));
82
+ }
83
+
84
+ const byId = new Map(BENCHMARK_GROUPS.map((group) => [group.id, group]));
85
+ const resolved = [];
86
+ for (const id of groupIds) {
87
+ const match = byId.get(id);
88
+ if (!match) {
89
+ throw new Error(`Unknown benchmark group: ${id}`);
90
+ }
91
+ resolved.push({ ...match, command: [...match.command] });
92
+ }
93
+ return resolved;
94
+ }
95
+
96
+ function roundMs(value) {
97
+ return Math.round(value);
98
+ }
99
+
100
+ function calculateMedian(values) {
101
+ if (values.length === 0) return 0;
102
+ const sorted = [...values].sort((left, right) => left - right);
103
+ const middle = Math.floor(sorted.length / 2);
104
+ if (sorted.length % 2 === 0) {
105
+ return roundMs((sorted[middle - 1] + sorted[middle]) / 2);
106
+ }
107
+ return sorted[middle];
108
+ }
109
+
110
+ function summarizeSamples(samplesMs) {
111
+ if (samplesMs.length === 0) {
112
+ return {
113
+ maxMs: 0,
114
+ meanMs: 0,
115
+ medianMs: 0,
116
+ minMs: 0,
117
+ samplesMs: [],
118
+ };
119
+ }
120
+ const total = samplesMs.reduce((sum, value) => sum + value, 0);
121
+ return {
122
+ maxMs: Math.max(...samplesMs),
123
+ meanMs: roundMs(total / samplesMs.length),
124
+ medianMs: calculateMedian(samplesMs),
125
+ minMs: Math.min(...samplesMs),
126
+ samplesMs,
127
+ };
128
+ }
129
+
130
+ function buildJUnitCommand(command, junitPath) {
131
+ const [binary, subcommand, ...rest] = command;
132
+ if (binary === 'bun' && subcommand === 'test') {
133
+ return [
134
+ binary,
135
+ subcommand,
136
+ ...rest,
137
+ '--reporter=junit',
138
+ '--reporter-outfile',
139
+ junitPath,
140
+ ];
141
+ }
142
+ return [...command];
143
+ }
144
+
145
+ function runCommand(command, options = {}, spawnSync = defaultSpawnSync) {
146
+ const [binary, ...args] = command;
147
+ const result = spawnSync(binary, args, {
148
+ cwd: rootDir,
149
+ encoding: 'utf8',
150
+ stdio: 'pipe',
151
+ ...options,
152
+ });
153
+
154
+ if (result.error) {
155
+ throw result.error;
156
+ }
157
+
158
+ if ((result.status ?? 1) !== 0) {
159
+ throw new Error(result.stderr || `Command failed: ${command.join(' ')}`);
160
+ }
161
+
162
+ return result;
163
+ }
164
+
165
+ function materializeJUnitFile(expectedPath) {
166
+ if (fs.existsSync(expectedPath)) {
167
+ return expectedPath;
43
168
  }
44
- const mean = times.reduce((a, b) => a + b, 0) / times.length;
45
- return { name: 'autoDetect()', mean: Math.round(mean), unit: 'ms', samples: ITERATIONS };
169
+
170
+ if (fs.existsSync(DEFAULT_JUNIT_PATH)) {
171
+ fs.mkdirSync(path.dirname(expectedPath), { recursive: true });
172
+ fs.copyFileSync(DEFAULT_JUNIT_PATH, expectedPath);
173
+ return expectedPath;
174
+ }
175
+
176
+ throw new Error(`Benchmark run did not produce JUnit output at ${expectedPath}`);
177
+ }
178
+
179
+ function removeMaterializedOutputs(expectedPath) {
180
+ const directory = path.dirname(expectedPath);
181
+ const extension = path.extname(expectedPath);
182
+ const basename = path.basename(expectedPath, extension);
183
+
184
+ if (!fs.existsSync(directory)) {
185
+ return;
186
+ }
187
+
188
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
189
+ if (!entry.isFile() || !entry.name.endsWith(extension)) {
190
+ continue;
191
+ }
192
+
193
+ if (entry.name === `${basename}${extension}` || entry.name.startsWith(`${basename}.`)) {
194
+ fs.rmSync(path.join(directory, entry.name), { force: true });
195
+ }
196
+ }
197
+ }
198
+
199
+ function indexXmlFiles(directory) {
200
+ return new Map(
201
+ walk(directory, '.xml').map((filePath) => {
202
+ const stats = fs.statSync(filePath);
203
+ return [filePath, `${stats.size}:${stats.mtimeMs}`];
204
+ }),
205
+ );
206
+ }
207
+
208
+ function listChangedXmlFiles(directory, beforeIndex) {
209
+ return walk(directory, '.xml')
210
+ .filter((filePath) => {
211
+ const stats = fs.statSync(filePath);
212
+ const fingerprint = `${stats.size}:${stats.mtimeMs}`;
213
+ return beforeIndex.get(filePath) !== fingerprint;
214
+ })
215
+ .sort((left, right) => left.localeCompare(right));
46
216
  }
47
217
 
48
- function benchmarkDetectFramework() {
49
- const { detectFramework } = require(path.join(rootDir, 'lib', 'project-discovery.js'));
50
- const times = [];
51
- for (let i = 0; i < ITERATIONS; i++) {
218
+ function materializeJUnitFiles(expectedPath, sourceFiles) {
219
+ const uniqueSources = [];
220
+ const seen = new Set();
221
+
222
+ for (const sourceFile of sourceFiles) {
223
+ if (typeof sourceFile !== 'string' || seen.has(sourceFile) || !fs.existsSync(sourceFile)) {
224
+ continue;
225
+ }
226
+ seen.add(sourceFile);
227
+ uniqueSources.push(sourceFile);
228
+ }
229
+
230
+ if (uniqueSources.length === 0) {
231
+ return [materializeJUnitFile(expectedPath)];
232
+ }
233
+
234
+ return uniqueSources.map((sourceFile, index) => {
235
+ const destination = index === 0
236
+ ? expectedPath
237
+ : expectedPath.replace(/\.xml$/, `.${index + 1}.xml`);
238
+
239
+ if (sourceFile === destination) {
240
+ return destination;
241
+ }
242
+
243
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
244
+ fs.copyFileSync(sourceFile, destination);
245
+ return destination;
246
+ });
247
+ }
248
+
249
+ function buildGroupProfile(group, xmlFiles) {
250
+ const metrics = parseJUnitFiles(xmlFiles);
251
+ return buildProfile({
252
+ integrationSkipped: true,
253
+ label: `benchmark-${group.id}`,
254
+ }, metrics);
255
+ }
256
+
257
+ function runBenchmarkGroup(group, options = {}) {
258
+ const profileDir = path.resolve(rootDir, options.profileDir || DEFAULT_PROFILE_DIR);
259
+ const samples = options.samples || DEFAULT_SAMPLES;
260
+ const spawnSync = options.spawnSync || defaultSpawnSync;
261
+
262
+ fs.mkdirSync(profileDir, { recursive: true });
263
+
264
+ const xmlFiles = [];
265
+ const samplesMs = [];
266
+
267
+ for (let sampleIndex = 0; sampleIndex < samples; sampleIndex += 1) {
268
+ const junitPath = path.join(profileDir, `${group.id}.sample-${sampleIndex + 1}.xml`);
269
+ removeMaterializedOutputs(junitPath);
270
+ fs.rmSync(DEFAULT_JUNIT_PATH, { force: true });
271
+ const beforeXmlFiles = indexXmlFiles(path.join(rootDir, 'test-results'));
272
+ const command = buildJUnitCommand(group.command, junitPath);
52
273
  const start = performance.now();
53
- detectFramework(rootDir);
54
- times.push(performance.now() - start);
274
+ runCommand(command, {}, spawnSync);
275
+ samplesMs.push(roundMs(performance.now() - start));
276
+ const changedXmlFiles = listChangedXmlFiles(path.join(rootDir, 'test-results'), beforeXmlFiles);
277
+ xmlFiles.push(...materializeJUnitFiles(junitPath, [
278
+ fs.existsSync(junitPath) ? junitPath : null,
279
+ ...changedXmlFiles,
280
+ ]));
55
281
  }
56
- const mean = times.reduce((a, b) => a + b, 0) / times.length;
57
- return { name: 'detectFramework()', mean: Math.round(mean), unit: 'ms', samples: ITERATIONS };
282
+
283
+ const summary = summarizeSamples(samplesMs);
284
+ const profile = buildGroupProfile(group, xmlFiles);
285
+ const profilePath = path.join(profileDir, `${group.id}.profile.json`);
286
+ fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
287
+
288
+ return {
289
+ command: group.command.join(' '),
290
+ groupId: group.id,
291
+ groupLabel: group.label,
292
+ profilePath: path.relative(rootDir, profilePath).replace(/\\/g, '/'),
293
+ samples,
294
+ ...summary,
295
+ };
58
296
  }
59
297
 
60
- const results = [
61
- benchmarkCLIStartup(),
62
- benchmarkAutoDetect(),
63
- benchmarkDetectFramework()
64
- ];
298
+ function buildBenchmarkResults(groups, results, timestamp = new Date().toISOString()) {
299
+ const totalMedianMs = results.reduce((sum, result) => sum + result.medianMs, 0);
300
+ const slowestGroup = [...results]
301
+ .sort((left, right) => right.medianMs - left.medianMs || left.groupId.localeCompare(right.groupId))[0] || null;
65
302
 
66
- const jsonMode = process.argv.includes('--json');
303
+ return {
304
+ groups: results,
305
+ requestedGroups: groups.map((group) => group.id),
306
+ samples: results[0]?.samples || DEFAULT_SAMPLES,
307
+ slowestGroup: slowestGroup
308
+ ? { groupId: slowestGroup.groupId, groupLabel: slowestGroup.groupLabel, medianMs: slowestGroup.medianMs }
309
+ : null,
310
+ timestamp,
311
+ totalMedianMs,
312
+ };
313
+ }
314
+
315
+ function formatResultLine(result) {
316
+ return ` ${result.groupLabel}: median ${result.medianMs}ms (min ${result.minMs}ms, max ${result.maxMs}ms)`;
317
+ }
318
+
319
+ function main(argv = process.argv.slice(2), deps = {}) {
320
+ const args = parseArgs(argv);
321
+ const groups = resolveGroups(args.groups);
322
+ const results = groups.map((group) => runBenchmarkGroup(group, {
323
+ profileDir: args.profileDir,
324
+ samples: args.samples,
325
+ spawnSync: deps.spawnSync,
326
+ }));
67
327
 
68
- if (jsonMode) {
69
- process.stdout.write(JSON.stringify(results));
70
- } else {
71
- console.log('\n Forge Performance Benchmarks');
72
- console.log(' ===========================\n');
73
- for (const r of results) {
74
- const status = r.mean < 1000 ? 'PASS' : r.mean < 3000 ? 'WARN' : 'SLOW';
75
- console.log(` ${status} ${r.name}: ${r.mean}${r.unit} (${r.samples} samples)`);
328
+ const payload = buildBenchmarkResults(groups, results);
329
+ const outputPath = path.resolve(rootDir, args.output || DEFAULT_OUTPUT);
330
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
331
+ fs.writeFileSync(outputPath, JSON.stringify(payload, null, 2));
332
+
333
+ if (args.json) {
334
+ process.stdout.write(JSON.stringify(payload));
335
+ return payload;
336
+ }
337
+
338
+ console.log('\n Forge Test Benchmark Baselines');
339
+ console.log(' =============================\n');
340
+ for (const result of results) {
341
+ console.log(formatResultLine(result));
76
342
  }
77
- console.log('');
343
+ console.log(`\n Total median: ${payload.totalMedianMs}ms`);
344
+ console.log(` Results saved to: ${path.relative(rootDir, outputPath).replace(/\\/g, '/')}\n`);
345
+ return payload;
346
+ }
78
347
 
79
- // Write results file
80
- const outputPath = path.join(rootDir, 'benchmark-results.json');
81
- fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
82
- console.log(` Results saved to: benchmark-results.json\n`);
348
+ if (require.main === module) {
349
+ main();
83
350
  }
84
351
 
85
- module.exports = results;
352
+ module.exports = {
353
+ BENCHMARK_GROUPS,
354
+ DEFAULT_OUTPUT,
355
+ DEFAULT_PROFILE_DIR,
356
+ DEFAULT_SAMPLES,
357
+ buildBenchmarkResults,
358
+ buildGroupProfile,
359
+ buildJUnitCommand,
360
+ calculateMedian,
361
+ formatResultLine,
362
+ main,
363
+ parseArgs,
364
+ resolveGroups,
365
+ roundMs,
366
+ materializeJUnitFile,
367
+ materializeJUnitFiles,
368
+ runBenchmarkGroup,
369
+ runCommand,
370
+ summarizeSamples,
371
+ };
@@ -32,26 +32,61 @@ function normalizeSingleIssuePayload(payload) {
32
32
  return {};
33
33
  }
34
34
 
35
+ function readStdinPayload() {
36
+ const raw = fs.readFileSync(0, 'utf8');
37
+ if (!raw.trim()) {
38
+ throw new Error('Expected JSON payload on stdin');
39
+ }
40
+
41
+ const payload = JSON.parse(raw);
42
+ return {
43
+ currentIssue: normalizeSingleIssuePayload(payload.currentIssue),
44
+ openIssues: normalizeListPayload(payload.openIssues),
45
+ inProgressIssues: normalizeListPayload(payload.inProgressIssues),
46
+ taskFile: payload.taskFile,
47
+ repositoryRoot: payload.repositoryRoot || process.cwd(),
48
+ };
49
+ }
50
+
35
51
  function main() {
36
- const [
37
- currentIssueFile,
38
- openIssuesFile,
39
- inProgressIssuesFile,
40
- taskFile,
41
- repositoryRoot = process.cwd(),
42
- ] = process.argv.slice(2);
52
+ const args = process.argv.slice(2);
53
+ let currentIssue;
54
+ let openIssues;
55
+ let taskFile;
56
+ let repositoryRoot;
43
57
 
44
- if (!currentIssueFile || !openIssuesFile || !inProgressIssuesFile || !taskFile) {
45
- throw new Error(
46
- 'Usage: dep-guard-analyze.js <current-issue.json> <open-issues.json> <in-progress-issues.json> <task-file> [repository-root]',
47
- );
48
- }
58
+ if (args[0] === '--stdin') {
59
+ const payload = readStdinPayload();
60
+ currentIssue = payload.currentIssue;
61
+ openIssues = [
62
+ ...payload.openIssues,
63
+ ...payload.inProgressIssues,
64
+ ].filter((issue) => issue && issue.id && issue.id !== currentIssue.id);
65
+ taskFile = payload.taskFile;
66
+ repositoryRoot = payload.repositoryRoot;
67
+ } else {
68
+ const [
69
+ currentIssueFile,
70
+ openIssuesFile,
71
+ inProgressIssuesFile,
72
+ taskFileArg,
73
+ repositoryRootArg = process.cwd(),
74
+ ] = args;
49
75
 
50
- const currentIssue = normalizeSingleIssuePayload(readJsonFile(currentIssueFile));
51
- const openIssues = [
52
- ...normalizeListPayload(readJsonFile(openIssuesFile)),
53
- ...normalizeListPayload(readJsonFile(inProgressIssuesFile)),
54
- ].filter((issue) => issue && issue.id && issue.id !== currentIssue.id);
76
+ if (!currentIssueFile || !openIssuesFile || !inProgressIssuesFile || !taskFileArg) {
77
+ throw new Error(
78
+ 'Usage: dep-guard-analyze.js <current-issue.json> <open-issues.json> <in-progress-issues.json> <task-file> [repository-root]',
79
+ );
80
+ }
81
+
82
+ currentIssue = normalizeSingleIssuePayload(readJsonFile(currentIssueFile));
83
+ openIssues = [
84
+ ...normalizeListPayload(readJsonFile(openIssuesFile)),
85
+ ...normalizeListPayload(readJsonFile(inProgressIssuesFile)),
86
+ ].filter((issue) => issue && issue.id && issue.id !== currentIssue.id);
87
+ taskFile = taskFileArg;
88
+ repositoryRoot = repositoryRootArg;
89
+ }
55
90
 
56
91
  const result = analyzePhase3Dependencies({
57
92
  currentIssue,
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const {
5
+ parseActiveIssueLines,
6
+ renderKeywordRippleReport,
7
+ } = require('../lib/dep-guard/keyword-ripple.js');
8
+
9
+ function main() {
10
+ try {
11
+ const report = renderKeywordRippleReport({
12
+ issueId: process.env.ISSUE_ID || '',
13
+ sourceTitle: process.env.SOURCE_TITLE || '',
14
+ activeIssues: parseActiveIssueLines(process.env.LIST_OUTPUT || ''),
15
+ });
16
+
17
+ if (report.overlapCount > 0) {
18
+ process.stdout.write(`⚠️ ${report.output}\n`);
19
+ return;
20
+ }
21
+
22
+ process.stdout.write(`✅ ${report.output}\n`);
23
+ } catch (error) {
24
+ console.error(`dep-guard-keyword-ripple: ${error.message}`);
25
+ process.exit(1);
26
+ }
27
+ }
28
+
29
+ main();
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+
6
+ function readAnalysis() {
7
+ const raw = fs.readFileSync(0, 'utf8');
8
+ if (!raw.trim()) {
9
+ throw new Error('Expected JSON analysis payload on stdin');
10
+ }
11
+ return JSON.parse(raw);
12
+ }
13
+
14
+ function renderPhase3Review(analysis) {
15
+ const detectorCategories = Object.entries(analysis.scores ?? {})
16
+ .filter(([name, score]) => name !== 'rubric' && Number(score) > 0)
17
+ .map(([name]) => name);
18
+
19
+ const lines = [
20
+ '',
21
+ `Structured dependency review for ${analysis.currentIssue.id}...`,
22
+ '',
23
+ ];
24
+
25
+ if (!Array.isArray(analysis.issues) || analysis.issues.length === 0) {
26
+ lines.push('No conflicts detected');
27
+ return `${lines.join('\n')}\n`;
28
+ }
29
+
30
+ const seenPairs = new Set();
31
+ for (const finding of analysis.issues) {
32
+ const key = `${analysis.currentIssue.id}->${finding.targetIssueId}`;
33
+ if (seenPairs.has(key)) {
34
+ continue;
35
+ }
36
+ seenPairs.add(key);
37
+ lines.push(`Issue pair: ${analysis.currentIssue.id} -> ${finding.targetIssueId}`);
38
+ }
39
+
40
+ lines.push(`Rubric score: ${analysis.scores?.rubric ?? 0}`);
41
+ lines.push(
42
+ `Confidence: ${(analysis.confidence?.score ?? 0).toFixed(2)}${
43
+ analysis.confidence?.belowThreshold ? ' (below 70% threshold)' : ''
44
+ }`,
45
+ );
46
+ lines.push(`Detector categories: ${detectorCategories.join(', ') || 'none'}`);
47
+ lines.push(`Needs user decision: ${analysis.needsUserDecision ? 'yes' : 'no'}`);
48
+
49
+ if (Array.isArray(analysis.detectorConflicts) && analysis.detectorConflicts.length > 0) {
50
+ lines.push('Conflicts:');
51
+ for (const conflict of analysis.detectorConflicts) {
52
+ lines.push(` - ${conflict}`);
53
+ }
54
+ }
55
+
56
+ if (Array.isArray(analysis.proposals) && analysis.proposals.length > 0) {
57
+ lines.push('');
58
+ lines.push('Proposed dependency updates:');
59
+ for (const proposal of analysis.proposals) {
60
+ lines.push(` - ${proposal.dependentIssueId} depends on ${proposal.dependsOnIssueId}`);
61
+ if (Array.isArray(proposal.pros) && proposal.pros.length > 0) {
62
+ lines.push(` Pros: ${proposal.pros.join('; ')}`);
63
+ }
64
+ if (Array.isArray(proposal.cons) && proposal.cons.length > 0) {
65
+ lines.push(` Cons: ${proposal.cons.join('; ')}`);
66
+ }
67
+ }
68
+ }
69
+
70
+ return `${lines.join('\n')}\n`;
71
+ }
72
+
73
+ function main() {
74
+ process.stdout.write(renderPhase3Review(readAnalysis()));
75
+ }
76
+
77
+ try {
78
+ main();
79
+ } catch (error) {
80
+ process.stderr.write(`${error.message}\n`);
81
+ process.exit(1);
82
+ }
83
+
84
+ module.exports = {
85
+ renderPhase3Review,
86
+ };