ai-foreman 1.7.20 → 1.7.21
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 +12 -2
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +18 -6
- package/dist/adapters/claude.d.ts +16 -0
- package/dist/adapters/claude.js +28 -3
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +99 -5
- package/dist/adapters/recovering.d.ts +9 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +7 -0
- package/dist/agentRun.js +24 -13
- package/dist/branch/currentGuard.d.ts +1 -0
- package/dist/branch/currentGuard.js +1 -0
- package/dist/branch/finalization.js +6 -1
- package/dist/branch/git.d.ts +1 -0
- package/dist/branch/git.js +15 -1
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +90 -4
- package/dist/buildAdmission.js +2 -0
- package/dist/buildAssignment.d.ts +33 -0
- package/dist/buildAssignment.js +215 -0
- package/dist/buildInterventions.d.ts +90 -0
- package/dist/buildInterventions.js +334 -0
- package/dist/buildOwnershipReconciliation.d.ts +19 -0
- package/dist/buildOwnershipReconciliation.js +41 -0
- package/dist/buildOwnershipRepair.d.ts +33 -0
- package/dist/buildOwnershipRepair.js +111 -0
- package/dist/buildRuns.d.ts +6 -0
- package/dist/buildRuns.js +23 -17
- package/dist/buildSyntheticWork.d.ts +2 -0
- package/dist/buildSyntheticWork.js +30 -0
- package/dist/buildWorkAdmission.d.ts +17 -0
- package/dist/buildWorkAdmission.js +57 -0
- package/dist/buildWorkAuthorization.d.ts +2 -0
- package/dist/buildWorkAuthorization.js +23 -0
- package/dist/buildWorkContext.d.ts +3 -0
- package/dist/buildWorkContext.js +6 -0
- package/dist/buildWorkMigration.d.ts +9 -0
- package/dist/buildWorkMigration.js +206 -0
- package/dist/buildWorkReconciliation.d.ts +12 -0
- package/dist/buildWorkReconciliation.js +70 -0
- package/dist/builderGuidanceFollowup.d.ts +15 -0
- package/dist/builderGuidanceFollowup.js +34 -0
- package/dist/cli/doctor.js +3 -2
- package/dist/cli/graph.d.ts +2 -0
- package/dist/cli/graph.js +75 -0
- package/dist/cli/manager.d.ts +8 -0
- package/dist/cli/manager.js +234 -12
- package/dist/cli/recovery.js +3 -2
- package/dist/cli/runtimeAuthPrompt.js +28 -8
- package/dist/cli/start.js +152 -48
- package/dist/cli/status.js +20 -5
- package/dist/cli/tickets.js +9 -7
- package/dist/continuity.d.ts +11 -0
- package/dist/continuity.js +22 -6
- package/dist/foreman.d.ts +10 -2
- package/dist/foreman.js +330 -39
- package/dist/graph/boundary.d.ts +15 -0
- package/dist/graph/boundary.js +50 -0
- package/dist/graph/bridge.d.ts +9 -0
- package/dist/graph/bridge.js +146 -0
- package/dist/graph/bridge.py +205 -0
- package/dist/graph/config.d.ts +11 -0
- package/dist/graph/config.js +40 -0
- package/dist/graph/corpus.d.ts +16 -0
- package/dist/graph/corpus.js +188 -0
- package/dist/graph/derived.d.ts +17 -0
- package/dist/graph/derived.js +73 -0
- package/dist/graph/host.d.ts +4 -0
- package/dist/graph/host.js +160 -0
- package/dist/graph/install.d.ts +3 -0
- package/dist/graph/install.js +50 -0
- package/dist/graph/lifecycle.d.ts +11 -0
- package/dist/graph/lifecycle.js +124 -0
- package/dist/graph/maintenance.d.ts +43 -0
- package/dist/graph/maintenance.js +301 -0
- package/dist/graph/manager.d.ts +9 -0
- package/dist/graph/manager.js +92 -0
- package/dist/graph/preparation.d.ts +41 -0
- package/dist/graph/preparation.js +43 -0
- package/dist/graph/read.d.ts +27 -0
- package/dist/graph/read.js +152 -0
- package/dist/graph/semantic.d.ts +17 -0
- package/dist/graph/semantic.js +27 -0
- package/dist/graph/session.d.ts +8 -0
- package/dist/graph/session.js +92 -0
- package/dist/graph/sources.d.ts +4 -0
- package/dist/graph/sources.js +11 -0
- package/dist/graph/storage.d.ts +38 -0
- package/dist/graph/storage.js +159 -0
- package/dist/graph/turn.d.ts +17 -0
- package/dist/graph/turn.js +360 -0
- package/dist/graph/util.d.ts +5 -0
- package/dist/graph/util.js +51 -0
- package/dist/handoffs.d.ts +1 -0
- package/dist/handoffs.js +5 -1
- package/dist/humanDecision.d.ts +21 -2
- package/dist/humanDecision.js +169 -11
- package/dist/index.js +2 -0
- package/dist/installOwnership.d.ts +46 -0
- package/dist/installOwnership.js +440 -0
- package/dist/linuxProbeInventory.d.ts +12 -0
- package/dist/linuxProbeInventory.js +48 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.js +7 -1
- package/dist/managerEvidence.d.ts +25 -0
- package/dist/managerEvidence.js +326 -0
- package/dist/managerEvidenceArtifacts.d.ts +38 -0
- package/dist/managerEvidenceArtifacts.js +118 -0
- package/dist/managerPacket.d.ts +2 -0
- package/dist/managerPacket.js +23 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +2 -2
- package/dist/observability.js +1 -1
- package/dist/processIdentity.js +7 -0
- package/dist/providerPhase.d.ts +13 -0
- package/dist/providerPhase.js +26 -0
- package/dist/qaAuthorizedDisposition.d.ts +24 -0
- package/dist/qaAuthorizedDisposition.js +26 -0
- package/dist/qaBuildGate.d.ts +4 -0
- package/dist/qaBuildGate.js +216 -0
- package/dist/qaBuilderCoverage.d.ts +5 -0
- package/dist/qaBuilderCoverage.js +78 -0
- package/dist/qaContractCoverage.d.ts +10 -0
- package/dist/qaContractCoverage.js +55 -0
- package/dist/qaContractDelivery.d.ts +23 -0
- package/dist/qaContractDelivery.js +145 -0
- package/dist/qaContractFreshness.d.ts +15 -0
- package/dist/qaContractFreshness.js +36 -0
- package/dist/qaDeliveryJournal.d.ts +10 -0
- package/dist/qaDeliveryJournal.js +16 -1
- package/dist/qaEffectiveConfig.d.ts +20 -0
- package/dist/qaEffectiveConfig.js +40 -0
- package/dist/qaEvidenceReader.d.ts +14 -0
- package/dist/qaEvidenceReader.js +176 -0
- package/dist/qaFailureDelivery.d.ts +2 -0
- package/dist/qaFailureDelivery.js +107 -10
- package/dist/qaHandbackMigration.d.ts +1 -1
- package/dist/qaHandbackMigration.js +3 -3
- package/dist/qaPreparation.d.ts +26 -0
- package/dist/qaPreparation.js +150 -0
- package/dist/qaPreparationInputs.d.ts +8 -0
- package/dist/qaPreparationInputs.js +47 -0
- package/dist/qaPreparationMetrics.d.ts +145 -0
- package/dist/qaPreparationMetrics.js +120 -0
- package/dist/qaPreparationPolicy.d.ts +5 -0
- package/dist/qaPreparationPolicy.js +51 -0
- package/dist/qaPreparationStore.d.ts +157 -0
- package/dist/qaPreparationStore.js +412 -0
- package/dist/qaProtocolV2.d.ts +28 -0
- package/dist/qaProtocolV2.js +12 -0
- package/dist/qaReview.d.ts +6 -0
- package/dist/qaReview.js +234 -24
- package/dist/qaTimeline.d.ts +18 -0
- package/dist/qaTimeline.js +123 -0
- package/dist/qaVerificationContract.d.ts +17 -0
- package/dist/qaVerificationContract.js +236 -0
- package/dist/readinessCleanup.js +12 -5
- package/dist/recoveryGuidance.d.ts +9 -0
- package/dist/recoveryGuidance.js +13 -0
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +1 -0
- package/dist/runtimeReadiness.js +13 -1
- package/dist/sessionIdentity.d.ts +2 -0
- package/dist/sessionIdentity.js +14 -0
- package/dist/sessionLifecycle.js +3 -3
- package/dist/sources/sourceRegistry.js +7 -0
- package/dist/stateTransfer.js +50 -1
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.js +18 -8
- package/dist/terminalStyle.d.ts +21 -0
- package/dist/terminalStyle.js +50 -0
- package/dist/ticketPopulation.d.ts +1 -0
- package/dist/ticketPopulation.js +5 -1
- package/dist/tickets/commands.js +2 -2
- package/dist/tickets/queue.js +1 -1
- package/dist/tickets/ticketSchema.d.ts +198 -0
- package/dist/tickets/ticketSchema.js +2 -0
- package/dist/workflowDb.d.ts +51 -5
- package/dist/workflowDb.js +338 -7
- package/dist/workflowReader.d.ts +109 -2
- package/dist/workflowReader.js +48 -4
- package/package.json +84 -7
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- package/scripts/graph-evaluation.mjs +85 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BuilderAdapter } from "./adapters/types.js";
|
|
2
|
+
import { type BuilderGuidanceFollowup } from "./buildAssignment.js";
|
|
3
|
+
import type { QaFixResult } from "./qaReview.js";
|
|
4
|
+
/** One journaled mutation shared by current-branch and isolated-worktree resume. */
|
|
5
|
+
export declare function deliverBuilderGuidanceFollowup(scope: {
|
|
6
|
+
projectDir: string;
|
|
7
|
+
runId: string;
|
|
8
|
+
ticketId: string;
|
|
9
|
+
worktree: string;
|
|
10
|
+
}, instruction: string, followup: BuilderGuidanceFollowup, host: {
|
|
11
|
+
prepare(instruction: string): Promise<BuilderAdapter>;
|
|
12
|
+
validateRequirements(): void;
|
|
13
|
+
recordSession?(adapter: BuilderAdapter): void;
|
|
14
|
+
completed?(operationId: string, adapter: BuilderAdapter): Promise<void>;
|
|
15
|
+
}): Promise<QaFixResult>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { collectBuilderContractCoverage } from "./qaBuilderCoverage.js";
|
|
2
|
+
import { maintainGraphTask } from "./graph/boundary.js";
|
|
3
|
+
import { ensureBuilderContract } from "./qaBuildGate.js";
|
|
4
|
+
import { graphContext, sendGraphTurn } from "./graph/turn.js";
|
|
5
|
+
import { beginBuildAssignment, finishBuildAssignment } from "./buildAssignment.js";
|
|
6
|
+
import { parseStepStatus } from "./foreman.js";
|
|
7
|
+
/** One journaled mutation shared by current-branch and isolated-worktree resume. */
|
|
8
|
+
export async function deliverBuilderGuidanceFollowup(scope, instruction, followup, host) {
|
|
9
|
+
try {
|
|
10
|
+
const adapter = await host.prepare(instruction);
|
|
11
|
+
await adapter.prepareSession?.();
|
|
12
|
+
host.validateRequirements();
|
|
13
|
+
host.recordSession?.(adapter);
|
|
14
|
+
instruction += "\n" + await ensureBuilderContract(scope.projectDir, scope.runId, scope.ticketId, scope.worktree, adapter);
|
|
15
|
+
const assignment = beginBuildAssignment(scope.projectDir, scope.runId, scope.ticketId, scope.worktree, instruction, followup, adapter);
|
|
16
|
+
// The handback policy prevents provider wrappers from retrying this mutation.
|
|
17
|
+
const result = await sendGraphTurn(adapter, assignment.instruction, graphContext(scope.projectDir, scope.worktree, "manager-guidance", assignment.instruction, { logicalTaskId: assignment.operationId, operationId: assignment.operationId, runId: scope.runId, workId: scope.ticketId }), { handback: true, logicalActionId: assignment.operationId });
|
|
18
|
+
const status = parseStepStatus(result.text);
|
|
19
|
+
if (!result.isError && !result.failure && status.kind === "done")
|
|
20
|
+
await collectBuilderContractCoverage(scope.projectDir, scope.runId, scope.ticketId, scope.worktree, adapter, assignment.operationId, assignment.instruction, result);
|
|
21
|
+
const rejection = finishBuildAssignment(scope.projectDir, assignment, result, status);
|
|
22
|
+
if (!rejection && !result.isError && status.kind === "done")
|
|
23
|
+
await maintainGraphTask(scope.projectDir, scope.worktree, assignment.operationId, adapter).catch(() => undefined);
|
|
24
|
+
await host.completed?.(assignment.operationId, adapter);
|
|
25
|
+
if (rejection || result.isError || result.failure || status.kind !== "done" || !result.turnId)
|
|
26
|
+
return { ok: false, detail: rejection ?? result.failure?.diagnostics ?? `Builder follow-up stopped with ${status.kind}; reconcile its result before continuing`, response: result.text };
|
|
27
|
+
return { ok: true, response: result.text, summary: status.summary ?? "Builder applied waiting guidance", providerTurnId: result.turnId };
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// A thrown submission leaves the assignment in progress. The existing
|
|
31
|
+
// assignment fence prevents another Builder turn without reconciliation.
|
|
32
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -11,6 +11,7 @@ import { resolveExecutablePath, sanitizeDiagnostics } from "../runtimeReadiness.
|
|
|
11
11
|
import { ObservabilityReader, ObservabilityStore } from "../observability.js";
|
|
12
12
|
import { WorkflowReader } from "../workflowReader.js";
|
|
13
13
|
import { WorkflowDb } from "../workflowDb.js";
|
|
14
|
+
import { error, success, warning } from "../terminalStyle.js";
|
|
14
15
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))?.version;
|
|
15
16
|
function commandVersion(command) {
|
|
16
17
|
const result = spawnSync(command, ["--version"], {
|
|
@@ -41,12 +42,12 @@ export function buildDoctorCommand() {
|
|
|
41
42
|
}
|
|
42
43
|
let errors = 0;
|
|
43
44
|
const report = (ok, label, detail) => {
|
|
44
|
-
console.log(`${ok ? "ok" : "!!"} ${label}${detail ? ` — ${detail}` : ""}`);
|
|
45
|
+
console.log(`${ok ? success("ok") : error("!!")} ${label}${detail ? ` — ${detail}` : ""}`);
|
|
45
46
|
if (!ok)
|
|
46
47
|
errors++;
|
|
47
48
|
};
|
|
48
49
|
const warn = (label, detail) => {
|
|
49
|
-
console.log(
|
|
50
|
+
console.log(`${warning("--")} ${label}${detail ? ` — ${detail}` : ""}`);
|
|
50
51
|
};
|
|
51
52
|
report(Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10) >= 20, "node >=20", process.version);
|
|
52
53
|
report(Boolean(PACKAGE_VERSION), "foreman package version", PACKAGE_VERSION);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { pruneGraph } from "../graph/lifecycle.js";
|
|
2
|
+
import { fileSemanticHost } from "../graph/semantic.js";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { assertGraphConfig, assertGraphReadRequest, DEFAULT_GRAPH_CONFIG } from "rafi-spec";
|
|
8
|
+
import { loadGraphConfig } from "../graph/config.js";
|
|
9
|
+
import { adoptGraph, disableGraph, refreshGraph, ensureGraphInstallation } from "../graph/maintenance.js";
|
|
10
|
+
import { acquireGraphView, graphStatus, readGraph } from "../graph/read.js";
|
|
11
|
+
import { captureGraphCorpus } from "../graph/corpus.js";
|
|
12
|
+
export function buildGraphCommand() {
|
|
13
|
+
const command = new Command("graph").description("Adopt and inspect scoped local Graphify evidence. Reads never install or refresh.");
|
|
14
|
+
command.command("status").argument("[project]", "project configuration root", ".").option("--json", "emit structured status").action((project) => {
|
|
15
|
+
const result = graphStatus(resolve(project));
|
|
16
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17
|
+
});
|
|
18
|
+
command.command("adopt").argument("[project]", "project configuration root", ".").option("--yes", "accept the displayed scope and selective maintenance policy").option("--config <file>", "complete graph policy JSON").option("--code-only", "explicitly omit semantic extraction").action(async (project, opts) => {
|
|
19
|
+
const root = resolve(project);
|
|
20
|
+
const config = opts.config ? JSON.parse(readFileSync(resolve(opts.config), "utf8")) : structuredClone(DEFAULT_GRAPH_CONFIG);
|
|
21
|
+
if (opts.codeOnly)
|
|
22
|
+
config.mode = "code-only";
|
|
23
|
+
assertGraphConfig(config);
|
|
24
|
+
console.log(`Graphify adoption policy:\n${JSON.stringify(config, null, 2)}\nReuses a compatible installation or installs pinned Graphify in a Rafi-owned isolated environment. Initial extraction and selective maintenance are authorized by acceptance.`);
|
|
25
|
+
if (!opts.yes) {
|
|
26
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
27
|
+
throw new Error("Adoption requires --yes and a complete policy in noninteractive mode");
|
|
28
|
+
const { confirm, isCancel } = await import("@clack/prompts");
|
|
29
|
+
const accepted = await confirm({ message: "Accept this Graphify scope and policy?", initialValue: true });
|
|
30
|
+
if (isCancel(accepted) || !accepted)
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const adoption = adoptGraph(root, { config, authorization: "explicit" });
|
|
34
|
+
const python = await ensureGraphInstallation(root);
|
|
35
|
+
const result = await refreshGraph(root, root, adoption.initialOperationId, { python });
|
|
36
|
+
console.log(JSON.stringify(result, null, 2));
|
|
37
|
+
if (result.state !== "published")
|
|
38
|
+
throw new Error(result.reason ?? "Initial graph unavailable");
|
|
39
|
+
});
|
|
40
|
+
command.command("refresh").argument("[project]", "project configuration root", ".").option("--workspace <path>", "authorized source workspace").option("--task <id>", "stable task id for idempotent retry").option("--semantic-request <file>", "write captured host extraction packet (requires --task)").option("--semantic-result <file>", "consume exact host result (requires --task)").action(async (project, opts) => {
|
|
41
|
+
if ((opts.semanticRequest || opts.semanticResult) && !opts.task)
|
|
42
|
+
throw new Error("Semantic exchange requires a stable --task ID");
|
|
43
|
+
if (opts.semanticRequest && opts.semanticResult)
|
|
44
|
+
throw new Error("Choose request or result exchange, not both");
|
|
45
|
+
const root = resolve(project), workspace = opts.workspace ? resolve(opts.workspace) : root;
|
|
46
|
+
const result = await refreshGraph(root, workspace, opts.task ?? randomUUID(), { semanticHost: fileSemanticHost({ requestPath: opts.semanticRequest ? resolve(opts.semanticRequest) : undefined, resultPath: opts.semanticResult ? resolve(opts.semanticResult) : undefined }) });
|
|
47
|
+
console.log(JSON.stringify(result, null, 2));
|
|
48
|
+
if (result.state !== "published")
|
|
49
|
+
throw new Error(result.reason ?? "Graph refresh unavailable");
|
|
50
|
+
});
|
|
51
|
+
command.command("prune").argument("[project]", "project configuration root", ".").action((project) => console.log(JSON.stringify(pruneGraph(resolve(project)), null, 2)));
|
|
52
|
+
command.command("disable").argument("[project]", "project configuration root", ".").action((project) => { disableGraph(resolve(project)); console.log("Graph integration disabled; retained evidence preserved."); });
|
|
53
|
+
command.command("enable").argument("[project]", "project configuration root", ".").action((project) => {
|
|
54
|
+
const root = resolve(project), prior = loadGraphConfig(root);
|
|
55
|
+
if (!prior.adoption)
|
|
56
|
+
throw new Error("No prior adoption; use graph adopt");
|
|
57
|
+
adoptGraph(root, { config: { ...prior.adoption.config, enabled: true }, authorization: "explicit" });
|
|
58
|
+
console.log("Graph integration enabled under its accepted policy.");
|
|
59
|
+
});
|
|
60
|
+
for (const operation of ["query", "node", "neighbors", "path", "impact"]) {
|
|
61
|
+
command.command(operation).argument("<query>", "query text or node id").argument("[target]", "target node for path").option("--project <path>", "configuration/source project root", ".").option("--json", "structured evidence").option("--direction <direction>", "incoming | outgoing | both", "both").action(async (query, target, opts) => {
|
|
62
|
+
const root = resolve(opts.project);
|
|
63
|
+
const op = { operation, ...(operation === "query" ? { query } : { seeds: [query] }), ...(target ? { target } : {}), direction: opts.direction };
|
|
64
|
+
assertGraphReadRequest({ kind: "rafi_graph_request", version: 1, requestId: "cli", operations: [op] });
|
|
65
|
+
const config = loadGraphConfig(root);
|
|
66
|
+
const corpus = config.enabled ? captureGraphCorpus(root, root, config) : undefined;
|
|
67
|
+
const view = acquireGraphView(root, root, corpus);
|
|
68
|
+
const result = "graph" in view ? await readGraph(view, op) : view;
|
|
69
|
+
console.log(JSON.stringify(result, null, 2));
|
|
70
|
+
if (result.status === "unavailable" || result.status === "invalid-request")
|
|
71
|
+
throw new Error("Graph evidence unavailable; see the structured limitation above");
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return command;
|
|
75
|
+
}
|
package/dist/cli/manager.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { ManagerEvidenceService } from "../managerEvidence.js";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import type { EffortLevel } from "../adapters/types.js";
|
|
4
|
+
import type { ManagerEvidencePageV2 } from "rafi-spec";
|
|
3
5
|
import type { ExternalDiagnosticMode } from "../diagnostics.js";
|
|
4
6
|
export interface ManagerCommandOptions {
|
|
5
7
|
resolveProject?: (project: string | undefined) => string;
|
|
@@ -15,3 +17,9 @@ export declare function runManager(projectDir: string, options: {
|
|
|
15
17
|
fast?: boolean;
|
|
16
18
|
external?: ExternalDiagnosticMode;
|
|
17
19
|
}): Promise<void>;
|
|
20
|
+
/** Only original user commands enter here. Model output and retained reports cannot export raw bytes. */
|
|
21
|
+
export declare function executeManagerHostCommand(service: ManagerEvidenceService, command: string, write: (text: string) => void, complete?: boolean): boolean;
|
|
22
|
+
/** Drain one pinned snapshot and expand every referenced artifact before host close.
|
|
23
|
+
* Errors are returned as incomplete output; an expired snapshot is never refreshed.
|
|
24
|
+
* This host output is deliberately independent of bounded model packets. */
|
|
25
|
+
export declare function completeManagerHostEvidence(service: ManagerEvidenceService, first: ManagerEvidencePageV2): ManagerEvidencePageV2;
|
package/dist/cli/manager.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
import { dispatchWithGraphAccess } from "../graph/session.js";
|
|
2
|
+
import { createManagerGraphEvidence, parseManagerGraphRequest } from "../graph/manager.js";
|
|
3
|
+
import { prepareOwnershipRepair, repairOwnership } from "../buildOwnershipRepair.js";
|
|
4
|
+
import { BuildInterventionControl, parseManagerAction } from "../buildInterventions.js";
|
|
1
5
|
import { randomUUID } from "node:crypto";
|
|
2
6
|
import { createInterface } from "node:readline/promises";
|
|
3
7
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
8
|
import { resolve } from "node:path";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { ManagerEvidenceService, parseManagerEvidenceRequestV2 } from "../managerEvidence.js";
|
|
13
|
+
import { buildManagerEvidencePacketV2 } from "../managerPacket.js";
|
|
5
14
|
import { Command } from "commander";
|
|
6
15
|
import { createRoleBuilder, readOnlyPermissionConfig } from "../agentRun.js";
|
|
7
16
|
import { Log } from "../log.js";
|
|
8
17
|
import { buildManagerEvidencePacket, buildManagerProjectPacket } from "../managerPacket.js";
|
|
9
18
|
import { ManagerSessionRecorder } from "../observability.js";
|
|
10
19
|
import { collectManagerProjectDiagnostics, executeManagerEvidenceRequest, MANAGER_LOOKUP_MAX_ROUNDS, parseManagerEvidenceRequest, resolveManagerQuestionRuns } from "../projectDiagnostics.js";
|
|
20
|
+
import { accent, sanitizeTerminalText } from "../terminalStyle.js";
|
|
11
21
|
export function buildManagerCommand(options = {}) {
|
|
12
22
|
return new Command("manager")
|
|
13
|
-
.description("
|
|
23
|
+
.description("Inspect retained builds and enqueue explicitly scoped Builder/QA guidance.")
|
|
14
24
|
.argument(options.requireProject ? "<project>" : "[project]", "project directory")
|
|
15
25
|
.option("--run <run-id>", "set the initial focused build run")
|
|
16
26
|
.option("--ask <question>", "ask one question and exit")
|
|
@@ -19,6 +29,7 @@ export function buildManagerCommand(options = {}) {
|
|
|
19
29
|
.option("--effort <level>", "low | medium | high | xhigh")
|
|
20
30
|
.option("--fast", "use the provider fast mode")
|
|
21
31
|
.option("--external <mode>", "auto | on | off", "auto")
|
|
32
|
+
.addHelpText("after", "\nHost evidence commands (also accepted by --ask):\n /qa-work <run>\n /qa-attempts <run> <ticket>\n /qa-report <run> <ticket> <attempt> [occurrence]\n /qa-timeline <run> <ticket>\n /qa-conflicts <run>\n /qa-instruction <run> <work> <instruction>\n /qa-repair-plan <run> <work>\n /qa-repair <JSON request>\n /more <cursor>\n /artifact <handle>\n /qa-export <run> <ticket> <attempt> [occurrence]\n /qa-export <handle> Export protected raw bytes to a private local file.\nScoped controls:\n /guide-builder <run> <work> <text>\n /guide-qa <run> <work> <text>\n /guide-both <run> <work> <text>\n /pause <run> <work> [run]\n /withdraw <run> <work> <instruction>\n /supersede <run> <work> <instruction> <text>\n /request-attempt <run> <work> <reason>\n /answer-question <run> <work> <decision> <revision> <text>\n")
|
|
22
33
|
.action(async (project, opts) => {
|
|
23
34
|
const root = options.resolveProject ? options.resolveProject(project) : resolve(project ?? ".");
|
|
24
35
|
await runManager(root, {
|
|
@@ -30,15 +41,27 @@ export function buildManagerCommand(options = {}) {
|
|
|
30
41
|
export async function runManager(projectDir, options) {
|
|
31
42
|
if (!options.ask && (!input.isTTY || !output.isTTY))
|
|
32
43
|
throw new Error("non-TTY Manager use requires --ask <question>");
|
|
44
|
+
const evidence = new ManagerEvidenceService(projectDir);
|
|
45
|
+
if (options.ask && (options.ask.trim().startsWith("/") || parseManagerAction(options.ask))) {
|
|
46
|
+
try {
|
|
47
|
+
if (!executeManagerHostCommand(evidence, options.ask, text => output.write(text), true))
|
|
48
|
+
throw new Error("Unknown Manager host command; see manager --help");
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
evidence.close();
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
33
55
|
const initialCollection = collectManagerProjectDiagnostics(projectDir, { initialFocusRunId: options.runId, question: options.ask, external: "off" });
|
|
34
56
|
const initialReport = initialCollection.report;
|
|
35
57
|
let currentFocusRunId = initialReport.initialFocusRunId;
|
|
36
58
|
let referencedRunIds = [currentFocusRunId];
|
|
37
|
-
|
|
38
|
-
output.write(
|
|
39
|
-
output.write(
|
|
59
|
+
const managerLabel = (text) => accent(text, { stream: output, env: process.env, machineReadable: Boolean(options.ask) });
|
|
60
|
+
output.write(`${managerLabel("rafi manager")}: ${initialReport.totalRunCount} retained build run${initialReport.totalRunCount === 1 ? "" : "s"}\n`);
|
|
61
|
+
output.write(`${managerLabel("rafi manager")}: verified active run ${sanitizeTerminalText(initialReport.verifiedActiveRunId ?? "none")}\n`);
|
|
62
|
+
output.write(`${managerLabel("rafi manager")}: initial focus ${sanitizeTerminalText(currentFocusRunId)}\n`);
|
|
40
63
|
if (initialReport.staleRecoveryRunIds.length)
|
|
41
|
-
output.write(
|
|
64
|
+
output.write(`${managerLabel("rafi manager")}: stale recovery state ${initialReport.staleRecoveryRunIds.map(sanitizeTerminalText).join(", ")}\n`);
|
|
42
65
|
const metadata = new ManagerSessionRecorder(projectDir);
|
|
43
66
|
const managerSessionId = randomUUID();
|
|
44
67
|
const startedAt = new Date().toISOString();
|
|
@@ -52,6 +75,9 @@ export async function runManager(projectDir, options) {
|
|
|
52
75
|
const activeRole = role = await createRoleBuilder({ projectDir, role: "manager", agent: options.agent, model: options.model, effort: options.effort, fast: options.fast,
|
|
53
76
|
yes: true, allowSwitch: false, label: "Manager", log: new Log(), permissionConfig: denyManagerTools(), sandboxMode: "read-only", persistSessionBindings: false });
|
|
54
77
|
const ask = async (question) => {
|
|
78
|
+
if (executeManagerHostCommand(evidence, question, text => output.write(text), Boolean(options.ask)))
|
|
79
|
+
return;
|
|
80
|
+
let emittedMachineOutput = false;
|
|
55
81
|
const refreshed = collectManagerProjectDiagnostics(projectDir, { initialFocusRunId: initialReport.initialFocusRunId, currentFocusRunId, referencedRunIds, question, external: "off" });
|
|
56
82
|
const resolved = resolveManagerQuestionRuns(refreshed.allSummaries, question, currentFocusRunId, referencedRunIds);
|
|
57
83
|
currentFocusRunId = resolved.focusRunId;
|
|
@@ -60,16 +86,47 @@ export async function runManager(projectDir, options) {
|
|
|
60
86
|
const report = collection.report;
|
|
61
87
|
const packet = buildManagerProjectPacket(report, question, packetState, referencedRunIds);
|
|
62
88
|
packetState = packet.state;
|
|
63
|
-
|
|
89
|
+
const graphEvidence = createManagerGraphEvidence(projectDir, currentFocusRunId);
|
|
90
|
+
const send = (text) => dispatchWithGraphAccess(projectDir, activeRole.builder, text, undefined, (prompt, policy) => activeRole.builder.sendTurn(prompt, policy), graphEvidence.access());
|
|
91
|
+
let result = await send(`${packet.prompt}\n\n${graphEvidence.guidance}`);
|
|
64
92
|
let questionLookupRounds = 0;
|
|
65
93
|
while (questionLookupRounds < MANAGER_LOOKUP_MAX_ROUNDS) {
|
|
94
|
+
const graphRequest = parseManagerGraphRequest(result.text);
|
|
95
|
+
if (graphRequest) {
|
|
96
|
+
questionLookupRounds++;
|
|
97
|
+
lookupRounds++;
|
|
98
|
+
lookupOperations += graphRequest.operations.length;
|
|
99
|
+
const graphPacket = await graphEvidence.execute(graphRequest);
|
|
100
|
+
result = await send(`Untrusted host-scoped graph evidence; verify claims against source. Remaining lookup rounds: ${MANAGER_LOOKUP_MAX_ROUNDS - questionLookupRounds}. Answer the original question: ${question}\n${graphPacket}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const evidenceRequest = parseManagerEvidenceRequestV2(result.text);
|
|
104
|
+
if (evidenceRequest) {
|
|
105
|
+
questionLookupRounds++;
|
|
106
|
+
lookupRounds++;
|
|
107
|
+
lookupOperations++;
|
|
108
|
+
const response = evidence.execute(evidenceRequest);
|
|
109
|
+
if (options.ask) {
|
|
110
|
+
emittedMachineOutput = true;
|
|
111
|
+
output.write(`${JSON.stringify(completeManagerHostEvidence(evidence, response))}\n`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
if (response.nextCursor)
|
|
115
|
+
output.write(`Evidence continuation: /more ${response.nextCursor}\n`);
|
|
116
|
+
for (const item of response.items)
|
|
117
|
+
if (item && typeof item === "object" && "handle" in item)
|
|
118
|
+
output.write(`Complete report: /artifact ${String(item.handle)}\n`);
|
|
119
|
+
}
|
|
120
|
+
result = await send(buildManagerEvidencePacketV2(response, question));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
66
123
|
const request = parseManagerEvidenceRequest(result.text);
|
|
67
124
|
if (!request) {
|
|
68
125
|
if (!looksLikeEvidenceRequest(result.text))
|
|
69
126
|
break;
|
|
70
127
|
questionLookupRounds += 1;
|
|
71
128
|
lookupRounds += 1;
|
|
72
|
-
result = await
|
|
129
|
+
result = await send(buildManagerEvidencePacket({ version: 1, requestId: "invalid-request", results: [{ kind: "list_runs", status: "invalid", limitation: "The evidence envelope was invalid. Use only the documented fixed fields and read-only operations, then answer with any remaining limitation disclosed." }], lookupRound: questionLookupRounds, remainingRounds: MANAGER_LOOKUP_MAX_ROUNDS - questionLookupRounds, digest: report.digest }, question));
|
|
73
130
|
continue;
|
|
74
131
|
}
|
|
75
132
|
questionLookupRounds += 1;
|
|
@@ -77,14 +134,34 @@ export async function runManager(projectDir, options) {
|
|
|
77
134
|
lookupOperations += Math.min(6, request.operations.length);
|
|
78
135
|
const response = executeManagerEvidenceRequest(projectDir, request, collection, questionLookupRounds);
|
|
79
136
|
packetState.lastEvidenceScope = request.operations.flatMap(operation => "runIds" in operation ? operation.runIds : []);
|
|
80
|
-
result = await
|
|
137
|
+
result = await send(buildManagerEvidencePacket(response, question));
|
|
81
138
|
}
|
|
82
139
|
const unfulfilled = parseManagerEvidenceRequest(result.text);
|
|
140
|
+
const pendingEvidence = parseManagerEvidenceRequestV2(result.text);
|
|
141
|
+
if (pendingEvidence) {
|
|
142
|
+
const page = evidence.execute(pendingEvidence);
|
|
143
|
+
if (options.ask) {
|
|
144
|
+
emittedMachineOutput = true;
|
|
145
|
+
output.write(`${JSON.stringify(completeManagerHostEvidence(evidence, page))}\n`);
|
|
146
|
+
}
|
|
147
|
+
else
|
|
148
|
+
output.write(`${JSON.stringify(page)}\n${page.nextCursor ? `Continue with /more ${page.nextCursor}` : "Browse reports with /qa-attempts <run> <ticket> and /qa-report <run> <ticket> <attempt>"}\n`);
|
|
149
|
+
}
|
|
83
150
|
if (unfulfilled) {
|
|
84
151
|
const limitation = { version: 1, requestId: unfulfilled.requestId, results: unfulfilled.operations.slice(0, 6).map(operation => ({ kind: operation.kind, status: "limited", limitation: "the two-round evidence lookup budget is exhausted; answer with this limitation disclosed" })), lookupRound: MANAGER_LOOKUP_MAX_ROUNDS, remainingRounds: 0, digest: report.digest };
|
|
85
|
-
result = await
|
|
152
|
+
result = await send(buildManagerEvidencePacket(limitation, question));
|
|
153
|
+
}
|
|
154
|
+
if (!pendingEvidence) {
|
|
155
|
+
const answer = parseManagerEvidenceRequest(result.text) || parseManagerGraphRequest(result.text) || looksLikeEvidenceRequest(result.text)
|
|
156
|
+
? "Manager could not complete an answer within the bounded evidence lookup budget. Continue with /qa-attempts <run> <ticket> or /qa-report <run> <ticket> <attempt>."
|
|
157
|
+
: result.text.trim();
|
|
158
|
+
// One-shot evidence requests can mix JSON with prose, so add the label
|
|
159
|
+
// only when this answer stream has not emitted machine-readable data.
|
|
160
|
+
// Model-provided text is sanitized only for display, never for storage.
|
|
161
|
+
if (!emittedMachineOutput)
|
|
162
|
+
output.write(`${accent("Manager:", { stream: output, env: process.env })} `);
|
|
163
|
+
output.write(`${sanitizeTerminalText(answer)}\n`);
|
|
86
164
|
}
|
|
87
|
-
output.write(`${parseManagerEvidenceRequest(result.text) || looksLikeEvidenceRequest(result.text) ? "Manager could not complete an answer within the bounded evidence lookup budget; the available project report remains partial." : result.text.trim()}\n`);
|
|
88
165
|
usage = await activeRole.builder.sessionUsage?.() ?? usage;
|
|
89
166
|
metadata.record({ sessionId: managerSessionId, runId: initialReport.initialFocusRunId, provider: activeRole.builder.agent, startedAt, reportDigest: report.digest,
|
|
90
167
|
inputTokens: usage?.inputTokens, outputTokens: usage?.outputTokens, costUsd: usage?.authoritativeCostUsd, scope: "project", latestFocusRunId: currentFocusRunId,
|
|
@@ -99,11 +176,11 @@ export async function runManager(projectDir, options) {
|
|
|
99
176
|
readline.on("SIGINT", onInterrupt);
|
|
100
177
|
try {
|
|
101
178
|
while (true) {
|
|
102
|
-
const question = await readline.question("manager>
|
|
179
|
+
const question = await readline.question(`${accent("manager>", { stream: output, env: process.env })} `);
|
|
103
180
|
if (question.trim() === "/exit")
|
|
104
181
|
break;
|
|
105
182
|
if (question.trim())
|
|
106
|
-
await ask(question
|
|
183
|
+
await ask(question);
|
|
107
184
|
}
|
|
108
185
|
}
|
|
109
186
|
catch (error) {
|
|
@@ -124,7 +201,152 @@ export async function runManager(projectDir, options) {
|
|
|
124
201
|
finally {
|
|
125
202
|
await role?.builder.close().catch(() => { });
|
|
126
203
|
metadata.close();
|
|
204
|
+
evidence.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Only original user commands enter here. Model output and retained reports cannot export raw bytes. */
|
|
208
|
+
export function executeManagerHostCommand(service, command, write, complete = false) {
|
|
209
|
+
if (command.trim().startsWith("/qa-repair ")) {
|
|
210
|
+
write(`${JSON.stringify(repairOwnership(service.projectDir, JSON.parse(command.trim().slice("/qa-repair ".length)), command))}\n`);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (command.trim().startsWith("/qa-repair-plan ")) {
|
|
214
|
+
const args = command.trim().split(/\s+/);
|
|
215
|
+
if (args.length !== 3)
|
|
216
|
+
throw new Error("/qa-repair-plan requires run and work identities");
|
|
217
|
+
write(`${JSON.stringify(prepareOwnershipRepair(service.projectDir, args[1], args[2]))}\n`);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
const proposed = parseManagerAction(command);
|
|
221
|
+
if (proposed) {
|
|
222
|
+
const control = new BuildInterventionControl(service.projectDir, proposed.runId, proposed.workId);
|
|
223
|
+
try {
|
|
224
|
+
const request = parseManagerAction(command, proposed.requestId, control.revision());
|
|
225
|
+
const queued = control.enqueue(request, command);
|
|
226
|
+
write(`${JSON.stringify(queued)}\n${request.action === "withdraw" ? "Withdrawn before reservation." : "Queued at the next eligible safe boundary. An active provider turn continues; a stopped owner must resume to consume this instruction."}\n`);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
control.close();
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (!command.trim().startsWith("/"))
|
|
234
|
+
return false;
|
|
235
|
+
const [name, ...args] = command.trim().split(/\s+/);
|
|
236
|
+
if (name === "/artifact" || name === "/qa-export" && args.length === 1) {
|
|
237
|
+
if (args.length !== 1)
|
|
238
|
+
throw new Error(`${name} requires one host artifact handle`);
|
|
239
|
+
if (name === "/artifact")
|
|
240
|
+
write(`${JSON.stringify(service.artifacts.metadata(args[0]))}\n${service.artifacts.bytes(args[0]).toString("utf8")}\n`);
|
|
241
|
+
else {
|
|
242
|
+
const path = join(mkdtempSync(join(tmpdir(), "rafi-qa-evidence-")), "protected-evidence.txt");
|
|
243
|
+
writeFileSync(path, service.artifacts.bytes(args[0], true), { mode: 0o600 });
|
|
244
|
+
write(`Protected raw evidence exported to ${path}\n${JSON.stringify(service.artifacts.metadata(args[0]))}\n`);
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
if (name === "/more") {
|
|
249
|
+
if (args.length !== 1)
|
|
250
|
+
throw new Error("/more requires one cursor");
|
|
251
|
+
const page = service.more(args[0]);
|
|
252
|
+
write(`${JSON.stringify(complete ? completeManagerHostEvidence(service, page) : page)}\n`);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
const requestId = randomUUID();
|
|
256
|
+
const runId = args[0] ?? "";
|
|
257
|
+
const workId = args[1] ?? "";
|
|
258
|
+
let page;
|
|
259
|
+
if (name === "/qa-work" || name === "/qa-conflicts") {
|
|
260
|
+
if (args.length !== 1)
|
|
261
|
+
throw new Error(`${name} requires a run ID`);
|
|
262
|
+
page = service.execute({ version: 2, requestId, operation: { kind: name === "/qa-work" ? "list_build_work" : "get_ownership_conflicts", runId } });
|
|
263
|
+
}
|
|
264
|
+
else if (name === "/qa-attempts" || name === "/qa-timeline") {
|
|
265
|
+
if (args.length !== 2)
|
|
266
|
+
throw new Error(`${name} requires run and ticket IDs`);
|
|
267
|
+
page = service.execute({ version: 2, requestId, operation: { kind: name === "/qa-attempts" ? "list_qa_attempts" : "get_qa_timeline", runId, workId } });
|
|
268
|
+
}
|
|
269
|
+
else if (name === "/qa-instruction") {
|
|
270
|
+
if (args.length !== 3)
|
|
271
|
+
throw new Error("/qa-instruction requires run, work and instruction identities");
|
|
272
|
+
page = service.execute({ version: 2, requestId, operation: { kind: "get_intervention_status", runId, workId, instructionId: args[2] } });
|
|
273
|
+
}
|
|
274
|
+
else if (name === "/qa-report" || name === "/qa-export") {
|
|
275
|
+
if (args.length < 3 || args.length > 4)
|
|
276
|
+
throw new Error(`${name} requires run, ticket, attempt and optional occurrence IDs`);
|
|
277
|
+
const attemptId = args[2];
|
|
278
|
+
const attempts = service.execute({ version: 2, requestId, operation: { kind: "list_qa_attempts", runId, workId } });
|
|
279
|
+
const rows = completeManagerHostEvidence(service, attempts).items;
|
|
280
|
+
const attempt = rows.find(row => row && typeof row === "object" && "attemptId" in row && row.attemptId === attemptId);
|
|
281
|
+
if (!attempt)
|
|
282
|
+
throw new Error("Attempt is not retained in this run/ticket; use /qa-attempts first");
|
|
283
|
+
if (!attempt.reports.length) {
|
|
284
|
+
write(`${JSON.stringify(rows.find(row => row && typeof row === "object" && "attemptId" in row && row.attemptId === attemptId))}\nNo retained failure report body; inspect verification or attempt status above.\n`);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
const occurrenceId = args[3] ?? (attempt.reports.length === 1 ? attempt.reports[0].occurrenceId : undefined);
|
|
288
|
+
if (!occurrenceId)
|
|
289
|
+
throw new Error("Report identity is ambiguous; specify a retained occurrence ID");
|
|
290
|
+
page = service.execute({ version: 2, requestId, operation: { kind: "get_qa_report", runId, workId, attemptId, occurrenceId, snapshotId: attempts.snapshotId } });
|
|
291
|
+
const artifact = page.items[0];
|
|
292
|
+
if (artifact?.handle) {
|
|
293
|
+
if (name === "/qa-export") {
|
|
294
|
+
const path = join(mkdtempSync(join(tmpdir(), "rafi-qa-evidence-")), "protected-evidence.txt");
|
|
295
|
+
writeFileSync(path, service.artifacts.bytes(artifact.handle, true), { mode: 0o600 });
|
|
296
|
+
write(`Protected raw evidence exported to ${path}\n${JSON.stringify({ ...service.artifacts.metadata(artifact.handle), snapshotId: page.snapshotId, asOf: page.asOf })}\n`);
|
|
297
|
+
}
|
|
298
|
+
else
|
|
299
|
+
write(`${JSON.stringify({ ...service.artifacts.metadata(artifact.handle), snapshotId: page.snapshotId, asOf: page.asOf, availability: page.availability, omissions: page.omissions.filter(item => !item.startsWith("Complete host rendering:")) })}\n${service.artifacts.bytes(artifact.handle).toString("utf8")}\n`);
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else
|
|
304
|
+
return false;
|
|
305
|
+
write(`${JSON.stringify(complete ? completeManagerHostEvidence(service, page) : page)}\n`);
|
|
306
|
+
if (!complete && page.nextCursor)
|
|
307
|
+
write(`Continue with /more ${page.nextCursor}\n`);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
/** Drain one pinned snapshot and expand every referenced artifact before host close.
|
|
311
|
+
* Errors are returned as incomplete output; an expired snapshot is never refreshed.
|
|
312
|
+
* This host output is deliberately independent of bounded model packets. */
|
|
313
|
+
export function completeManagerHostEvidence(service, first) {
|
|
314
|
+
const result = structuredClone(first);
|
|
315
|
+
result.redactions = service.hostRedactions(first.snapshotId);
|
|
316
|
+
result.items = [];
|
|
317
|
+
const seen = new Set();
|
|
318
|
+
let page = first;
|
|
319
|
+
while (true) {
|
|
320
|
+
for (const item of page.items) {
|
|
321
|
+
const row = item;
|
|
322
|
+
if (row?.metadataArtifact)
|
|
323
|
+
result.items.push(service.hostMetadataItem(row.metadataArtifact.handle));
|
|
324
|
+
else if (row?.handle) {
|
|
325
|
+
if (seen.has(row.handle))
|
|
326
|
+
continue;
|
|
327
|
+
seen.add(row.handle);
|
|
328
|
+
const { handle: _, ...metadata } = service.artifacts.metadata(row.handle);
|
|
329
|
+
result.items.push({ ...metadata, body: service.artifacts.bytes(row.handle).toString("utf8") });
|
|
330
|
+
}
|
|
331
|
+
else
|
|
332
|
+
result.items.push(item);
|
|
333
|
+
}
|
|
334
|
+
if (!page.nextCursor || page.error)
|
|
335
|
+
break;
|
|
336
|
+
const next = service.more(page.nextCursor);
|
|
337
|
+
if (!next.error && next.snapshotId !== first.snapshotId)
|
|
338
|
+
throw new Error("Evidence continuation changed snapshot identity");
|
|
339
|
+
page = next;
|
|
340
|
+
}
|
|
341
|
+
delete result.nextCursor;
|
|
342
|
+
result.complete = page.complete;
|
|
343
|
+
if (page.error) {
|
|
344
|
+
result.error = page.error;
|
|
345
|
+
result.restartAction = page.restartAction;
|
|
346
|
+
result.omissions.push(...page.omissions);
|
|
127
347
|
}
|
|
348
|
+
result.omissions = result.omissions.filter(item => !item.startsWith("Complete host rendering:") && !item.includes("additional redaction spans are available"));
|
|
349
|
+
return result;
|
|
128
350
|
}
|
|
129
351
|
function denyManagerTools() {
|
|
130
352
|
return { ...readOnlyPermissionConfig(), allowBash: [], allowTools: [], escalateBash: [""], escalateTools: ["Read", "Glob", "Grep", "Edit", "Write", "MultiEdit", "NotebookEdit", "TodoWrite", "WebFetch", "WebSearch", "Bash"] };
|
package/dist/cli/recovery.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isLiveProcessIdentity } from "../processIdentity.js";
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import { accent, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import { readBuildRuns } from "../buildRuns.js";
|
|
5
6
|
import { WorkflowDb } from "../workflowDb.js";
|
|
@@ -16,8 +17,8 @@ export function buildAttachCommand() {
|
|
|
16
17
|
const db = new WorkflowDb(projectDir);
|
|
17
18
|
try {
|
|
18
19
|
const supervisor = db.supervisorState(run.runId);
|
|
19
|
-
console.log(
|
|
20
|
-
console.log(
|
|
20
|
+
console.log(`${accent("rafi:")} run ${sanitizeTerminalText(run.runId)}`);
|
|
21
|
+
console.log(`${accent("rafi:")} phase ${sanitizeTerminalText("phase" in run && typeof run.phase === "string" ? run.phase : run.checkpoint)}; status ${run.status === "completed" ? success(run.status) : warning(run.status)}`);
|
|
21
22
|
console.log(`rafi: supervisor ${supervisor?.status ?? "unavailable"}${supervisor?.pid ? ` pid=${supervisor.pid}` : ""}`);
|
|
22
23
|
const attempts = db.recoveryAttempts(run.runId);
|
|
23
24
|
console.log(`rafi: recovery attempts ${attempts.length}`);
|
|
@@ -2,7 +2,7 @@ import { select, isCancel, log } from "@clack/prompts";
|
|
|
2
2
|
import { requireClaudeSDK } from "../adapters/claude.js";
|
|
3
3
|
import { checkRuntimeReady, RuntimeAuthError, runtimeCommandLabel, } from "../runtimeAuth.js";
|
|
4
4
|
import { BuildOwnershipError } from "../buildAdmission.js";
|
|
5
|
-
import { RuntimeCleanupError, resolveExecutablePath } from "../runtimeReadiness.js";
|
|
5
|
+
import { RuntimeCleanupError, runtimeCleanupRecoveryHelp, resolveExecutablePath } from "../runtimeReadiness.js";
|
|
6
6
|
import { otherRuntime, runtimeDisplayName } from "./runtimeSelection.js";
|
|
7
7
|
import { currentActivity } from "../activity.js";
|
|
8
8
|
import { durableHumanDecision } from "../humanDecision.js";
|
|
@@ -36,7 +36,7 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
36
36
|
return { runtime, model: opts.model, fellBack: false, executable };
|
|
37
37
|
}
|
|
38
38
|
catch (err) {
|
|
39
|
-
if (err instanceof
|
|
39
|
+
if (err instanceof BuildOwnershipError || (err instanceof RuntimeCleanupError && opts.build))
|
|
40
40
|
throw err;
|
|
41
41
|
const failure = err instanceof RuntimeAuthError
|
|
42
42
|
? err
|
|
@@ -46,20 +46,21 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
46
46
|
stderr: err instanceof Error ? err.message : String(err),
|
|
47
47
|
cause: err,
|
|
48
48
|
});
|
|
49
|
-
if (nonInteractive && !opts.durable) {
|
|
49
|
+
if (nonInteractive && !opts.durable && !(failure.cleanupUnverified && process.stdin.isTTY && process.stdout.isTTY)) {
|
|
50
50
|
throw failure;
|
|
51
51
|
}
|
|
52
52
|
const fallbackRuntime = otherRuntime(runtime);
|
|
53
|
+
const canSwitch = allowSwitch && !failure.cleanupUnverified;
|
|
53
54
|
const choice = opts.durable && !opts.choose
|
|
54
|
-
? await durableRuntimeRecovery(opts.durable, failure, opts.label, fallbackRuntime,
|
|
55
|
+
? await durableRuntimeRecovery(opts.durable, failure, opts.label, fallbackRuntime, canSwitch)
|
|
55
56
|
: opts.choose
|
|
56
|
-
? await opts.choose(failure, { otherRuntime: fallbackRuntime, allowSwitch })
|
|
57
|
-
: await promptRuntimeRecovery(failure, opts.label, fallbackRuntime,
|
|
57
|
+
? await opts.choose(failure, { otherRuntime: fallbackRuntime, allowSwitch: canSwitch })
|
|
58
|
+
: await promptRuntimeRecovery(failure, opts.label, fallbackRuntime, canSwitch);
|
|
58
59
|
if (choice === "retry") {
|
|
59
60
|
currentActivity()?.note(`rafi: retrying ${runtime} readiness check`);
|
|
60
61
|
continue;
|
|
61
62
|
}
|
|
62
|
-
if (choice === "switch" &&
|
|
63
|
+
if (choice === "switch" && canSwitch) {
|
|
63
64
|
currentActivity()?.note(`rafi: checking fallback runtime ${fallbackRuntime}`);
|
|
64
65
|
try {
|
|
65
66
|
const probe = await check(projectDir, fallbackRuntime);
|
|
@@ -76,7 +77,7 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
76
77
|
return { runtime: fallbackRuntime, model: undefined, fellBack: true, executable };
|
|
77
78
|
}
|
|
78
79
|
catch (switchErr) {
|
|
79
|
-
if (switchErr instanceof
|
|
80
|
+
if (switchErr instanceof BuildOwnershipError || (switchErr instanceof RuntimeCleanupError && opts.build))
|
|
80
81
|
throw switchErr;
|
|
81
82
|
const switchFailure = switchErr instanceof RuntimeAuthError
|
|
82
83
|
? switchErr
|
|
@@ -130,6 +131,25 @@ async function durableRuntimeRecovery(scope, failure, label, fallbackRuntime, al
|
|
|
130
131
|
}
|
|
131
132
|
async function promptRuntimeRecovery(err, label, fallbackRuntime, allowSwitch) {
|
|
132
133
|
log.error(err.message);
|
|
134
|
+
if (err.cleanupUnverified) {
|
|
135
|
+
while (true) {
|
|
136
|
+
const choice = await select({
|
|
137
|
+
message: `${label} is paused until probe cleanup is verified. Choose a resolution.`,
|
|
138
|
+
options: [
|
|
139
|
+
{ value: "retry", label: "Recheck cleanup after repairing process visibility" },
|
|
140
|
+
{ value: "help", label: "Show platform-specific resolution approaches" },
|
|
141
|
+
{ value: "cancel", label: "Cancel deliberately; keep project files" },
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
if (isCancel(choice) || choice === "cancel")
|
|
145
|
+
return "cancel";
|
|
146
|
+
if (choice === "help") {
|
|
147
|
+
log.info(runtimeCleanupRecoveryHelp());
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
return "retry";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
133
153
|
log.info("Cancel stops this command and keeps project files in place. It does not uninstall packages, delete generated files, or change configuration.");
|
|
134
154
|
if (!allowSwitch) {
|
|
135
155
|
log.info("Switching runtimes is disabled while resuming because session IDs are runtime-specific.");
|