forgedock 1.0.22 → 1.1.2

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 (184) hide show
  1. package/README.md +255 -211
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/cinema.mjs +320 -0
  4. package/bin/demo.mjs +92 -7
  5. package/bin/engine/phases.mjs +168 -0
  6. package/bin/engine/projector.mjs +32 -0
  7. package/bin/engine/reconcile.mjs +11 -0
  8. package/bin/engine/runlog.mjs +79 -0
  9. package/bin/engine/state.mjs +32 -0
  10. package/bin/engine-cli.mjs +222 -0
  11. package/bin/engine.mjs +138 -0
  12. package/bin/forgedock.mjs +896 -1540
  13. package/bin/hooks/interactive-engine.mjs +509 -0
  14. package/bin/hooks/pre-tool-use.mjs +498 -0
  15. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  16. package/bin/journey.mjs +841 -0
  17. package/bin/registry.mjs +139 -0
  18. package/bin/report.mjs +447 -0
  19. package/bin/runner.mjs +136 -20
  20. package/bin/settings-hook.mjs +346 -0
  21. package/bin/tui.mjs +6 -3
  22. package/commands/adopt.md +4 -1
  23. package/commands/analytics.md +4 -1
  24. package/commands/audit-agents.md +110 -27
  25. package/commands/audit.md +20 -1
  26. package/commands/autopilot.md +84 -12
  27. package/commands/changelog.md +2 -1
  28. package/commands/ci-audit.md +2 -1
  29. package/commands/cleanup.md +161 -1
  30. package/commands/compat-audit.md +2 -1
  31. package/commands/deploy-info.md +63 -4
  32. package/commands/design-bench.md +6 -1
  33. package/commands/design-render-critique-loop.md +2 -1
  34. package/commands/design.md +1 -0
  35. package/commands/diagnose.md +2 -1
  36. package/commands/explain.md +2 -1
  37. package/commands/forge-stats.md +122 -2
  38. package/commands/forgedock-init.md +1 -1
  39. package/commands/geo-audit.md +3 -0
  40. package/commands/incident-response.md +2 -1
  41. package/commands/issue.md +24 -3
  42. package/commands/milestone.md +77 -12
  43. package/commands/optimize.md +2 -1
  44. package/commands/orchestrate/config.md +67 -0
  45. package/commands/orchestrate/phase-1-resolve.md +159 -0
  46. package/commands/orchestrate/phase-2-triage.md +126 -0
  47. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  48. package/commands/orchestrate/phase-3-dependency.md +591 -0
  49. package/commands/orchestrate/phase-4-execution.md +962 -0
  50. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  51. package/commands/orchestrate/phase-6-report.md +198 -0
  52. package/commands/orchestrate/safety.md +62 -0
  53. package/commands/orchestrate.md +25 -2027
  54. package/commands/pipeline-health/config.md +34 -0
  55. package/commands/pipeline-health/phase-1-context.md +180 -0
  56. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  57. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  58. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  59. package/commands/pipeline-health/phase-5-report.md +284 -0
  60. package/commands/pipeline-health/phase-6-summary.md +52 -0
  61. package/commands/pipeline-health.md +25 -2401
  62. package/commands/pipeline-resume.md +24 -1
  63. package/commands/pipeline-status.md +65 -0
  64. package/commands/qa-sweep.md +4 -1
  65. package/commands/quality-gate.md +326 -42
  66. package/commands/replay.md +2 -1
  67. package/commands/review-pr-agents/api.md +135 -0
  68. package/commands/review-pr-agents/auth.md +137 -0
  69. package/commands/review-pr-agents/billing.md +101 -0
  70. package/commands/review-pr-agents/concurrency.md +145 -0
  71. package/commands/review-pr-agents/database.md +103 -0
  72. package/commands/review-pr-agents/frontend.md +89 -0
  73. package/commands/review-pr-agents/infra.md +385 -0
  74. package/commands/review-pr-agents/protocols.md +185 -0
  75. package/commands/review-pr-agents/scraper.md +185 -0
  76. package/commands/review-pr-agents/security.md +406 -0
  77. package/commands/review-pr-agents.md +45 -1722
  78. package/commands/review-pr-staging.md +196 -40
  79. package/commands/review-pr.md +612 -56
  80. package/commands/rollback.md +2 -1
  81. package/commands/scope.md +2 -1
  82. package/commands/security-audit.md +2 -1
  83. package/commands/signal-planner.md +623 -0
  84. package/commands/sync-ecosystem.md +1 -0
  85. package/commands/test-gate.md +17 -1
  86. package/commands/validate.md +2 -1
  87. package/commands/work-on/build/architect.md +28 -0
  88. package/commands/work-on/build/context.md +2 -0
  89. package/commands/work-on/build/implement.md +19 -5
  90. package/commands/work-on/build/validate.md +62 -6
  91. package/commands/work-on/build.md +187 -30
  92. package/commands/work-on/close.md +132 -5
  93. package/commands/work-on/decompose.md +19 -1
  94. package/commands/work-on/investigate.md +117 -2
  95. package/commands/work-on/review.md +48 -4
  96. package/commands/work-on-monolithic.md +8 -2
  97. package/commands/work-on.md +247 -7
  98. package/docs/BRAND.md +16 -5
  99. package/docs/CI.md +2 -2
  100. package/docs/CODEX.md +36 -1
  101. package/docs/CONFIG.md +44 -42
  102. package/docs/FORGE-PROTOCOL.md +15 -1064
  103. package/docs/anthropic-devrel-outreach.md +2 -2
  104. package/docs/articles/command-decomposition-token-savings.md +214 -0
  105. package/docs/articles/model-release-playbook.md +131 -0
  106. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  107. package/docs/claude-breakpoints.json +75 -5
  108. package/docs/design/abc-benchmark.md +1 -1
  109. package/docs/design/design-spec-schema.md +1 -1
  110. package/docs/design/divergent-generation.md +1 -1
  111. package/docs/design/eval-gate-runbook.md +178 -0
  112. package/docs/design/show-hn-draft.md +26 -1
  113. package/docs/design/token-cost-baseline-1298.md +59 -0
  114. package/docs/eval/README.md +117 -0
  115. package/docs/eval/corpus/seed-v1.json +6 -0
  116. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  117. package/docs/site/annotations-explained.md +24 -0
  118. package/docs/site/command-learning-path.md +51 -14
  119. package/docs/site/command-reference.md +339 -113
  120. package/docs/site/for-companies.md +95 -0
  121. package/docs/site/forge-annotation-protocol.md +12 -256
  122. package/docs/site/getting-started.md +58 -12
  123. package/docs/site/github-is-the-memory.md +113 -0
  124. package/docs/site/how-it-works.md +3 -13
  125. package/docs/site/index.md +10 -2
  126. package/docs/spec/eval-run-result.md +147 -0
  127. package/docs/spec/forge-protocol-v1.md +441 -18
  128. package/docs/spec/label-state-machine.md +55 -0
  129. package/docs/spec/review-protocol.md +158 -0
  130. package/docs/spec-graph-schema.md +28 -0
  131. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  132. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  133. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  134. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  135. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  136. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  137. package/examples/forgedock-demo/README.md +41 -22
  138. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  139. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  140. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  141. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  142. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  143. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  144. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  145. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  146. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  147. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  148. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  149. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  150. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  151. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  152. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  153. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  154. package/examples/forgedock-demo/src/db.js +36 -11
  155. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  156. package/examples/forgedock-demo/src/server.js +4 -1
  157. package/package.json +12 -4
  158. package/packages/protocol/LICENSE +21 -0
  159. package/packages/protocol/src/cli.js +135 -0
  160. package/packages/protocol/src/emit.js +146 -0
  161. package/packages/protocol/src/index.js +43 -0
  162. package/packages/protocol/src/parse.js +229 -0
  163. package/packages/protocol/src/types.js +171 -0
  164. package/packages/protocol/src/validate.js +122 -0
  165. package/scripts/bench-topology-cost.mjs +372 -0
  166. package/scripts/check-command-docs-drift.sh +198 -0
  167. package/scripts/check-protocol-restatements.sh +176 -0
  168. package/scripts/check-registry/README.md +72 -0
  169. package/scripts/check-registry/manifest.json +6 -0
  170. package/scripts/code-index.sh +454 -0
  171. package/scripts/conformance-check.mjs +168 -0
  172. package/scripts/doctor-pipeline-state.sh +484 -0
  173. package/scripts/eval-gate-baseline.json +17 -0
  174. package/scripts/eval-gate-scorecard.mjs +242 -0
  175. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  176. package/scripts/flaky-quarantine.sh +241 -0
  177. package/scripts/forge-annotation.sh +567 -0
  178. package/scripts/issue-dedup.sh +236 -0
  179. package/scripts/validate-annotation-node.mjs +233 -0
  180. package/scripts/verify-env-vars.sh +18 -2
  181. package/scripts/verify-host-headers.sh +4 -4
  182. package/scripts/verify-route-registration.sh +2 -2
  183. package/scripts/worktree-lifecycle.sh +202 -0
  184. package/templates/devdocs/agent/using-forgedock.md +14 -0
