@entelligentsia/forgecli 1.0.21 → 1.0.36
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/CHANGELOG.md +346 -0
- package/README.md +2 -0
- package/dist/CHANGELOG-forge-plugin.md +281 -0
- package/dist/bin/argv.d.ts +2 -2
- package/dist/bin/argv.js +25 -0
- package/dist/bin/argv.js.map +1 -1
- package/dist/bin/forge.js +12 -0
- package/dist/bin/forge.js.map +1 -1
- package/dist/bin/init.d.ts +23 -0
- package/dist/bin/init.js +123 -0
- package/dist/bin/init.js.map +1 -0
- package/dist/bin/uninstall.d.ts +20 -0
- package/dist/bin/uninstall.js +141 -0
- package/dist/bin/uninstall.js.map +1 -0
- package/dist/extensions/forgecli/claude-bootstrap/bootstrap.d.ts +40 -0
- package/dist/extensions/forgecli/claude-bootstrap/bootstrap.js +593 -0
- package/dist/extensions/forgecli/claude-bootstrap/bootstrap.js.map +1 -0
- package/dist/extensions/forgecli/claude-bootstrap/settings-merge.d.ts +46 -0
- package/dist/extensions/forgecli/claude-bootstrap/settings-merge.js +245 -0
- package/dist/extensions/forgecli/claude-bootstrap/settings-merge.js.map +1 -0
- package/dist/extensions/forgecli/claude-bootstrap/uninstall.d.ts +23 -0
- package/dist/extensions/forgecli/claude-bootstrap/uninstall.js +215 -0
- package/dist/extensions/forgecli/claude-bootstrap/uninstall.js.map +1 -0
- package/dist/extensions/forgecli/dashboard/component.js +10 -7
- package/dist/extensions/forgecli/dashboard/component.js.map +1 -1
- package/dist/extensions/forgecli/forge-tools.d.ts +1 -0
- package/dist/extensions/forgecli/forge-tools.js +73 -0
- package/dist/extensions/forgecli/forge-tools.js.map +1 -1
- package/dist/extensions/forgecli/lib/forge-root.d.ts +5 -0
- package/dist/extensions/forgecli/lib/forge-root.js +14 -1
- package/dist/extensions/forgecli/lib/forge-root.js.map +1 -1
- package/dist/extensions/forgecli/orchestrators/bug/bug-body.d.ts +1 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-body.js +65 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-body.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-id.d.ts +23 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-id.js +140 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-id.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-phase-dispatch.d.ts +54 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-phase-dispatch.js +349 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-phase-dispatch.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-phases.d.ts +8 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-phases.js +60 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-phases.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-state.d.ts +14 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-state.js +100 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-state.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-triage-routing.d.ts +72 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-triage-routing.js +204 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-triage-routing.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-verdict-loop.d.ts +38 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-verdict-loop.js +166 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-verdict-loop.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-verdict.d.ts +3 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-verdict.js +55 -0
- package/dist/extensions/forgecli/orchestrators/bug/bug-verdict.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/bug/run-bug-command.d.ts +7 -0
- package/dist/extensions/forgecli/orchestrators/bug/run-bug-command.js +293 -0
- package/dist/extensions/forgecli/orchestrators/bug/run-bug-command.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/bug/run-bug-pipeline.d.ts +2 -0
- package/dist/extensions/forgecli/orchestrators/bug/run-bug-pipeline.js +501 -0
- package/dist/extensions/forgecli/orchestrators/bug/run-bug-pipeline.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/bug/run-bug-types.d.ts +41 -0
- package/dist/extensions/forgecli/orchestrators/bug/run-bug-types.js +5 -0
- package/dist/extensions/forgecli/orchestrators/bug/run-bug-types.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-entry.d.ts +43 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-entry.js +85 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-entry.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-misc.d.ts +8 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-misc.js +37 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-misc.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-notify.d.ts +28 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-notify.js +45 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-notify.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-transcript-session.d.ts +26 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-transcript-session.js +75 -0
- package/dist/extensions/forgecli/orchestrators/common/orchestrator-transcript-session.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/common/summary-recovery.d.ts +24 -0
- package/dist/extensions/forgecli/orchestrators/common/summary-recovery.js +37 -0
- package/dist/extensions/forgecli/orchestrators/common/summary-recovery.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/fix-bug.d.ts +9 -92
- package/dist/extensions/forgecli/orchestrators/fix-bug.js +23 -1695
- package/dist/extensions/forgecli/orchestrators/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/orchestrators/run-sprint.d.ts +3 -12
- package/dist/extensions/forgecli/orchestrators/run-sprint.js +97 -270
- package/dist/extensions/forgecli/orchestrators/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/orchestrators/run-task.d.ts +10 -214
- package/dist/extensions/forgecli/orchestrators/run-task.js +31 -1481
- package/dist/extensions/forgecli/orchestrators/run-task.js.map +1 -1
- package/dist/extensions/forgecli/orchestrators/sprint/sprint-ceremony.d.ts +33 -0
- package/dist/extensions/forgecli/orchestrators/sprint/sprint-ceremony.js +135 -0
- package/dist/extensions/forgecli/orchestrators/sprint/sprint-ceremony.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/sprint/sprint-state.d.ts +18 -0
- package/dist/extensions/forgecli/orchestrators/sprint/sprint-state.js +55 -0
- package/dist/extensions/forgecli/orchestrators/sprint/sprint-state.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/task/run-task-command.d.ts +9 -0
- package/dist/extensions/forgecli/orchestrators/task/run-task-command.js +174 -0
- package/dist/extensions/forgecli/orchestrators/task/run-task-command.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/task/run-task-pipeline.d.ts +2 -0
- package/dist/extensions/forgecli/orchestrators/task/run-task-pipeline.js +494 -0
- package/dist/extensions/forgecli/orchestrators/task/run-task-pipeline.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/task/run-task-types.d.ts +62 -0
- package/dist/extensions/forgecli/orchestrators/task/run-task-types.js +5 -0
- package/dist/extensions/forgecli/orchestrators/task/run-task-types.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/task/task-body.d.ts +4 -0
- package/dist/extensions/forgecli/orchestrators/task/task-body.js +48 -0
- package/dist/extensions/forgecli/orchestrators/task/task-body.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/task/task-events.d.ts +63 -0
- package/dist/extensions/forgecli/orchestrators/task/task-events.js +185 -0
- package/dist/extensions/forgecli/orchestrators/task/task-events.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/task/task-gates.d.ts +34 -0
- package/dist/extensions/forgecli/orchestrators/task/task-gates.js +78 -0
- package/dist/extensions/forgecli/orchestrators/task/task-gates.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/task/task-phase-dispatch.d.ts +42 -0
- package/dist/extensions/forgecli/orchestrators/task/task-phase-dispatch.js +370 -0
- package/dist/extensions/forgecli/orchestrators/task/task-phase-dispatch.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/task/task-phases.d.ts +14 -0
- package/dist/extensions/forgecli/orchestrators/task/task-phases.js +26 -0
- package/dist/extensions/forgecli/orchestrators/task/task-phases.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/task/task-record.d.ts +9 -0
- package/dist/extensions/forgecli/orchestrators/task/task-record.js +58 -0
- package/dist/extensions/forgecli/orchestrators/task/task-record.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/task/task-state.d.ts +14 -0
- package/dist/extensions/forgecli/orchestrators/task/task-state.js +35 -0
- package/dist/extensions/forgecli/orchestrators/task/task-state.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/task/task-verdict-loop.d.ts +36 -0
- package/dist/extensions/forgecli/orchestrators/task/task-verdict-loop.js +152 -0
- package/dist/extensions/forgecli/orchestrators/task/task-verdict-loop.js.map +1 -0
- package/dist/extensions/forgecli/update/forge-update-command.js +10 -7
- package/dist/extensions/forgecli/update/forge-update-command.js.map +1 -1
- package/dist/forge-payload/.base-pack/commands/approve.md +2 -2
- package/dist/forge-payload/.base-pack/commands/check-agent.md +2 -2
- package/dist/forge-payload/.base-pack/commands/collate.md +2 -2
- package/dist/forge-payload/.base-pack/commands/commit.md +2 -2
- package/dist/forge-payload/.base-pack/commands/enhance.md +2 -2
- package/dist/forge-payload/.base-pack/commands/fix-bug.md +2 -2
- package/dist/forge-payload/.base-pack/commands/implement.md +2 -2
- package/dist/forge-payload/.base-pack/commands/init.md +278 -0
- package/dist/forge-payload/.base-pack/commands/new-sprint.md +2 -2
- package/dist/forge-payload/.base-pack/commands/plan-sprint.md +2 -2
- package/dist/forge-payload/.base-pack/commands/plan.md +2 -2
- package/dist/forge-payload/.base-pack/commands/retro.md +2 -2
- package/dist/forge-payload/.base-pack/commands/review-code.md +2 -2
- package/dist/forge-payload/.base-pack/commands/review-plan.md +2 -2
- package/dist/forge-payload/.base-pack/commands/run-sprint.md +2 -2
- package/dist/forge-payload/.base-pack/commands/run-task.md +2 -2
- package/dist/forge-payload/.base-pack/commands/validate.md +2 -2
- package/dist/forge-payload/.base-pack/workflows/_fragments/event-emission-schema.md +4 -0
- package/dist/forge-payload/.base-pack/workflows/_fragments/event-vocabulary.md +88 -0
- package/dist/forge-payload/.base-pack/workflows/collator_agent.md +5 -6
- package/dist/forge-payload/.base-pack/workflows/commit_task.md +41 -38
- package/dist/forge-payload/.base-pack/workflows/implement_plan.md +3 -3
- package/dist/forge-payload/.base-pack/workflows/migrate_structural.md +1 -1
- package/dist/forge-payload/.base-pack/workflows-js/wfl-fix-bug.js +42 -6
- package/dist/forge-payload/.base-pack/workflows-js/wfl-init.js +449 -0
- package/dist/forge-payload/.base-pack/workflows-js/wfl-run-task.js +32 -1
- package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
- package/dist/forge-payload/.schemas/enum-catalog.json +2 -2
- package/dist/forge-payload/.schemas/event.schema.json +8 -3
- package/dist/forge-payload/.schemas/migrations.json +141 -0
- package/dist/forge-payload/commands/add-pipeline.md +1 -1
- package/dist/forge-payload/commands/add-task.md +3 -3
- package/dist/forge-payload/commands/ask.md +1 -1
- package/dist/forge-payload/commands/check-agent.md +1 -1
- package/dist/forge-payload/commands/config.md +1 -1
- package/dist/forge-payload/commands/health.md +1 -1
- package/dist/forge-payload/commands/init.md +62 -7
- package/dist/forge-payload/commands/rebuild.md +3 -3
- package/dist/forge-payload/commands/remove.md +1 -1
- package/dist/forge-payload/commands/repair.md +1 -1
- package/dist/forge-payload/commands/report-bug.md +1 -1
- package/dist/forge-payload/commands/status.md +1 -1
- package/dist/forge-payload/commands/update.md +3 -3
- package/dist/forge-payload/hooks/lib/common.cjs +228 -0
- package/dist/forge-payload/hooks/lib/plugin-detection.cjs +106 -0
- package/dist/forge-payload/hooks/lib/update-msg.cjs +23 -0
- package/dist/forge-payload/hooks/lib/update-url.cjs +46 -0
- package/dist/forge-payload/hooks/lib/write-registry.js +53 -0
- package/dist/forge-payload/init/discovery/discover-database.md +32 -0
- package/dist/forge-payload/init/discovery/discover-processes.md +31 -0
- package/dist/forge-payload/init/discovery/discover-routing.md +31 -0
- package/dist/forge-payload/init/discovery/discover-stack.md +33 -0
- package/dist/forge-payload/init/discovery/discover-testing.md +34 -0
- package/dist/forge-payload/init/generation/generate-commands.md +171 -0
- package/dist/forge-payload/init/generation/generate-kb-doc.md +60 -0
- package/dist/forge-payload/init/generation/generate-knowledge-base.md +56 -0
- package/dist/forge-payload/init/generation/generate-persona.md +73 -0
- package/dist/forge-payload/init/generation/generate-personas.md +54 -0
- package/dist/forge-payload/init/generation/generate-skill.md +66 -0
- package/dist/forge-payload/init/generation/generate-skills.md +36 -0
- package/dist/forge-payload/init/generation/generate-template.md +60 -0
- package/dist/forge-payload/init/generation/generate-templates.md +39 -0
- package/dist/forge-payload/init/generation/generate-tools.md +133 -0
- package/dist/forge-payload/init/generation/generate-workflows.md +78 -0
- package/dist/forge-payload/init/phases/phase-1-collect.md +10 -2
- package/dist/forge-payload/init/phases/phase-3-materialize.md +1 -1
- package/dist/forge-payload/init/phases/phase-4-register.md +8 -0
- package/dist/forge-payload/init/workflow-gen-plan.json +17 -0
- package/dist/forge-payload/integrity.json +16 -16
- package/dist/forge-payload/meta/store-schema/event.schema.md +7 -0
- package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +4 -0
- package/dist/forge-payload/meta/workflows/_fragments/event-vocabulary.md +88 -0
- package/dist/forge-payload/meta/workflows/meta-collate.md +5 -6
- package/dist/forge-payload/meta/workflows/meta-commit.md +46 -43
- package/dist/forge-payload/meta/workflows/meta-fix-bug.md +7 -2
- package/dist/forge-payload/meta/workflows/meta-implement.md +3 -3
- package/dist/forge-payload/meta/workflows/meta-migrate.md +1 -1
- package/dist/forge-payload/meta/workflows/meta-orchestrate.md +4 -1
- package/dist/forge-payload/schemas/enum-catalog.json +2 -2
- package/dist/forge-payload/schemas/event.schema.json +8 -3
- package/dist/forge-payload/schemas/structure-manifest.json +5 -12
- package/dist/forge-payload/tools/commit-task.cjs +218 -0
- package/dist/forge-payload/tools/forge-preflight.cjs +268 -0
- package/dist/forge-payload/tools/lib/paths.cjs +12 -11
- package/dist/forge-payload/tools/lib/pricing.cjs +31 -11
- package/dist/forge-payload/tools/query-logger.cjs +34 -0
- package/dist/forge-payload/tools/store-cli.cjs +6 -1
- package/dist/forge-payload/tools/substitute-placeholders.cjs +5 -6
- package/package.json +2 -2
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// commit-task.cjs — deterministic commit choreography (forge-engineering#40).
|
|
4
|
+
//
|
|
5
|
+
// The commit phase was the most expensive phase of the SDLC pipeline
|
|
6
|
+
// (15–31% of run input tokens across instrumented runs) because an LLM
|
|
7
|
+
// re-derived deterministic choreography turn-by-turn: gate, state check,
|
|
8
|
+
// staging-set discovery, boundary verification, commit, terminal status
|
|
9
|
+
// transition. This tool owns the whole sequence; the agent supplies only the
|
|
10
|
+
// commit message (the single judgement-worthy step).
|
|
11
|
+
//
|
|
12
|
+
// Staging-set derivation (the commit boundary mirrors the task boundary):
|
|
13
|
+
// 1. record.path — the task/bug artifact directory (always staged)
|
|
14
|
+
// 2. record.summaries.implementation.files_changed — provenance recorded
|
|
15
|
+
// by the implement phase (PHASE_SUMMARY_SCHEMA.files_changed)
|
|
16
|
+
// 3. --also <path> extras (each validated to stay inside the project root)
|
|
17
|
+
//
|
|
18
|
+
// Usage:
|
|
19
|
+
// node commit-task.cjs --task <id> | --bug <id>
|
|
20
|
+
// --message <msg> commit subject/body (required unless --dry-run)
|
|
21
|
+
// [--trailer <line>] optional Co-authored-by trailer
|
|
22
|
+
// [--also <path>] extra path to stage (repeatable)
|
|
23
|
+
// [--skip-gate] skip preflight-gate (orchestrator already ran it)
|
|
24
|
+
// [--force] bypass the status precondition
|
|
25
|
+
// [--dry-run] print the staging plan as JSON; no writes
|
|
26
|
+
//
|
|
27
|
+
// Exit codes: 0 ok | 1 failure (message on stderr).
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
const { spawnSync } = require('node:child_process');
|
|
32
|
+
const { findProjectRoot } = require('./lib/project-root.cjs');
|
|
33
|
+
|
|
34
|
+
const ALLOWED_STATUS = { task: 'approved', bug: 'in-progress' };
|
|
35
|
+
const TERMINAL_STATUS = { task: 'committed', bug: 'fixed' };
|
|
36
|
+
|
|
37
|
+
function fail(msg) {
|
|
38
|
+
console.error(`commit-task: ${msg}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const opts = { also: [] };
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const a = argv[i];
|
|
46
|
+
switch (a) {
|
|
47
|
+
case '--task': opts.entityKind = 'task'; opts.recordId = argv[++i]; break;
|
|
48
|
+
case '--bug': opts.entityKind = 'bug'; opts.recordId = argv[++i]; break;
|
|
49
|
+
case '--message': opts.message = argv[++i]; break;
|
|
50
|
+
case '--trailer': opts.trailer = argv[++i]; break;
|
|
51
|
+
case '--also': opts.also.push(argv[++i]); break;
|
|
52
|
+
case '--skip-gate': opts.skipGate = true; break;
|
|
53
|
+
case '--force': opts.force = true; break;
|
|
54
|
+
case '--dry-run': opts.dryRun = true; break;
|
|
55
|
+
default: fail(`unknown argument: ${a}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!opts.entityKind || !opts.recordId) {
|
|
59
|
+
fail('usage: commit-task.cjs --task <id> | --bug <id> --message <msg> [--trailer <line>] [--also <path>]... [--skip-gate] [--force] [--dry-run]');
|
|
60
|
+
}
|
|
61
|
+
if (!opts.dryRun && (!opts.message || !opts.message.trim())) {
|
|
62
|
+
fail('--message is required (the commit message is the only input the agent supplies)');
|
|
63
|
+
}
|
|
64
|
+
return opts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function git(root, args, { allowFail = false } = {}) {
|
|
68
|
+
const r = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
69
|
+
if (r.status !== 0 && !allowFail) {
|
|
70
|
+
fail(`git ${args.join(' ')} failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
71
|
+
}
|
|
72
|
+
return r;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Resolve a repo-relative path and reject anything escaping the project root.
|
|
76
|
+
function insideRoot(root, p) {
|
|
77
|
+
const abs = path.resolve(root, p);
|
|
78
|
+
const rel = path.relative(root, abs);
|
|
79
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return null;
|
|
80
|
+
return rel;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
85
|
+
const root = findProjectRoot();
|
|
86
|
+
if (!root) fail('no .forge/config.json found above the current directory');
|
|
87
|
+
|
|
88
|
+
// 1. Record read (store facade — never raw .forge/store writes).
|
|
89
|
+
const store = require('./store.cjs');
|
|
90
|
+
const record = opts.entityKind === 'task' ? store.getTask(opts.recordId) : store.getBug(opts.recordId);
|
|
91
|
+
if (!record) fail(`${opts.entityKind} ${opts.recordId} not found in the store`);
|
|
92
|
+
|
|
93
|
+
// 2. Status precondition (deterministic version of the Pipeline Step Guard).
|
|
94
|
+
const allowed = ALLOWED_STATUS[opts.entityKind];
|
|
95
|
+
if (record.status !== allowed && !opts.force) {
|
|
96
|
+
const hint = opts.entityKind === 'task' ? "; /forge:approve must complete first" : '';
|
|
97
|
+
fail(`× ${opts.recordId} is in state '${record.status}' — commit requires '${allowed}'${hint}. Use --force to bypass (operator-gated, user-invoked re-runs only).`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 3. Preflight gate (skippable when the orchestrator already ran it).
|
|
101
|
+
if (!opts.skipGate) {
|
|
102
|
+
const gate = spawnSync(process.execPath,
|
|
103
|
+
[path.join(__dirname, 'preflight-gate.cjs'), '--phase', 'commit', `--${opts.entityKind}`, opts.recordId],
|
|
104
|
+
{ cwd: root, encoding: 'utf8' });
|
|
105
|
+
if (gate.status !== 0) {
|
|
106
|
+
fail(`preflight gate failed (exit ${gate.status}):\n${(gate.stderr || gate.stdout || '').trim()}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 4. Staging set: artifact dir + implementation provenance + --also extras.
|
|
111
|
+
const stage = [];
|
|
112
|
+
const warnings = [];
|
|
113
|
+
if (record.path) {
|
|
114
|
+
const rel = insideRoot(root, record.path);
|
|
115
|
+
if (rel && fs.existsSync(path.join(root, rel))) stage.push(rel);
|
|
116
|
+
else warnings.push(`artifact dir missing on disk: ${record.path}`);
|
|
117
|
+
}
|
|
118
|
+
const provenance = record.summaries?.implementation?.files_changed;
|
|
119
|
+
if (Array.isArray(provenance) && provenance.length > 0) {
|
|
120
|
+
for (const p of provenance) {
|
|
121
|
+
const rel = insideRoot(root, p);
|
|
122
|
+
if (rel === null) { warnings.push(`provenance path outside the project root, skipped: ${p}`); continue; }
|
|
123
|
+
if (!fs.existsSync(path.join(root, rel))) {
|
|
124
|
+
// Deleted files are legitimate changes — git add stages deletions of
|
|
125
|
+
// tracked paths; only warn when git knows nothing about the path.
|
|
126
|
+
const known = git(root, ['ls-files', '--error-unmatch', '--', rel], { allowFail: true });
|
|
127
|
+
if (known.status !== 0) { warnings.push(`provenance path not found (skipped): ${p}`); continue; }
|
|
128
|
+
}
|
|
129
|
+
stage.push(rel);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
warnings.push('no files_changed provenance in summaries.implementation — staging the artifact dir only. Pass source files via --also if the task changed code.');
|
|
133
|
+
}
|
|
134
|
+
for (const p of opts.also) {
|
|
135
|
+
const rel = insideRoot(root, p);
|
|
136
|
+
if (rel === null) fail(`--also path outside the project root: ${p}`);
|
|
137
|
+
stage.push(rel);
|
|
138
|
+
}
|
|
139
|
+
let stageSet = [...new Set(stage)];
|
|
140
|
+
if (stageSet.length === 0) fail('nothing to stage — no artifact dir, no provenance, no --also paths');
|
|
141
|
+
|
|
142
|
+
// Pre-filter gitignored paths (live-run finding, forge-engineering#40):
|
|
143
|
+
// `git add` of the whole set is all-or-nothing — one ignored path aborts
|
|
144
|
+
// everything. check-ignore each path; warn-skip the ignored ones.
|
|
145
|
+
const ignored = [];
|
|
146
|
+
for (const rel of stageSet) {
|
|
147
|
+
const ci = git(root, ['check-ignore', '-q', '--', rel], { allowFail: true });
|
|
148
|
+
if (ci.status === 0) ignored.push(rel);
|
|
149
|
+
}
|
|
150
|
+
if (ignored.length > 0) {
|
|
151
|
+
warnings.push(`gitignored path(s) skipped from the staging set: ${ignored.join(', ')} — track them or stage explicitly with git add -f outside this tool.`);
|
|
152
|
+
stageSet = stageSet.filter((p) => !ignored.includes(p));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const w of warnings) console.error(`commit-task: warning: ${w}`);
|
|
156
|
+
|
|
157
|
+
if (opts.dryRun) {
|
|
158
|
+
process.stdout.write(JSON.stringify({ dryRun: true, entityKind: opts.entityKind, recordId: opts.recordId, stage: stageSet }, null, 2) + '\n');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 5. Commit-boundary guard: a pre-populated index means someone else's
|
|
163
|
+
// changes would be swept into this commit. Abort loudly (Iron Law:
|
|
164
|
+
// the commit boundary mirrors the task boundary).
|
|
165
|
+
const preStaged = git(root, ['diff', '--cached', '--name-only']).stdout.trim();
|
|
166
|
+
if (preStaged) {
|
|
167
|
+
fail(`index already has staged changes — refusing to sweep them into the ${opts.recordId} commit:\n${preStaged}\nUnstage them (git reset) or commit them separately, then re-run.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 6. Terminal status transition helper — through store-cli (single source
|
|
171
|
+
// of truth for transition legality — never reimplemented here).
|
|
172
|
+
const target = TERMINAL_STATUS[opts.entityKind];
|
|
173
|
+
const transition = (context) => {
|
|
174
|
+
const updArgs = [path.join(__dirname, 'store-cli.cjs'), 'update-status', opts.entityKind, opts.recordId, 'status', target];
|
|
175
|
+
if (opts.force) updArgs.push('--force'); // --force bypasses the transition map too (user-invoked re-runs)
|
|
176
|
+
const upd = spawnSync(process.execPath, updArgs, { cwd: root, encoding: 'utf8' });
|
|
177
|
+
if (upd.status !== 0) {
|
|
178
|
+
fail(`${context} but status transition to '${target}' failed:\n${(upd.stderr || upd.stdout || '').trim()}`);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// 7. Stage exactly the derived set. A clean staging set is a legitimate
|
|
183
|
+
// terminal state (e.g. a bug whose fix is already at HEAD): no commit,
|
|
184
|
+
// but the record is still sealed (live-run finding, forge-engineering#40).
|
|
185
|
+
const noop = (why) => {
|
|
186
|
+
console.error(`commit-task: ${why} — nothing to commit; sealing the record without a commit.`);
|
|
187
|
+
transition('no-op commit');
|
|
188
|
+
process.stdout.write(JSON.stringify(
|
|
189
|
+
{ ok: true, committed: false, reason: 'nothing-to-commit', skippedIgnored: ignored, status: target }, null, 2) + '\n');
|
|
190
|
+
};
|
|
191
|
+
if (stageSet.length === 0) {
|
|
192
|
+
noop('entire staging set is gitignored');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
git(root, ['add', '--', ...stageSet]);
|
|
196
|
+
const staged = git(root, ['diff', '--cached', '--name-only']).stdout.trim().split('\n').filter(Boolean);
|
|
197
|
+
if (staged.length === 0) {
|
|
198
|
+
noop('working tree already clean for the staging set');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 8. Commit. Message comes from the agent; the optional trailer is appended
|
|
203
|
+
// after a blank line per git convention.
|
|
204
|
+
const message = opts.trailer ? `${opts.message.trim()}\n\n${opts.trailer.trim()}\n` : `${opts.message.trim()}\n`;
|
|
205
|
+
git(root, ['commit', '-m', message]);
|
|
206
|
+
const sha = git(root, ['rev-parse', 'HEAD']).stdout.trim();
|
|
207
|
+
|
|
208
|
+
transition(`commit ${sha} created`);
|
|
209
|
+
|
|
210
|
+
process.stdout.write(JSON.stringify({ ok: true, committed: true, sha, staged, status: target }, null, 2) + '\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
main();
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error(`commit-task: ${err.message}`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// forge-preflight.cjs — FORGE-S27-T01 (item A1).
|
|
5
|
+
//
|
|
6
|
+
// Bundles the deterministic pre-dispatch glue the LLM orchestrator currently
|
|
7
|
+
// hand-runs turn-by-turn — FORGE_ROOT resolution, config reconciliation,
|
|
8
|
+
// generation-manifest state, calibration-baseline freshness, MASTER_INDEX
|
|
9
|
+
// hashing, structure check, run timestamp — into ONE compact JSON blob,
|
|
10
|
+
// emitted once. The orchestrator reads this single blob instead of issuing
|
|
11
|
+
// ~20 separate round-trips before the first phase dispatch.
|
|
12
|
+
//
|
|
13
|
+
// Design: a PURE AGGREGATOR. It composes the existing deterministic tools
|
|
14
|
+
// (generation-manifest.cjs's hashContent, check-structure.cjs's
|
|
15
|
+
// checkNamespaces/validateManifest) and reads config — it does NOT reimplement
|
|
16
|
+
// their logic, so there is one source of truth per concern.
|
|
17
|
+
//
|
|
18
|
+
// Contract:
|
|
19
|
+
// - Reads and checks only by default; no implicit side effects (idempotent).
|
|
20
|
+
// - Fast-fail-safe: any probed-concern failure is captured into warnings[]
|
|
21
|
+
// and sets ok:false; the tool NEVER throws uncaught and never half-writes
|
|
22
|
+
// state. It always emits a parseable JSON blob on stdout.
|
|
23
|
+
// - Output is keyed for a freshness/idempotency guard via masterIndexHash +
|
|
24
|
+
// configMtime so callers can skip recompute when the blob is current.
|
|
25
|
+
//
|
|
26
|
+
// Usage:
|
|
27
|
+
// node forge-preflight.cjs [--sprint <id>] [--task <id>] [--bug <id>]
|
|
28
|
+
// [--path <project-root>]
|
|
29
|
+
|
|
30
|
+
// Fast-fail-safe: an uncaught error must still surface as a blob, not a stack
|
|
31
|
+
// trace. We register a last-resort handler that emits ok:false and exits 0
|
|
32
|
+
// (the orchestrator branches on blob.ok, not on the exit code).
|
|
33
|
+
process.on('uncaughtException', (err) => {
|
|
34
|
+
try {
|
|
35
|
+
process.stdout.write(JSON.stringify({
|
|
36
|
+
ok: false,
|
|
37
|
+
forgeRoot: null,
|
|
38
|
+
masterIndexHash: null,
|
|
39
|
+
generatedAt: new Date().toISOString(),
|
|
40
|
+
warnings: [`uncaught: ${err && err.message ? err.message : String(err)}`],
|
|
41
|
+
}) + '\n');
|
|
42
|
+
} catch (_) { /* nothing more we can do */ }
|
|
43
|
+
process.exit(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const fs = require('fs');
|
|
47
|
+
const path = require('path');
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Arg parsing (tolerant — unknown flags are ignored, not fatal).
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
function parseArgs(argv) {
|
|
53
|
+
const out = { projectRoot: process.cwd() };
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
const a = argv[i];
|
|
56
|
+
if (a === '--sprint' && argv[i + 1]) { out.sprintId = argv[++i]; }
|
|
57
|
+
else if (a === '--task' && argv[i + 1]) { out.taskId = argv[++i]; }
|
|
58
|
+
else if (a === '--bug' && argv[i + 1]) { out.bugId = argv[++i]; }
|
|
59
|
+
else if (a === '--path' && argv[i + 1]) { out.projectRoot = path.resolve(argv[++i]); }
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Pure helpers. Each concern is wrapped so a single failure becomes a warning
|
|
66
|
+
// rather than aborting the whole preflight.
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
// Compose generation-manifest.cjs's hashing primitive — single source of truth
|
|
70
|
+
// for content hashing across the plugin.
|
|
71
|
+
function loadHashContent(forgeRoot) {
|
|
72
|
+
try {
|
|
73
|
+
const mod = require(path.join(forgeRoot, 'tools', 'generation-manifest.cjs'));
|
|
74
|
+
if (mod && typeof mod.hashContent === 'function') return mod.hashContent;
|
|
75
|
+
} catch (_) { /* fall through to local */ }
|
|
76
|
+
// Fallback that matches generation-manifest's algorithm (sha256 hex) so the
|
|
77
|
+
// blob is still useful if the module cannot be required.
|
|
78
|
+
const crypto = require('crypto');
|
|
79
|
+
return (content) => crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readConfig(projectRoot) {
|
|
83
|
+
const configPath = path.join(projectRoot, '.forge', 'config.json');
|
|
84
|
+
const stat = fs.statSync(configPath); // throws if missing -> caught by caller
|
|
85
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
86
|
+
const config = JSON.parse(raw); // throws on malformed -> caught by caller
|
|
87
|
+
return { config, configMtime: stat.mtimeMs };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hashMasterIndex(projectRoot, config, hashContent) {
|
|
91
|
+
const engineering = (config.paths && config.paths.engineering) || 'engineering';
|
|
92
|
+
const indexPath = path.join(projectRoot, engineering, 'MASTER_INDEX.md');
|
|
93
|
+
if (!fs.existsSync(indexPath)) return null;
|
|
94
|
+
return hashContent(fs.readFileSync(indexPath, 'utf8'));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// calibrationFresh carries ACTIONABLE detail, not a bare boolean: when stale,
|
|
98
|
+
// it names the remedy and the reason so the orchestrator can act on the blob
|
|
99
|
+
// without re-deriving the signal.
|
|
100
|
+
function assessCalibration(config, masterIndexHash) {
|
|
101
|
+
const baseline = config.calibrationBaseline;
|
|
102
|
+
if (!baseline) {
|
|
103
|
+
return { fresh: false, suggest: '/forge:calibrate', reason: 'no calibrationBaseline in config' };
|
|
104
|
+
}
|
|
105
|
+
if (masterIndexHash && baseline.masterIndexHash && baseline.masterIndexHash !== masterIndexHash) {
|
|
106
|
+
return { fresh: false, suggest: '/forge:calibrate', reason: 'masterIndexHash drift since last calibration' };
|
|
107
|
+
}
|
|
108
|
+
return { fresh: true, lastCalibrated: baseline.lastCalibrated || null };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Compose check-structure.cjs's namespace check. Returns actionable detail.
|
|
112
|
+
function assessStructure(forgeRoot, projectRoot, warnings) {
|
|
113
|
+
try {
|
|
114
|
+
const cs = require(path.join(forgeRoot, 'tools', 'check-structure.cjs'));
|
|
115
|
+
const manifestPath = path.join(forgeRoot, 'schemas', 'structure-manifest.json');
|
|
116
|
+
if (typeof cs.checkNamespaces !== 'function' || !fs.existsSync(manifestPath)) {
|
|
117
|
+
return { ok: null, reason: 'structure check unavailable' };
|
|
118
|
+
}
|
|
119
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
120
|
+
const result = cs.checkNamespaces(manifest, projectRoot, {});
|
|
121
|
+
// checkNamespaces returns a structured result; treat any missing-file
|
|
122
|
+
// namespace as "not ok" but non-fatal (a warning, not a throw).
|
|
123
|
+
const allOk = result && (result.ok === true || result.allPresent === true ||
|
|
124
|
+
(Array.isArray(result.namespaces) && result.namespaces.every((n) => n.missing === 0 || (n.missing && n.missing.length === 0))));
|
|
125
|
+
if (!allOk) warnings.push('structure check reported missing namespace files');
|
|
126
|
+
return { ok: !!allOk };
|
|
127
|
+
} catch (err) {
|
|
128
|
+
warnings.push(`structure check error: ${err.message}`);
|
|
129
|
+
return { ok: null, reason: err.message };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// generation-manifest status — actionable manifest state.
|
|
134
|
+
function assessManifest(forgeRoot, projectRoot, warnings) {
|
|
135
|
+
const manifestPath = path.join(projectRoot, '.forge', 'schemas', 'generation-manifest.json');
|
|
136
|
+
// The manifest lives in the generated instance; absence is informational,
|
|
137
|
+
// not an error (e.g. fresh project or fast-mode).
|
|
138
|
+
if (!fs.existsSync(manifestPath)) {
|
|
139
|
+
return { present: false, reason: 'no generation-manifest in instance (informational)' };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
143
|
+
const count = manifest.files ? Object.keys(manifest.files).length : 0;
|
|
144
|
+
return { present: true, trackedFiles: count };
|
|
145
|
+
} catch (err) {
|
|
146
|
+
warnings.push(`generation-manifest parse error: ${err.message}`);
|
|
147
|
+
return { present: true, error: err.message };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Resolve FORGE_ROOT via forgeRef-based cache scan — mirrors manage-config.cjs
|
|
152
|
+
// Priority 2 logic (FORGE-S29-T03). paths.forgeRoot is deprecated and no longer read.
|
|
153
|
+
function resolveForgeRootFromConfig(config) {
|
|
154
|
+
const forgeRef = config.paths && config.paths.forgeRef;
|
|
155
|
+
if (!forgeRef) return null;
|
|
156
|
+
const homeDir = require('os').homedir();
|
|
157
|
+
const candidates = [
|
|
158
|
+
path.join(homeDir, '.claude', 'plugins', 'cache', 'forge', 'forge', forgeRef),
|
|
159
|
+
path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'skillforge', 'forge', 'forge', forgeRef),
|
|
160
|
+
];
|
|
161
|
+
for (const c of candidates) {
|
|
162
|
+
try {
|
|
163
|
+
const pluginJsonPath = path.join(c, '.claude-plugin', 'plugin.json');
|
|
164
|
+
if (fs.existsSync(pluginJsonPath)) {
|
|
165
|
+
const manifest = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
|
|
166
|
+
if (manifest.version === forgeRef) return c;
|
|
167
|
+
}
|
|
168
|
+
} catch (_) { /* try next candidate */ }
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Main aggregation.
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
function preflight(opts) {
|
|
177
|
+
const warnings = [];
|
|
178
|
+
const blob = {
|
|
179
|
+
ok: true,
|
|
180
|
+
forgeRoot: null,
|
|
181
|
+
sprintId: opts.sprintId || null,
|
|
182
|
+
configReconciled: false,
|
|
183
|
+
manifestState: null,
|
|
184
|
+
calibrationFresh: null,
|
|
185
|
+
masterIndexHash: null,
|
|
186
|
+
configMtime: null,
|
|
187
|
+
structureOk: null,
|
|
188
|
+
generatedAt: new Date().toISOString(),
|
|
189
|
+
warnings,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// 1. Config — the load-bearing input. A failure here is fatal to ok.
|
|
193
|
+
let config;
|
|
194
|
+
try {
|
|
195
|
+
const r = readConfig(opts.projectRoot);
|
|
196
|
+
config = r.config;
|
|
197
|
+
blob.configMtime = r.configMtime;
|
|
198
|
+
blob.configReconciled = true;
|
|
199
|
+
} catch (err) {
|
|
200
|
+
blob.ok = false;
|
|
201
|
+
warnings.push(`config unreadable: ${err.message}`);
|
|
202
|
+
return blob; // fast-fail-safe: cannot proceed without config, but no throw
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 2. FORGE_ROOT resolution — mirrors manage-config.cjs Priority 2 (forgeRef cache scan).
|
|
206
|
+
// paths.forgeRoot is deprecated (FORGE-S29-T03); we resolve via forgeRef only.
|
|
207
|
+
// blob.forgeRoot is still emitted for backward-compatible telemetry.
|
|
208
|
+
blob.forgeRoot = resolveForgeRootFromConfig(config);
|
|
209
|
+
if (!blob.forgeRoot) {
|
|
210
|
+
const forgeRef = (config.paths && config.paths.forgeRef) || null;
|
|
211
|
+
const hint = forgeRef
|
|
212
|
+
? `Cannot resolve Forge plugin root from forgeRef "${forgeRef}" — run /forge:update to refresh.`
|
|
213
|
+
: 'paths.forgeRef missing from config — cannot resolve Forge plugin root.';
|
|
214
|
+
blob.ok = false;
|
|
215
|
+
warnings.push(hint);
|
|
216
|
+
return blob;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const hashContent = loadHashContent(blob.forgeRoot);
|
|
220
|
+
|
|
221
|
+
// 3. MASTER_INDEX hash (freshness key + calibration input).
|
|
222
|
+
try {
|
|
223
|
+
blob.masterIndexHash = hashMasterIndex(opts.projectRoot, config, hashContent);
|
|
224
|
+
if (!blob.masterIndexHash) warnings.push('MASTER_INDEX.md absent — hash null');
|
|
225
|
+
} catch (err) {
|
|
226
|
+
warnings.push(`master-index hash error: ${err.message}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 4. Calibration freshness (actionable).
|
|
230
|
+
try {
|
|
231
|
+
blob.calibrationFresh = assessCalibration(config, blob.masterIndexHash);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
warnings.push(`calibration assessment error: ${err.message}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 5. Manifest state (actionable).
|
|
237
|
+
try {
|
|
238
|
+
blob.manifestState = assessManifest(blob.forgeRoot, opts.projectRoot, warnings);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
warnings.push(`manifest assessment error: ${err.message}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 6. Structure check.
|
|
244
|
+
try {
|
|
245
|
+
const s = assessStructure(blob.forgeRoot, opts.projectRoot, warnings);
|
|
246
|
+
blob.structureOk = s.ok;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
warnings.push(`structure assessment error: ${err.message}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Aggregate verdict: ok stays true for soft/informational warnings (absent
|
|
252
|
+
// master index, missing optional manifest); it is only flipped false above
|
|
253
|
+
// for hard failures (no config / no forgeRoot). This keeps the orchestrator
|
|
254
|
+
// running on a healthy-but-fresh project while still surfacing advisories.
|
|
255
|
+
return blob;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// CLI entry.
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
if (require.main === module) {
|
|
262
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
263
|
+
const blob = preflight(opts);
|
|
264
|
+
process.stdout.write(JSON.stringify(blob) + '\n');
|
|
265
|
+
process.exit(0); // exit code is always 0; callers branch on blob.ok
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = { preflight, parseArgs, assessCalibration };
|
|
@@ -7,23 +7,24 @@
|
|
|
7
7
|
* consistent across substitute-placeholders.cjs, check-structure.cjs, etc.
|
|
8
8
|
*
|
|
9
9
|
* Exported API:
|
|
10
|
-
* getCommandsSubdir(
|
|
10
|
+
* getCommandsSubdir() — returns 'forge' (fixed namespace)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Commands subdirectory name under .claude/commands/.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* CLI-first redesign: the namespace is FIXED to 'forge'. Project-prefix
|
|
17
|
+
* command namespaces (/acme:*, /hello:*) are retired — every project gets
|
|
18
|
+
* the same /forge:* surface, matching what the 4ge bootstrap vendors into
|
|
19
|
+
* .claude/commands/forge/. The prefix-derived namespace existed to avoid
|
|
20
|
+
* collisions with the Forge *plugin's* own /forge:* commands; moot now
|
|
21
|
+
* that the plugin mechanism is retired.
|
|
18
22
|
*
|
|
19
|
-
* @param {string}
|
|
20
|
-
* @returns {string}
|
|
23
|
+
* @param {string} [_prefix] — vestigial, ignored (kept for caller compat)
|
|
24
|
+
* @returns {string} always 'forge'
|
|
21
25
|
*/
|
|
22
|
-
function getCommandsSubdir(
|
|
23
|
-
|
|
24
|
-
throw new Error('paths.getCommandsSubdir: prefix must be a non-empty string');
|
|
25
|
-
}
|
|
26
|
-
return prefix.toLowerCase();
|
|
26
|
+
function getCommandsSubdir(_prefix) {
|
|
27
|
+
return 'forge';
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
module.exports = { getCommandsSubdir };
|
|
@@ -13,9 +13,15 @@
|
|
|
13
13
|
* Returns number (may be 0), or null for unknown model.
|
|
14
14
|
*
|
|
15
15
|
* This module is a pure library — no CLI, no process.exit.
|
|
16
|
-
* Published Anthropic pricing (USD/MTok → divide by 1,000,000 for per-token rate)
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Published Anthropic pricing (USD/MTok → divide by 1,000,000 for per-token rate).
|
|
17
|
+
* Verified against Anthropic support + the pi vendor model table on 2026-06-08.
|
|
18
|
+
* cacheWrite uses the 5-minute-TTL rate; Forge phases run inside the 5-min cache
|
|
19
|
+
* window, so the 5-min write rate is the one that bills (1-hour TTL is higher but
|
|
20
|
+
* not used). All Opus 4.x generations share one rate tier:
|
|
21
|
+
* claude-opus-4-5: input $5.00, output $25.00, cacheRead $0.50, cacheWrite $6.25
|
|
22
|
+
* claude-opus-4-6: input $5.00, output $25.00, cacheRead $0.50, cacheWrite $6.25
|
|
23
|
+
* claude-opus-4-7: input $5.00, output $25.00, cacheRead $0.50, cacheWrite $6.25
|
|
24
|
+
* claude-opus-4-8: input $5.00, output $25.00, cacheRead $0.50, cacheWrite $6.25
|
|
19
25
|
* claude-sonnet-4-5: input $3.00, output $15.00, cacheRead $0.30, cacheWrite $3.75
|
|
20
26
|
* claude-sonnet-4-6: input $3.00, output $15.00, cacheRead $0.30, cacheWrite $3.75
|
|
21
27
|
* claude-haiku-3-5: input $0.80, output $4.00, cacheRead $0.08, cacheWrite $1.00
|
|
@@ -24,16 +30,28 @@
|
|
|
24
30
|
// All rates in USD per token (divide MTok rate by 1,000,000)
|
|
25
31
|
const MODEL_PRICING = Object.freeze({
|
|
26
32
|
'claude-opus-4-5': {
|
|
27
|
-
input:
|
|
28
|
-
output:
|
|
29
|
-
cacheRead:
|
|
30
|
-
cacheWrite:
|
|
33
|
+
input: 5.00 / 1_000_000,
|
|
34
|
+
output: 25.00 / 1_000_000,
|
|
35
|
+
cacheRead: 0.50 / 1_000_000,
|
|
36
|
+
cacheWrite: 6.25 / 1_000_000,
|
|
37
|
+
},
|
|
38
|
+
'claude-opus-4-6': {
|
|
39
|
+
input: 5.00 / 1_000_000,
|
|
40
|
+
output: 25.00 / 1_000_000,
|
|
41
|
+
cacheRead: 0.50 / 1_000_000,
|
|
42
|
+
cacheWrite: 6.25 / 1_000_000,
|
|
31
43
|
},
|
|
32
44
|
'claude-opus-4-7': {
|
|
33
|
-
input:
|
|
34
|
-
output:
|
|
35
|
-
cacheRead:
|
|
36
|
-
cacheWrite:
|
|
45
|
+
input: 5.00 / 1_000_000,
|
|
46
|
+
output: 25.00 / 1_000_000,
|
|
47
|
+
cacheRead: 0.50 / 1_000_000,
|
|
48
|
+
cacheWrite: 6.25 / 1_000_000,
|
|
49
|
+
},
|
|
50
|
+
'claude-opus-4-8': {
|
|
51
|
+
input: 5.00 / 1_000_000,
|
|
52
|
+
output: 25.00 / 1_000_000,
|
|
53
|
+
cacheRead: 0.50 / 1_000_000,
|
|
54
|
+
cacheWrite: 6.25 / 1_000_000,
|
|
37
55
|
},
|
|
38
56
|
'claude-sonnet-4-5': {
|
|
39
57
|
input: 3.00 / 1_000_000,
|
|
@@ -63,7 +81,9 @@ const MODEL_PRICING = Object.freeze({
|
|
|
63
81
|
*/
|
|
64
82
|
const MODEL_FAMILIES = [
|
|
65
83
|
// Opus family
|
|
84
|
+
'claude-opus-4-8',
|
|
66
85
|
'claude-opus-4-7',
|
|
86
|
+
'claude-opus-4-6',
|
|
67
87
|
'claude-opus-4-5',
|
|
68
88
|
// Sonnet family
|
|
69
89
|
'claude-sonnet-4-6',
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// query-logger.cjs — PostToolUse hook: logs store-cli query invocations
|
|
4
|
+
// Reads TOOL_INPUT env var, appends entry to .forge/store/query-log.jsonl
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const toolInput = process.env.TOOL_INPUT || '';
|
|
11
|
+
if (!/store-cli(?:\.cjs)?["']?\s+query/.test(toolInput)) process.exit(0);
|
|
12
|
+
|
|
13
|
+
const logEntry = {
|
|
14
|
+
timestamp: new Date().toISOString(),
|
|
15
|
+
tool: 'store-cli',
|
|
16
|
+
command: 'query',
|
|
17
|
+
input: toolInput.substring(0, 500),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let storeRel = '.forge/store';
|
|
21
|
+
try {
|
|
22
|
+
const cfgPath = path.join(process.cwd(), '.forge', 'config.json');
|
|
23
|
+
if (fs.existsSync(cfgPath)) {
|
|
24
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
25
|
+
if (cfg.paths?.store) storeRel = cfg.paths.store;
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
|
|
29
|
+
const logPath = path.join(process.cwd(), storeRel, 'query-log.jsonl');
|
|
30
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
31
|
+
fs.appendFileSync(logPath, JSON.stringify(logEntry) + '\n');
|
|
32
|
+
} catch {
|
|
33
|
+
// Silent fail — hooks must not crash the session
|
|
34
|
+
}
|
|
@@ -77,7 +77,12 @@ const PHASE_SUMMARY_SCHEMA = {
|
|
|
77
77
|
verdict: { type: 'string', enum: ['approved', 'revision', 'n/a'] },
|
|
78
78
|
written_at: { type: 'string' },
|
|
79
79
|
artifact_ref:{ type: 'string' },
|
|
80
|
-
route: { type: 'string', enum: ['A', 'B'] }
|
|
80
|
+
route: { type: 'string', enum: ['A', 'B'] },
|
|
81
|
+
// forge-engineering#40: implement-phase file provenance. The implement
|
|
82
|
+
// workflow records the repo-relative paths it created/modified;
|
|
83
|
+
// commit-task.cjs derives its staging set from this list instead of the
|
|
84
|
+
// LLM re-deriving the change surface from git each run.
|
|
85
|
+
files_changed: { type: 'array', items: { type: 'string', maxLength: 300 }, maxItems: 100 }
|
|
81
86
|
},
|
|
82
87
|
additionalProperties: false
|
|
83
88
|
};
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* output to the appropriate output directories.
|
|
10
10
|
*
|
|
11
11
|
* Output path mapping (--target claude-code, default):
|
|
12
|
-
* base-pack/commands/ → <outRoot>/.claude/commands
|
|
12
|
+
* base-pack/commands/ → <outRoot>/.claude/commands/forge/ (fixed namespace)
|
|
13
13
|
* base-pack/personas/ → <outRoot>/.forge/personas/
|
|
14
14
|
* base-pack/skills/ → <outRoot>/.forge/skills/
|
|
15
15
|
* base-pack/workflows/ → <outRoot>/.forge/workflows/
|
|
@@ -116,8 +116,8 @@ const RUNTIME_PASSTHROUGH_KEYS = new Set([
|
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
118
|
* Maps a base-pack subdirectory name to an output directory path relative to
|
|
119
|
-
* the project root. The 'commands' entry is
|
|
120
|
-
*
|
|
119
|
+
* the project root. The 'commands' entry is the fixed 'forge' namespace via
|
|
120
|
+
* getCommandsSubdir() — see walkBasePack.
|
|
121
121
|
*/
|
|
122
122
|
const SUBDIR_OUTPUT_MAP = {
|
|
123
123
|
personas: path.join('.forge', 'personas'),
|
|
@@ -392,9 +392,8 @@ function renderDeploymentTable(envs) {
|
|
|
392
392
|
function walkBasePack(basePack, map, outRoot, dryRun, io, categoryFilter) {
|
|
393
393
|
const warn = (io && io.warn) || ((msg) => process.stderr.write(msg + '\n'));
|
|
394
394
|
|
|
395
|
-
//
|
|
396
|
-
const
|
|
397
|
-
const commandsSubdir = prefix ? getCommandsSubdir(prefix) : 'forge';
|
|
395
|
+
// Commands namespace is fixed to 'forge' (CLI-first redesign)
|
|
396
|
+
const commandsSubdir = getCommandsSubdir();
|
|
398
397
|
|
|
399
398
|
// Sorted readdir for deterministic idempotent output (Advisory Note 7)
|
|
400
399
|
const topEntries = fs.readdirSync(basePack).sort();
|