@izkac/forgekit 0.1.0

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 (143) hide show
  1. package/bin/forge.mjs +100 -0
  2. package/bin/forgekit.mjs +84 -0
  3. package/bin/review.mjs +82 -0
  4. package/package.json +46 -0
  5. package/scripts/prepack.mjs +78 -0
  6. package/scripts/run-tests.mjs +43 -0
  7. package/src/adr.mjs +236 -0
  8. package/src/adr.test.mjs +170 -0
  9. package/src/change.mjs +234 -0
  10. package/src/change.test.mjs +83 -0
  11. package/src/cleanup-sessions.mjs +70 -0
  12. package/src/config.mjs +103 -0
  13. package/src/defer.mjs +75 -0
  14. package/src/doctor.mjs +341 -0
  15. package/src/doctor.test.mjs +114 -0
  16. package/src/init.mjs +575 -0
  17. package/src/install.mjs +777 -0
  18. package/src/install.test.mjs +104 -0
  19. package/src/integrity-check.mjs +58 -0
  20. package/src/integrity.mjs +317 -0
  21. package/src/integrity.test.mjs +296 -0
  22. package/src/lib/workspaces.mjs +55 -0
  23. package/src/lib.mjs +138 -0
  24. package/src/models.defaults.json +41 -0
  25. package/src/new-session.mjs +82 -0
  26. package/src/openspec-overlays/README.md +19 -0
  27. package/src/openspec-overlays/openspec-apply-change-footer.md +14 -0
  28. package/src/openspec-overlays/opsx-apply-completion-step.md +1 -0
  29. package/src/openspec-overlays/opsx-apply-implement-step.md +11 -0
  30. package/src/paths.mjs +92 -0
  31. package/src/plan-engine.mjs +260 -0
  32. package/src/plan-engine.test.mjs +245 -0
  33. package/src/preferences.defaults.json +78 -0
  34. package/src/preferences.mjs +438 -0
  35. package/src/preferences.test.mjs +174 -0
  36. package/src/record-evidence.mjs +204 -0
  37. package/src/record-evidence.test.mjs +260 -0
  38. package/src/resolve-model.mjs +312 -0
  39. package/src/resolve-model.test.mjs +194 -0
  40. package/src/review/carryforward.mjs +413 -0
  41. package/src/review/carryforward.test.mjs +587 -0
  42. package/src/review/cli.test.mjs +117 -0
  43. package/src/review/export.mjs +172 -0
  44. package/src/review/export.test.mjs +197 -0
  45. package/src/review/fixtures/valid-review.json +42 -0
  46. package/src/review/lib.mjs +894 -0
  47. package/src/review/lib.test.mjs +266 -0
  48. package/src/review/merge-tentative.mjs +292 -0
  49. package/src/review/merge-tentative.test.mjs +363 -0
  50. package/src/review/new-review.mjs +200 -0
  51. package/src/review/render.mjs +108 -0
  52. package/src/review/schema-consistency.test.mjs +83 -0
  53. package/src/review/schema.json +196 -0
  54. package/src/review/signals.mjs +144 -0
  55. package/src/review/signals.test.mjs +62 -0
  56. package/src/score-cli.mjs +68 -0
  57. package/src/score.mjs +489 -0
  58. package/src/score.test.mjs +253 -0
  59. package/src/session-reminder.mjs +168 -0
  60. package/src/session-status.mjs +70 -0
  61. package/src/set-models.mjs +186 -0
  62. package/src/set-phase.mjs +177 -0
  63. package/src/set-phase.test.mjs +317 -0
  64. package/src/set-prefs.mjs +294 -0
  65. package/src/spine.mjs +91 -0
  66. package/src/triage-prompt.mjs +175 -0
  67. package/src/triage-prompt.test.mjs +50 -0
  68. package/src/vendor-openspec-overlays.mjs +176 -0
  69. package/src/vendor-openspec-overlays.test.mjs +62 -0
  70. package/vendor/skills/archive-to-adr/SKILL.md +149 -0
  71. package/vendor/skills/forge/SKILL.md +136 -0
  72. package/vendor/skills/forge/phases/brainstorm.md +23 -0
  73. package/vendor/skills/forge/phases/finish.md +87 -0
  74. package/vendor/skills/forge/phases/implement.md +76 -0
  75. package/vendor/skills/forge/phases/plan-openspec.md +40 -0
  76. package/vendor/skills/forge/phases/plan-specs.md +97 -0
  77. package/vendor/skills/forge/phases/review.md +25 -0
  78. package/vendor/skills/forge/phases/verify.md +120 -0
  79. package/vendor/skills/forge/references/forge-layout.md +85 -0
  80. package/vendor/skills/forge/references/pace.md +115 -0
  81. package/vendor/skills/forge/references/plan-routing.md +51 -0
  82. package/vendor/skills/forge/references/runtime-integrity.md +157 -0
  83. package/vendor/skills/forge/references/substantial-work.md +37 -0
  84. package/vendor/skills/forge/references/tdd-core.md +29 -0
  85. package/vendor/skills/forge/references/test-evidence.md +30 -0
  86. package/vendor/skills/forge/references/test-strategy.md +68 -0
  87. package/vendor/skills/forge/skills/NOTICE.md +17 -0
  88. package/vendor/skills/forge/skills/brainstorming/SKILL.md +120 -0
  89. package/vendor/skills/forge/skills/requesting-code-review/SKILL.md +67 -0
  90. package/vendor/skills/forge/skills/requesting-code-review/code-reviewer.md +146 -0
  91. package/vendor/skills/forge/skills/subagent-driven-development/SKILL.md +87 -0
  92. package/vendor/skills/forge/skills/systematic-debugging/SKILL.md +234 -0
  93. package/vendor/skills/forge/skills/systematic-debugging/condition-based-waiting.md +115 -0
  94. package/vendor/skills/forge/skills/systematic-debugging/defense-in-depth.md +122 -0
  95. package/vendor/skills/forge/skills/systematic-debugging/find-polluter.sh +63 -0
  96. package/vendor/skills/forge/skills/systematic-debugging/root-cause-tracing.md +169 -0
  97. package/vendor/skills/forge/skills/test-driven-development/SKILL.md +290 -0
  98. package/vendor/skills/forge/skills/test-driven-development/testing-anti-patterns.md +299 -0
  99. package/vendor/skills/forge/skills/verification-before-completion/SKILL.md +59 -0
  100. package/vendor/skills/forge/subagents/final-reviewer-prompt.md +53 -0
  101. package/vendor/skills/forge/subagents/implementer-prompt.md +38 -0
  102. package/vendor/skills/forge/subagents/task-reviewer-prompt.md +61 -0
  103. package/vendor/skills/git-resolve-adr-conflict/SKILL.md +132 -0
  104. package/vendor/skills/thorough-code-review/SKILL.md +290 -0
  105. package/vendor/skills/thorough-code-review/examples/accepted-risks-janus.md +32 -0
  106. package/vendor/skills/thorough-code-review/examples.md +133 -0
  107. package/vendor/skills/thorough-code-review/reference/accepted-risks.md +26 -0
  108. package/vendor/skills/thorough-code-review/reference/lenses.md +96 -0
  109. package/vendor/skills/thorough-code-review/reference/phase1-scout.md +62 -0
  110. package/vendor/skills/thorough-code-review/reference/phase1c-coverage.md +44 -0
  111. package/vendor/skills/thorough-code-review/reference/phase2-skeptic.md +105 -0
  112. package/vendor/skills/thorough-code-review/reference/report-schema.json +222 -0
  113. package/vendor/skills/thorough-code-review/reference/report-template.md +115 -0
  114. package/vendor/skills/thorough-code-review/reference/severity-rubric.md +49 -0
  115. package/vendor/skills/thorough-code-review/reference/signals-preflight.md +55 -0
  116. package/vendor/templates/adr/README.md +7 -0
  117. package/vendor/templates/adr/decisions.md +141 -0
  118. package/vendor/templates/adr/hooks/check-pending-adrs.mjs +74 -0
  119. package/vendor/templates/adr/hooks/check-pending-adrs.sh +3 -0
  120. package/vendor/templates/adr/hooks/openspec-archive-agent-message.mjs +52 -0
  121. package/vendor/templates/adr/hooks/openspec-archive-agent-message.sh +3 -0
  122. package/vendor/templates/project/claude/commands/forge-apply.md +75 -0
  123. package/vendor/templates/project/claude/commands/forge-brainstorm.md +7 -0
  124. package/vendor/templates/project/claude/commands/forge-build.md +17 -0
  125. package/vendor/templates/project/claude/commands/forge-plan.md +12 -0
  126. package/vendor/templates/project/claude/commands/forge-skip.md +14 -0
  127. package/vendor/templates/project/claude/commands/forge-status.md +16 -0
  128. package/vendor/templates/project/claude/commands/forge.md +16 -0
  129. package/vendor/templates/project/claude/hooks/forge-prompt-hook.mjs +73 -0
  130. package/vendor/templates/project/claude/hooks/forge-session-start.mjs +19 -0
  131. package/vendor/templates/project/claude/hooks/forge-triage-hook.mjs +77 -0
  132. package/vendor/templates/project/claude/rules/forge.md +16 -0
  133. package/vendor/templates/project/codex/rules/forge.md +10 -0
  134. package/vendor/templates/project/cursor/commands/forge-apply.md +75 -0
  135. package/vendor/templates/project/cursor/commands/forge-brainstorm.md +10 -0
  136. package/vendor/templates/project/cursor/commands/forge-build.md +17 -0
  137. package/vendor/templates/project/cursor/commands/forge-plan.md +15 -0
  138. package/vendor/templates/project/cursor/commands/forge-skip.md +14 -0
  139. package/vendor/templates/project/cursor/commands/forge-status.md +16 -0
  140. package/vendor/templates/project/cursor/commands/forge.md +16 -0
  141. package/vendor/templates/project/cursor/hooks/forge-session-start.mjs +30 -0
  142. package/vendor/templates/project/cursor/hooks/forge-session-start.sh +3 -0
  143. package/vendor/templates/project/cursor/rules/forge.mdc +21 -0
