ai-foreman 1.7.19 → 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 +29 -5
- 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 +92 -5
- package/dist/buildAdmission.d.ts +61 -0
- package/dist/buildAdmission.js +286 -0
- package/dist/buildApproval.d.ts +2 -0
- package/dist/buildApproval.js +17 -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/buildLaunchGate.d.ts +1 -0
- package/dist/buildLaunchGate.js +36 -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 +24 -1
- package/dist/buildRuns.js +216 -70
- 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.d.ts +5 -0
- package/dist/cli/runtimeAuthPrompt.js +41 -7
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +2091 -1849
- package/dist/cli/status.js +20 -5
- package/dist/cli/tickets.js +9 -7
- package/dist/continuity.d.ts +16 -1
- package/dist/continuity.js +68 -12
- package/dist/foreman.d.ts +12 -4
- package/dist/foreman.js +397 -60
- 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 -10
- package/dist/index.js +4 -1
- 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.d.ts +21 -1
- package/dist/processIdentity.js +87 -5
- 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 +293 -41
- 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.d.ts +19 -0
- package/dist/readinessCleanup.js +122 -0
- package/dist/readinessGate.d.ts +1 -0
- package/dist/readinessGate.js +82 -0
- package/dist/recoveryGuidance.d.ts +17 -0
- package/dist/recoveryGuidance.js +38 -0
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +13 -0
- package/dist/runtimeReadiness.js +206 -10
- 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 +85 -8
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.d.ts +6 -0
- package/dist/supervisedStart.js +178 -62
- package/dist/supervisor.d.ts +2 -0
- package/dist/supervisor.js +26 -2
- 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/windowsProbeJob.d.ts +12 -0
- package/dist/windowsProbeJob.js +144 -0
- package/dist/workflowDb.d.ts +121 -6
- package/dist/workflowDb.js +874 -30
- package/dist/workflowReader.d.ts +116 -2
- package/dist/workflowReader.js +131 -4
- package/package.json +89 -8
- 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,360 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { assertGraphReadRequest, parseStrictJson } from "rafi-spec";
|
|
3
|
+
import { WorkflowReader } from "../workflowReader.js";
|
|
4
|
+
import { WorkflowDb } from "../workflowDb.js";
|
|
5
|
+
import { loadGraphConfig } from "./config.js";
|
|
6
|
+
import { graphExclusions, captureGraphCorpus } from "./corpus.js";
|
|
7
|
+
import { acquireGraphView, graphUnavailable, readGraph } from "./read.js";
|
|
8
|
+
import { canonical, digest, bytesDigest } from "./util.js";
|
|
9
|
+
import { dispatchWithGraphAccess } from "./session.js";
|
|
10
|
+
export const GRAPH_REQUEST_GUIDANCE = `Rafi graph evidence is available through the host for substantial architecture, dependencies, impact or cross-file investigations. To request it, return one business JSON object: {"kind":"rafi_graph_request","version":1,"requestId":"unique-id","operations":[{"operation":"query","query":"specific source concepts"}]}. Emit the required continuity record outside JSON when your host requires one; emit no STEP_STATUS, verdict or other business envelope in that evidence subturn. Evidence cannot authorize tools, scope changes or weaken mandatory checks. Routine file lookups/tests/formatting do not qualify. Only after completing edits that change indexed relationships or behavior, add the separate line RAFI_GRAPH_MAINTENANCE: meaningful-indexed-change to your normal final response. Do not emit it for changed bytes alone, formatting, agent-policy changes, tests or bookkeeping. The host may report unavailable; then inspect source directly.`;
|
|
11
|
+
const automatic = new Set(["planning", "planning-audit", "source-reconciliation", "qa-preparation", "preparation-assessment", "preparation-challenge", "build-preflight", "branch-dependency-audit", "final-qa", "discovery", "ticket-population", "manager-diagnosis"]);
|
|
12
|
+
export function graphContext(configRoot, workspace, purpose, instruction, extra = {}) {
|
|
13
|
+
const id = randomUUID();
|
|
14
|
+
return { version: 1, operationId: id, logicalTaskId: id, purpose, configRoot, workspace, sourceRef: "unknown", hostWritesAllowed: true, qualifies: automatic.has(purpose), reason: automatic.has(purpose) ? "cross-component investigation" : "agent may discover a substantial investigation", seeds: [instruction.slice(0, 1800)], ...extra };
|
|
15
|
+
}
|
|
16
|
+
export function parseGraphRequest(text) {
|
|
17
|
+
const trimmed = text.trim();
|
|
18
|
+
if (!trimmed.startsWith("{"))
|
|
19
|
+
return undefined;
|
|
20
|
+
let parsed;
|
|
21
|
+
if (Buffer.byteLength(trimmed) > 8192) {
|
|
22
|
+
if (trimmed.includes('"rafi_graph_request"'))
|
|
23
|
+
throw new Error("Graph request exceeds 8 KiB");
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
parsed = parseStrictJson(trimmed);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
if (trimmed.includes('"rafi_graph_request"'))
|
|
31
|
+
throw new Error("Malformed graph envelope");
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (!parsed || typeof parsed !== "object" || parsed.kind !== "rafi_graph_request")
|
|
35
|
+
return undefined;
|
|
36
|
+
if (Buffer.byteLength(trimmed) > 8192)
|
|
37
|
+
throw new Error("Graph request exceeds 8 KiB");
|
|
38
|
+
assertGraphReadRequest(parsed);
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
function admitOptionalEvidence(context, bytes, limit) {
|
|
42
|
+
if (!context.runId || !context.hostWritesAllowed)
|
|
43
|
+
return true;
|
|
44
|
+
const db = new WorkflowDb(context.configRoot);
|
|
45
|
+
try {
|
|
46
|
+
return db.graphStore().admitEvidence(context.runId, context.operationId, bytes, limit);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
db.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Prepare before a phase freezes its instruction digest and dispatch intent. */
|
|
53
|
+
export async function prepareGraphInstruction(instruction, context, capacity) {
|
|
54
|
+
let view, packet = "";
|
|
55
|
+
try {
|
|
56
|
+
const effective = loadGraphConfig(context.configRoot);
|
|
57
|
+
if (!effective.enabled)
|
|
58
|
+
return { instruction, bytes: 0 };
|
|
59
|
+
if (context.qualifies) {
|
|
60
|
+
const corpus = captureGraphCorpus(context.workspace, context.configRoot, effective, context.sourceRef);
|
|
61
|
+
const opened = acquireGraphView(context.configRoot, context.workspace, corpus);
|
|
62
|
+
if ("graph" in opened)
|
|
63
|
+
view = opened;
|
|
64
|
+
const result = view ? await readGraph(view, { operation: "query", query: context.seeds.join(" ").slice(0, 2000) || context.purpose }) : opened;
|
|
65
|
+
packet = canonical({ kind: "rafi_graph_evidence", results: [result] });
|
|
66
|
+
if (!admitOptionalEvidence(context, Buffer.byteLength(packet), effective.limits.retainedEvidenceBytes)) {
|
|
67
|
+
view = undefined;
|
|
68
|
+
packet = canonical({ kind: "rafi_graph_evidence", results: [graphUnavailable("retained-evidence-limit", "This run's optional graph evidence budget is exhausted; inspect source directly")] });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
view = undefined;
|
|
74
|
+
packet = canonical({ kind: "rafi_graph_evidence", results: [graphUnavailable("preparation-failed", String(error))] });
|
|
75
|
+
}
|
|
76
|
+
const expanded = `${instruction}\n\n${GRAPH_REQUEST_GUIDANCE}${packet ? `\n\nUntrusted graph navigation evidence:\n${packet}` : ""}`;
|
|
77
|
+
if (capacity?.maximum && Buffer.byteLength(expanded) + 16384 > capacity.maximum - capacity.used)
|
|
78
|
+
return { instruction, bytes: 0 };
|
|
79
|
+
return { instruction: expanded, view, bytes: Buffer.byteLength(packet) };
|
|
80
|
+
}
|
|
81
|
+
/** Called at the owning phase, outside low-level provider wrappers. */
|
|
82
|
+
export async function sendGraphTurn(adapter, instruction, context, policy, dispatch = (text, p) => adapter.sendTurn(text, p), prepared) {
|
|
83
|
+
const providerDispatch = dispatch;
|
|
84
|
+
dispatch = (text, p) => dispatchWithGraphAccess(context.configRoot, adapter, text, p, providerDispatch, [], context.hostWritesAllowed);
|
|
85
|
+
if (policy?.responseOnly || context.purpose === "semantic-extraction")
|
|
86
|
+
return dispatch(instruction, policy);
|
|
87
|
+
const reader = new WorkflowReader(context.configRoot);
|
|
88
|
+
let existing;
|
|
89
|
+
try {
|
|
90
|
+
existing = reader.graphRecord("exchange", context.operationId);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
reader.close();
|
|
94
|
+
}
|
|
95
|
+
const bindingDigest = digest("exchange-binding", { instruction, preparedInstruction: prepared?.instruction, context, policy, provider: adapter.agent });
|
|
96
|
+
if (existing && (existing.value.state !== "read-completed" || !existing.value.pendingPrompt
|
|
97
|
+
|| existing.value.bindingDigest !== bindingDigest || !adapter.sessionId()
|
|
98
|
+
|| existing.value.sessionRef !== adapter.sessionId()))
|
|
99
|
+
throw new Error("Graph exchange requires bound operation recovery; provider work cannot be replayed");
|
|
100
|
+
let effective;
|
|
101
|
+
try {
|
|
102
|
+
effective = loadGraphConfig(context.configRoot);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (existing)
|
|
106
|
+
throw new Error("Graph recovery policy unavailable; explicit reconciliation required");
|
|
107
|
+
return dispatch(context.qualifies ? `${instruction}\n\nGraph evidence unavailable: ${String(error).slice(0, 500)}. Inspect source directly without weakening mandatory checks.` : instruction, policy);
|
|
108
|
+
}
|
|
109
|
+
if (!effective.enabled) {
|
|
110
|
+
if (existing)
|
|
111
|
+
throw new Error("Graph recovery access revoked; explicit reconciliation required");
|
|
112
|
+
return dispatch(instruction, policy);
|
|
113
|
+
}
|
|
114
|
+
let boundRequirements;
|
|
115
|
+
const validateAuthority = () => {
|
|
116
|
+
if (!context.runId || !context.workId || !["implementation", "remediation", "manager-guidance"].includes(context.purpose))
|
|
117
|
+
return;
|
|
118
|
+
const db = new WorkflowDb(context.configRoot);
|
|
119
|
+
try {
|
|
120
|
+
const admission = db.assertAdmittedWork(context.runId, context.workId);
|
|
121
|
+
if (boundRequirements && boundRequirements !== admission.requirementsDigest)
|
|
122
|
+
throw new Error("Graph continuation scope changed; reconcile the active assignment");
|
|
123
|
+
boundRequirements = admission.requirementsDigest;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
db.close();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
validateAuthority();
|
|
130
|
+
if (existing && existing.value.authorityDigest !== boundRequirements)
|
|
131
|
+
throw new Error("Graph recovery work authority changed; explicit reconciliation required");
|
|
132
|
+
let view = prepared?.view, acquired = Boolean(prepared);
|
|
133
|
+
let initialPacket = "";
|
|
134
|
+
const deliveredStatuses = [];
|
|
135
|
+
const state = existing?.value ?? { bindingDigest, authorityDigest: boundRequirements, version: 1, id: context.operationId, parentId: context.logicalTaskId, sessionRef: adapter.sessionId(), rounds: 0, bytes: 0, state: "ready", requests: {} };
|
|
136
|
+
let exchangeRevision = existing?.revision ?? 0;
|
|
137
|
+
const persist = () => {
|
|
138
|
+
if (!context.hostWritesAllowed)
|
|
139
|
+
return;
|
|
140
|
+
const db = new WorkflowDb(context.configRoot);
|
|
141
|
+
try {
|
|
142
|
+
exchangeRevision = db.graphStore().put("exchange", state.id, state, exchangeRevision);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
db.close();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
if (existing) {
|
|
149
|
+
acquired = true;
|
|
150
|
+
if (state.generationId) {
|
|
151
|
+
const opened = acquireGraphView(context.configRoot, context.workspace, undefined, state.generationId);
|
|
152
|
+
if (!("graph" in opened))
|
|
153
|
+
throw new Error("Graph recovery evidence unavailable or revoked; explicit reconciliation required");
|
|
154
|
+
view = opened;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const query = async (request) => {
|
|
158
|
+
const current = loadGraphConfig(context.configRoot);
|
|
159
|
+
if (!current.enabled || current.policyDigest !== effective.policyDigest)
|
|
160
|
+
return canonical({ kind: "rafi_graph_evidence", results: [graphUnavailable("policy-changed", "Graph access changed during this operation")] });
|
|
161
|
+
if (!acquired) {
|
|
162
|
+
acquired = true;
|
|
163
|
+
try {
|
|
164
|
+
const corpus = captureGraphCorpus(context.workspace, context.configRoot, effective, context.sourceRef);
|
|
165
|
+
const opened = acquireGraphView(context.configRoot, context.workspace, corpus);
|
|
166
|
+
if ("graph" in opened)
|
|
167
|
+
view = opened;
|
|
168
|
+
else
|
|
169
|
+
return canonical({ kind: "rafi_graph_evidence", results: [opened] });
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return canonical({ kind: "rafi_graph_evidence", results: [graphUnavailable("freshness-check-failed", String(error))] });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const results = [];
|
|
176
|
+
for (const operation of request.operations)
|
|
177
|
+
results.push(view ? await readGraph(view, operation) : graphUnavailable("missing-graph", "No usable generation in this workspace"));
|
|
178
|
+
deliveredStatuses.push(...results.map(r => r.status));
|
|
179
|
+
const packet = canonical({ kind: "rafi_graph_evidence", requestId: request.requestId, results });
|
|
180
|
+
if (!admitOptionalEvidence(context, state.bytes + Buffer.byteLength(packet), effective.limits.retainedEvidenceBytes))
|
|
181
|
+
return canonical({ kind: "rafi_graph_evidence", requestId: request.requestId, results: [graphUnavailable("retained-evidence-limit", "This run's optional graph evidence budget is exhausted; inspect source directly")] });
|
|
182
|
+
return Buffer.byteLength(packet) <= effective.limits.packetBytes ? packet : canonical({ kind: "rafi_graph_evidence", requestId: request.requestId, results: [graphUnavailable("packet-limit", "Request one narrower operation; combined results exceed the packet budget")] });
|
|
183
|
+
};
|
|
184
|
+
if (context.qualifies && !prepared && !existing) {
|
|
185
|
+
initialPacket = await query({ kind: "rafi_graph_request", version: 1, requestId: "initial", operations: [{ operation: "query", query: context.seeds.join(" ").slice(0, 2000) || context.purpose }] });
|
|
186
|
+
state.bytes = Buffer.byteLength(initialPacket);
|
|
187
|
+
state.operations = 1;
|
|
188
|
+
}
|
|
189
|
+
if (prepared && !existing) {
|
|
190
|
+
state.bytes = prepared.bytes;
|
|
191
|
+
state.operations = prepared.bytes ? 1 : 0;
|
|
192
|
+
}
|
|
193
|
+
let prompt = state.pendingPrompt ?? prepared?.instruction ?? `${instruction}\n\n${GRAPH_REQUEST_GUIDANCE}${initialPacket ? `\n\nUntrusted graph navigation evidence (verify in source):\n${initialPacket}` : ""}`;
|
|
194
|
+
// The read view belongs to the actual branch/snapshot. Retained evidence belongs
|
|
195
|
+
// to the durable source workspace, never a temporary snapshot's lifetime.
|
|
196
|
+
const accesses = new Map();
|
|
197
|
+
const accessForDelivery = () => {
|
|
198
|
+
if (!view)
|
|
199
|
+
return undefined;
|
|
200
|
+
const existing = accesses.get(view.generation.id);
|
|
201
|
+
if (existing)
|
|
202
|
+
return existing;
|
|
203
|
+
const paths = view.generation.inputs.filter(i => !i.sourceId).map(i => i.path);
|
|
204
|
+
const workspace = context.accessWorkspace ?? context.workspace;
|
|
205
|
+
const access = {
|
|
206
|
+
policyDigest: effective.policyDigest, workspace, paths,
|
|
207
|
+
exclusionsDigest: context.accessWorkspace
|
|
208
|
+
? digest("exclusions", graphExclusions(workspace, paths).text)
|
|
209
|
+
: view.generation.files["exclusions-digest"] ?? digest("exclusions", graphExclusions(workspace, paths).text),
|
|
210
|
+
projectExclusionsDigest: digest("exclusions", graphExclusions(context.configRoot, paths).text),
|
|
211
|
+
sourceVersions: effective.config?.sourceVersions ?? {},
|
|
212
|
+
};
|
|
213
|
+
accesses.set(view.generation.id, access);
|
|
214
|
+
return access;
|
|
215
|
+
};
|
|
216
|
+
const protect = (values) => {
|
|
217
|
+
if (!context.hostWritesAllowed || !view)
|
|
218
|
+
return;
|
|
219
|
+
const access = accessForDelivery();
|
|
220
|
+
const db = new WorkflowDb(context.configRoot);
|
|
221
|
+
try {
|
|
222
|
+
for (const value of values)
|
|
223
|
+
if (value)
|
|
224
|
+
db.registerGraphEvidence(value, access);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
db.close();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
let totalCost = state.usageTotals?.costUsd ?? 0, totalTurns = state.usageTotals?.numTurns ?? 0, costAuthoritative = state.usageTotals?.costAuthoritative ?? true, usageKnown = state.usageTotals?.known ?? true, inputTokens = state.usageTotals?.inputTokens ?? 0, outputTokens = state.usageTotals?.outputTokens ?? 0;
|
|
231
|
+
const accumulated = (result) => ({ ...result,
|
|
232
|
+
costUsd: totalCost, numTurns: totalTurns, costAuthoritative,
|
|
233
|
+
inputTokens: usageKnown ? inputTokens : undefined, outputTokens: usageKnown ? outputTokens : undefined,
|
|
234
|
+
usage: usageKnown ? { scope: "turn-delta", inputTokens, outputTokens } : undefined,
|
|
235
|
+
});
|
|
236
|
+
for (;;) {
|
|
237
|
+
const usage = await adapter.contextUsage?.();
|
|
238
|
+
// Optional evidence that changed before its first disclosure must not poison a
|
|
239
|
+
// fresh conversation. Frozen prepared/recovery instructions require reconciliation.
|
|
240
|
+
if (view && state.rounds === 0 && !existing) {
|
|
241
|
+
const checked = acquireGraphView(context.configRoot, context.workspace, undefined, view.generation.id);
|
|
242
|
+
if (!("graph" in checked)) {
|
|
243
|
+
if (prepared)
|
|
244
|
+
throw new Error("Prepared graph evidence changed before dispatch; prepare a fresh source-based instruction");
|
|
245
|
+
view = undefined;
|
|
246
|
+
prompt = `${instruction}\n\nGraph evidence unavailable: access changed before delivery. Inspect source directly without weakening mandatory checks.`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!prepared && usage?.maximum && Buffer.byteLength(prompt) + 16384 > usage.maximum - usage.used) {
|
|
250
|
+
prompt = state.rounds ? "Graph evidence omitted for context budget. Continue the original frozen work using source inspection; preserve all mandatory requirements and return its normal final response." : instruction;
|
|
251
|
+
}
|
|
252
|
+
protect([prompt]);
|
|
253
|
+
state.state = "continuation-intended";
|
|
254
|
+
state.sessionRef = adapter.sessionId();
|
|
255
|
+
state.pendingPrompt = undefined;
|
|
256
|
+
persist();
|
|
257
|
+
let result;
|
|
258
|
+
try {
|
|
259
|
+
validateAuthority();
|
|
260
|
+
result = await dispatchWithGraphAccess(context.configRoot, adapter, prompt, { ...policy, logicalActionId: context.logicalTaskId }, providerDispatch, accessForDelivery() ? [accessForDelivery()] : [], context.hostWritesAllowed);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
state.state = "uncertain";
|
|
264
|
+
persist();
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
state.sessionRef = adapter.sessionId();
|
|
268
|
+
protect([result.text, result.rawResponse, result.cleanedResponse, result.providerInstruction]);
|
|
269
|
+
if (context.hostWritesAllowed) {
|
|
270
|
+
const receipt = { version: 1, operationId: context.operationId, logicalTaskId: context.logicalTaskId, purpose: context.purpose, sessionRef: adapter.sessionId(), workspaceRef: view?.corpus?.binding.workspaceRef ?? digest("workspace-path", context.workspace), decision: view ? "used" : context.qualifies ? "unavailable" : "available", packetDigest: bytesDigest(prompt), generationIds: view ? [view.generation.id] : [], reason: context.reason, deliveredAt: new Date().toISOString() };
|
|
271
|
+
const db = new WorkflowDb(context.configRoot);
|
|
272
|
+
try {
|
|
273
|
+
db.graphStore().put("receipt", `${context.operationId}:${state.rounds}`, { ...receipt, runId: context.runId, workId: context.workId, providerTurnId: result.turnId, providerSession: result.providerMetadata, hostInstructionDigest: bytesDigest(prompt), providerInstructionDigest: result.providerInstruction ? bytesDigest(result.providerInstruction) : undefined, dispatchState: result.failure?.dispatchState ?? (result.isError ? "unknown" : "completed"), bytes: Buffer.byteLength(prompt), resultStatuses: deliveredStatuses, usage: result.usage, costUsd: result.costAuthoritative ? result.costUsd : undefined, elapsedObservation: "provider return" });
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
db.close();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
totalCost += result.costUsd;
|
|
280
|
+
totalTurns += result.numTurns;
|
|
281
|
+
costAuthoritative &&= result.costAuthoritative === true;
|
|
282
|
+
usageKnown &&= result.usage?.scope === "turn-delta" && result.usage.inputTokens !== undefined && result.usage.outputTokens !== undefined;
|
|
283
|
+
if (result.usage?.scope === "turn-delta") {
|
|
284
|
+
inputTokens += result.usage.inputTokens ?? 0;
|
|
285
|
+
outputTokens += result.usage.outputTokens ?? 0;
|
|
286
|
+
}
|
|
287
|
+
state.usageTotals = { costUsd: totalCost, numTurns: totalTurns, costAuthoritative, known: usageKnown, inputTokens, outputTokens };
|
|
288
|
+
if (result.isError || result.failure) {
|
|
289
|
+
state.state = result.failure?.dispatchState === "unknown" ? "uncertain" : "completed";
|
|
290
|
+
persist();
|
|
291
|
+
return accumulated(result);
|
|
292
|
+
}
|
|
293
|
+
if (result.continuityErrors?.length) {
|
|
294
|
+
state.state = "uncertain";
|
|
295
|
+
persist();
|
|
296
|
+
return accumulated({ ...result, isError: true });
|
|
297
|
+
}
|
|
298
|
+
let request;
|
|
299
|
+
let protocolError;
|
|
300
|
+
try {
|
|
301
|
+
request = parseGraphRequest(result.cleanedResponse ?? result.text);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
protocolError = `Invalid graph request: ${String(error).slice(0, 500)}`;
|
|
305
|
+
}
|
|
306
|
+
if (!request && !protocolError) {
|
|
307
|
+
state.state = "completed";
|
|
308
|
+
state.finalTurnId = result.turnId;
|
|
309
|
+
persist();
|
|
310
|
+
return accumulated(result);
|
|
311
|
+
}
|
|
312
|
+
// One terminal limitation returns control to the owning business protocol.
|
|
313
|
+
// It performs no read and cannot recursively reopen the evidence exchange.
|
|
314
|
+
if (protocolError || state.rounds >= effective.limits.rounds || (state.operations ?? 0) + (request?.operations.length ?? 0) > effective.limits.rounds * 4) {
|
|
315
|
+
if (state.terminalLimitationSent) {
|
|
316
|
+
state.state = "completed";
|
|
317
|
+
persist();
|
|
318
|
+
return accumulated({ ...result, isError: true, text: "Graph exchange returned another request after its terminal limitation" });
|
|
319
|
+
}
|
|
320
|
+
if (policy?.handback)
|
|
321
|
+
adapter.acceptHandbackTurn?.(result);
|
|
322
|
+
state.terminalLimitationSent = true;
|
|
323
|
+
state.rounds++;
|
|
324
|
+
prompt = `${protocolError ?? "Graph round budget exhausted"}. No further graph requests are accepted in this action. Inspect authorized source directly, preserve all mandatory checks, and return the original task's normal business response. Do not replay completed side effects.`;
|
|
325
|
+
state.pendingPrompt = prompt;
|
|
326
|
+
state.generationId = view?.generation.id;
|
|
327
|
+
state.state = "read-completed";
|
|
328
|
+
persist();
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
request = request;
|
|
332
|
+
const hash = digest("request", request), prior = state.requests[request.requestId];
|
|
333
|
+
if (prior && prior.payloadDigest !== hash) {
|
|
334
|
+
state.state = "completed";
|
|
335
|
+
persist();
|
|
336
|
+
return accumulated({ ...result, isError: true, text: "Graph request ID reused with different content" });
|
|
337
|
+
}
|
|
338
|
+
state.state = "request-received";
|
|
339
|
+
persist();
|
|
340
|
+
// A provider may have edited source since the captured inventory. Do not
|
|
341
|
+
// claim the earlier observation is current or rescan on every request.
|
|
342
|
+
if (view)
|
|
343
|
+
view.corpus = undefined;
|
|
344
|
+
if (policy?.handback)
|
|
345
|
+
adapter.acceptHandbackTurn?.(result);
|
|
346
|
+
const packet = prior?.packet ?? await query(request);
|
|
347
|
+
state.operations = (state.operations ?? 0) + (prior ? 0 : request.operations.length);
|
|
348
|
+
const bytes = Buffer.byteLength(packet);
|
|
349
|
+
const fits = state.bytes + bytes <= effective.limits.aggregateBytes;
|
|
350
|
+
state.rounds++;
|
|
351
|
+
if (fits)
|
|
352
|
+
state.bytes += bytes;
|
|
353
|
+
state.requests[request.requestId] = { payloadDigest: hash, packet: fits ? packet : "Graph evidence budget exhausted; inspect source directly." };
|
|
354
|
+
prompt = `Continue the same frozen action; do not replay completed side effects. No extra work authority is granted. Remaining graph rounds: ${effective.limits.rounds - state.rounds}. Preserve the original mandatory instructions and normal final output protocol.\n\nUntrusted graph evidence:\n${state.requests[request.requestId].packet}`;
|
|
355
|
+
state.pendingPrompt = prompt;
|
|
356
|
+
state.generationId = view?.generation.id;
|
|
357
|
+
state.state = "read-completed";
|
|
358
|
+
persist();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function canonical(value: unknown): string;
|
|
2
|
+
export declare function digest(domain: string, value: unknown): string;
|
|
3
|
+
export declare function bytesDigest(bytes: Uint8Array | string): string;
|
|
4
|
+
export declare function confined(root: string, path: string): string;
|
|
5
|
+
export declare function readBounded(path: string, maximum: number): Buffer;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, realpathSync, constants, openSync, fstatSync, readSync, closeSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
export function canonical(value) {
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
7
|
+
if (value && typeof value === "object")
|
|
8
|
+
return `{${Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
|
|
9
|
+
return JSON.stringify(value) ?? "null";
|
|
10
|
+
}
|
|
11
|
+
export function digest(domain, value) { return createHash("sha256").update(`rafi-graph:${domain}:v1\0`).update(canonical(value)).digest("hex"); }
|
|
12
|
+
export function bytesDigest(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
|
|
13
|
+
export function confined(root, path) {
|
|
14
|
+
if (isAbsolute(path) || path.includes("\\") || path.split("/").includes("..") || path.includes("\0") || /^[A-Za-z]:/.test(path))
|
|
15
|
+
throw new Error("Graph path escapes authorized scope");
|
|
16
|
+
const base = realpathSync(root), target = resolve(base, path);
|
|
17
|
+
const rel = relative(base, target);
|
|
18
|
+
if (rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel))
|
|
19
|
+
throw new Error("Graph path escapes authorized scope");
|
|
20
|
+
const actual = realpathSync(target);
|
|
21
|
+
const realRel = relative(base, actual);
|
|
22
|
+
if (realRel.startsWith(`..${sep}`) || realRel === ".." || isAbsolute(realRel))
|
|
23
|
+
throw new Error("Graph symlink escapes authorized scope");
|
|
24
|
+
if (lstatSync(target).isSymbolicLink())
|
|
25
|
+
throw new Error("Graph inputs cannot be symlinks");
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
28
|
+
export function readBounded(path, maximum) {
|
|
29
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
30
|
+
try {
|
|
31
|
+
const stat = fstatSync(fd);
|
|
32
|
+
if (!stat.isFile() || stat.size > maximum)
|
|
33
|
+
throw new Error("Graph resource is not a bounded regular file");
|
|
34
|
+
const parts = [];
|
|
35
|
+
let size = 0;
|
|
36
|
+
for (;;) {
|
|
37
|
+
const buffer = Buffer.alloc(Math.min(65536, maximum - size + 1));
|
|
38
|
+
const count = readSync(fd, buffer, 0, buffer.length, null);
|
|
39
|
+
if (!count)
|
|
40
|
+
break;
|
|
41
|
+
size += count;
|
|
42
|
+
if (size > maximum)
|
|
43
|
+
throw new Error("Graph resource grew beyond byte limit");
|
|
44
|
+
parts.push(buffer.subarray(0, count));
|
|
45
|
+
}
|
|
46
|
+
return Buffer.concat(parts, size);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
closeSync(fd);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/handoffs.d.ts
CHANGED
|
@@ -87,6 +87,7 @@ export declare class HandoffService {
|
|
|
87
87
|
finalizeFailure?: boolean;
|
|
88
88
|
guidance?: string;
|
|
89
89
|
}): Promise<HandoffTransferResult>;
|
|
90
|
+
private acceptStagedOwned;
|
|
90
91
|
private failStaged;
|
|
91
92
|
inspect(runId: string, generation?: number): {
|
|
92
93
|
manifest: HandoffManifestV1;
|
package/dist/handoffs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dispatchWithGraphAccess, withGraphSessionAccess } from "./graph/session.js";
|
|
1
2
|
import { durableHumanDecision } from "./humanDecision.js";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -271,6 +272,9 @@ export class HandoffService {
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
async acceptStaged(staged, successor, options = {}) {
|
|
275
|
+
return withGraphSessionAccess(this.projectDir, successor, () => this.acceptStagedOwned(staged, successor, options));
|
|
276
|
+
}
|
|
277
|
+
async acceptStagedOwned(staged, successor, options = {}) {
|
|
274
278
|
const db = new WorkflowDb(this.projectDir);
|
|
275
279
|
try {
|
|
276
280
|
const qaBoundary = staged.manifest.role === "qa";
|
|
@@ -294,7 +298,7 @@ export class HandoffService {
|
|
|
294
298
|
const sendAcceptance = async (prompt) => {
|
|
295
299
|
const hostPromptDigest = db.putEvidence("handoff", Buffer.from(prompt));
|
|
296
300
|
db.appendContinuityEvent({ runId: staged.manifest.runId, role: "host", kind: "handoff_acceptance_intended", payload: { generation: staged.manifest.generation, attempt: acceptanceAttempts.length + 1, hostPromptDigest, preparedRef }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0 });
|
|
297
|
-
const response = await successor.sendTurn(
|
|
301
|
+
const response = await dispatchWithGraphAccess(this.projectDir, successor, prompt, undefined, (text, policy) => successor.sendTurn(text, policy));
|
|
298
302
|
const events = qaBoundary && response.turnId ? await collectHandoffTerminalEvents(successor, response.turnId) : [];
|
|
299
303
|
const attempt = {
|
|
300
304
|
hostPromptDigest,
|
package/dist/humanDecision.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { RunObserver } from "./observability.js";
|
|
2
2
|
import type { HumanDecisionChoice } from "rafi-spec";
|
|
3
|
+
export declare class HumanDecisionCancelled extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
3
6
|
export declare class HumanDecisionRequired extends Error {
|
|
4
7
|
readonly decisionId: string;
|
|
5
8
|
readonly runId: string;
|
|
6
|
-
constructor(decisionId: string, runId: string, prompt: string);
|
|
9
|
+
constructor(decisionId: string, runId: string, prompt: string, projectDir?: string, choices?: HumanDecisionChoice[]);
|
|
7
10
|
}
|
|
8
11
|
export declare function durableHumanDecision<T>(input: {
|
|
9
12
|
projectDir: string;
|
|
@@ -14,5 +17,21 @@ export declare function durableHumanDecision<T>(input: {
|
|
|
14
17
|
ticketId?: string;
|
|
15
18
|
defer?: boolean;
|
|
16
19
|
observer?: RunObserver;
|
|
17
|
-
operation: () => Promise<T>;
|
|
20
|
+
operation: (signal?: AbortSignal) => Promise<T>;
|
|
21
|
+
onAnswered?: (decision: import("rafi-spec").PendingHumanDecision) => void;
|
|
18
22
|
}): Promise<T>;
|
|
23
|
+
/** Fixed choices deliver their meaning, rather than a generated menu identifier. */
|
|
24
|
+
export declare function decisionResponse(decision: {
|
|
25
|
+
answer?: string;
|
|
26
|
+
selectedChoiceId?: string;
|
|
27
|
+
choices: HumanDecisionChoice[];
|
|
28
|
+
}): string | undefined;
|
|
29
|
+
/** Returns false on safe pause/EOF. Never launches a provider or grants capabilities. */
|
|
30
|
+
export declare function servicePendingHumanDecisions(input: {
|
|
31
|
+
projectDir: string;
|
|
32
|
+
runId: string;
|
|
33
|
+
tickets?: readonly string[];
|
|
34
|
+
scopeRevision?: string;
|
|
35
|
+
observer?: RunObserver;
|
|
36
|
+
prompt?: (decision: import("rafi-spec").PendingHumanDecision, signal?: AbortSignal) => Promise<string | undefined>;
|
|
37
|
+
}): Promise<boolean>;
|