@@ -0,0 +1,171 @@
1
+ /**
2
+ * FORGE Annotation Protocol v1.0 — type definitions and constants.
3
+ * @license MIT
4
+ */
5
+
6
+ /** @enum {string} Annotation categories */
7
+ export const Category = {
8
+ LIFECYCLE: 'lifecycle',
9
+ CROSS_ARTIFACT: 'cross_artifact',
10
+ CONTROL: 'control',
11
+ };
12
+
13
+ /** @enum {string} Sentinel states */
14
+ export const SentinelState = {
15
+ COMPLETE: 'complete',
16
+ PARTIAL: 'partial',
17
+ INTERRUPTED: 'interrupted',
18
+ };
19
+
20
+ /**
21
+ * All 13 reserved annotation types from spec §4.
22
+ * Spec ref: §4.1 lifecycle, §4.2 cross-artifact, §4.3 control/error markers.
23
+ */
24
+ export const RESERVED_TYPES = {
25
+ // §4.1 Lifecycle annotations
26
+ INVESTIGATOR: {
27
+ type: 'INVESTIGATOR',
28
+ category: Category.LIFECYCLE,
29
+ completionSentinel: 'INVESTIGATION:COMPLETE',
30
+ partialSentinel: null,
31
+ inlineValue: false,
32
+ requiredFields: ['Verdict', 'Confidence', 'Severity', 'Task Type', 'Decomposition Assessment'],
33
+ verdictValues: ['CONFIRMED', 'PARTIAL', 'INVALID'],
34
+ confidenceValues: ['HIGH', 'MEDIUM', 'LOW'],
35
+ severityValues: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'],
36
+ taskTypeValues: ['Bug Fix', 'Feature', 'Refactor', 'Maintenance', 'Investigation'],
37
+ },
38
+ DECOMPOSED: {
39
+ type: 'DECOMPOSED',
40
+ category: Category.LIFECYCLE,
41
+ completionSentinel: 'FORGE:DECOMPOSED:COMPLETE',
42
+ partialSentinel: null,
43
+ inlineValue: false,
44
+ requiredFields: [],
45
+ },
46
+ CONTRACT: {
47
+ type: 'CONTRACT',
48
+ category: Category.LIFECYCLE,
49
+ completionSentinel: null, // no explicit completion sentinel defined in spec §4.1
50
+ partialSentinel: null,
51
+ inlineValue: false,
52
+ requiredFields: ['Task type'],
53
+ },
54
+ CONTEXT: {
55
+ type: 'CONTEXT',
56
+ category: Category.LIFECYCLE,
57
+ completionSentinel: 'FORGE:CONTEXT:COMPLETE',
58
+ partialSentinel: 'FORGE:CONTEXT:PARTIAL',
59
+ inlineValue: false,
60
+ requiredFields: [],
61
+ },
62
+ ARCHITECT: {
63
+ type: 'ARCHITECT',
64
+ category: Category.LIFECYCLE,
65
+ completionSentinel: 'FORGE:ARCHITECT:COMPLETE',
66
+ partialSentinel: 'FORGE:ARCHITECT:PARTIAL',
67
+ inlineValue: false,
68
+ requiredFields: [],
69
+ },
70
+ BUILDER: {
71
+ type: 'BUILDER',
72
+ category: Category.LIFECYCLE,
73
+ completionSentinel: 'FORGE:BUILDER:COMPLETE',
74
+ partialSentinel: null,
75
+ inlineValue: false,
76
+ requiredFields: ['Branch', 'Commits', 'Files changed'],
77
+ },
78
+ REVIEWER: {
79
+ type: 'REVIEWER',
80
+ category: Category.LIFECYCLE,
81
+ completionSentinel: null, // spec §4.1 does not define a sentinel for REVIEWER
82
+ partialSentinel: null,
83
+ inlineValue: false,
84
+ requiredFields: ['Verdict'],
85
+ verdictValues: ['APPROVED', 'CHANGES_REQUESTED', 'COMMENTED'],
86
+ },
87
+ TRAJECTORY: {
88
+ type: 'TRAJECTORY',
89
+ category: Category.LIFECYCLE,
90
+ completionSentinel: null,
91
+ partialSentinel: null,
92
+ inlineValue: false,
93
+ requiredFields: [],
94
+ },
95
+
96
+ // §4.2 Cross-artifact annotations
97
+ KNOWLEDGE_GIST: {
98
+ type: 'KNOWLEDGE_GIST',
99
+ category: Category.CROSS_ARTIFACT,
100
+ completionSentinel: null,
101
+ partialSentinel: null,
102
+ inlineValue: true,
103
+ requiredFields: [],
104
+ },
105
+ MILESTONE_INDEX: {
106
+ type: 'MILESTONE_INDEX',
107
+ category: Category.CROSS_ARTIFACT,
108
+ completionSentinel: null,
109
+ partialSentinel: null,
110
+ inlineValue: true,
111
+ requiredFields: [],
112
+ },
113
+ PRIOR_GIST: {
114
+ type: 'PRIOR_GIST',
115
+ category: Category.CROSS_ARTIFACT,
116
+ completionSentinel: null,
117
+ partialSentinel: null,
118
+ inlineValue: true,
119
+ requiredFields: [],
120
+ },
121
+
122
+ // §4.3 Control and error markers
123
+ REVIEW_STARTED: {
124
+ type: 'REVIEW_STARTED',
125
+ category: Category.CONTROL,
126
+ completionSentinel: null,
127
+ partialSentinel: null,
128
+ inlineValue: false,
129
+ controlMarker: true,
130
+ requiredFields: [],
131
+ },
132
+ ANCESTRY_FAILED: {
133
+ type: 'ANCESTRY_FAILED',
134
+ category: Category.CONTROL,
135
+ completionSentinel: null,
136
+ partialSentinel: null,
137
+ inlineValue: false,
138
+ controlMarker: true,
139
+ requiredFields: [],
140
+ },
141
+ GATE_FAILED: {
142
+ type: 'GATE_FAILED',
143
+ category: Category.CONTROL,
144
+ completionSentinel: null,
145
+ partialSentinel: null,
146
+ inlineValue: false,
147
+ controlMarker: true,
148
+ requiredFields: [],
149
+ },
150
+ PUSH_BLOCKED: {
151
+ type: 'PUSH_BLOCKED',
152
+ category: Category.CONTROL,
153
+ completionSentinel: null,
154
+ partialSentinel: null,
155
+ inlineValue: false,
156
+ controlMarker: true,
157
+ requiredFields: [],
158
+ },
159
+ PUSH_FAILED: {
160
+ type: 'PUSH_FAILED',
161
+ category: Category.CONTROL,
162
+ completionSentinel: null,
163
+ partialSentinel: null,
164
+ inlineValue: false,
165
+ controlMarker: true,
166
+ requiredFields: [],
167
+ },
168
+ };
169
+
170
+ /** Set of all reserved type strings for fast lookup */
171
+ export const RESERVED_TYPE_NAMES = new Set(Object.keys(RESERVED_TYPES));
@@ -0,0 +1,122 @@
1
+ /**
2
+ * FORGE Annotation Protocol v1.0 — validator.
3
+ *
4
+ * validate(annotation) → { valid: boolean, errors: string[] }
5
+ *
6
+ * Checks:
7
+ * - Completion sentinel presence (§3.2) — for types that define one
8
+ * - Required field presence (§4.1 per-type field tables)
9
+ * - Required field enum values (Verdict, Confidence, Severity, Task Type)
10
+ * - Inline-value non-empty (§3.4)
11
+ * - Partial sentinel marking (§3.3) — generates a warning, not an error
12
+ *
13
+ * @license MIT
14
+ */
15
+
16
+ import { RESERVED_TYPES, SentinelState } from './types.js';
17
+
18
+ /**
19
+ * Validate a parsed annotation against the spec.
20
+ *
21
+ * @param {import('./index.js').ParsedAnnotation} annotation
22
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
23
+ */
24
+ export function validate(annotation) {
25
+ if (!annotation || typeof annotation !== 'object') {
26
+ throw new TypeError('validate() expects a ParsedAnnotation object');
27
+ }
28
+
29
+ const errors = [];
30
+ const warnings = [];
31
+ const { type, sentinelState, inlineValue, fields, isReserved, isControl } = annotation;
32
+
33
+ // Unknown types: tolerate per §7.2.4 — no validation errors
34
+ if (!isReserved) {
35
+ return { valid: true, errors: [], warnings: [`Type "${type}" is not a reserved type; skipping field validation (§7.2.4)`] };
36
+ }
37
+
38
+ const typeDef = RESERVED_TYPES[type];
39
+
40
+ // Control markers: presence is the signal; no body or fields to validate
41
+ if (isControl) {
42
+ return { valid: true, errors: [], warnings: [] };
43
+ }
44
+
45
+ // Inline-value form (§3.4): value must be non-empty
46
+ if (typeDef.inlineValue) {
47
+ if (inlineValue === null || inlineValue === '') {
48
+ errors.push(`Inline-value annotation "${type}" must carry a non-empty value (§3.4)`);
49
+ }
50
+ return { valid: errors.length === 0, errors, warnings };
51
+ }
52
+
53
+ // Completion sentinel check (§3.2): annotations with a defined sentinel must be complete.
54
+ // Both PARTIAL and INTERRUPTED states mean the annotation is not safely consumable.
55
+ if (typeDef.completionSentinel) {
56
+ if (sentinelState === SentinelState.PARTIAL) {
57
+ // Partial: producer was interrupted and marked it — consumer SHOULD request re-run (§3.3).
58
+ // Treat as invalid since consumers MUST NOT treat it as complete (§3.2).
59
+ errors.push(
60
+ `Annotation "${type}" is marked PARTIAL (${typeDef.partialSentinel ?? 'PARTIAL sentinel'}) — not safe to consume (§3.2/§3.3)`,
61
+ );
62
+ warnings.push(
63
+ `Annotation "${type}" is marked PARTIAL — consumer SHOULD request re-run (§3.3)`,
64
+ );
65
+ } else if (sentinelState === SentinelState.INTERRUPTED) {
66
+ errors.push(
67
+ `Annotation "${type}" is missing its completion sentinel "<!-- ${typeDef.completionSentinel} -->" (§3.2)`,
68
+ );
69
+ }
70
+ }
71
+
72
+ // Required field checks per type
73
+ for (const fieldName of typeDef.requiredFields) {
74
+ if (!(fieldName in fields) || fields[fieldName] === '') {
75
+ errors.push(`Required field "${fieldName}" is missing or empty in "${type}" annotation`);
76
+ }
77
+ }
78
+
79
+ // INVESTIGATOR-specific enum validation
80
+ if (type === 'INVESTIGATOR') {
81
+ if (fields['Verdict'] && !typeDef.verdictValues.includes(fields['Verdict'])) {
82
+ errors.push(
83
+ `Invalid Verdict "${fields['Verdict']}" — must be one of: ${typeDef.verdictValues.join(', ')}`,
84
+ );
85
+ }
86
+ if (fields['Confidence'] && !typeDef.confidenceValues.includes(fields['Confidence'])) {
87
+ errors.push(
88
+ `Invalid Confidence "${fields['Confidence']}" — must be one of: ${typeDef.confidenceValues.join(', ')}`,
89
+ );
90
+ }
91
+ if (fields['Severity'] && !typeDef.severityValues.includes(fields['Severity'])) {
92
+ errors.push(
93
+ `Invalid Severity "${fields['Severity']}" — must be one of: ${typeDef.severityValues.join(', ')}`,
94
+ );
95
+ }
96
+ if (fields['Task Type'] && !typeDef.taskTypeValues.includes(fields['Task Type'])) {
97
+ errors.push(
98
+ `Invalid Task Type "${fields['Task Type']}" — must be one of: ${typeDef.taskTypeValues.join(', ')}`,
99
+ );
100
+ }
101
+ // Decomposition Assessment must start with YES or NO
102
+ if (fields['Decomposition Assessment']) {
103
+ const da = fields['Decomposition Assessment'];
104
+ if (!/^(YES|NO)\b/.test(da)) {
105
+ errors.push(
106
+ `Decomposition Assessment must start with "YES" or "NO", got: "${da}"`,
107
+ );
108
+ }
109
+ }
110
+ }
111
+
112
+ // REVIEWER-specific enum validation
113
+ if (type === 'REVIEWER') {
114
+ if (fields['Verdict'] && !typeDef.verdictValues.includes(fields['Verdict'])) {
115
+ errors.push(
116
+ `Invalid Verdict "${fields['Verdict']}" — must be one of: ${typeDef.verdictValues.join(', ')}`,
117
+ );
118
+ }
119
+ }
120
+
121
+ return { valid: errors.length === 0, errors, warnings };
122
+ }
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/bench-topology-cost.mjs — end-to-end token-cost benchmark for pipeline topologies (#1279)
4
+ *
5
+ * Deterministic, zero-dependency aggregator for comparing per-issue token cost across
6
+ * ForgeDock pipeline topologies (e.g. "spawned" vs "inline-sequential").
7
+ *
8
+ * Measures the cost reduction claim from the agent topology refactor (#1254):
9
+ * eliminating 6-8 fresh-context establishments per standard run reduces token cost.
10
+ *
11
+ * Benchmark corpus: the 5 seeded issues in examples/forgedock-demo/
12
+ * Issue 1 — Bug / security: DELETE is missing an auth check
13
+ * Issue 2 — Feature / security: Safe filtering for GET /notes
14
+ * Issue 3 — Refactor: Extract the router module
15
+ * Issue 4 — Performance: O(1) findById
16
+ * Issue 5 — Docs: Add an API reference
17
+ *
18
+ * Input schema (runs.json):
19
+ * {
20
+ * "runs": [
21
+ * {
22
+ * "topology": "spawned | inline-sequential",
23
+ * "date": "ISO-8601",
24
+ * "claude_model": "claude-sonnet-4-5 | ...",
25
+ * "issues": [
26
+ * {
27
+ * "number": 1,
28
+ * "title": "string",
29
+ * "input_tokens": 12345,
30
+ * "output_tokens": 2345,
31
+ * "cache_read_tokens": 1000,
32
+ * "cache_write_tokens": 500,
33
+ * "quality_gate_passed": true
34
+ * }
35
+ * ]
36
+ * }
37
+ * ]
38
+ * }
39
+ *
40
+ * Measurements can be populated from:
41
+ * 1. Session log parsing (when bin/runner.mjs emits usage metadata — see #1295)
42
+ * 2. Manual capture from Claude Code session summaries
43
+ *
44
+ * Usage:
45
+ * node scripts/bench-topology-cost.mjs <runs.json> # read file, print scorecard JSON
46
+ * node scripts/bench-topology-cost.mjs - # read runs JSON from stdin
47
+ * cat runs.json | node scripts/bench-topology-cost.mjs # (no arg) read from stdin
48
+ *
49
+ * Exit codes:
50
+ * 0 = scorecard emitted successfully
51
+ * 1 = invalid input (bad JSON, missing required fields, unknown topology)
52
+ * 2 = quality gate regression detected (scorecard still emitted to stdout)
53
+ */
54
+
55
+ const CORPUS_ISSUES = [1, 2, 3, 4, 5];
56
+ const KNOWN_TOPOLOGIES = new Set(["spawned", "inline-sequential"]);
57
+ const TOKEN_FIELDS = ["input_tokens", "output_tokens", "cache_read_tokens", "cache_write_tokens"];
58
+
59
+ /**
60
+ * Total tokens billed for a single issue run.
61
+ * Cache reads are billed at a lower rate but still count against context budget.
62
+ * cache_write_tokens are not billed but indicate context establishment cost.
63
+ */
64
+ export function totalTokens(issue) {
65
+ return (issue.input_tokens || 0) + (issue.output_tokens || 0);
66
+ }
67
+
68
+ /**
69
+ * Total context establishment cost: input + cache_write (fresh-context proxy).
70
+ * Reducing cache_write_tokens is the primary signal for the topology refactor —
71
+ * fewer spawned agents means fewer fresh context establishments.
72
+ */
73
+ export function contextEstablishmentTokens(issue) {
74
+ return (issue.input_tokens || 0) + (issue.cache_write_tokens || 0);
75
+ }
76
+
77
+ /**
78
+ * Per-run aggregate: sum and per-issue breakdown.
79
+ */
80
+ export function aggregateRun(run) {
81
+ const issues = run.issues || [];
82
+ const perIssue = issues.map((iss) => ({
83
+ number: iss.number,
84
+ title: iss.title || `Issue #${iss.number}`,
85
+ total_tokens: totalTokens(iss),
86
+ context_tokens: contextEstablishmentTokens(iss),
87
+ input_tokens: iss.input_tokens || 0,
88
+ output_tokens: iss.output_tokens || 0,
89
+ cache_read_tokens: iss.cache_read_tokens || 0,
90
+ cache_write_tokens: iss.cache_write_tokens || 0,
91
+ quality_gate_passed: iss.quality_gate_passed ?? null,
92
+ }));
93
+
94
+ const totals = perIssue.reduce(
95
+ (acc, iss) => {
96
+ acc.total_tokens += iss.total_tokens;
97
+ acc.context_tokens += iss.context_tokens;
98
+ acc.input_tokens += iss.input_tokens;
99
+ acc.output_tokens += iss.output_tokens;
100
+ acc.cache_read_tokens += iss.cache_read_tokens;
101
+ acc.cache_write_tokens += iss.cache_write_tokens;
102
+ return acc;
103
+ },
104
+ {
105
+ total_tokens: 0,
106
+ context_tokens: 0,
107
+ input_tokens: 0,
108
+ output_tokens: 0,
109
+ cache_read_tokens: 0,
110
+ cache_write_tokens: 0,
111
+ },
112
+ );
113
+
114
+ const issueCount = perIssue.length;
115
+ const qualityGatePassed = perIssue.every((iss) => iss.quality_gate_passed !== false);
116
+ const qualityGateRegressions = perIssue.filter((iss) => iss.quality_gate_passed === false);
117
+
118
+ return {
119
+ topology: run.topology,
120
+ date: run.date || null,
121
+ claude_model: run.claude_model || null,
122
+ issue_count: issueCount,
123
+ per_issue: perIssue,
124
+ totals,
125
+ averages:
126
+ issueCount > 0
127
+ ? {
128
+ total_tokens: round(totals.total_tokens / issueCount),
129
+ context_tokens: round(totals.context_tokens / issueCount),
130
+ input_tokens: round(totals.input_tokens / issueCount),
131
+ output_tokens: round(totals.output_tokens / issueCount),
132
+ cache_read_tokens: round(totals.cache_read_tokens / issueCount),
133
+ cache_write_tokens: round(totals.cache_write_tokens / issueCount),
134
+ }
135
+ : null,
136
+ quality_gates: {
137
+ all_passed: qualityGatePassed,
138
+ regressions: qualityGateRegressions.map((iss) => iss.number),
139
+ },
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Compute delta between two aggregated runs (topology comparison).
145
+ * Positive delta means `after` uses MORE tokens than `before` (regression).
146
+ * Negative delta means `after` uses FEWER tokens (improvement).
147
+ */
148
+ export function computeDelta(before, after) {
149
+ if (!before || !after) return null;
150
+
151
+ const pct = (b, a) => (b === 0 ? null : round(((a - b) / b) * 100));
152
+
153
+ return {
154
+ total_tokens: {
155
+ before: before.totals.total_tokens,
156
+ after: after.totals.total_tokens,
157
+ delta: after.totals.total_tokens - before.totals.total_tokens,
158
+ delta_pct: pct(before.totals.total_tokens, after.totals.total_tokens),
159
+ },
160
+ context_tokens: {
161
+ before: before.totals.context_tokens,
162
+ after: after.totals.context_tokens,
163
+ delta: after.totals.context_tokens - before.totals.context_tokens,
164
+ delta_pct: pct(before.totals.context_tokens, after.totals.context_tokens),
165
+ },
166
+ cache_write_tokens: {
167
+ before: before.totals.cache_write_tokens,
168
+ after: after.totals.cache_write_tokens,
169
+ delta: after.totals.cache_write_tokens - before.totals.cache_write_tokens,
170
+ delta_pct: pct(before.totals.cache_write_tokens, after.totals.cache_write_tokens),
171
+ },
172
+ quality_gates_unchanged:
173
+ before.quality_gates.all_passed && after.quality_gates.all_passed,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Validate the runs file shape. Throws on any violation.
179
+ */
180
+ export function validate(data) {
181
+ if (!data || typeof data !== "object") throw new Error("runs file must be a JSON object");
182
+ if (!Array.isArray(data.runs) || data.runs.length === 0) {
183
+ throw new Error("runs file must contain a non-empty `runs` array");
184
+ }
185
+
186
+ for (let i = 0; i < data.runs.length; i++) {
187
+ const run = data.runs[i];
188
+ if (!run.topology) {
189
+ throw new Error(`runs[${i}] must have a "topology" field`);
190
+ }
191
+ if (!KNOWN_TOPOLOGIES.has(run.topology)) {
192
+ throw new Error(
193
+ `runs[${i}].topology "${run.topology}" is unknown. Valid values: ${[...KNOWN_TOPOLOGIES].join(", ")}`,
194
+ );
195
+ }
196
+ if (!Array.isArray(run.issues) || run.issues.length === 0) {
197
+ throw new Error(`runs[${i}] (topology: ${run.topology}) must have a non-empty "issues" array`);
198
+ }
199
+ if (run.issues.length !== CORPUS_ISSUES.length) {
200
+ process.stderr.write(
201
+ `WARNING: runs[${i}] (topology: ${run.topology}) has ${run.issues.length} issue(s) ` +
202
+ `but the standard corpus is ${CORPUS_ISSUES.length} issues. ` +
203
+ `Partial runs produce non-comparable deltas.\n`,
204
+ );
205
+ }
206
+ for (let j = 0; j < run.issues.length; j++) {
207
+ const iss = run.issues[j];
208
+ if (typeof iss.number !== "number") {
209
+ throw new Error(`runs[${i}].issues[${j}] must have a numeric "number" field`);
210
+ }
211
+ for (const field of TOKEN_FIELDS) {
212
+ if (iss[field] !== undefined && typeof iss[field] !== "number") {
213
+ throw new Error(
214
+ `runs[${i}].issues[${j}].${field} must be a number (got ${typeof iss[field]})`,
215
+ );
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Aggregate all runs and produce the scorecard.
224
+ * Throws on validation failure.
225
+ */
226
+ export function aggregate(data) {
227
+ validate(data);
228
+
229
+ const aggregated = data.runs.map(aggregateRun);
230
+
231
+ // Group by topology for comparison
232
+ const byTopology = {};
233
+ for (const run of aggregated) {
234
+ if (!byTopology[run.topology]) byTopology[run.topology] = [];
235
+ byTopology[run.topology].push(run);
236
+ }
237
+
238
+ // Pick the most recent run per topology for comparison
239
+ const latestByTopology = {};
240
+ for (const [topology, runs] of Object.entries(byTopology)) {
241
+ latestByTopology[topology] = runs[runs.length - 1];
242
+ }
243
+
244
+ // Compute delta: spawned (baseline) vs inline-sequential (after refactor)
245
+ const baseline = latestByTopology["spawned"] || null;
246
+ const refactored = latestByTopology["inline-sequential"] || null;
247
+ const delta = computeDelta(baseline, refactored);
248
+
249
+ // Quality gate regression check
250
+ const qualityRegressions = aggregated
251
+ .filter((r) => !r.quality_gates.all_passed)
252
+ .map((r) => ({ topology: r.topology, date: r.date, regressions: r.quality_gates.regressions }));
253
+
254
+ return {
255
+ corpus: {
256
+ description: "ForgeDock demo repo — 5 seeded issues (examples/forgedock-demo/)",
257
+ issue_numbers: CORPUS_ISSUES,
258
+ },
259
+ runs: aggregated,
260
+ topology_comparison: delta
261
+ ? {
262
+ baseline_topology: "spawned",
263
+ refactored_topology: "inline-sequential",
264
+ delta,
265
+ verdict:
266
+ delta.total_tokens.delta < 0
267
+ ? "IMPROVEMENT"
268
+ : delta.total_tokens.delta === 0
269
+ ? "NEUTRAL"
270
+ : "REGRESSION",
271
+ quality_gates_confirmed: delta.quality_gates_unchanged,
272
+ }
273
+ : null,
274
+ quality_gate_summary: {
275
+ all_runs_passed: qualityRegressions.length === 0,
276
+ regressions: qualityRegressions,
277
+ },
278
+ };
279
+ }
280
+
281
+ function round(x) {
282
+ return Math.round(x * 100) / 100;
283
+ }
284
+
285
+ async function readInput(arg) {
286
+ const { readFileSync } = await import("node:fs");
287
+ if (!arg || arg === "-") {
288
+ return readFileSync(0, "utf8"); // fd 0 = stdin
289
+ }
290
+ return readFileSync(arg, "utf8");
291
+ }
292
+
293
+ function printUsage() {
294
+ process.stdout.write(
295
+ [
296
+ "Usage:",
297
+ " node scripts/bench-topology-cost.mjs <runs.json>",
298
+ " node scripts/bench-topology-cost.mjs - (read from stdin)",
299
+ "",
300
+ "Input schema (runs.json):",
301
+ ' { "runs": [{ "topology": "spawned|inline-sequential", "date": "ISO-8601",',
302
+ ' "claude_model": "string", "issues": [{ "number": 1, "title": "string",',
303
+ ' "input_tokens": 0, "output_tokens": 0, "cache_read_tokens": 0,',
304
+ ' "cache_write_tokens": 0, "quality_gate_passed": true }] }] }',
305
+ "",
306
+ "Benchmark corpus: examples/forgedock-demo/ issues #1–#5",
307
+ "Topologies measured: spawned (baseline) vs inline-sequential (refactored)",
308
+ "",
309
+ "Output: JSON scorecard to stdout",
310
+ "Exit 0 = OK, Exit 1 = invalid input, Exit 2 = quality gate regression",
311
+ ].join("\n") + "\n",
312
+ );
313
+ }
314
+
315
+ async function main() {
316
+ const arg = process.argv[2];
317
+
318
+ if (arg === "--help" || arg === "-h") {
319
+ printUsage();
320
+ process.exit(0);
321
+ }
322
+
323
+ if (!arg) {
324
+ // No argument and no piped input — print usage
325
+ const isTTY = process.stdin.isTTY;
326
+ if (isTTY) {
327
+ printUsage();
328
+ process.exit(0);
329
+ }
330
+ }
331
+
332
+ let raw;
333
+ try {
334
+ raw = await readInput(arg);
335
+ } catch (e) {
336
+ process.stderr.write(`ERROR: cannot read input: ${e.message}\n`);
337
+ process.exit(1);
338
+ }
339
+
340
+ let data;
341
+ try {
342
+ data = JSON.parse(raw);
343
+ } catch (e) {
344
+ process.stderr.write(`ERROR: invalid JSON: ${e.message}\n`);
345
+ process.exit(1);
346
+ }
347
+
348
+ let scorecard;
349
+ try {
350
+ scorecard = aggregate(data);
351
+ } catch (e) {
352
+ process.stderr.write(`ERROR: ${e.message}\n`);
353
+ process.exit(1);
354
+ }
355
+
356
+ process.stdout.write(JSON.stringify(scorecard, null, 2) + "\n");
357
+
358
+ if (!scorecard.quality_gate_summary.all_runs_passed) {
359
+ const regressions = scorecard.quality_gate_summary.regressions;
360
+ process.stderr.write(
361
+ `WARNING: quality gate regressions detected in ${regressions.length} run(s). ` +
362
+ `Issues with failures: ${regressions.map((r) => `[${r.topology}] #${r.regressions.join(", #")}`).join("; ")}\n`,
363
+ );
364
+ process.exit(2);
365
+ }
366
+ }
367
+
368
+ // Run as CLI only when invoked directly, not when imported by tests.
369
+ import { fileURLToPath } from "node:url";
370
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
371
+ main();
372
+ }