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.
- package/README.md +255 -211
- package/bin/batch-runner.mjs +339 -0
- package/bin/cinema.mjs +320 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +168 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +32 -0
- package/bin/engine-cli.mjs +222 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +896 -1540
- package/bin/hooks/interactive-engine.mjs +509 -0
- package/bin/hooks/pre-tool-use.mjs +498 -0
- package/bin/hooks/subagent-stop-enforce.mjs +197 -0
- package/bin/journey.mjs +841 -0
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +136 -20
- package/bin/settings-hook.mjs +346 -0
- package/bin/tui.mjs +6 -3
- package/commands/adopt.md +4 -1
- package/commands/analytics.md +4 -1
- package/commands/audit-agents.md +110 -27
- package/commands/audit.md +20 -1
- package/commands/autopilot.md +84 -12
- package/commands/changelog.md +2 -1
- package/commands/ci-audit.md +2 -1
- package/commands/cleanup.md +161 -1
- package/commands/compat-audit.md +2 -1
- package/commands/deploy-info.md +63 -4
- package/commands/design-bench.md +6 -1
- package/commands/design-render-critique-loop.md +2 -1
- package/commands/design.md +1 -0
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/forge-stats.md +122 -2
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +3 -0
- package/commands/incident-response.md +2 -1
- package/commands/issue.md +24 -3
- package/commands/milestone.md +77 -12
- package/commands/optimize.md +2 -1
- package/commands/orchestrate/config.md +67 -0
- package/commands/orchestrate/phase-1-resolve.md +159 -0
- package/commands/orchestrate/phase-2-triage.md +126 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
- package/commands/orchestrate/phase-3-dependency.md +591 -0
- package/commands/orchestrate/phase-4-execution.md +962 -0
- package/commands/orchestrate/phase-5-cleanup.md +47 -0
- package/commands/orchestrate/phase-6-report.md +198 -0
- package/commands/orchestrate/safety.md +62 -0
- package/commands/orchestrate.md +25 -2027
- package/commands/pipeline-health/config.md +34 -0
- package/commands/pipeline-health/phase-1-context.md +180 -0
- package/commands/pipeline-health/phase-2-metrics.md +1729 -0
- package/commands/pipeline-health/phase-3-analyze.md +204 -0
- package/commands/pipeline-health/phase-4-proposals.md +117 -0
- package/commands/pipeline-health/phase-5-report.md +284 -0
- package/commands/pipeline-health/phase-6-summary.md +52 -0
- package/commands/pipeline-health.md +25 -2401
- package/commands/pipeline-resume.md +24 -1
- package/commands/pipeline-status.md +65 -0
- package/commands/qa-sweep.md +4 -1
- package/commands/quality-gate.md +326 -42
- package/commands/replay.md +2 -1
- package/commands/review-pr-agents/api.md +135 -0
- package/commands/review-pr-agents/auth.md +137 -0
- package/commands/review-pr-agents/billing.md +101 -0
- package/commands/review-pr-agents/concurrency.md +145 -0
- package/commands/review-pr-agents/database.md +103 -0
- package/commands/review-pr-agents/frontend.md +89 -0
- package/commands/review-pr-agents/infra.md +385 -0
- package/commands/review-pr-agents/protocols.md +185 -0
- package/commands/review-pr-agents/scraper.md +185 -0
- package/commands/review-pr-agents/security.md +406 -0
- package/commands/review-pr-agents.md +45 -1722
- package/commands/review-pr-staging.md +196 -40
- package/commands/review-pr.md +612 -56
- package/commands/rollback.md +2 -1
- package/commands/scope.md +2 -1
- package/commands/security-audit.md +2 -1
- package/commands/signal-planner.md +623 -0
- package/commands/sync-ecosystem.md +1 -0
- package/commands/test-gate.md +17 -1
- package/commands/validate.md +2 -1
- package/commands/work-on/build/architect.md +28 -0
- package/commands/work-on/build/context.md +2 -0
- package/commands/work-on/build/implement.md +19 -5
- package/commands/work-on/build/validate.md +62 -6
- package/commands/work-on/build.md +187 -30
- package/commands/work-on/close.md +132 -5
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +48 -4
- package/commands/work-on-monolithic.md +8 -2
- package/commands/work-on.md +247 -7
- package/docs/BRAND.md +16 -5
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +44 -42
- package/docs/FORGE-PROTOCOL.md +15 -1064
- package/docs/anthropic-devrel-outreach.md +2 -2
- package/docs/articles/command-decomposition-token-savings.md +214 -0
- package/docs/articles/model-release-playbook.md +131 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
- package/docs/claude-breakpoints.json +75 -5
- package/docs/design/abc-benchmark.md +1 -1
- package/docs/design/design-spec-schema.md +1 -1
- package/docs/design/divergent-generation.md +1 -1
- package/docs/design/eval-gate-runbook.md +178 -0
- package/docs/design/show-hn-draft.md +26 -1
- package/docs/design/token-cost-baseline-1298.md +59 -0
- package/docs/eval/README.md +117 -0
- package/docs/eval/corpus/seed-v1.json +6 -0
- package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
- package/docs/site/annotations-explained.md +24 -0
- package/docs/site/command-learning-path.md +51 -14
- package/docs/site/command-reference.md +339 -113
- package/docs/site/for-companies.md +95 -0
- package/docs/site/forge-annotation-protocol.md +12 -256
- package/docs/site/getting-started.md +58 -12
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +10 -2
- package/docs/spec/eval-run-result.md +147 -0
- package/docs/spec/forge-protocol-v1.md +441 -18
- package/docs/spec/label-state-machine.md +55 -0
- package/docs/spec/review-protocol.md +158 -0
- package/docs/spec-graph-schema.md +28 -0
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
- package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
- package/examples/forgedock-demo/README.md +41 -22
- package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
- package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
- package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
- package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
- package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
- package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
- package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
- package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
- package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
- package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
- package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
- package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
- package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
- package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
- package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
- package/examples/forgedock-demo/scripts/smoke.js +18 -1
- package/examples/forgedock-demo/src/db.js +36 -11
- package/examples/forgedock-demo/src/routes/notes.js +106 -9
- package/examples/forgedock-demo/src/server.js +4 -1
- package/package.json +12 -4
- package/packages/protocol/LICENSE +21 -0
- package/packages/protocol/src/cli.js +135 -0
- package/packages/protocol/src/emit.js +146 -0
- package/packages/protocol/src/index.js +43 -0
- package/packages/protocol/src/parse.js +229 -0
- package/packages/protocol/src/types.js +171 -0
- package/packages/protocol/src/validate.js +122 -0
- package/scripts/bench-topology-cost.mjs +372 -0
- package/scripts/check-command-docs-drift.sh +198 -0
- package/scripts/check-protocol-restatements.sh +176 -0
- package/scripts/check-registry/README.md +72 -0
- package/scripts/check-registry/manifest.json +6 -0
- package/scripts/code-index.sh +454 -0
- package/scripts/conformance-check.mjs +168 -0
- package/scripts/doctor-pipeline-state.sh +484 -0
- package/scripts/eval-gate-baseline.json +17 -0
- package/scripts/eval-gate-scorecard.mjs +242 -0
- package/scripts/eval-scorecard-aggregator.mjs +252 -0
- package/scripts/flaky-quarantine.sh +241 -0
- package/scripts/forge-annotation.sh +567 -0
- package/scripts/issue-dedup.sh +236 -0
- package/scripts/validate-annotation-node.mjs +233 -0
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
- 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
|
+
}
|