@@ -0,0 +1,83 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import {
7
+ LENSES,
8
+ SCOPE_TYPES,
9
+ SEVERITIES,
10
+ ALL_VERDICTS,
11
+ CONFIDENCES,
12
+ STATS_KEYS,
13
+ } from './lib.mjs';
14
+
15
+ /**
16
+ * The agent-facing JSON Schema and the runtime validator in lib.mjs describe the
17
+ * same contract. They live in different trees (the skill vs. scripts/), so this
18
+ * test fails loudly if their enums ever drift apart.
19
+ */
20
+
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const schemaPath = path.join(
23
+ __dirname,
24
+ '..',
25
+ '..',
26
+ '..',
27
+ '..',
28
+ 'skills',
29
+ 'thorough-code-review',
30
+ 'reference',
31
+ 'report-schema.json',
32
+ );
33
+ const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
34
+
35
+ const runtimeSchemaPath = path.join(__dirname, 'schema.json');
36
+ const runtimeSchema = JSON.parse(fs.readFileSync(runtimeSchemaPath, 'utf8'));
37
+
38
+ const sorted = (arr) => [...arr].sort();
39
+
40
+ test('schema file exists and parses', () => {
41
+ assert.ok(schema && typeof schema === 'object');
42
+ });
43
+
44
+ test('lens enum matches lib.LENSES', () => {
45
+ assert.deepEqual(sorted(schema.properties.lenses.items.enum), sorted(LENSES));
46
+ });
47
+
48
+ test('scope.type enum matches lib.SCOPE_TYPES', () => {
49
+ assert.deepEqual(sorted(schema.properties.scope.properties.type.enum), sorted(SCOPE_TYPES));
50
+ });
51
+
52
+ test('finding.severity enum matches lib.SEVERITIES', () => {
53
+ assert.deepEqual(sorted(schema.$defs.finding.properties.severity.enum), sorted(SEVERITIES));
54
+ });
55
+
56
+ test('finding.verdict enum matches lib.ALL_VERDICTS', () => {
57
+ assert.deepEqual(sorted(schema.$defs.finding.properties.verdict.enum), sorted(ALL_VERDICTS));
58
+ });
59
+
60
+ test('finding.phase1_confidence enum matches lib.CONFIDENCES', () => {
61
+ assert.deepEqual(
62
+ sorted(schema.$defs.finding.properties.phase1_confidence.enum),
63
+ sorted(CONFIDENCES),
64
+ );
65
+ });
66
+
67
+ test('second_opinion verdict enum matches lib.ALL_VERDICTS', () => {
68
+ assert.deepEqual(
69
+ sorted(schema.$defs.finding.properties.second_opinion.properties.verdict.enum),
70
+ sorted(ALL_VERDICTS),
71
+ );
72
+ });
73
+
74
+ test('stats keys match lib.STATS_KEYS', () => {
75
+ assert.deepEqual(sorted(Object.keys(schema.properties.stats.properties)), sorted(STATS_KEYS));
76
+ });
77
+
78
+ test('runtime schema.json shares top-level property names with the reference schema', () => {
79
+ assert.deepEqual(
80
+ sorted(Object.keys(runtimeSchema.properties)),
81
+ sorted(Object.keys(schema.properties)),
82
+ );
83
+ });
@@ -0,0 +1,196 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://forgekit.local/schemas/code-review-report.json",
4
+ "title": "ThoroughCodeReviewReport",
5
+ "type": "object",
6
+ "required": ["review_id", "kind", "created_at", "scope", "lenses", "summary", "findings"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "review_id": { "type": "string", "minLength": 1 },
10
+ "kind": { "type": "string", "enum": ["review", "reverify"] },
11
+ "created_at": { "type": "string", "format": "date-time" },
12
+ "scope": {
13
+ "type": "object",
14
+ "required": ["type", "description"],
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "type": {
18
+ "type": "string",
19
+ "enum": ["uncommitted", "branch", "paths", "commit_range", "file"]
20
+ },
21
+ "description": { "type": "string", "minLength": 1 },
22
+ "base_sha": { "type": "string" },
23
+ "head_sha": { "type": "string" },
24
+ "paths": { "type": "array", "items": { "type": "string" } }
25
+ }
26
+ },
27
+ "lenses": {
28
+ "type": "array",
29
+ "minItems": 1,
30
+ "items": {
31
+ "type": "string",
32
+ "enum": [
33
+ "security",
34
+ "correctness",
35
+ "smells",
36
+ "architecture",
37
+ "performance",
38
+ "tests",
39
+ "contracts",
40
+ "errors",
41
+ "maintainability"
42
+ ]
43
+ }
44
+ },
45
+ "parent_report": { "type": "string" },
46
+ "summary": {
47
+ "type": "object",
48
+ "required": ["tentative_count"],
49
+ "additionalProperties": true,
50
+ "properties": {
51
+ "tentative_count": { "type": "integer", "minimum": 0 },
52
+ "confirmed": { "type": "integer", "minimum": 0 },
53
+ "downgraded": { "type": "integer", "minimum": 0 },
54
+ "false_positive": { "type": "integer", "minimum": 0 },
55
+ "needs_decision": { "type": "integer", "minimum": 0 },
56
+ "resolved": { "type": "integer", "minimum": 0 },
57
+ "still_open": { "type": "integer", "minimum": 0 },
58
+ "partially_fixed": { "type": "integer", "minimum": 0 },
59
+ "regressed": { "type": "integer", "minimum": 0 }
60
+ }
61
+ },
62
+ "findings": {
63
+ "type": "array",
64
+ "items": { "$ref": "#/$defs/finding" }
65
+ },
66
+ "dedupe_preflight": {
67
+ "type": "object",
68
+ "additionalProperties": false,
69
+ "properties": {
70
+ "count": { "type": "integer", "minimum": 0 },
71
+ "items": {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "object",
75
+ "required": ["id", "location", "claim"],
76
+ "properties": {
77
+ "id": { "type": "string" },
78
+ "location": { "type": "string" },
79
+ "claim": { "type": "string" }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ },
85
+ "coverage": {
86
+ "type": "object",
87
+ "description": "Recall / completeness pass ledger.",
88
+ "additionalProperties": true,
89
+ "properties": {
90
+ "files_reviewed": { "type": "array", "items": { "type": "string" } },
91
+ "files_skipped": { "type": "array", "items": { "type": "string" } },
92
+ "lenses_without_findings": {
93
+ "type": "array",
94
+ "items": {
95
+ "type": "object",
96
+ "required": ["lens", "reason"],
97
+ "properties": {
98
+ "lens": {
99
+ "type": "string",
100
+ "enum": [
101
+ "security",
102
+ "correctness",
103
+ "smells",
104
+ "architecture",
105
+ "performance",
106
+ "tests",
107
+ "contracts",
108
+ "errors",
109
+ "maintainability"
110
+ ]
111
+ },
112
+ "reason": { "type": "string", "minLength": 1 }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ },
118
+ "signals": {
119
+ "type": "object",
120
+ "description": "Signals pre-flight: deterministic tools run before the scout.",
121
+ "additionalProperties": true,
122
+ "properties": {
123
+ "tools": {
124
+ "type": "array",
125
+ "items": {
126
+ "type": "object",
127
+ "required": ["name"],
128
+ "properties": {
129
+ "name": { "type": "string", "minLength": 1 },
130
+ "status": { "type": "string", "enum": ["pass", "fail", "skipped"] },
131
+ "summary": { "type": "string" }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ },
137
+ "stats": {
138
+ "type": "object",
139
+ "description": "Pipeline stats: subagent counts and shortcut tallies for the run.",
140
+ "additionalProperties": false,
141
+ "properties": {
142
+ "scouts": { "type": "integer", "minimum": 0 },
143
+ "skeptics_dedicated": { "type": "integer", "minimum": 0 },
144
+ "skeptics_batched": { "type": "integer", "minimum": 0 },
145
+ "inline_verdicts": { "type": "integer", "minimum": 0 },
146
+ "grounded_skips": { "type": "integer", "minimum": 0 },
147
+ "carried_forward": { "type": "integer", "minimum": 0 },
148
+ "second_opinions": { "type": "integer", "minimum": 0 }
149
+ }
150
+ }
151
+ },
152
+ "$defs": {
153
+ "finding": {
154
+ "type": "object",
155
+ "required": ["id", "lens", "location", "claim", "severity", "verdict", "verdict_reason"],
156
+ "additionalProperties": false,
157
+ "properties": {
158
+ "id": { "type": "string", "pattern": "^(F|dup)-[0-9]{3}$" },
159
+ "lens": {
160
+ "type": "string",
161
+ "enum": [
162
+ "security",
163
+ "correctness",
164
+ "smells",
165
+ "architecture",
166
+ "performance",
167
+ "tests",
168
+ "contracts",
169
+ "errors",
170
+ "maintainability"
171
+ ]
172
+ },
173
+ "location": { "type": "string", "minLength": 1 },
174
+ "claim": { "type": "string", "minLength": 1 },
175
+ "evidence": { "type": "string" },
176
+ "severity": { "type": "string", "enum": ["critical", "important", "minor"] },
177
+ "phase1_confidence": { "type": "string", "enum": ["low", "medium", "high"] },
178
+ "original_severity": { "type": "string", "enum": ["critical", "important", "minor"] },
179
+ "verdict": {
180
+ "type": "string",
181
+ "enum": [
182
+ "confirmed",
183
+ "false_positive",
184
+ "downgraded",
185
+ "needs_decision",
186
+ "resolved",
187
+ "still_open",
188
+ "partially_fixed",
189
+ "regressed"
190
+ ]
191
+ },
192
+ "verdict_reason": { "type": "string", "minLength": 1 }
193
+ }
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Signals pre-flight: map a review scope to the workspaces it touches and emit
4
+ * the deterministic grounding commands (typecheck / lint / test) to run before
5
+ * the scout. Grounding the scout in real tool output cuts hallucinated findings
6
+ * and raises recall on the smells / contracts / tests lenses.
7
+ *
8
+ * Read-only and fast: it PLANS the commands rather than running them, so the
9
+ * agent can run exactly the relevant ones and read full output. Use `--json`
10
+ * for machine-readable output.
11
+ *
12
+ * Usage:
13
+ * review signals [--paths a,b] [--type <t>] [--base-branch main] [--json]
14
+ */
15
+
16
+ import path from 'node:path';
17
+ import { execFileSync } from 'node:child_process';
18
+ import { pathToFileURL } from 'node:url';
19
+ import { mapPathsToWorkspaces, suggestSignalCommands } from './lib.mjs';
20
+ import { loadWorkspaces } from '../lib/workspaces.mjs';
21
+
22
+ const CONTRACT_HINT = /(contract|openapi|routes?\/|\.openapi\.)/i;
23
+
24
+ /**
25
+ * @param {string[]} argv
26
+ */
27
+ export function parseArgs(argv) {
28
+ const opts = { paths: null, type: 'uncommitted', baseBranch: 'main', json: false, help: false };
29
+ for (let i = 0; i < argv.length; i += 1) {
30
+ const arg = argv[i];
31
+ if (arg === '--paths') opts.paths = argv[++i].split(',').map((s) => s.trim()).filter(Boolean);
32
+ else if (arg === '--type') opts.type = argv[++i];
33
+ else if (arg === '--base-branch') opts.baseBranch = argv[++i];
34
+ else if (arg === '--json') opts.json = true;
35
+ else if (arg === '--help' || arg === '-h') opts.help = true;
36
+ else throw new Error(`unknown argument: ${arg}`);
37
+ }
38
+ return opts;
39
+ }
40
+
41
+ /**
42
+ * @param {ReturnType<typeof parseArgs>} opts
43
+ * @param {string} cwd
44
+ * @returns {string[]}
45
+ */
46
+ function resolveScopePaths(opts, cwd) {
47
+ if (opts.paths) return opts.paths;
48
+ const run = (args) => execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
49
+ try {
50
+ if (opts.type === 'branch') {
51
+ return run(['diff', '--name-only', `${opts.baseBranch}...HEAD`]).split('\n').filter(Boolean);
52
+ }
53
+ const tracked = run(['diff', '--name-only', 'HEAD']).split('\n').filter(Boolean);
54
+ const untracked = run(['ls-files', '--others', '--exclude-standard']).split('\n').filter(Boolean);
55
+ return [...new Set([...tracked, ...untracked])];
56
+ } catch {
57
+ return [];
58
+ }
59
+ }
60
+
61
+ /**
62
+ * @param {ReturnType<typeof parseArgs>} opts
63
+ * @param {string} [cwd]
64
+ * @param {Array<{ name: string, dir: string, scripts?: Record<string, string> }>} [packagesOverride]
65
+ * @returns {{ exitCode: number, plan: Record<string, unknown>, message: string }}
66
+ */
67
+ export function runSignals(opts, cwd = process.cwd(), packagesOverride) {
68
+ const paths = resolveScopePaths(opts, cwd);
69
+ const packages = packagesOverride ?? loadWorkspaces(cwd);
70
+ const { workspaces, unmatched } = mapPathsToWorkspaces(paths, packages);
71
+ const commands = suggestSignalCommands(workspaces);
72
+ const contractTouch = paths.some((p) => CONTRACT_HINT.test(p));
73
+
74
+ const notes = [];
75
+ if (contractTouch) {
76
+ notes.push('Contract/route files touched — verify OpenAPI route-parity (contracts lens).');
77
+ }
78
+ if (unmatched.length > 0) {
79
+ notes.push(`${unmatched.length} path(s) outside any workspace — review with root tooling.`);
80
+ }
81
+
82
+ const plan = {
83
+ scope_paths: paths,
84
+ workspaces: workspaces.map((w) => w.name),
85
+ commands,
86
+ notes,
87
+ };
88
+
89
+ if (opts.json) {
90
+ return { exitCode: 0, plan, message: JSON.stringify(plan, null, 2) };
91
+ }
92
+
93
+ const lines = [];
94
+ lines.push(`Signals pre-flight — ${paths.length} path(s) in scope`);
95
+ lines.push(`Workspaces: ${workspaces.length ? workspaces.map((w) => w.name).join(', ') : '(none)'}`);
96
+ lines.push('');
97
+ lines.push('Run these grounding commands, then convert failures to tentative findings:');
98
+ if (commands.length === 0) lines.push(' (no workspace scripts detected for the scope)');
99
+ for (const c of commands) lines.push(` ${c}`);
100
+ if (notes.length > 0) {
101
+ lines.push('');
102
+ lines.push('Notes:');
103
+ for (const n of notes) lines.push(` - ${n}`);
104
+ }
105
+
106
+ return { exitCode: 0, plan, message: lines.join('\n') };
107
+ }
108
+
109
+ function printHelp() {
110
+ console.log(`Usage: review signals [options]
111
+
112
+ Plan the deterministic grounding commands for a review scope.
113
+
114
+ Options:
115
+ --paths a,b Comma list of scoped paths (default: detect from git)
116
+ --type <t> uncommitted | branch (git detection mode; default: uncommitted)
117
+ --base-branch <b> Merge base for --type branch (default: main)
118
+ --json Machine-readable output
119
+ -h, --help Show this help
120
+ `);
121
+ }
122
+
123
+ function main() {
124
+ try {
125
+ const opts = parseArgs(process.argv.slice(2));
126
+ if (opts.help) {
127
+ printHelp();
128
+ process.exit(0);
129
+ }
130
+ const result = runSignals(opts);
131
+ console.log(result.message);
132
+ process.exit(result.exitCode);
133
+ } catch (err) {
134
+ console.error(/** @type {Error} */ (err).message);
135
+ process.exit(1);
136
+ }
137
+ }
138
+
139
+ const isMain =
140
+ process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
141
+
142
+ if (isMain) {
143
+ main();
144
+ }
@@ -0,0 +1,62 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mapPathsToWorkspaces, suggestSignalCommands } from './lib.mjs';
4
+ import { parseArgs, runSignals } from './signals.mjs';
5
+
6
+ const PACKAGES = [
7
+ { name: '@janus/persona', dir: 'services/persona', scripts: { typecheck: 'tsc', test: 'vitest' } },
8
+ { name: '@janus/mercury', dir: 'services/mercury', scripts: { test: 'vitest' } },
9
+ { name: '@janus/persona-contract', dir: 'packages/persona-contract', scripts: { build: 'tsc' } },
10
+ ];
11
+
12
+ test('mapPathsToWorkspaces matches the deepest owning workspace', () => {
13
+ const { workspaces, unmatched } = mapPathsToWorkspaces(
14
+ [
15
+ 'services/persona/src/routes/profile.ts',
16
+ 'services/mercury/src/index.ts',
17
+ 'docs/thorough-code-review.md',
18
+ ],
19
+ PACKAGES,
20
+ );
21
+ assert.deepEqual(
22
+ workspaces.map((w) => w.name),
23
+ ['@janus/mercury', '@janus/persona'],
24
+ );
25
+ assert.deepEqual(unmatched, ['docs/thorough-code-review.md']);
26
+ });
27
+
28
+ test('mapPathsToWorkspaces normalizes backslashes and dedupes', () => {
29
+ const { workspaces } = mapPathsToWorkspaces(
30
+ ['services\\persona\\a.ts', 'services\\persona\\b.ts'],
31
+ PACKAGES,
32
+ );
33
+ assert.equal(workspaces.length, 1);
34
+ assert.equal(workspaces[0].name, '@janus/persona');
35
+ });
36
+
37
+ test('suggestSignalCommands emits only scripts that exist, in run order', () => {
38
+ const commands = suggestSignalCommands([
39
+ { name: '@janus/persona', scripts: { typecheck: 'tsc', test: 'vitest' } },
40
+ { name: '@janus/mercury', scripts: { test: 'vitest' } },
41
+ ]);
42
+ assert.deepEqual(commands, [
43
+ 'npm run typecheck -w @janus/persona',
44
+ 'npm run test -w @janus/persona',
45
+ 'npm run test -w @janus/mercury',
46
+ ]);
47
+ });
48
+
49
+ test('runSignals plans commands and flags contract touches', () => {
50
+ const opts = parseArgs(['--paths', 'services/persona/src/routes/profile.ts', '--json']);
51
+ const result = runSignals(opts, process.cwd(), PACKAGES);
52
+ assert.equal(result.exitCode, 0);
53
+ assert.deepEqual(result.plan.workspaces, ['@janus/persona']);
54
+ assert.ok(result.plan.commands.includes('npm run typecheck -w @janus/persona'));
55
+ assert.ok(result.plan.notes.some((n) => n.includes('route-parity')));
56
+ });
57
+
58
+ test('runSignals reports paths outside any workspace', () => {
59
+ const opts = parseArgs(['--paths', 'scripts/review/lib.mjs']);
60
+ const result = runSignals(opts, process.cwd(), PACKAGES);
61
+ assert.ok(result.plan.notes.some((n) => n.includes('outside any workspace')));
62
+ });
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Score the active Forge session (L2 scorecard).
4
+ *
5
+ * Usage:
6
+ * forge score [--session <id>] [--write] [--json]
7
+ *
8
+ * Without --write: print JSON to stdout.
9
+ * With --write: also write scorecard.json + scorecard.md into the session dir.
10
+ */
11
+
12
+ import { loadSession, readActive } from './lib.mjs';
13
+ import { formatScorecardMarkdown, scoreSession, writeSessionScorecard } from './score.mjs';
14
+
15
+ const args = process.argv.slice(2);
16
+ if (args[0] === '--help') {
17
+ process.stdout.write(
18
+ 'Usage: forge score [--session <id>] [--write] [--json]\n' +
19
+ ' Scores session artifacts (spine, deferrals, product-loop, evidence, pace).\n' +
20
+ ' --write save scorecard.json + scorecard.md into the session dir\n' +
21
+ ' --json print JSON (default); omit for markdown summary on stdout\n',
22
+ );
23
+ process.exit(0);
24
+ }
25
+
26
+ let sessionId = null;
27
+ let write = false;
28
+ let asJson = true;
29
+ for (let i = 0; i < args.length; i += 1) {
30
+ if (args[i] === '--session' && args[i + 1]) {
31
+ sessionId = args[i + 1];
32
+ i += 1;
33
+ } else if (args[i] === '--write') {
34
+ write = true;
35
+ } else if (args[i] === '--markdown' || args[i] === '--md') {
36
+ asJson = false;
37
+ } else if (args[i] === '--json') {
38
+ asJson = true;
39
+ }
40
+ }
41
+
42
+ if (!sessionId) {
43
+ const active = readActive();
44
+ sessionId = active?.sessionId;
45
+ }
46
+ if (!sessionId) {
47
+ process.stderr.write('No active session. Run forge new first.\n');
48
+ process.exit(1);
49
+ }
50
+
51
+ const { dir, session } = loadSession(sessionId);
52
+ const result = write
53
+ ? writeSessionScorecard({ sessionDir: dir, session })
54
+ : { card: scoreSession({ sessionDir: dir, session }), jsonPath: null, mdPath: null };
55
+
56
+ const { card } = result;
57
+ if (asJson) {
58
+ process.stdout.write(`${JSON.stringify(card, null, 2)}\n`);
59
+ } else {
60
+ process.stdout.write(formatScorecardMarkdown(card));
61
+ }
62
+
63
+ if (write && result.mdPath) {
64
+ process.stderr.write(`Wrote ${result.mdPath}\n`);
65
+ }
66
+
67
+ // Non-zero if grade is D/F — useful in CI / finish hooks
68
+ process.exit(card.grade === 'D' || card.grade === 'F' ? 1 : 0);