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,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/batch-runner.mjs — Batch driver for the ForgeDock pipeline eval harness (#1285)
|
|
4
|
+
*
|
|
5
|
+
* Iterates a corpus of GitHub issue numbers, invokes runCommand() (from
|
|
6
|
+
* bin/runner.mjs) for each issue against the /work-on spec, and writes a
|
|
7
|
+
* structured per-run results file consumable by scripts/eval-gate-scorecard.mjs.
|
|
8
|
+
*
|
|
9
|
+
* This is the batch/corpus increment of the standalone runtime — the natural
|
|
10
|
+
* next step after bin/runner.mjs's single-command foundational increment (#1151).
|
|
11
|
+
*
|
|
12
|
+
* Exports (all pure, no SDK, no network):
|
|
13
|
+
* loadCorpus(path) → {corpus_version, issues}
|
|
14
|
+
* makeRunResult(issue, status, opts) → RunResult object
|
|
15
|
+
* classifyRunnerResult(result) → "success" | "failure" | "incomplete"
|
|
16
|
+
* writeResults(outputPath, results, meta) → void
|
|
17
|
+
* runCorpus(opts) → Promise<RunResult[]>
|
|
18
|
+
*
|
|
19
|
+
* CLI usage:
|
|
20
|
+
* node bin/batch-runner.mjs <corpus.json> [output.json]
|
|
21
|
+
* node bin/batch-runner.mjs <corpus.json> [output.json] --dry-run
|
|
22
|
+
*
|
|
23
|
+
* ANTHROPIC_API_KEY must be set for live runs.
|
|
24
|
+
* FORGEDOCK_MODEL overrides the model (default: runner.mjs default).
|
|
25
|
+
*
|
|
26
|
+
* Corpus file format (docs/spec/eval-run-result.md):
|
|
27
|
+
* { "corpus_version": "v1", "issues": [1001, 1002, 1003] }
|
|
28
|
+
*
|
|
29
|
+
* Output file format:
|
|
30
|
+
* { "corpus_version": "v1", "runs": [ ...RunResult ] }
|
|
31
|
+
*
|
|
32
|
+
* See docs/spec/eval-run-result.md for the full JSON schema.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
36
|
+
import { dirname, resolve } from "node:path";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Corpus loading
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load and validate a corpus JSON file.
|
|
44
|
+
*
|
|
45
|
+
* Throws a descriptive Error if the file is missing, invalid JSON, or does not
|
|
46
|
+
* contain a non-empty integer array at the "issues" key.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} corpusPath - Absolute or relative path to the corpus JSON.
|
|
49
|
+
* @returns {{ corpus_version: string|null, issues: number[] }}
|
|
50
|
+
*/
|
|
51
|
+
export function loadCorpus(corpusPath) {
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = readFileSync(corpusPath, "utf-8");
|
|
55
|
+
} catch (e) {
|
|
56
|
+
throw new Error(`Cannot read corpus file "${corpusPath}": ${e.message}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let data;
|
|
60
|
+
try {
|
|
61
|
+
data = JSON.parse(raw);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
throw new Error(`Corpus file "${corpusPath}" contains invalid JSON: ${e.message}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!data || typeof data !== "object") {
|
|
67
|
+
throw new Error(`Corpus file must be a JSON object; got ${typeof data}`);
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(data.issues) || data.issues.length === 0) {
|
|
70
|
+
throw new Error(`Corpus file must contain a non-empty "issues" array`);
|
|
71
|
+
}
|
|
72
|
+
for (let i = 0; i < data.issues.length; i++) {
|
|
73
|
+
const n = data.issues[i];
|
|
74
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`issues[${i}]: must be a positive integer (got ${JSON.stringify(n)})`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
corpus_version: data.corpus_version ?? null,
|
|
83
|
+
issues: data.issues,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Run-result construction helpers
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Map a runCommand() result status + stop reason to a scorecard status.
|
|
93
|
+
*
|
|
94
|
+
* runCommand() returns:
|
|
95
|
+
* status: "complete" | "incomplete" | "dry-run" | "max-iterations"
|
|
96
|
+
*
|
|
97
|
+
* We map these to the eval-result schema's status:
|
|
98
|
+
* "success" — run completed cleanly (status "complete")
|
|
99
|
+
* "incomplete" — run was cut short (status "incomplete" or "max-iterations" or "dry-run")
|
|
100
|
+
*
|
|
101
|
+
* The caller is responsible for "failure" — that requires inspecting GitHub
|
|
102
|
+
* state (whether the issue reached workflow:merged). For the purposes of the
|
|
103
|
+
* batch driver, a clean runner completion is classified as "success"; the
|
|
104
|
+
* scorecard consumer or a post-run hook can downgrade to "failure" by
|
|
105
|
+
* inspecting the issue label state.
|
|
106
|
+
*
|
|
107
|
+
* @param {{ status: string, stopReason?: string }} result
|
|
108
|
+
* @returns {"success" | "incomplete"}
|
|
109
|
+
*/
|
|
110
|
+
export function classifyRunnerResult(result) {
|
|
111
|
+
if (result.status === "complete") return "success";
|
|
112
|
+
return "incomplete";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Construct a per-run result object conforming to docs/spec/eval-run-result.md.
|
|
117
|
+
*
|
|
118
|
+
* @param {number} issue - GitHub issue number.
|
|
119
|
+
* @param {"success"|"failure"|"incomplete"|"error"} status
|
|
120
|
+
* @param {object} [opts]
|
|
121
|
+
* @param {number} [opts.wallClockMs] - Wall-clock duration (ms).
|
|
122
|
+
* @param {number} [opts.interventionCount] - Human intervention count.
|
|
123
|
+
* @param {number|null} [opts.cost] - Token cost (null until #1255).
|
|
124
|
+
* @param {number} [opts.iterations] - Tool-loop iteration count.
|
|
125
|
+
* @param {string} [opts.stopReason] - Anthropic stop_reason or "max_iterations".
|
|
126
|
+
* @param {string|null} [opts.error] - Error message when status === "error".
|
|
127
|
+
* @param {string} [opts.specVersion] - Package version at run time.
|
|
128
|
+
* @param {string} [opts.model] - Model used.
|
|
129
|
+
* @param {string} [opts.runAt] - ISO-8601 timestamp.
|
|
130
|
+
* @returns {object}
|
|
131
|
+
*/
|
|
132
|
+
export function makeRunResult(issue, status, opts = {}) {
|
|
133
|
+
return {
|
|
134
|
+
issue,
|
|
135
|
+
status,
|
|
136
|
+
wallClockMs: opts.wallClockMs ?? 0,
|
|
137
|
+
interventionCount: opts.interventionCount ?? 0,
|
|
138
|
+
cost: opts.cost !== undefined ? opts.cost : null,
|
|
139
|
+
iterations: opts.iterations ?? null,
|
|
140
|
+
stopReason: opts.stopReason ?? null,
|
|
141
|
+
error: opts.error ?? null,
|
|
142
|
+
specVersion: opts.specVersion ?? null,
|
|
143
|
+
model: opts.model ?? null,
|
|
144
|
+
runAt: opts.runAt ?? new Date().toISOString(),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Output writing
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Serialize run results to a JSON file.
|
|
154
|
+
* Creates parent directories as needed.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} outputPath - Absolute or relative path to write.
|
|
157
|
+
* @param {object[]} results - Array of per-run result objects.
|
|
158
|
+
* @param {object} [meta] - Top-level metadata (corpus_version, spec_version, model, corpus_size).
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
161
|
+
export function writeResults(outputPath, results, meta = {}) {
|
|
162
|
+
const resolved = resolve(outputPath);
|
|
163
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
164
|
+
const payload = {
|
|
165
|
+
corpus_version: meta.corpus_version ?? null,
|
|
166
|
+
corpus_size: meta.corpus_size ?? null,
|
|
167
|
+
spec_version: meta.spec_version ?? null,
|
|
168
|
+
model: meta.model ?? null,
|
|
169
|
+
generated_at: new Date().toISOString(),
|
|
170
|
+
runs: results,
|
|
171
|
+
};
|
|
172
|
+
writeFileSync(resolved, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Batch orchestrator
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Run the full corpus headlessly.
|
|
181
|
+
*
|
|
182
|
+
* For each issue number in `corpus.issues`, invokes runCommand() against the
|
|
183
|
+
* /work-on spec in a shared working directory, records a per-run result, and
|
|
184
|
+
* returns the full array of results.
|
|
185
|
+
*
|
|
186
|
+
* Does NOT write output itself — callers should call writeResults() on the
|
|
187
|
+
* returned array. The CLI entry point below does this automatically.
|
|
188
|
+
*
|
|
189
|
+
* @param {object} opts
|
|
190
|
+
* @param {string} opts.commandsDir - Absolute path to commands/.
|
|
191
|
+
* @param {{ corpus_version: string|null, issues: number[] }} opts.corpus
|
|
192
|
+
* @param {string} [opts.cwd] - Working directory for runner (default process.cwd()).
|
|
193
|
+
* @param {string} [opts.apiKey] - Anthropic API key.
|
|
194
|
+
* @param {string} [opts.model] - Model id.
|
|
195
|
+
* @param {number} [opts.maxIterations] - Per-issue tool-loop bound.
|
|
196
|
+
* @param {boolean} [opts.dryRun] - If true, no API calls are made.
|
|
197
|
+
* @param {{log: Function, error?: Function}} [opts.logger]
|
|
198
|
+
* @returns {Promise<object[]>} - Array of per-run result objects.
|
|
199
|
+
*/
|
|
200
|
+
export async function runCorpus(opts = {}) {
|
|
201
|
+
const {
|
|
202
|
+
commandsDir,
|
|
203
|
+
corpus,
|
|
204
|
+
cwd = process.cwd(),
|
|
205
|
+
apiKey = process.env.ANTHROPIC_API_KEY,
|
|
206
|
+
model,
|
|
207
|
+
maxIterations,
|
|
208
|
+
dryRun = false,
|
|
209
|
+
logger = console,
|
|
210
|
+
} = opts;
|
|
211
|
+
|
|
212
|
+
// Lazy import of runCommand so this module stays import-safe without the SDK.
|
|
213
|
+
const { runCommand } = await import("./runner.mjs");
|
|
214
|
+
|
|
215
|
+
const results = [];
|
|
216
|
+
|
|
217
|
+
for (const issue of corpus.issues) {
|
|
218
|
+
logger.log(`[batch-runner] Starting issue #${issue}…`);
|
|
219
|
+
const startMs = Date.now();
|
|
220
|
+
let result;
|
|
221
|
+
try {
|
|
222
|
+
result = await runCommand({
|
|
223
|
+
commandsDir,
|
|
224
|
+
commandName: "work-on",
|
|
225
|
+
args: [String(issue)],
|
|
226
|
+
cwd,
|
|
227
|
+
apiKey,
|
|
228
|
+
...(model ? { model } : {}),
|
|
229
|
+
...(maxIterations !== undefined ? { maxIterations } : {}),
|
|
230
|
+
dryRun,
|
|
231
|
+
logger,
|
|
232
|
+
});
|
|
233
|
+
const wallClockMs = Date.now() - startMs;
|
|
234
|
+
const status = classifyRunnerResult(result);
|
|
235
|
+
results.push(
|
|
236
|
+
makeRunResult(issue, status, {
|
|
237
|
+
wallClockMs,
|
|
238
|
+
interventionCount: 0,
|
|
239
|
+
cost: null, // wired in once #1255 lands
|
|
240
|
+
iterations: result.iterations ?? null,
|
|
241
|
+
stopReason: result.stopReason ?? null,
|
|
242
|
+
model: model ?? result.model ?? null,
|
|
243
|
+
runAt: new Date().toISOString(),
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
logger.log(`[batch-runner] Issue #${issue} → ${status} (${wallClockMs}ms)`);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
const wallClockMs = Date.now() - startMs;
|
|
249
|
+
results.push(
|
|
250
|
+
makeRunResult(issue, "error", {
|
|
251
|
+
wallClockMs,
|
|
252
|
+
interventionCount: 0,
|
|
253
|
+
cost: null,
|
|
254
|
+
error: e.message,
|
|
255
|
+
model: model ?? null,
|
|
256
|
+
runAt: new Date().toISOString(),
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
logger.error
|
|
260
|
+
? logger.error(`[batch-runner] Issue #${issue} → error: ${e.message}`)
|
|
261
|
+
: logger.log(`[batch-runner] Issue #${issue} → error: ${e.message}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return results;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// CLI entry point
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
async function main() {
|
|
273
|
+
const [, , corpusArg, outputArg, ...flags] = process.argv;
|
|
274
|
+
const dryRun = flags.includes("--dry-run");
|
|
275
|
+
|
|
276
|
+
if (!corpusArg) {
|
|
277
|
+
process.stderr.write(
|
|
278
|
+
"Usage: node bin/batch-runner.mjs <corpus.json> [output.json] [--dry-run]\n",
|
|
279
|
+
);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Resolve paths relative to cwd of the calling shell, not __dirname.
|
|
284
|
+
const corpusPath = resolve(corpusArg);
|
|
285
|
+
const outputPath = outputArg ? resolve(outputArg) : resolve("eval-results.json");
|
|
286
|
+
|
|
287
|
+
let corpus;
|
|
288
|
+
try {
|
|
289
|
+
corpus = loadCorpus(corpusPath);
|
|
290
|
+
} catch (e) {
|
|
291
|
+
process.stderr.write(`ERROR: ${e.message}\n`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// commandsDir: sibling of bin/ at the repo root.
|
|
296
|
+
const { fileURLToPath } = await import("node:url");
|
|
297
|
+
const { join } = await import("node:path");
|
|
298
|
+
const binDir = dirname(fileURLToPath(import.meta.url));
|
|
299
|
+
const commandsDir = join(binDir, "..", "commands");
|
|
300
|
+
|
|
301
|
+
// Read package version for specVersion field.
|
|
302
|
+
let specVersion = null;
|
|
303
|
+
try {
|
|
304
|
+
const pkgPath = join(binDir, "..", "package.json");
|
|
305
|
+
specVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? null;
|
|
306
|
+
} catch {
|
|
307
|
+
// non-fatal — specVersion stays null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log(
|
|
311
|
+
`[batch-runner] Running ${corpus.issues.length} issue(s) from corpus "${corpus.corpus_version ?? "(no version)"}"${dryRun ? " (dry-run)" : ""}`,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const results = await runCorpus({
|
|
315
|
+
commandsDir,
|
|
316
|
+
corpus,
|
|
317
|
+
dryRun,
|
|
318
|
+
logger: console,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const meta = {
|
|
322
|
+
corpus_version: corpus.corpus_version,
|
|
323
|
+
corpus_size: corpus.issues.length,
|
|
324
|
+
spec_version: specVersion,
|
|
325
|
+
model: results.find((r) => r.model)?.model ?? null,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
writeResults(outputPath, results, meta);
|
|
329
|
+
console.log(`[batch-runner] Results written to ${outputPath}`);
|
|
330
|
+
|
|
331
|
+
const nSuccess = results.filter((r) => r.status === "success").length;
|
|
332
|
+
const nTotal = results.length;
|
|
333
|
+
console.log(`[batch-runner] ${nSuccess}/${nTotal} runs succeeded`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
import { fileURLToPath } from "node:url";
|
|
337
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
338
|
+
main();
|
|
339
|
+
}
|
package/bin/cinema.mjs
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/cinema.mjs — Cinematic rendering primitives for ForgeDock's TUI.
|
|
3
|
+
*
|
|
4
|
+
* Zero-dependency. Pure presentation: gradients, the block-art F mark, and
|
|
5
|
+
* (in Task 2) shimmer / reveal / progress animation primitives.
|
|
6
|
+
*
|
|
7
|
+
* Two independent gates (spec: degradation ladder):
|
|
8
|
+
* colorMode() — 'truecolor' | '256' | 'none' (NO_COLOR, TTY, TERM)
|
|
9
|
+
* motionEnabled() — animation frames on/off (TTY, FORGE_NO_MOTION, CI, --fast)
|
|
10
|
+
*
|
|
11
|
+
* All functions take env/stdout/argv as parameters (with process defaults)
|
|
12
|
+
* so tests never need module-cache busting.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Palette (spec: Chrome & Ember)
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export const CHROME_STOPS = ["#f7f3ea", "#d9cfba", "#a99a82", "#6e6252"];
|
|
20
|
+
export const EMBER_STOPS = ["#ff4d00", "#ff8c1a", "#ffd166"];
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Gates
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Decide the color capability of the terminal.
|
|
28
|
+
* @returns {'truecolor'|'256'|'none'}
|
|
29
|
+
*/
|
|
30
|
+
export function colorMode(env = process.env, stdout = process.stdout) {
|
|
31
|
+
if (stdout.isTTY !== true) return "none";
|
|
32
|
+
if (env.NO_COLOR) return "none";
|
|
33
|
+
if (env.TERM === "dumb") return "none";
|
|
34
|
+
const ct = (env.COLORTERM || "").toLowerCase();
|
|
35
|
+
if (ct.includes("truecolor") || ct.includes("24bit")) return "truecolor";
|
|
36
|
+
if ((env.TERM || "").includes("256")) return "256";
|
|
37
|
+
// Windows Terminal / modern conhost support truecolor but don't set COLORTERM.
|
|
38
|
+
if (process.platform === "win32") return "truecolor";
|
|
39
|
+
return "256";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Decide whether animation frames run. Independent of color:
|
|
44
|
+
* NO_COLOR keeps motion (monochrome choreography); non-TTY/CI/--fast kill it.
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function motionEnabled(
|
|
48
|
+
argv = process.argv,
|
|
49
|
+
env = process.env,
|
|
50
|
+
stdout = process.stdout,
|
|
51
|
+
) {
|
|
52
|
+
if (stdout.isTTY !== true) return false;
|
|
53
|
+
if (env.FORGE_NO_MOTION) return false;
|
|
54
|
+
if (env.CI) return false;
|
|
55
|
+
if (argv.includes("--fast")) return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Gradient math
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
function hexToRgb(hex) {
|
|
64
|
+
const h = hex.replace("#", "");
|
|
65
|
+
return [
|
|
66
|
+
parseInt(h.slice(0, 2), 16),
|
|
67
|
+
parseInt(h.slice(2, 4), 16),
|
|
68
|
+
parseInt(h.slice(4, 6), 16),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sample a multi-stop gradient at position t ∈ [0,1].
|
|
74
|
+
* @returns {[number, number, number]} RGB triple.
|
|
75
|
+
*/
|
|
76
|
+
export function sampleGradient(stops, t) {
|
|
77
|
+
const rgb = stops.map(hexToRgb);
|
|
78
|
+
const clamped = Math.min(Math.max(t, 0), 1);
|
|
79
|
+
const seg = Math.min(Math.floor(clamped * (rgb.length - 1)), rgb.length - 2);
|
|
80
|
+
const localT = clamped * (rgb.length - 1) - seg;
|
|
81
|
+
const [a, b] = [rgb[seg], rgb[seg + 1]];
|
|
82
|
+
const mix = (x, y) => Math.round(x + (y - x) * localT);
|
|
83
|
+
return [mix(a[0], b[0]), mix(a[1], b[1]), mix(a[2], b[2])];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Quantize an RGB triple to the xterm-256 6×6×6 color cube. */
|
|
87
|
+
function to256([r, g, b]) {
|
|
88
|
+
const q = (v) => Math.round((v / 255) * 5);
|
|
89
|
+
return 16 + 36 * q(r) + 6 * q(g) + q(b);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Foreground escape for an RGB triple in the given mode ('' for none). */
|
|
93
|
+
function fg(rgb, mode) {
|
|
94
|
+
if (mode === "none") return "";
|
|
95
|
+
if (mode === "256") return `\x1b[38;5;${to256(rgb)}m`;
|
|
96
|
+
return `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Render one line of text with a per-character horizontal gradient.
|
|
101
|
+
* Spaces stay uncolored. `phase` shifts the gradient start (for diagonals).
|
|
102
|
+
* In 'none' mode returns the input text verbatim (no reset appended).
|
|
103
|
+
*/
|
|
104
|
+
export function gradientLine(text, stops, mode, phase = 0) {
|
|
105
|
+
if (mode === "none") return text;
|
|
106
|
+
const chars = [...text];
|
|
107
|
+
const n = Math.max(chars.length - 1, 1);
|
|
108
|
+
let out = "";
|
|
109
|
+
for (let i = 0; i < chars.length; i++) {
|
|
110
|
+
if (chars[i] === " ") {
|
|
111
|
+
out += " ";
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const t = Math.min(i / n + phase, 1);
|
|
115
|
+
out += fg(sampleGradient(stops, t), mode) + chars[i];
|
|
116
|
+
}
|
|
117
|
+
return out + "\x1b[0m";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Render a block of lines with a diagonal gradient (each row's phase shifts
|
|
122
|
+
* slightly, approximating the logo's 135° gradient).
|
|
123
|
+
* @returns {string[]}
|
|
124
|
+
*/
|
|
125
|
+
export function gradientBlock(lines, stops, mode) {
|
|
126
|
+
return lines.map((line, row) => gradientLine(line, stops, mode, row * 0.06));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// The mark — block-art interpretation of the ForgeDock "F" (design approved
|
|
131
|
+
// from the 2026-07-03 brainstorm mockups; geometry may be pixel-tuned).
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
export const HERO_MARK = [
|
|
135
|
+
" ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
|
|
136
|
+
" ▄▄██████████████████",
|
|
137
|
+
" ▄█████████████▀▀▀▀▀▀▀▀▀",
|
|
138
|
+
" ▄██████████▀▀",
|
|
139
|
+
" ▀▀▀▀▀ ▄▄███████▀",
|
|
140
|
+
" ▄████████▀",
|
|
141
|
+
" ▄██████▀",
|
|
142
|
+
" ▀▀▀▀",
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
export const COMPACT_MARK = [
|
|
146
|
+
" ▄▄████████",
|
|
147
|
+
" ▄█████▀▀▀▀▀",
|
|
148
|
+
" ▀▀ ▄████▀",
|
|
149
|
+
" ▀▀▀",
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Render the chrome-gradient mark.
|
|
154
|
+
* @param {'hero'|'compact'} size
|
|
155
|
+
* @param {'truecolor'|'256'|'none'} mode
|
|
156
|
+
* @returns {string[]}
|
|
157
|
+
*/
|
|
158
|
+
export function renderMark(size, mode) {
|
|
159
|
+
const art = size === "hero" ? HERO_MARK : COMPACT_MARK;
|
|
160
|
+
if (mode === "none") return [...art];
|
|
161
|
+
return gradientBlock(art, CHROME_STOPS, mode);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Ember-gradient text shorthand (wordmarks, act headers). */
|
|
165
|
+
export function ember(text, mode) {
|
|
166
|
+
return gradientLine(text, EMBER_STOPS, mode);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Promise-based sleep for animation pacing. */
|
|
170
|
+
export function sleep(ms) {
|
|
171
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Animation primitives (Task 2)
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
import { box } from "./tui.mjs";
|
|
179
|
+
|
|
180
|
+
const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
181
|
+
|
|
182
|
+
/** Ember-tinted status glyph for a result row. */
|
|
183
|
+
function statusGlyph(ok, mode) {
|
|
184
|
+
const rgb = ok ? [255, 179, 71] : [224, 112, 80]; // amber / ember-red
|
|
185
|
+
const glyph = ok ? "✔" : "✖";
|
|
186
|
+
if (mode === "none") return glyph;
|
|
187
|
+
return `${fg(rgb, mode)}${glyph}\x1b[0m`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Dim wrapper honoring the mode gate. */
|
|
191
|
+
function dimText(s, mode) {
|
|
192
|
+
return mode === "none" ? s : `\x1b[2m${s}\x1b[22m`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Badge like [high] / [med] / [low], colorized per confidence. */
|
|
196
|
+
function badgeText(badge, mode) {
|
|
197
|
+
const label = badge === "medium" ? "med" : badge;
|
|
198
|
+
if (mode === "none") return `[${label}]`;
|
|
199
|
+
const rgb = badge === "high" ? [125, 219, 132] : badge === "medium" ? [232, 192, 96] : [224, 112, 80];
|
|
200
|
+
return `${fg(rgb, mode)}[${label}]\x1b[0m`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Ember-bordered fix card (wraps tui.box; border color applied when able). */
|
|
204
|
+
export function fixCard(lines, mode) {
|
|
205
|
+
const boxed = box(lines.map((l) => ` ${l} `), { title: "fix" });
|
|
206
|
+
if (mode === "none") return boxed;
|
|
207
|
+
const ember = fg([255, 107, 53], mode);
|
|
208
|
+
return boxed
|
|
209
|
+
.split("\n")
|
|
210
|
+
.map((l) => l.replace(/[╭╮╰╯─│]/gu, (m) => `${ember}${m}\x1b[0m`))
|
|
211
|
+
.join("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function formatRow(label, res, mode) {
|
|
215
|
+
const parts = [` ${statusGlyph(res.ok, mode)} ${label.padEnd(16)}`];
|
|
216
|
+
if (res.detail) parts.push(dimText(String(res.detail), mode));
|
|
217
|
+
if (res.badge) parts.push(badgeText(res.badge, mode));
|
|
218
|
+
return parts.join(" ");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Reveal rows one at a time: spinner while `run()` is in flight, settling to
|
|
223
|
+
* a ✔/✖ line (+ optional fix card). Without motion: run, print final line.
|
|
224
|
+
* Every row's `run` executes regardless of motion — this is display-only.
|
|
225
|
+
*/
|
|
226
|
+
export async function revealRows(
|
|
227
|
+
rows,
|
|
228
|
+
{ mode, motion, writer = process.stdout, interval = 80, minDisplayMs = 120 } = {},
|
|
229
|
+
) {
|
|
230
|
+
const results = [];
|
|
231
|
+
for (const row of rows) {
|
|
232
|
+
let res;
|
|
233
|
+
if (!motion) {
|
|
234
|
+
res = await row.run();
|
|
235
|
+
} else {
|
|
236
|
+
let frame = 0;
|
|
237
|
+
const spinnerColor = fg([255, 107, 53], mode);
|
|
238
|
+
const reset = mode === "none" ? "" : "\x1b[0m";
|
|
239
|
+
writer.write(` ${spinnerColor}${BRAILLE[0]}${reset} ${row.label}\n`);
|
|
240
|
+
const timer = setInterval(() => {
|
|
241
|
+
frame = (frame + 1) % BRAILLE.length;
|
|
242
|
+
writer.write(`\x1b[1A\x1b[2K ${spinnerColor}${BRAILLE[frame]}${reset} ${row.label}\n`);
|
|
243
|
+
}, interval);
|
|
244
|
+
const started = Date.now();
|
|
245
|
+
try {
|
|
246
|
+
res = await row.run();
|
|
247
|
+
const elapsed = Date.now() - started;
|
|
248
|
+
if (elapsed < minDisplayMs) await sleep(minDisplayMs - elapsed);
|
|
249
|
+
} finally {
|
|
250
|
+
clearInterval(timer);
|
|
251
|
+
}
|
|
252
|
+
writer.write("\x1b[1A\x1b[2K");
|
|
253
|
+
}
|
|
254
|
+
writer.write(formatRow(row.label, res, mode) + "\n");
|
|
255
|
+
if (!res.ok && res.fix && res.fix.length > 0) {
|
|
256
|
+
writer.write(fixCard(res.fix, mode) + "\n");
|
|
257
|
+
}
|
|
258
|
+
results.push(res);
|
|
259
|
+
}
|
|
260
|
+
return results;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* One-pass highlight sweep over gradient block art, settling to the static
|
|
265
|
+
* gradientBlock render. In-place redraw via cursor-up; never alt-screen.
|
|
266
|
+
*/
|
|
267
|
+
export async function shimmer(
|
|
268
|
+
lines,
|
|
269
|
+
stops,
|
|
270
|
+
{ mode, motion, writer = process.stdout, frames = 14, interval = 55 } = {},
|
|
271
|
+
) {
|
|
272
|
+
const settled = gradientBlock(lines, stops, mode);
|
|
273
|
+
if (!motion || mode === "none") {
|
|
274
|
+
writer.write(settled.join("\n") + "\n");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const width = Math.max(...lines.map((l) => l.length));
|
|
278
|
+
for (let f = 0; f <= frames; f++) {
|
|
279
|
+
const band = (f / frames) * (width + 16) - 8;
|
|
280
|
+
const frameLines = lines.map((line, row) => {
|
|
281
|
+
const chars = [...line];
|
|
282
|
+
let out = "";
|
|
283
|
+
for (let i = 0; i < chars.length; i++) {
|
|
284
|
+
if (chars[i] === " ") {
|
|
285
|
+
out += " ";
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const t = Math.min(i / Math.max(chars.length - 1, 1) + row * 0.06, 1);
|
|
289
|
+
let rgb = sampleGradient(stops, t);
|
|
290
|
+
const dist = Math.abs(i - band + row * 1.5);
|
|
291
|
+
if (dist < 6) {
|
|
292
|
+
const boost = 1 - dist / 6;
|
|
293
|
+
rgb = rgb.map((v) => Math.round(v + (255 - v) * boost));
|
|
294
|
+
}
|
|
295
|
+
out += `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${chars[i]}`;
|
|
296
|
+
}
|
|
297
|
+
return out + "\x1b[0m";
|
|
298
|
+
});
|
|
299
|
+
if (f > 0) writer.write(`\x1b[${lines.length}A`);
|
|
300
|
+
writer.write(frameLines.join("\n") + "\n");
|
|
301
|
+
await sleep(interval);
|
|
302
|
+
}
|
|
303
|
+
writer.write(`\x1b[${lines.length}A`);
|
|
304
|
+
writer.write(settled.join("\n") + "\n");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* One static frame of the molten progress bar: solid fill with a ▓▒ leading
|
|
309
|
+
* edge and dim ░ remainder. Caller redraws with cursor-up for animation.
|
|
310
|
+
*/
|
|
311
|
+
export function moltenBar(current, total, { width = 24, mode } = {}) {
|
|
312
|
+
const ratio = total === 0 ? 1 : Math.min(current / total, 1);
|
|
313
|
+
const filled = Math.round(ratio * width);
|
|
314
|
+
const solidLen = Math.max(filled - 2, 0);
|
|
315
|
+
const edge = "▓▒".slice(0, filled - solidLen);
|
|
316
|
+
const body = "█".repeat(solidLen) + edge;
|
|
317
|
+
const rest = "░".repeat(Math.max(width - filled, 0));
|
|
318
|
+
if (mode === "none") return `[${body}${rest}]`;
|
|
319
|
+
return gradientLine(body, EMBER_STOPS, mode) + dimText(rest, mode);
|
|
320
|
+
}
|