@entelligentsia/forgecli 1.0.10 → 1.0.20
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 +191 -0
- package/dist/CHANGELOG-forge-plugin.md +211 -0
- package/dist/bin/forge.js +0 -0
- package/dist/extensions/forgecli/config-layer.js.map +1 -1
- package/dist/extensions/forgecli/context-governor-compaction.d.ts +83 -0
- package/dist/extensions/forgecli/context-governor-compaction.js +302 -0
- package/dist/extensions/forgecli/context-governor-compaction.js.map +1 -0
- package/dist/extensions/forgecli/context-governor.d.ts +173 -0
- package/dist/extensions/forgecli/context-governor.js +618 -0
- package/dist/extensions/forgecli/context-governor.js.map +1 -0
- package/dist/extensions/forgecli/dashboard/component.d.ts +105 -0
- package/dist/extensions/forgecli/dashboard/component.js +861 -0
- package/dist/extensions/forgecli/dashboard/component.js.map +1 -0
- package/dist/extensions/forgecli/dashboard/register.d.ts +2 -0
- package/dist/extensions/forgecli/dashboard/register.js +31 -0
- package/dist/extensions/forgecli/dashboard/register.js.map +1 -0
- package/dist/extensions/forgecli/dashboard/theme.d.ts +27 -0
- package/dist/extensions/forgecli/dashboard/theme.js +91 -0
- package/dist/extensions/forgecli/dashboard/theme.js.map +1 -0
- package/dist/extensions/forgecli/dashboard/view-model.d.ts +35 -0
- package/dist/extensions/forgecli/dashboard/view-model.js +54 -0
- package/dist/extensions/forgecli/dashboard/view-model.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.js +126 -7
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-artifact-tool.js +2 -1
- package/dist/extensions/forgecli/forge-artifact-tool.js.map +1 -1
- package/dist/extensions/forgecli/forge-commands.js +1 -0
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-init/phase4-register.js +53 -0
- package/dist/extensions/forgecli/forge-init/phase4-register.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.d.ts +20 -1
- package/dist/extensions/forgecli/forge-subagent.js +23 -7
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/forge-tools.js +3 -1
- package/dist/extensions/forgecli/forge-tools.js.map +1 -1
- package/dist/extensions/forgecli/hook-dispatcher.d.ts +3 -1
- package/dist/extensions/forgecli/hook-dispatcher.js +37 -3
- package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
- package/dist/extensions/forgecli/index.js +38 -1
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/lib/halt-advisor.d.ts +59 -0
- package/dist/extensions/forgecli/lib/halt-advisor.js +113 -0
- package/dist/extensions/forgecli/lib/halt-advisor.js.map +1 -0
- package/dist/extensions/forgecli/migration-engine.js +25 -12
- package/dist/extensions/forgecli/migration-engine.js.map +1 -1
- package/dist/extensions/forgecli/orchestrator-status-bar.d.ts +26 -0
- package/dist/extensions/forgecli/orchestrator-status-bar.js +213 -0
- package/dist/extensions/forgecli/orchestrator-status-bar.js.map +1 -0
- package/dist/extensions/forgecli/orchestrator-tree.d.ts +96 -0
- package/dist/extensions/forgecli/orchestrator-tree.js +390 -0
- package/dist/extensions/forgecli/orchestrator-tree.js.map +1 -0
- package/dist/extensions/forgecli/project-orientation.js +12 -8
- package/dist/extensions/forgecli/project-orientation.js.map +1 -1
- package/dist/extensions/forgecli/regenerate.d.ts +16 -0
- package/dist/extensions/forgecli/regenerate.js +110 -0
- package/dist/extensions/forgecli/regenerate.js.map +1 -1
- package/dist/extensions/forgecli/run-sprint.d.ts +3 -1
- package/dist/extensions/forgecli/run-sprint.js +34 -3
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.d.ts +66 -1
- package/dist/extensions/forgecli/run-task.js +323 -12
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/thread-switcher.d.ts +4 -1
- package/dist/extensions/forgecli/thread-switcher.js +118 -762
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/viewport-events.js +32 -0
- package/dist/extensions/forgecli/viewport-events.js.map +1 -1
- package/dist/forge-payload/.base-pack/commands/fix-bug.md +1 -1
- package/dist/forge-payload/.base-pack/commands/run-sprint.md +1 -1
- package/dist/forge-payload/.base-pack/commands/run-task.md +1 -1
- package/dist/forge-payload/.base-pack/personas/architect.md +1 -1
- package/dist/forge-payload/.base-pack/personas/bug-fixer.md +1 -1
- package/dist/forge-payload/.base-pack/personas/collator.md +3 -3
- package/dist/forge-payload/.base-pack/personas/engineer.md +1 -1
- package/dist/forge-payload/.base-pack/personas/librarian.md +1 -1
- package/dist/forge-payload/.base-pack/personas/orchestrator.md +1 -1
- package/dist/forge-payload/.base-pack/personas/product-manager.md +1 -1
- package/dist/forge-payload/.base-pack/personas/qa-engineer.md +1 -1
- package/dist/forge-payload/.base-pack/personas/supervisor.md +1 -1
- package/dist/forge-payload/.base-pack/workflows/_fragments/event-emission-schema.md +1 -1
- package/dist/forge-payload/.base-pack/workflows/_fragments/friction-emit.md +1 -1
- package/dist/forge-payload/.base-pack/workflows/_fragments/iron-laws.md +1 -1
- package/dist/forge-payload/.base-pack/workflows/_fragments/progress-reporting.md +2 -2
- package/dist/forge-payload/.base-pack/workflows/_fragments/store-cli-verbs.md +11 -2
- package/dist/forge-payload/.base-pack/workflows/architect_approve.md +6 -7
- package/dist/forge-payload/.base-pack/workflows/architect_review_sprint_completion.md +2 -2
- package/dist/forge-payload/.base-pack/workflows/architect_sprint_intake.md +2 -2
- package/dist/forge-payload/.base-pack/workflows/architect_sprint_plan.md +5 -5
- package/dist/forge-payload/.base-pack/workflows/collator_agent.md +4 -6
- package/dist/forge-payload/.base-pack/workflows/commit_task.md +5 -6
- package/dist/forge-payload/.base-pack/workflows/enhance.md +5 -5
- package/dist/forge-payload/.base-pack/workflows/implement_plan.md +15 -7
- package/dist/forge-payload/.base-pack/workflows/migrate_structural.md +12 -13
- package/dist/forge-payload/.base-pack/workflows/plan_task.md +12 -6
- package/dist/forge-payload/.base-pack/workflows/review_code.md +12 -11
- package/dist/forge-payload/.base-pack/workflows/review_plan.md +12 -11
- package/dist/forge-payload/.base-pack/workflows/sprint_retrospective.md +3 -3
- package/dist/forge-payload/.base-pack/workflows/triage.md +12 -9
- package/dist/forge-payload/.base-pack/workflows/update_implementation.md +2 -2
- package/dist/forge-payload/.base-pack/workflows/update_plan.md +2 -2
- package/dist/forge-payload/.base-pack/workflows/validate_task.md +9 -9
- package/dist/forge-payload/.base-pack/workflows-js/wfl-fix-bug.js +490 -0
- package/dist/forge-payload/.base-pack/workflows-js/wfl-run-sprint.js +416 -0
- package/dist/forge-payload/.base-pack/workflows-js/wfl-run-task.js +608 -0
- package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
- package/dist/forge-payload/.schemas/config.schema.json +2 -3
- package/dist/forge-payload/.schemas/enum-catalog.json +2 -2
- package/dist/forge-payload/.schemas/event.schema.json +16 -0
- package/dist/forge-payload/.schemas/migrations.json +359 -18
- package/dist/forge-payload/commands/health.md +29 -0
- package/dist/forge-payload/commands/rebuild.md +143 -15
- package/dist/forge-payload/commands/update.md +28 -27
- package/dist/forge-payload/hooks/preflight-session.cjs +99 -0
- package/dist/forge-payload/init/phases/phase-3-materialize.md +18 -5
- package/dist/forge-payload/integrity.json +7 -6
- package/dist/forge-payload/meta/fragments/tool-discipline.md +1 -1
- package/dist/forge-payload/meta/personas/meta-architect.md +1 -1
- package/dist/forge-payload/meta/personas/meta-bug-fixer.md +1 -1
- package/dist/forge-payload/meta/personas/meta-collator.md +7 -7
- package/dist/forge-payload/meta/personas/meta-engineer.md +1 -1
- package/dist/forge-payload/meta/personas/meta-orchestrator.md +1 -1
- package/dist/forge-payload/meta/personas/meta-supervisor.md +1 -1
- package/dist/forge-payload/meta/tool-specs/store-cli.spec.md +1 -1
- package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +1 -1
- package/dist/forge-payload/meta/workflows/_fragments/friction-emit.md +1 -1
- package/dist/forge-payload/meta/workflows/_fragments/iron-laws.md +1 -1
- package/dist/forge-payload/meta/workflows/_fragments/progress-reporting.md +2 -2
- package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +11 -2
- package/dist/forge-payload/meta/workflows/meta-approve.md +6 -7
- package/dist/forge-payload/meta/workflows/meta-bug-triage.md +12 -9
- package/dist/forge-payload/meta/workflows/meta-collate.md +5 -7
- package/dist/forge-payload/meta/workflows/meta-commit.md +5 -6
- package/dist/forge-payload/meta/workflows/meta-enhance.md +5 -5
- package/dist/forge-payload/meta/workflows/meta-fix-bug.md +35 -11
- package/dist/forge-payload/meta/workflows/meta-implement.md +15 -7
- package/dist/forge-payload/meta/workflows/meta-migrate.md +13 -14
- package/dist/forge-payload/meta/workflows/meta-new-sprint.md +3 -3
- package/dist/forge-payload/meta/workflows/meta-orchestrate.md +138 -39
- package/dist/forge-payload/meta/workflows/meta-plan-sprint.md +6 -6
- package/dist/forge-payload/meta/workflows/meta-plan-task.md +12 -6
- package/dist/forge-payload/meta/workflows/meta-retro.md +4 -4
- package/dist/forge-payload/meta/workflows/meta-retrospective.md +4 -4
- package/dist/forge-payload/meta/workflows/meta-review-implementation.md +12 -11
- package/dist/forge-payload/meta/workflows/meta-review-plan.md +12 -11
- package/dist/forge-payload/meta/workflows/meta-review-sprint-completion.md +3 -3
- package/dist/forge-payload/meta/workflows/meta-sprint-intake.md +3 -3
- package/dist/forge-payload/meta/workflows/meta-sprint-plan.md +6 -6
- package/dist/forge-payload/meta/workflows/meta-update-implementation.md +2 -2
- package/dist/forge-payload/meta/workflows/meta-update-plan.md +2 -2
- package/dist/forge-payload/meta/workflows/meta-validate.md +9 -9
- package/dist/forge-payload/schemas/config.schema.json +2 -3
- package/dist/forge-payload/schemas/enum-catalog.json +2 -2
- package/dist/forge-payload/schemas/event.schema.json +16 -0
- package/dist/forge-payload/schemas/structure-manifest.json +75 -73
- package/dist/forge-payload/skills/refresh-kb-links/SKILL.md +14 -7
- package/dist/forge-payload/tools/banners.cjs +29 -10
- package/dist/forge-payload/tools/check-structure.cjs +88 -7
- package/dist/forge-payload/tools/collate.cjs +48 -2
- package/dist/forge-payload/tools/manage-config.cjs +5 -7
- package/dist/forge-payload/tools/parse-gates.cjs +73 -1
- package/dist/forge-payload/tools/postflight-gate.cjs +298 -0
- package/dist/forge-payload/tools/preflight-gate.cjs +47 -0
- package/dist/forge-payload/tools/substitute-placeholders.cjs +5 -4
- package/dist/forge-payload/tools/verify-phase.cjs +17 -0
- package/package.json +2 -2
- package/dist/bin/forgecli.d.ts +0 -2
- package/dist/bin/forgecli.js +0 -6
- package/dist/bin/forgecli.js.map +0 -1
- package/dist/extensions/forgecli/config-tui/index.d.ts +0 -5
- package/dist/extensions/forgecli/config-tui/index.js +0 -5
- package/dist/extensions/forgecli/config-tui/index.js.map +0 -1
- package/dist/extensions/forgecli/loaders/persona-skill-loader.d.ts +0 -45
- package/dist/extensions/forgecli/loaders/persona-skill-loader.js +0 -227
- package/dist/extensions/forgecli/loaders/persona-skill-loader.js.map +0 -1
- package/dist/extensions/forgecli/loaders/template-render.d.ts +0 -20
- package/dist/extensions/forgecli/loaders/template-render.js +0 -85
- package/dist/extensions/forgecli/loaders/template-render.js.map +0 -1
- package/dist/extensions/forgecli/loaders/workflow-loader.d.ts +0 -41
- package/dist/extensions/forgecli/loaders/workflow-loader.js +0 -164
- package/dist/extensions/forgecli/loaders/workflow-loader.js.map +0 -1
- package/dist/forge-payload/.base-pack/workflows/fix_bug.md +0 -446
- package/dist/forge-payload/.base-pack/workflows/orchestrate_task.md +0 -928
- package/dist/forge-payload/.base-pack/workflows/run_sprint.md +0 -225
|
@@ -19,16 +19,20 @@ import * as path from "node:path";
|
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { assertAudience, CallerContextStore } from "./audience-gate.js";
|
|
21
21
|
import { loadLayeredConfig } from "./config-layer.js";
|
|
22
|
+
import { buildGovernorFactory } from "./context-governor.js";
|
|
23
|
+
import { buildForgeCompactionFactory } from "./context-governor-compaction.js";
|
|
22
24
|
import { loadForgePersona, runForgeSubagent } from "./forge-subagent.js";
|
|
23
25
|
import { getSubagentTools } from "./forge-tools.js";
|
|
24
26
|
import { readPersonaDir, readPipelineNames } from "./lib/catalog-helpers.js";
|
|
25
27
|
import { discoverForgeConfigCached } from "./lib/forge-config.js";
|
|
28
|
+
import { resolveAdvisorModel, runHaltAdvisor } from "./lib/halt-advisor.js";
|
|
26
29
|
import { checkMaterialization } from "./lib/manifest-checker.js";
|
|
27
30
|
import { runOrchestratorPreflight } from "./lib/orchestrator-preflight.js";
|
|
28
31
|
import { isStateStale as isJsonStateStale, readJsonState, taskStateFilePath, writeJsonState, } from "./lib/state-helpers.js";
|
|
29
32
|
import { resolveModelForPhase } from "./model-resolver.js";
|
|
30
33
|
import { loadWorkflow } from "./parsers/workflow-loader.js";
|
|
31
34
|
import { getSessionRegistry } from "./session-registry.js";
|
|
35
|
+
import { getOrchestratorTree } from "./orchestrator-tree.js";
|
|
32
36
|
import { OrchestratorTranscriptWriter } from "./subagent/orchestrator-transcript.js";
|
|
33
37
|
import { resolveToCanonicalId, resolveToolDir } from "./store-resolver.js";
|
|
34
38
|
import { attachViewportObserver } from "./viewport-events.js";
|
|
@@ -224,6 +228,49 @@ export function emitEvent(storeCli, cwd, sprintId, event) {
|
|
|
224
228
|
});
|
|
225
229
|
return { ok: result.status === 0, stderr: typeof result.stderr === "string" ? result.stderr : "" };
|
|
226
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Emit a phase event for an INCOMPLETE attempt (cancelled / failed) so its
|
|
233
|
+
* provider-billed tokens reach the store. Bug B: the cancel and halt-on-failure
|
|
234
|
+
* branches used to return without emitting, so collate's COST_REPORT
|
|
235
|
+
* under-counted real spend by exactly the aborted passes (CART-S02-T03
|
|
236
|
+
* baseline: 259,950 tokens across two aborted plan attempts, invisible).
|
|
237
|
+
*
|
|
238
|
+
* The event is the canonical phase event (schema-unchanged) with
|
|
239
|
+
* `verdict: "aborted" | "failed"` marking the outcome.
|
|
240
|
+
*
|
|
241
|
+
* Zero-token attempts are skipped — there is no spend to account, and a
|
|
242
|
+
* token-less event would be flagged as a husk by collate's ingestion-quality
|
|
243
|
+
* pass. Never throws: emission must not perturb the cancel/halt return paths.
|
|
244
|
+
*
|
|
245
|
+
* @param opts.decorate Optional event mutation hook applied before emit
|
|
246
|
+
* (fix-bug uses it for the BUG_TYPE_TOKENS `type` field).
|
|
247
|
+
* @returns true when the event was emitted and store-cli accepted it.
|
|
248
|
+
*/
|
|
249
|
+
export function emitIncompletePhaseEvent(opts) {
|
|
250
|
+
try {
|
|
251
|
+
const { emitCtx, outcome } = opts;
|
|
252
|
+
const u = emitCtx.usage;
|
|
253
|
+
if (u.input + u.output + u.cacheRead + u.cacheWrite <= 0) {
|
|
254
|
+
opts.onDebug?.({ kind: "incomplete_emit_skipped", reason: "no-tokens", outcome });
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
const judgement = { verdict: outcome };
|
|
258
|
+
if (opts.notes)
|
|
259
|
+
judgement.notes = opts.notes;
|
|
260
|
+
const event = buildPhaseEvent({ ...emitCtx, judgement });
|
|
261
|
+
opts.decorate?.(event);
|
|
262
|
+
const res = emitEvent(emitCtx.storeCli, emitCtx.cwd, emitCtx.sprintId, event);
|
|
263
|
+
opts.onDebug?.(res.ok
|
|
264
|
+
? { kind: "incomplete_emit_ok", eventId: event.eventId, outcome }
|
|
265
|
+
: { kind: "incomplete_emit_failed", stderr: res.stderr, outcome });
|
|
266
|
+
return res.ok;
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
270
|
+
opts.onDebug?.({ kind: "incomplete_emit_failed", stderr: msg, outcome: opts.outcome });
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
227
274
|
export function judgementFromSummary(record, phaseRole, summaryKeyByRole) {
|
|
228
275
|
if (!record || !record.summaries)
|
|
229
276
|
return undefined;
|
|
@@ -348,13 +395,71 @@ export function composeTaskBody(subWorkflowMd, taskId, summariesBlock) {
|
|
|
348
395
|
return parts.join("\n");
|
|
349
396
|
}
|
|
350
397
|
export function runPreflightGate(preflightGate, role, taskId, cwd, entityType) {
|
|
398
|
+
const outcome = runPreflightGateWithData(preflightGate, role, taskId, cwd, entityType);
|
|
399
|
+
return outcome.result;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Run postflight-gate.cjs after a phase subagent returns, before FSM advance.
|
|
403
|
+
* Mirrors runPreflightGateWithData — same argv-array discipline, same structured-JSON
|
|
404
|
+
* parsing from stdout on exit 1.
|
|
405
|
+
*
|
|
406
|
+
* Returns:
|
|
407
|
+
* "ok" — gate passed (or no outputs block for this phase); advance may proceed.
|
|
408
|
+
* "unsatisfied" — gate failed; do NOT advance FSM; halt and call runHaltAdvisor.
|
|
409
|
+
* "error" — gate binary missing or parse error; treat as pass-through (additive).
|
|
410
|
+
*/
|
|
411
|
+
export function runPostflightGate(postflightGate, role, taskId, cwd) {
|
|
412
|
+
if (!fs.existsSync(postflightGate)) {
|
|
413
|
+
// postflight-gate.cjs not present in this forgeRoot — pass through (additive).
|
|
414
|
+
return { result: "ok", gateFailure: null };
|
|
415
|
+
}
|
|
416
|
+
const spawnResult = spawnSync("node", [postflightGate, "--phase", role, "--task", taskId], { cwd, encoding: "utf8" });
|
|
417
|
+
if (spawnResult.status === 0)
|
|
418
|
+
return { result: "ok", gateFailure: null };
|
|
419
|
+
if (spawnResult.status === 2)
|
|
420
|
+
return { result: "error", gateFailure: null };
|
|
421
|
+
// Exit 1: parse structured JSON from stdout
|
|
422
|
+
let gateFailure = null;
|
|
423
|
+
try {
|
|
424
|
+
const stdout = typeof spawnResult.stdout === "string" ? spawnResult.stdout.trim() : "";
|
|
425
|
+
if (stdout) {
|
|
426
|
+
const parsed = JSON.parse(stdout);
|
|
427
|
+
if (parsed && typeof parsed.reasonCode === "string") {
|
|
428
|
+
gateFailure = parsed;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// stdout not valid JSON — gate failure but no structured data
|
|
434
|
+
}
|
|
435
|
+
return { result: "unsatisfied", gateFailure };
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Upgraded variant that returns structured failure data alongside the status enum.
|
|
439
|
+
* Callers that need the advisory data should use this function directly.
|
|
440
|
+
*/
|
|
441
|
+
export function runPreflightGateWithData(preflightGate, role, taskId, cwd, entityType) {
|
|
351
442
|
const entityFlag = entityType === "bug" ? "--bug" : "--task";
|
|
352
|
-
const
|
|
353
|
-
if (
|
|
354
|
-
return "proceed";
|
|
355
|
-
if (
|
|
356
|
-
return "escalate";
|
|
357
|
-
|
|
443
|
+
const spawnResult = spawnSync("node", [preflightGate, "--phase", role, entityFlag, taskId], { cwd, encoding: "utf8" });
|
|
444
|
+
if (spawnResult.status === 0)
|
|
445
|
+
return { result: "proceed", gateFailure: null };
|
|
446
|
+
if (spawnResult.status === 2)
|
|
447
|
+
return { result: "escalate", gateFailure: null };
|
|
448
|
+
// Exit 1: parse structured JSON from stdout
|
|
449
|
+
let gateFailure = null;
|
|
450
|
+
try {
|
|
451
|
+
const stdout = typeof spawnResult.stdout === "string" ? spawnResult.stdout.trim() : "";
|
|
452
|
+
if (stdout) {
|
|
453
|
+
const parsed = JSON.parse(stdout);
|
|
454
|
+
if (parsed && typeof parsed.reasonCode === "string") {
|
|
455
|
+
gateFailure = parsed;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// stdout not valid JSON — gate failure but no structured data
|
|
461
|
+
}
|
|
462
|
+
return { result: "halt", gateFailure };
|
|
358
463
|
}
|
|
359
464
|
// ── Per-task orchestrator pipeline (FORGE-S21-T03 extracted) ──────────────
|
|
360
465
|
// The entire phase loop was inline in registerRunTask. It is now a standalone
|
|
@@ -369,6 +474,8 @@ export { extractTurnPreview } from "./viewport-renderer.js";
|
|
|
369
474
|
// ── runTaskPipeline ──────────────────────────────────────────────────────
|
|
370
475
|
export async function runTaskPipeline(opts) {
|
|
371
476
|
const { taskId, cwd, ctx, forgeRoot, storeCli, preflightGate, registry, resumeFromState, onPhaseEvent } = opts;
|
|
477
|
+
// Bridge: OrchestratorTree for the dashboard overlay.
|
|
478
|
+
const tree = getOrchestratorTree();
|
|
372
479
|
// Load per-phase model routing config once at task entry (Plan 16 Slice 2).
|
|
373
480
|
// Empty / absent config produces inherit for every phase — no behaviour change.
|
|
374
481
|
// N-B-E: surface schema errors to caller (Decision 9 — orchestrators fail-fast).
|
|
@@ -513,9 +620,16 @@ export async function runTaskPipeline(opts) {
|
|
|
513
620
|
}
|
|
514
621
|
// ── 6a. Preflight gate ────────────────────────────────────────
|
|
515
622
|
if (fs.existsSync(preflightGate)) {
|
|
516
|
-
const
|
|
517
|
-
if (
|
|
518
|
-
|
|
623
|
+
const preflightOutcome = runPreflightGateWithData(preflightGate, phase.role, taskId, cwd);
|
|
624
|
+
if (preflightOutcome.result === "halt") {
|
|
625
|
+
// Render structured failure reason if available.
|
|
626
|
+
if (preflightOutcome.gateFailure) {
|
|
627
|
+
ctx.ui.notify(`× forge:run-task — preflight gate failed for phase ${phase.role} ` +
|
|
628
|
+
`[${preflightOutcome.gateFailure.reasonCode}]: ${preflightOutcome.gateFailure.detail}`, "error");
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
ctx.ui.notify(`× forge:run-task — preflight gate failed for phase ${phase.role} (exit 1); halting.`, "error");
|
|
632
|
+
}
|
|
519
633
|
writeState(cwd, {
|
|
520
634
|
taskId,
|
|
521
635
|
phaseIndex: currentPhaseIndex,
|
|
@@ -524,6 +638,18 @@ export async function runTaskPipeline(opts) {
|
|
|
524
638
|
lastError: `preflight gate exit 1 for ${phase.role}`,
|
|
525
639
|
savedAt: new Date().toISOString(),
|
|
526
640
|
});
|
|
641
|
+
// Spawn halt-recovery advisor (Tier 1, best-effort — non-fatal).
|
|
642
|
+
if (preflightOutcome.gateFailure) {
|
|
643
|
+
const advisorModel = resolveAdvisorModel(modelRoutingConfig, ctx.model);
|
|
644
|
+
void runHaltAdvisor({
|
|
645
|
+
gateFailure: preflightOutcome.gateFailure,
|
|
646
|
+
advisorModel,
|
|
647
|
+
taskId,
|
|
648
|
+
cwd,
|
|
649
|
+
ctx: { ui: ctx.ui },
|
|
650
|
+
forgeRoot,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
527
653
|
return {
|
|
528
654
|
status: "halted",
|
|
529
655
|
lastPhaseIndex: currentPhaseIndex,
|
|
@@ -531,7 +657,7 @@ export async function runTaskPipeline(opts) {
|
|
|
531
657
|
lastError: `preflight gate exit 1 for ${phase.role}`,
|
|
532
658
|
};
|
|
533
659
|
}
|
|
534
|
-
if (
|
|
660
|
+
if (preflightOutcome.result === "escalate") {
|
|
535
661
|
ctx.ui.notify(`× forge:run-task — preflight gate escalated for phase ${phase.role} (exit 2); manual intervention required.`, "error");
|
|
536
662
|
writeState(cwd, {
|
|
537
663
|
taskId,
|
|
@@ -646,6 +772,15 @@ export async function runTaskPipeline(opts) {
|
|
|
646
772
|
persona: phase.personaNoun,
|
|
647
773
|
});
|
|
648
774
|
registry.startPhase(taskId, phase.role, currentPhaseIndex);
|
|
775
|
+
// Bridge: register phase in OrchestratorTree.
|
|
776
|
+
const iteration = (opts.resumeFromState?.iterationCounts?.[phase.role] ?? 0) + 1;
|
|
777
|
+
const phaseNodeId = `${taskId}:${phase.role}:${iteration}`;
|
|
778
|
+
tree.startNode(phaseNodeId, {
|
|
779
|
+
parentId: taskId,
|
|
780
|
+
label: `${phase.role}:${iteration}`,
|
|
781
|
+
kind: "leaf",
|
|
782
|
+
promptPreview: taskBody.slice(0, 200),
|
|
783
|
+
});
|
|
649
784
|
// Capture the first stream-observed model on turn_end (IL10 visibility).
|
|
650
785
|
// If pi auto-substitutes or setModel silently no-ops, this line will diverge
|
|
651
786
|
// from requested_model — exactly the diagnostic signal we want.
|
|
@@ -680,6 +815,29 @@ export async function runTaskPipeline(opts) {
|
|
|
680
815
|
verboseKeys: { messageKey: MESSAGE_KEY },
|
|
681
816
|
afterEach: refreshStatus,
|
|
682
817
|
});
|
|
818
|
+
// ── Context governor injection (completes FORGE-S30-T07) ──────
|
|
819
|
+
// Per-phase factories built HERE because only the pipeline knows the
|
|
820
|
+
// `${personaNoun}/${role}` phase key — pi never sets persona/phase on
|
|
821
|
+
// ExtensionContext, and the parent session's registerHookDispatcher
|
|
822
|
+
// governor never sees subagent tool traffic (dormant-governor defect,
|
|
823
|
+
// CART-S02-T03 benchmark). Flag-gated: FORGE_CTX_GOVERNOR=1.
|
|
824
|
+
// buildGovernorFactory — Mechanisms A/B/C/D in the subagent
|
|
825
|
+
// buildForgeCompactionFactory — Mechanism E with warm-tier path opts
|
|
826
|
+
// (previously injected from index.ts with NO opts → warm-tier dead)
|
|
827
|
+
const phaseKey = `${phase.personaNoun}/${phase.role}`;
|
|
828
|
+
const sprintIdForSummaries = /^(.*)-T\d+$/.exec(taskId)?.[1];
|
|
829
|
+
const governorFactories = process.env.FORGE_CTX_GOVERNOR === "1"
|
|
830
|
+
? [
|
|
831
|
+
buildGovernorFactory({ phaseKey, cwd }),
|
|
832
|
+
buildForgeCompactionFactory({
|
|
833
|
+
cwd,
|
|
834
|
+
phaseKey,
|
|
835
|
+
entityId: taskId,
|
|
836
|
+
sprintId: sprintIdForSummaries,
|
|
837
|
+
}),
|
|
838
|
+
]
|
|
839
|
+
: [];
|
|
840
|
+
const phaseExtensionFactories = [...(opts.extensionFactories ?? []), ...governorFactories];
|
|
683
841
|
let result;
|
|
684
842
|
try {
|
|
685
843
|
// FORGE-BUG-040: wrap the runForgeSubagent dispatch in the phase
|
|
@@ -698,6 +856,7 @@ export async function runTaskPipeline(opts) {
|
|
|
698
856
|
modelRegistry: ctx.modelRegistry,
|
|
699
857
|
signal: opts.signal,
|
|
700
858
|
customTools: opts.forgeToolDefs ? getSubagentTools(opts.forgeToolDefs, persona.name) : undefined,
|
|
859
|
+
extensionFactories: phaseExtensionFactories.length > 0 ? phaseExtensionFactories : undefined,
|
|
701
860
|
}));
|
|
702
861
|
}
|
|
703
862
|
catch (err) {
|
|
@@ -728,7 +887,42 @@ export async function runTaskPipeline(opts) {
|
|
|
728
887
|
if (result.stopReason === "aborted" || opts.signal?.aborted) {
|
|
729
888
|
ctx.ui.notify(`⊘ forge:run-task — ${taskId} phase ${phase.role} cancelled.`, "info");
|
|
730
889
|
registry.completePhase(taskId, phase.role, "cancelled");
|
|
890
|
+
tree.completeNode(phaseNodeId, "cancelled");
|
|
731
891
|
registry.confirmCancelled(taskId);
|
|
892
|
+
// Bug B: account the billed tokens of this aborted attempt before returning.
|
|
893
|
+
{
|
|
894
|
+
const abortSprintId = readTaskRecord(taskId, storeCli, cwd)?.sprintId;
|
|
895
|
+
if (abortSprintId) {
|
|
896
|
+
emitIncompletePhaseEvent({
|
|
897
|
+
emitCtx: {
|
|
898
|
+
entityType: "task",
|
|
899
|
+
taskId,
|
|
900
|
+
sprintId: abortSprintId,
|
|
901
|
+
phase,
|
|
902
|
+
iteration: (iterationCounts[phase.role] ?? 0) + 1,
|
|
903
|
+
startMs: phaseStart,
|
|
904
|
+
endMs: Date.now(),
|
|
905
|
+
model: result.model ?? "unknown",
|
|
906
|
+
provider: result.provider ?? "unknown",
|
|
907
|
+
usage: {
|
|
908
|
+
input: result.usage.input,
|
|
909
|
+
output: result.usage.output,
|
|
910
|
+
cacheRead: result.usage.cacheRead,
|
|
911
|
+
cacheWrite: result.usage.cacheWrite,
|
|
912
|
+
},
|
|
913
|
+
judgement: undefined,
|
|
914
|
+
storeCli,
|
|
915
|
+
cwd,
|
|
916
|
+
},
|
|
917
|
+
outcome: "aborted",
|
|
918
|
+
notes: result.errorMessage ?? result.stopReason ?? undefined,
|
|
919
|
+
onDebug: writeDebug,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
writeDebug({ kind: "incomplete_emit_skipped", reason: "no-sprintId", outcome: "aborted" });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
732
926
|
// ADR-S21-01: preserve state file so cancelled runs are resumable
|
|
733
927
|
writeState(cwd, {
|
|
734
928
|
taskId,
|
|
@@ -746,6 +940,40 @@ export async function runTaskPipeline(opts) {
|
|
|
746
940
|
ctx.ui.notify(`× forge:run-task — phase ${phase.role} failed (exit ${result.exitCode})` +
|
|
747
941
|
(result.errorMessage ? `: ${result.errorMessage}` : "") +
|
|
748
942
|
(result.stopReason ? ` [${result.stopReason}]` : ""), "error");
|
|
943
|
+
// Bug B: account the billed tokens of this failed attempt before returning.
|
|
944
|
+
{
|
|
945
|
+
const failSprintId = readTaskRecord(taskId, storeCli, cwd)?.sprintId;
|
|
946
|
+
if (failSprintId) {
|
|
947
|
+
emitIncompletePhaseEvent({
|
|
948
|
+
emitCtx: {
|
|
949
|
+
entityType: "task",
|
|
950
|
+
taskId,
|
|
951
|
+
sprintId: failSprintId,
|
|
952
|
+
phase,
|
|
953
|
+
iteration: (iterationCounts[phase.role] ?? 0) + 1,
|
|
954
|
+
startMs: phaseStart,
|
|
955
|
+
endMs: Date.now(),
|
|
956
|
+
model: result.model ?? "unknown",
|
|
957
|
+
provider: result.provider ?? "unknown",
|
|
958
|
+
usage: {
|
|
959
|
+
input: result.usage.input,
|
|
960
|
+
output: result.usage.output,
|
|
961
|
+
cacheRead: result.usage.cacheRead,
|
|
962
|
+
cacheWrite: result.usage.cacheWrite,
|
|
963
|
+
},
|
|
964
|
+
judgement: undefined,
|
|
965
|
+
storeCli,
|
|
966
|
+
cwd,
|
|
967
|
+
},
|
|
968
|
+
outcome: "failed",
|
|
969
|
+
notes: result.errorMessage ?? result.stopReason ?? undefined,
|
|
970
|
+
onDebug: writeDebug,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
writeDebug({ kind: "incomplete_emit_skipped", reason: "no-sprintId", outcome: "failed" });
|
|
975
|
+
}
|
|
976
|
+
}
|
|
749
977
|
writeState(cwd, {
|
|
750
978
|
taskId,
|
|
751
979
|
phaseIndex: currentPhaseIndex,
|
|
@@ -854,7 +1082,7 @@ export async function runTaskPipeline(opts) {
|
|
|
854
1082
|
const verdict = readVerdict(taskId, phase.role, storeCli, cwd);
|
|
855
1083
|
if (verdict === "missing") {
|
|
856
1084
|
ctx.ui.notify(`× forge:run-task — verdict missing for phase ${phase.role} after subagent completed. ` +
|
|
857
|
-
"Subagent may have crashed or failed to write summaries.
|
|
1085
|
+
"Subagent may have crashed or failed to write summaries. Halting for advisory.", "error");
|
|
858
1086
|
writeState(cwd, {
|
|
859
1087
|
taskId,
|
|
860
1088
|
phaseIndex: currentPhaseIndex,
|
|
@@ -863,8 +1091,35 @@ export async function runTaskPipeline(opts) {
|
|
|
863
1091
|
lastError: `verdict missing for ${phase.role}`,
|
|
864
1092
|
savedAt: new Date().toISOString(),
|
|
865
1093
|
});
|
|
1094
|
+
// A missing verdict IS a postflight-outputs failure: the canonical
|
|
1095
|
+
// phase summary the subagent must write (e.g. summaries.code_review)
|
|
1096
|
+
// was never recorded, so there is no verdict to route on. review-phase
|
|
1097
|
+
// workflows declare no `outputs` block, so runPostflightGate is a
|
|
1098
|
+
// pass-through here — this readVerdict check is the effective gate.
|
|
1099
|
+
// Route it through the halt-recovery advisor (FORGE-S26-T18), the same
|
|
1100
|
+
// hand-off the postflight-gate-unsatisfied branch uses, instead of a
|
|
1101
|
+
// bare escalation — so the strongest configured model can diagnose the
|
|
1102
|
+
// missing-summary cause. Best-effort, non-fatal (FORGE-S26-T19 parity).
|
|
1103
|
+
const gateFailure = {
|
|
1104
|
+
phase: phase.role,
|
|
1105
|
+
reasonCode: "verdict-missing",
|
|
1106
|
+
detail: `Phase '${phase.role}' completed but no verdict was found in the store. ` +
|
|
1107
|
+
"The canonical phase summary was not written, so the orchestrator has no verdict to route on.",
|
|
1108
|
+
remediation: "Re-run the phase and ensure the subagent's forge_store set-summary call " +
|
|
1109
|
+
'uses args:["<recordId>", "<phaseKey>"] with the literal phase key as args[1] ' +
|
|
1110
|
+
"(e.g. code_review), and that the call exits zero before the subagent returns.",
|
|
1111
|
+
};
|
|
1112
|
+
const advisorModel = resolveAdvisorModel(modelRoutingConfig, ctx.model);
|
|
1113
|
+
void runHaltAdvisor({
|
|
1114
|
+
gateFailure,
|
|
1115
|
+
advisorModel,
|
|
1116
|
+
taskId,
|
|
1117
|
+
cwd,
|
|
1118
|
+
ctx: { ui: ctx.ui },
|
|
1119
|
+
forgeRoot,
|
|
1120
|
+
});
|
|
866
1121
|
return {
|
|
867
|
-
status: "
|
|
1122
|
+
status: "halted",
|
|
868
1123
|
lastPhaseIndex: currentPhaseIndex,
|
|
869
1124
|
iterationCounts,
|
|
870
1125
|
lastError: `verdict missing for ${phase.role}`,
|
|
@@ -917,8 +1172,57 @@ export async function runTaskPipeline(opts) {
|
|
|
917
1172
|
}
|
|
918
1173
|
// verdict === "approved": fall through to advance
|
|
919
1174
|
}
|
|
1175
|
+
// Postflight gate: evaluate `outputs` block after subagent returns,
|
|
1176
|
+
// before FSM status advance (FORGE-S26-T19). Hard enforcement in forge-cli;
|
|
1177
|
+
// plugin LLM route treats postflight as advisory. On UNSATISFIED: do not
|
|
1178
|
+
// advance currentPhaseIndex, halt, hand off to existing runHaltAdvisor.
|
|
1179
|
+
{
|
|
1180
|
+
const postflightGatePath = preflightGate.replace("preflight-gate.cjs", "postflight-gate.cjs");
|
|
1181
|
+
const postflightOutcome = runPostflightGate(postflightGatePath, phase.role, taskId, cwd);
|
|
1182
|
+
if (postflightOutcome.result === "unsatisfied") {
|
|
1183
|
+
if (postflightOutcome.gateFailure) {
|
|
1184
|
+
ctx.ui.notify(`× forge:run-task — postflight gate failed for phase ${phase.role} ` +
|
|
1185
|
+
`[${postflightOutcome.gateFailure.reasonCode}]: ${postflightOutcome.gateFailure.detail}`, "error");
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
ctx.ui.notify(`× forge:run-task — postflight gate failed for phase ${phase.role}; halting.`, "error");
|
|
1189
|
+
}
|
|
1190
|
+
// Do NOT advance FSM — write state at current phaseIndex (halted)
|
|
1191
|
+
writeState(cwd, {
|
|
1192
|
+
taskId,
|
|
1193
|
+
phaseIndex: currentPhaseIndex,
|
|
1194
|
+
iterationCounts,
|
|
1195
|
+
halted: true,
|
|
1196
|
+
lastError: `postflight gate exit 1 for ${phase.role}`,
|
|
1197
|
+
savedAt: new Date().toISOString(),
|
|
1198
|
+
});
|
|
1199
|
+
// Spawn halt-recovery advisor (Tier 1, best-effort — non-fatal).
|
|
1200
|
+
if (postflightOutcome.gateFailure) {
|
|
1201
|
+
const advisorModel = resolveAdvisorModel(modelRoutingConfig, ctx.model);
|
|
1202
|
+
void runHaltAdvisor({
|
|
1203
|
+
gateFailure: postflightOutcome.gateFailure,
|
|
1204
|
+
advisorModel,
|
|
1205
|
+
taskId,
|
|
1206
|
+
cwd,
|
|
1207
|
+
ctx: { ui: ctx.ui },
|
|
1208
|
+
forgeRoot,
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
return {
|
|
1212
|
+
status: "halted",
|
|
1213
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
1214
|
+
iterationCounts,
|
|
1215
|
+
lastError: `postflight gate exit 1 for ${phase.role}`,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
// "ok" or "error" — proceed to advance
|
|
1219
|
+
}
|
|
920
1220
|
// ── Advance to next phase ─────────────────────────────────────
|
|
921
1221
|
registry.completePhase(taskId, phase.role, "completed");
|
|
1222
|
+
tree.completeNode(phaseNodeId, "completed");
|
|
1223
|
+
tree.setNodeUsage(phaseNodeId, { input: result.usage.input, output: result.usage.output, cacheRead: result.usage.cacheRead });
|
|
1224
|
+
if (result.model)
|
|
1225
|
+
tree.setNodeModel(phaseNodeId, result.model, result.provider ?? "");
|
|
922
1226
|
writeState(cwd, {
|
|
923
1227
|
taskId,
|
|
924
1228
|
phaseIndex: currentPhaseIndex,
|
|
@@ -1058,6 +1362,9 @@ export function registerRunTask(pi, options = {}) {
|
|
|
1058
1362
|
// steal arrow keys from ctx.ui.select / ctx.ui.confirm dialogs.
|
|
1059
1363
|
const registry = getSessionRegistry();
|
|
1060
1364
|
registry.startSession(taskId);
|
|
1365
|
+
// Bridge: also register in OrchestratorTree for the dashboard overlay.
|
|
1366
|
+
const tree = getOrchestratorTree();
|
|
1367
|
+
tree.startNode(taskId, { label: taskId, kind: "orchestrator" });
|
|
1061
1368
|
const signal = registry.getAbortSignal(taskId);
|
|
1062
1369
|
const pipelineResult = await runTaskPipeline({
|
|
1063
1370
|
taskId,
|
|
@@ -1070,19 +1377,23 @@ export function registerRunTask(pi, options = {}) {
|
|
|
1070
1377
|
resumeFromState,
|
|
1071
1378
|
signal,
|
|
1072
1379
|
forgeToolDefs: options.forgeToolDefs,
|
|
1380
|
+
extensionFactories: options.extensionFactories,
|
|
1073
1381
|
});
|
|
1074
1382
|
// ── Handle result ────────────────────────────────────────────────
|
|
1075
1383
|
if (pipelineResult.status === "completed") {
|
|
1076
1384
|
registry.completeSession(taskId, "completed");
|
|
1385
|
+
tree.completeNode(taskId, "completed");
|
|
1077
1386
|
ctx.ui.notify(`〇 forge:run-task — ${taskId} pipeline complete (${PHASES.length} phases).`, "info");
|
|
1078
1387
|
}
|
|
1079
1388
|
else if (pipelineResult.status === "cancelled") {
|
|
1080
1389
|
// confirmCancelled was already called by the pipeline, but
|
|
1081
1390
|
// completeSession("cancelled") ensures the session ends cleanly.
|
|
1082
1391
|
registry.completeSession(taskId, "cancelled");
|
|
1392
|
+
tree.completeNode(taskId, "cancelled");
|
|
1083
1393
|
}
|
|
1084
1394
|
else {
|
|
1085
1395
|
registry.completeSession(taskId, "failed");
|
|
1396
|
+
tree.completeNode(taskId, "failed");
|
|
1086
1397
|
}
|
|
1087
1398
|
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1088
1399
|
ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
|