ai-foreman 1.7.6 → 1.7.7
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 +16 -1
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +29 -4
- package/dist/adapters/claude.d.ts +33 -2
- package/dist/adapters/claude.js +208 -17
- package/dist/adapters/codex.d.ts +11 -1
- package/dist/adapters/codex.js +195 -20
- package/dist/adapters/recovering.d.ts +8 -2
- package/dist/adapters/recovering.js +23 -3
- package/dist/adapters/sessionFailure.d.ts +18 -0
- package/dist/adapters/sessionFailure.js +51 -0
- package/dist/adapters/types.d.ts +52 -9
- package/dist/agentRun.d.ts +4 -1
- package/dist/agentRun.js +96 -9
- package/dist/branch/currentGuard.d.ts +34 -0
- package/dist/branch/currentGuard.js +65 -0
- package/dist/branch/planner.d.ts +1 -1
- package/dist/branch/planner.js +2 -7
- package/dist/branch/prefix.d.ts +8 -0
- package/dist/branch/prefix.js +26 -0
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +8 -0
- package/dist/branch/runner.d.ts +14 -2
- package/dist/branch/runner.js +77 -17
- package/dist/buildRuns.d.ts +13 -4
- package/dist/buildRuns.js +85 -19
- package/dist/cli/handoffs.d.ts +2 -0
- package/dist/cli/handoffs.js +74 -0
- package/dist/cli/start.js +940 -42
- package/dist/cli/tickets.js +199 -3
- package/dist/continuity.d.ts +86 -0
- package/dist/continuity.js +365 -0
- package/dist/foreman.d.ts +5 -1
- package/dist/foreman.js +41 -6
- package/dist/handoffs.d.ts +69 -0
- package/dist/handoffs.js +326 -0
- package/dist/log.d.ts +1 -1
- package/dist/permissions/policy.d.ts +5 -1
- package/dist/permissions/policy.js +48 -1
- package/dist/qaReview.d.ts +6 -6
- package/dist/qaReview.js +20 -14
- package/dist/sessionAvailability.d.ts +15 -0
- package/dist/sessionAvailability.js +57 -0
- package/dist/sessionIdentity.d.ts +33 -0
- package/dist/sessionIdentity.js +132 -0
- package/dist/sessionLifecycle.d.ts +102 -3
- package/dist/sessionLifecycle.js +578 -20
- package/dist/statusReporter.d.ts +58 -8
- package/dist/statusReporter.js +210 -14
- package/dist/ticketPlanning.js +8 -0
- package/dist/ticketPopulation.js +88 -12
- package/dist/tickets/commands.js +20 -12
- package/dist/tickets/groupReset.d.ts +25 -0
- package/dist/tickets/groupReset.js +279 -0
- package/dist/tickets/groups.d.ts +36 -0
- package/dist/tickets/groups.js +68 -0
- package/dist/tickets/importer.js +27 -3
- package/dist/tickets/recommendations.js +31 -7
- package/dist/tickets/reset.d.ts +1 -0
- package/dist/tickets/reset.js +1 -0
- package/dist/tickets/setupConfig.d.ts +1 -0
- package/dist/tickets/setupConfig.js +10 -2
- package/dist/tickets/stateDb.d.ts +34 -0
- package/dist/tickets/stateDb.js +207 -0
- package/dist/tickets/ticketLoader.js +5 -2
- package/dist/tickets/validate.js +4 -0
- package/dist/workflowDb.d.ts +99 -2
- package/dist/workflowDb.js +434 -8
- package/package.json +31 -3
package/dist/handoffs.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { continuityInstruction, mergeContinuityDeltas, parseContinuityDelta } from "./continuity.js";
|
|
5
|
+
import { WorkflowDb } from "./workflowDb.js";
|
|
6
|
+
export const HANDOFF_CACHE_RETENTION_DAYS = 30;
|
|
7
|
+
export const HANDOFF_ACCEPTED = "HANDOFF_ACCEPTED";
|
|
8
|
+
export const HANDOFF_REQUEST_START = "RAFI_HANDOFF_REQUEST_START";
|
|
9
|
+
export const HANDOFF_REQUEST_END = "RAFI_HANDOFF_REQUEST_END";
|
|
10
|
+
export function parseBuilderHandoffRequest(text) {
|
|
11
|
+
const starts = [...text.matchAll(/RAFI_HANDOFF_REQUEST_START/g)];
|
|
12
|
+
const ends = [...text.matchAll(/RAFI_HANDOFF_REQUEST_END/g)];
|
|
13
|
+
if (starts.length === 0 && ends.length === 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (starts.length !== 1 || ends.length !== 1 || starts[0].index >= ends[0].index)
|
|
16
|
+
throw new Error("malformed structured Builder handoff request envelope");
|
|
17
|
+
const raw = text.slice(starts[0].index + HANDOFF_REQUEST_START.length, ends[0].index).trim();
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new Error("Builder handoff request is not valid JSON");
|
|
24
|
+
}
|
|
25
|
+
const expected = new Set(["version", "reason", "decisions", "constraints", "discoveries", "completed_actions", "evidence", "failures", "blockers", "open_work", "next_action", "role_state"]);
|
|
26
|
+
const unknown = Object.keys(parsed).filter((key) => !expected.has(key));
|
|
27
|
+
if (unknown.length)
|
|
28
|
+
throw new Error(`Builder handoff request has unknown fields: ${unknown.join(", ")}`);
|
|
29
|
+
if (parsed.version !== 1 || typeof parsed.reason !== "string" || !parsed.reason.trim() || !parsed.role_state || typeof parsed.role_state !== "object" || Array.isArray(parsed.role_state))
|
|
30
|
+
throw new Error("Builder handoff request is missing required v1 fields");
|
|
31
|
+
const list = (key) => {
|
|
32
|
+
const value = parsed[key];
|
|
33
|
+
if (!Array.isArray(value) || value.length > 500 || value.some((item) => typeof item !== "string" || !item.trim() || item.length > 4_000))
|
|
34
|
+
throw new Error(`Builder handoff request field ${key} must be a bounded string array`);
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
if (typeof parsed.next_action !== "string" || !parsed.next_action.trim() || parsed.next_action.length > 4_000)
|
|
38
|
+
throw new Error("Builder handoff next_action must be a non-empty bounded string");
|
|
39
|
+
return {
|
|
40
|
+
version: 1,
|
|
41
|
+
reason: clean(parsed.reason, 2_000),
|
|
42
|
+
delta: {
|
|
43
|
+
version: 1,
|
|
44
|
+
decisions: list("decisions"), constraints: list("constraints"), discoveries: list("discoveries"),
|
|
45
|
+
completedActions: list("completed_actions"), evidence: list("evidence"), failures: list("failures"),
|
|
46
|
+
blockers: list("blockers"), openWork: list("open_work"), nextAction: parsed.next_action.trim(),
|
|
47
|
+
},
|
|
48
|
+
roleState: sanitizeObject(parsed.role_state),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export class HandoffLoopError extends Error {
|
|
52
|
+
runId;
|
|
53
|
+
constructor(runId) {
|
|
54
|
+
super(`third consecutive Builder-requested handoff for run ${runId} has been paused; record a useful verified action or use guided recovery`);
|
|
55
|
+
this.runId = runId;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Durable, validated ownership transfer; cache copies are never authoritative. */
|
|
59
|
+
export class HandoffService {
|
|
60
|
+
projectDir;
|
|
61
|
+
cacheRoot;
|
|
62
|
+
constructor(projectDir) {
|
|
63
|
+
this.projectDir = resolve(projectDir);
|
|
64
|
+
this.cacheRoot = join(this.projectDir, ".rafi", "cache", "handoffs");
|
|
65
|
+
}
|
|
66
|
+
stage(input, now = new Date()) {
|
|
67
|
+
this.pruneExpiredCache(now);
|
|
68
|
+
const db = new WorkflowDb(this.projectDir);
|
|
69
|
+
try {
|
|
70
|
+
db.ensureRun(input.runId);
|
|
71
|
+
if (input.requestedByBuilder && this.consecutiveBuilderRequests(db, input.runId) >= 2) {
|
|
72
|
+
db.appendContinuityEvent({
|
|
73
|
+
runId: input.runId, role: "host", kind: "builder_handoff_loop_paused",
|
|
74
|
+
payload: { reason: clean(input.reason, 2_000), recovery: `rafi build:resume --run ${input.runId} --guided-recovery` },
|
|
75
|
+
authoritativeStateRevision: db.continuityHead(input.runId, "builder")?.authoritativeStateRevision ?? 0,
|
|
76
|
+
}, now);
|
|
77
|
+
db.setContinuityHeadState(input.runId, "builder", "degraded", now);
|
|
78
|
+
throw new HandoffLoopError(input.runId);
|
|
79
|
+
}
|
|
80
|
+
const checkpoints = db.continuityCheckpoints(input.runId, input.role);
|
|
81
|
+
const head = db.continuityHead(input.runId, input.role);
|
|
82
|
+
const runHead = db.continuityHead(input.runId, "run") ?? head;
|
|
83
|
+
if (!head || !runHead || checkpoints.length === 0 || (head.state !== "current" && !input.allowNonCurrentContinuity)) {
|
|
84
|
+
throw new Error(`cannot hand off ${input.role}: the latest continuity checkpoint is ${head?.state ?? "missing"}`);
|
|
85
|
+
}
|
|
86
|
+
const prior = db.handoffs(input.runId).at(-1);
|
|
87
|
+
const generation = (prior?.generation ?? 0) + 1;
|
|
88
|
+
const resources = (input.resources ?? []).map((resource) => ({
|
|
89
|
+
label: resource.label,
|
|
90
|
+
digest: digest(resource.content),
|
|
91
|
+
authoritative: resource.authoritative,
|
|
92
|
+
}));
|
|
93
|
+
resources.unshift({ label: "continuity-checkpoint", digest: head.digest, authoritative: true }, { label: "authoritative-run-state", digest: runHead.digest, authoritative: true });
|
|
94
|
+
const manifest = {
|
|
95
|
+
version: 1,
|
|
96
|
+
runId: input.runId,
|
|
97
|
+
generation,
|
|
98
|
+
role: input.role,
|
|
99
|
+
reason: clean(input.reason, 2_000),
|
|
100
|
+
...(input.predecessorSessionId ? { predecessorSessionId: input.predecessorSessionId } : {}),
|
|
101
|
+
...(input.predecessorSessionRef ? { predecessorSessionRef: input.predecessorSessionRef } : {}),
|
|
102
|
+
...(prior ? { predecessorManifestDigest: prior.manifestDigest } : {}),
|
|
103
|
+
continuityCheckpointDigest: head.digest,
|
|
104
|
+
authoritativeStateDigest: runHead.digest,
|
|
105
|
+
cumulative: mergeContinuityDeltas(checkpoints.map((checkpoint) => checkpoint.delta)),
|
|
106
|
+
roleState: sanitizeObject(input.roleState ?? {}),
|
|
107
|
+
lineage: db.handoffs(input.runId).map((item) => item.manifestDigest),
|
|
108
|
+
...(input.sessionUsage ? { sessionUsage: input.sessionUsage } : {}),
|
|
109
|
+
compactionCount: input.compactionCount,
|
|
110
|
+
compactMaximum: input.compactMaximum,
|
|
111
|
+
resources,
|
|
112
|
+
createdAt: now.toISOString(),
|
|
113
|
+
};
|
|
114
|
+
const markdown = renderHandoffMarkdown(manifest);
|
|
115
|
+
const lineage = db.stageHandoff(manifest, markdown);
|
|
116
|
+
db.appendContinuityEvent({
|
|
117
|
+
runId: input.runId, role: "host", kind: input.requestedByBuilder ? "builder_handoff_requested" : "handoff_requested",
|
|
118
|
+
payload: {
|
|
119
|
+
generation, reason: manifest.reason,
|
|
120
|
+
occupancy: input.roleState?.contextSample ?? input.roleState?.occupancy ?? "unavailable",
|
|
121
|
+
sessionUsage: input.sessionUsage ?? "unavailable",
|
|
122
|
+
compactionCount: input.compactionCount, compactMaximum: input.compactMaximum, resources,
|
|
123
|
+
},
|
|
124
|
+
authoritativeStateRevision: head.authoritativeStateRevision,
|
|
125
|
+
}, now);
|
|
126
|
+
const cacheDirectory = this.materialize(manifest, markdown);
|
|
127
|
+
return { manifest, markdown, lineage, cacheDirectory };
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
db.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async transfer(input, createSuccessor) {
|
|
134
|
+
const staged = this.stage(input);
|
|
135
|
+
const successor = await createSuccessor(staged);
|
|
136
|
+
return this.acceptStaged(staged, successor);
|
|
137
|
+
}
|
|
138
|
+
async acceptStaged(staged, successor) {
|
|
139
|
+
const db = new WorkflowDb(this.projectDir);
|
|
140
|
+
try {
|
|
141
|
+
const response = await successor.sendTurn([
|
|
142
|
+
"Accept this validated Rafi handoff. Do not repeat completed work and reconcile host receipts before side effects.",
|
|
143
|
+
staged.markdown,
|
|
144
|
+
`Manifest JSON: ${JSON.stringify(staged.manifest)}`,
|
|
145
|
+
`Reply with ${HANDOFF_ACCEPTED} on the first line, then ${continuityInstruction()}`,
|
|
146
|
+
].join("\n\n"));
|
|
147
|
+
const parsed = parseContinuityDelta(response.text);
|
|
148
|
+
const sessionId = successor.sessionId();
|
|
149
|
+
const observedSuccessorRef = successor.sessionRef?.();
|
|
150
|
+
const successorRef = observedSuccessorRef ? { ...observedSuccessorRef, generation: staged.manifest.generation, validatedAt: new Date().toISOString() } : undefined;
|
|
151
|
+
if (!response.text.trimStart().startsWith(HANDOFF_ACCEPTED) || !parsed.delta || !sessionId || (staged.manifest.predecessorSessionRef && !successorRef)) {
|
|
152
|
+
db.failHandoff(staged.manifest.runId, staged.manifest.generation, "successor did not provide HANDOFF_ACCEPTED, a valid continuity delta, and a fresh session identity");
|
|
153
|
+
await successor.close().catch(() => { });
|
|
154
|
+
throw new Error("fresh successor failed validated handoff acceptance; predecessor retains the lease");
|
|
155
|
+
}
|
|
156
|
+
if (staged.manifest.predecessorSessionId && sessionId === staged.manifest.predecessorSessionId) {
|
|
157
|
+
db.failHandoff(staged.manifest.runId, staged.manifest.generation, "successor reused predecessor session ID");
|
|
158
|
+
await successor.close().catch(() => { });
|
|
159
|
+
throw new Error("handoff successor must be a genuinely fresh provider session");
|
|
160
|
+
}
|
|
161
|
+
if (successorRef)
|
|
162
|
+
successor.adoptSessionRef?.(successorRef);
|
|
163
|
+
db.appendContinuityEvent({ runId: staged.manifest.runId, role: staged.manifest.role, kind: "handoff_successor_accepted", payload: { generation: staged.manifest.generation, sessionId, sessionRef: successorRef, delta: parsed.delta }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0, sessionRef: successorRef });
|
|
164
|
+
const checkpoint = db.publishContinuityCheckpoint({ runId: staged.manifest.runId, role: staged.manifest.role, delta: parsed.delta, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0, sessionRef: successorRef });
|
|
165
|
+
const lineage = db.acceptHandoff(staged.manifest.runId, staged.manifest.generation, successorRef ?? sessionId);
|
|
166
|
+
return { ...staged, lineage, successor, successorSessionId: sessionId, acceptanceCheckpointDigest: checkpoint.digest };
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const current = db.handoff(staged.manifest.runId, staged.manifest.generation);
|
|
170
|
+
if (current?.state === "staged")
|
|
171
|
+
db.failHandoff(staged.manifest.runId, staged.manifest.generation, error instanceof Error ? error.message : String(error));
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
db.close();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
inspect(runId, generation) {
|
|
179
|
+
const db = new WorkflowDb(this.projectDir);
|
|
180
|
+
try {
|
|
181
|
+
const selected = generation ?? db.handoffs(runId).at(-1)?.generation;
|
|
182
|
+
if (selected === undefined)
|
|
183
|
+
throw new Error(`no handoff history found for run ${runId}`);
|
|
184
|
+
const value = db.handoffContent(runId, selected);
|
|
185
|
+
if (!value)
|
|
186
|
+
throw new Error(`handoff ${runId}/${selected} not found`);
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
db.close();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
loadStaged(runId, generation) {
|
|
194
|
+
const value = this.inspect(runId, generation);
|
|
195
|
+
if (value.lineage.state !== "staged")
|
|
196
|
+
throw new Error(`handoff ${runId}/${generation} is ${value.lineage.state}, not staged`);
|
|
197
|
+
return { ...value, cacheDirectory: this.materialize(value.manifest, value.markdown) };
|
|
198
|
+
}
|
|
199
|
+
pruneCache(runId, keepLatest = 1) {
|
|
200
|
+
if (!Number.isSafeInteger(keepLatest) || keepLatest < 0)
|
|
201
|
+
throw new Error("--keep-latest must be a non-negative safe integer");
|
|
202
|
+
const runDir = this.safeRunCache(runId);
|
|
203
|
+
if (!existsSync(runDir))
|
|
204
|
+
return [];
|
|
205
|
+
const generations = readdirSync(runDir).filter((name) => /^\d+$/.test(name)).map(Number).sort((a, b) => b - a);
|
|
206
|
+
const removed = [];
|
|
207
|
+
for (const generation of generations.slice(keepLatest)) {
|
|
208
|
+
const target = join(runDir, String(generation));
|
|
209
|
+
rmSync(target, { recursive: true, force: true });
|
|
210
|
+
removed.push(target);
|
|
211
|
+
}
|
|
212
|
+
return removed;
|
|
213
|
+
}
|
|
214
|
+
pruneExpiredCache(now = new Date(), retentionDays = HANDOFF_CACHE_RETENTION_DAYS) {
|
|
215
|
+
if (!existsSync(this.cacheRoot))
|
|
216
|
+
return [];
|
|
217
|
+
const db = new WorkflowDb(this.projectDir);
|
|
218
|
+
const protectedRuns = new Set(db.resumableRuns().map((run) => run.runId));
|
|
219
|
+
db.close();
|
|
220
|
+
const cutoff = now.getTime() - retentionDays * 86_400_000;
|
|
221
|
+
const removed = [];
|
|
222
|
+
for (const runId of readdirSync(this.cacheRoot)) {
|
|
223
|
+
if (protectedRuns.has(runId))
|
|
224
|
+
continue;
|
|
225
|
+
const runDir = this.safeRunCache(runId);
|
|
226
|
+
for (const generation of existsSync(runDir) ? readdirSync(runDir) : []) {
|
|
227
|
+
const target = join(runDir, generation);
|
|
228
|
+
if (statSync(target).mtimeMs < cutoff) {
|
|
229
|
+
rmSync(target, { recursive: true, force: true });
|
|
230
|
+
removed.push(target);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return removed;
|
|
235
|
+
}
|
|
236
|
+
materialize(manifest, markdown) {
|
|
237
|
+
const runDir = this.safeRunCache(manifest.runId);
|
|
238
|
+
mkdirSync(runDir, { recursive: true });
|
|
239
|
+
const target = join(runDir, String(manifest.generation));
|
|
240
|
+
if (existsSync(target))
|
|
241
|
+
return target;
|
|
242
|
+
const temporary = mkdtempSync(join(runDir, `.stage-${manifest.generation}-`));
|
|
243
|
+
writeFileSync(join(temporary, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
244
|
+
writeFileSync(join(temporary, "handoff.md"), markdown, { encoding: "utf8", mode: 0o600 });
|
|
245
|
+
renameSync(temporary, target);
|
|
246
|
+
return target;
|
|
247
|
+
}
|
|
248
|
+
safeRunCache(runId) {
|
|
249
|
+
if (!/^[A-Za-z0-9._-]+$/.test(runId) || runId === "." || runId === "..")
|
|
250
|
+
throw new Error("unsafe handoff run ID");
|
|
251
|
+
const path = join(this.cacheRoot, runId);
|
|
252
|
+
if (!path.startsWith(`${this.cacheRoot}/`))
|
|
253
|
+
throw new Error("unsafe handoff cache path");
|
|
254
|
+
return path;
|
|
255
|
+
}
|
|
256
|
+
consecutiveBuilderRequests(db, runId) {
|
|
257
|
+
let count = 0;
|
|
258
|
+
let usefulSinceLastRequest = false;
|
|
259
|
+
const events = db.continuityEvents(runId);
|
|
260
|
+
for (const event of events) {
|
|
261
|
+
if (event.kind === "turn_completed" || event.kind === "turn_completed_after_repair") {
|
|
262
|
+
const payload = event.payload && typeof event.payload === "object" ? event.payload : undefined;
|
|
263
|
+
const delta = payload?.delta;
|
|
264
|
+
if (isContinuityDelta(delta) && useful(delta))
|
|
265
|
+
usefulSinceLastRequest = true;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (event.kind === "builder_handoff_requested") {
|
|
269
|
+
if (usefulSinceLastRequest)
|
|
270
|
+
count = 0;
|
|
271
|
+
count += 1;
|
|
272
|
+
usefulSinceLastRequest = false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return count;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
export function renderHandoffMarkdown(manifest) {
|
|
279
|
+
const section = (title, values) => [`## ${title}`, "", ...(values.length ? values.map((value) => `- ${value}`) : ["- None recorded."]), ""];
|
|
280
|
+
return [
|
|
281
|
+
`# Rafi handoff — ${manifest.runId} / generation ${manifest.generation}`,
|
|
282
|
+
"",
|
|
283
|
+
`Role: ${manifest.role}`,
|
|
284
|
+
`Reason: ${manifest.reason}`,
|
|
285
|
+
`Created: ${manifest.createdAt}`,
|
|
286
|
+
`Compactions: ${manifest.compactionCount}/${manifest.compactMaximum}`,
|
|
287
|
+
"",
|
|
288
|
+
...section("Decisions", manifest.cumulative.decisions),
|
|
289
|
+
...section("Constraints", manifest.cumulative.constraints),
|
|
290
|
+
...section("Discoveries", manifest.cumulative.discoveries),
|
|
291
|
+
...section("Completed work and evidence", [...manifest.cumulative.completedActions, ...manifest.cumulative.evidence]),
|
|
292
|
+
...section("Failures and blockers", [...manifest.cumulative.failures, ...manifest.cumulative.blockers]),
|
|
293
|
+
...section("Remaining actions", [...manifest.cumulative.openWork, `Next: ${manifest.cumulative.nextAction}`]),
|
|
294
|
+
"## Role state",
|
|
295
|
+
"",
|
|
296
|
+
"```json",
|
|
297
|
+
JSON.stringify(manifest.roleState, null, 2),
|
|
298
|
+
"```",
|
|
299
|
+
"",
|
|
300
|
+
"## Authoritative source digests",
|
|
301
|
+
"",
|
|
302
|
+
"| Source | Digest | Authoritative |",
|
|
303
|
+
"|---|---|---|",
|
|
304
|
+
...manifest.resources.map((resource) => `| ${escapeCell(resource.label)} | \`${resource.digest}\` | ${resource.authoritative ? "yes" : "no"} |`),
|
|
305
|
+
"",
|
|
306
|
+
].join("\n");
|
|
307
|
+
}
|
|
308
|
+
export function writeHandoffInspection(path, content) {
|
|
309
|
+
const target = resolve(path);
|
|
310
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
311
|
+
writeFileSync(target, content, { encoding: "utf8", mode: 0o600 });
|
|
312
|
+
}
|
|
313
|
+
function useful(delta) {
|
|
314
|
+
return delta.completedActions.length > 0 || delta.evidence.length > 0 || delta.decisions.length > 0 || delta.discoveries.length > 0 || delta.blockers.length > 0;
|
|
315
|
+
}
|
|
316
|
+
function isContinuityDelta(value) {
|
|
317
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
318
|
+
return false;
|
|
319
|
+
const candidate = value;
|
|
320
|
+
return candidate.version === 1
|
|
321
|
+
&& [candidate.completedActions, candidate.evidence, candidate.decisions, candidate.discoveries, candidate.blockers].every(Array.isArray);
|
|
322
|
+
}
|
|
323
|
+
function digest(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
324
|
+
function clean(value, maximum) { return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum); }
|
|
325
|
+
function sanitizeObject(value) { return JSON.parse(JSON.stringify(value, (key, entry) => /credential|secret|token|password|raw.?transcript|hidden.?reasoning/i.test(key) ? undefined : typeof entry === "string" ? entry.slice(0, 20_000) : entry)); }
|
|
326
|
+
function escapeCell(value) { return value.replace(/\|/g, "\\|").replace(/[\r\n]+/g, " "); }
|
package/dist/log.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** A single structured log record. */
|
|
2
2
|
export interface LogRecord {
|
|
3
3
|
ts: string;
|
|
4
|
-
event: "batch-start" | "batch-end" | "branch-plan" | "branch-start" | "branch-session" | "branch-resume" | "branch-issue" | "branch-complete" | "branch-push" | "github-readiness-failed" | "gitlab-readiness-failed" | "pr-created" | "pr-existing" | "pr-failed" | "pr-auto-merge-enabled" | "pr-auto-merge-failed" | "mr-created" | "mr-existing" | "mr-failed" | "mr-auto-merge-enabled" | "mr-auto-merge-failed" | "branch-auto-merge-wait" | "branch-direct-merge" | "step" | "permission" | "escalation" | "needs_input" | "preflight" | "rafi-plan" | "ticket-populate" | "uninstall-proposal" | "qa" | "qa-fix" | "qa-protected-files-changed" | "qa-evidence" | "agent-status" | "error";
|
|
4
|
+
event: "batch-start" | "batch-end" | "branch-plan" | "branch-start" | "branch-session" | "branch-resume" | "branch-issue" | "branch-complete" | "branch-push" | "github-readiness-failed" | "gitlab-readiness-failed" | "pr-created" | "pr-existing" | "pr-failed" | "pr-auto-merge-enabled" | "pr-auto-merge-failed" | "mr-created" | "mr-existing" | "mr-failed" | "mr-auto-merge-enabled" | "mr-auto-merge-failed" | "branch-auto-merge-wait" | "branch-direct-merge" | "step" | "permission" | "escalation" | "needs_input" | "preflight" | "rafi-plan" | "ticket-populate" | "uninstall-proposal" | "qa" | "qa-fix" | "qa-protected-files-changed" | "qa-evidence" | "agent-status" | "context-lifecycle" | "handoff-transfer" | "recovery-handoff-accepted" | "guided-recovery-required" | "safe-boundary-paused" | "session-unavailable" | "error";
|
|
5
5
|
[key: string]: unknown;
|
|
6
6
|
}
|
|
7
7
|
/** Append-only JSONL logger. One line per record, plus an echo to the console. */
|
|
@@ -11,10 +11,14 @@ export interface Classification {
|
|
|
11
11
|
export declare class PermissionPolicy {
|
|
12
12
|
private readonly config;
|
|
13
13
|
private readonly cwd;
|
|
14
|
-
|
|
14
|
+
private readonly options;
|
|
15
|
+
constructor(config: PermissionConfig, cwd: string, options?: {
|
|
16
|
+
currentBranchWorkflow?: boolean;
|
|
17
|
+
});
|
|
15
18
|
classify(req: PermissionRequest): Classification;
|
|
16
19
|
private classifyBash;
|
|
17
20
|
private matchesAllowedPrefix;
|
|
18
21
|
private filePath;
|
|
19
22
|
private insideCwd;
|
|
23
|
+
private isGitMetadata;
|
|
20
24
|
}
|
|
@@ -17,9 +17,11 @@ const SHELL_SUBSTITUTION = { pattern: /\$\(|`/, reason: "shell substitution is n
|
|
|
17
17
|
export class PermissionPolicy {
|
|
18
18
|
config;
|
|
19
19
|
cwd;
|
|
20
|
-
|
|
20
|
+
options;
|
|
21
|
+
constructor(config, cwd, options = {}) {
|
|
21
22
|
this.config = config;
|
|
22
23
|
this.cwd = cwd;
|
|
24
|
+
this.options = options;
|
|
23
25
|
}
|
|
24
26
|
classify(req) {
|
|
25
27
|
const { toolName, input } = req;
|
|
@@ -31,6 +33,9 @@ export class PermissionPolicy {
|
|
|
31
33
|
}
|
|
32
34
|
if (FILE_TOOLS.has(toolName)) {
|
|
33
35
|
const target = this.filePath(input);
|
|
36
|
+
if (this.options.currentBranchWorkflow && target && this.isGitMetadata(target)) {
|
|
37
|
+
return { decision: "escalate", reason: "current-branch workflow forbids direct writes to Git metadata" };
|
|
38
|
+
}
|
|
34
39
|
if (target && !this.insideCwd(target)) {
|
|
35
40
|
return {
|
|
36
41
|
decision: "escalate",
|
|
@@ -47,6 +52,9 @@ export class PermissionPolicy {
|
|
|
47
52
|
return { decision: "escalate", reason: `tool ${toolName} not recognized` };
|
|
48
53
|
}
|
|
49
54
|
classifyBash(command) {
|
|
55
|
+
if (this.options.currentBranchWorkflow && isCurrentWorkflowGitMutation(command)) {
|
|
56
|
+
return { decision: "escalate", reason: "current-branch workflow leaves all Git lifecycle and review operations to the user" };
|
|
57
|
+
}
|
|
50
58
|
// Always-escalate list is checked first against the full command string —
|
|
51
59
|
// a dangerous pattern anywhere in a chained command escalates the whole call.
|
|
52
60
|
const hitEscalate = this.config.escalateBash.find((p) => command.includes(p));
|
|
@@ -89,4 +97,43 @@ export class PermissionPolicy {
|
|
|
89
97
|
const root = resolve(this.cwd);
|
|
90
98
|
return abs === root || abs.startsWith(root + "/");
|
|
91
99
|
}
|
|
100
|
+
isGitMetadata(target) {
|
|
101
|
+
const abs = isAbsolute(target) ? resolve(target) : resolve(this.cwd, target);
|
|
102
|
+
const root = resolve(this.cwd);
|
|
103
|
+
const relative = abs === root ? "" : abs.startsWith(root + "/") ? abs.slice(root.length + 1) : "";
|
|
104
|
+
return relative === ".git" || relative.startsWith(".git/");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function isCurrentWorkflowGitMutation(command) {
|
|
108
|
+
const segments = command.split(SHELL_SPLIT).map((part) => part.trim()).filter(Boolean);
|
|
109
|
+
for (const segment of segments) {
|
|
110
|
+
if (/^(?:gh|glab)(?:\s|$)/.test(segment))
|
|
111
|
+
return true;
|
|
112
|
+
const invocation = /(?:^|\s)git\s+([\s\S]+)$/i.exec(segment);
|
|
113
|
+
if (!invocation)
|
|
114
|
+
continue;
|
|
115
|
+
let remainder = invocation[1].trim();
|
|
116
|
+
const globalOption = /^(?:(?:-C|-c|--git-dir|--work-tree|--namespace|--exec-path)\s+(?:"[^"]+"|'[^']+'|\S+)|--(?:git-dir|work-tree|namespace|exec-path)=\S+|--(?:no-pager|paginate|no-replace-objects|literal-pathspecs|glob-pathspecs|noglob-pathspecs|icase-pathspecs|bare))(?:\s+|$)/i;
|
|
117
|
+
while (globalOption.test(remainder))
|
|
118
|
+
remainder = remainder.replace(globalOption, "").trimStart();
|
|
119
|
+
const match = /^([a-z][a-z-]*)([\s\S]*)$/i.exec(remainder);
|
|
120
|
+
if (!match)
|
|
121
|
+
continue;
|
|
122
|
+
const subcommand = match[1].toLowerCase();
|
|
123
|
+
const args = match[2].trim();
|
|
124
|
+
if (["add", "am", "apply", "checkout", "cherry-pick", "clean", "commit", "fetch", "merge", "mv", "pull", "push", "rebase", "reset", "restore", "revert", "rm", "stash", "switch", "tag", "worktree"].includes(subcommand))
|
|
125
|
+
return true;
|
|
126
|
+
if (subcommand === "branch" && args && !/^(?:--show-current|-a|--all|-r|--remotes|-v|-vv|--verbose)(?:\s|$)/.test(args))
|
|
127
|
+
return true;
|
|
128
|
+
if (subcommand === "branch")
|
|
129
|
+
continue;
|
|
130
|
+
if (subcommand === "remote" && /^(?:$|-v|--verbose|get-url(?:\s|$)|show(?:\s|$))/.test(args))
|
|
131
|
+
continue;
|
|
132
|
+
if (["annotate", "blame", "cat-file", "describe", "diff", "diff-tree", "for-each-ref", "grep", "log", "ls-files", "ls-remote", "ls-tree", "merge-base", "name-rev", "rev-list", "rev-parse", "shortlog", "show", "show-ref", "status", "symbolic-ref", "version", "whatchanged"].includes(subcommand))
|
|
133
|
+
continue;
|
|
134
|
+
// Configured aliases and unfamiliar porcelain can mutate refs or the
|
|
135
|
+
// index; current mode permits only an explicit read-only subset.
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
92
139
|
}
|
package/dist/qaReview.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { SessionStrategy } from "rafi-spec";
|
|
2
|
-
import type { BuilderAdapter } from "./adapters/types.js";
|
|
1
|
+
import type { ProviderSessionRefV1, SessionStrategy } from "rafi-spec";
|
|
2
|
+
import type { BuilderAdapter, CompactResult } from "./adapters/types.js";
|
|
3
3
|
import type { TicketDef } from "./tickets/ticketSchema.js";
|
|
4
4
|
export interface QaStreamState {
|
|
5
5
|
sessionId?: string;
|
|
6
|
+
sessionRef?: ProviderSessionRefV1;
|
|
6
7
|
reviews: number;
|
|
7
8
|
modificationViolations: number;
|
|
8
9
|
}
|
|
@@ -26,6 +27,8 @@ export interface IsolatedQaOptions {
|
|
|
26
27
|
qaStrategy: SessionStrategy;
|
|
27
28
|
state: QaStreamState;
|
|
28
29
|
createQa: (cwd: string, sessionId?: string) => Promise<BuilderAdapter>;
|
|
30
|
+
/** Host-owned ordinary QA boundary. Fresh strategies must return a validated successor. */
|
|
31
|
+
sessionBoundary?: (adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>;
|
|
29
32
|
fix: (issues: string) => Promise<{
|
|
30
33
|
ok: boolean;
|
|
31
34
|
detail?: string;
|
|
@@ -46,8 +49,5 @@ export interface IsolatedQaResult {
|
|
|
46
49
|
}
|
|
47
50
|
export declare function runIsolatedQa(opts: IsolatedQaOptions): Promise<IsolatedQaResult>;
|
|
48
51
|
export declare function buildQaReviewHandoff(ticket: TicketDef, builderSummary: string, diffDigest: string, validationChecklist: string[]): string;
|
|
49
|
-
export declare function compactWithRetry(adapter: BuilderAdapter): Promise<
|
|
50
|
-
ok: boolean;
|
|
51
|
-
error?: string;
|
|
52
|
-
}>;
|
|
52
|
+
export declare function compactWithRetry(adapter: BuilderAdapter): Promise<CompactResult>;
|
|
53
53
|
export declare function buildDurableQaFixHandoff(ticket: TicketDef, issues: string, worktree: string, priorResult: string, diffDigest: string): string;
|
package/dist/qaReview.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SessionUnavailableError } from "./adapters/sessionFailure.js";
|
|
1
2
|
import { currentActivity, withActivityPhase } from "./activity.js";
|
|
2
3
|
import { buildQaFixInstruction, buildQaInstruction, parseStepStatus } from "./foreman.js";
|
|
3
4
|
import { createDisposableQaSnapshotAsync } from "./qaSnapshot.js";
|
|
@@ -42,21 +43,17 @@ async function oneReview(opts, cycle) {
|
|
|
42
43
|
const snapshot = await withActivityPhase("preparing disposable QA snapshot", () => createDisposableQaSnapshotAsync(opts.builderWorktree, progress));
|
|
43
44
|
let qa;
|
|
44
45
|
try {
|
|
45
|
-
const resume = opts.qaStrategy === "compact" && opts.state.modificationViolations === 0 ? opts.state.sessionId : undefined;
|
|
46
|
-
qa = await opts.createQa(snapshot.path, resume);
|
|
47
|
-
if (opts.qaStrategy === "compact" && opts.state.reviews > 0 && resume) {
|
|
48
|
-
const compacted = await withActivityPhase("compacting QA session", () => compactWithRetry(qa));
|
|
49
|
-
if (!compacted.ok) {
|
|
50
|
-
await qa.close();
|
|
51
|
-
qa = await opts.createQa(snapshot.path);
|
|
52
|
-
opts.state.sessionId = undefined;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
46
|
const handoff = buildQaReviewHandoff(opts.ticket, opts.builderSummary, snapshot.manifest.diffDigest, loadTicketSetupConfigWithDefaults(opts.builderWorktree).build.validation_checklist);
|
|
47
|
+
// Every disposable snapshot has a distinct cwd and therefore must have a
|
|
48
|
+
// fresh provider conversation. Cumulative QA state remains in the durable
|
|
49
|
+
// continuity/checkpoint stream; an old provider session is never moved
|
|
50
|
+
// into a newly-created /tmp/rafi-qa-* directory.
|
|
51
|
+
qa = await opts.createQa(snapshot.path);
|
|
56
52
|
const turn = await qa.sendTurn(handoff);
|
|
57
53
|
const status = parseStepStatus(turn.text);
|
|
58
54
|
opts.state.reviews += 1;
|
|
59
55
|
opts.state.sessionId = qa.sessionId();
|
|
56
|
+
opts.state.sessionRef = qa.sessionRef?.();
|
|
60
57
|
const changes = await withActivityPhase("checking QA file changes", () => snapshot.qaChanges());
|
|
61
58
|
if (changes.length) {
|
|
62
59
|
opts.state.modificationViolations += 1;
|
|
@@ -99,10 +96,19 @@ export async function compactWithRetry(adapter) {
|
|
|
99
96
|
return { ok: false, error: "native compaction unavailable" };
|
|
100
97
|
let error = "";
|
|
101
98
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
try {
|
|
100
|
+
const result = await adapter.compact();
|
|
101
|
+
if (result.ok)
|
|
102
|
+
return { ok: true };
|
|
103
|
+
if (result.failure?.category === "session-unavailable")
|
|
104
|
+
return result;
|
|
105
|
+
error = result.error ?? "compaction failed";
|
|
106
|
+
}
|
|
107
|
+
catch (failure) {
|
|
108
|
+
if (failure instanceof SessionUnavailableError)
|
|
109
|
+
return { ok: false, error: failure.message, failure: failure.failure };
|
|
110
|
+
error = failure instanceof Error ? failure.message : String(failure);
|
|
111
|
+
}
|
|
106
112
|
}
|
|
107
113
|
return { ok: false, error };
|
|
108
114
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
2
|
+
import { ClaudeAdapter } from "./adapters/claude.js";
|
|
3
|
+
import { CodexAdapter } from "./adapters/codex.js";
|
|
4
|
+
import type { BuilderAdapterOptions } from "./adapters/types.js";
|
|
5
|
+
export interface ResolveSessionAvailabilityOptions {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
configRoot?: string;
|
|
8
|
+
workspaceIdentity?: string;
|
|
9
|
+
runtimeExecutable?: string;
|
|
10
|
+
now?: Date;
|
|
11
|
+
}
|
|
12
|
+
/** Provider-backed exact-session probe. Codex attaches with thread/resume and never starts a turn. */
|
|
13
|
+
export declare function resolveProviderSessionAvailability(ref: ProviderSessionRefV1, options?: ResolveSessionAvailabilityOptions): Promise<SessionAvailabilityV1>;
|
|
14
|
+
/** Create a validated exact adapter for callers that need one after a successful probe. */
|
|
15
|
+
export declare function createValidatedProviderAdapter(ref: ProviderSessionRefV1, options: Omit<BuilderAdapterOptions, "resumeSessionId" | "resumeSessionRef">): Promise<ClaudeAdapter | CodexAdapter>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ClaudeAdapter, probeClaudeSession } from "./adapters/claude.js";
|
|
2
|
+
import { CodexAdapter } from "./adapters/codex.js";
|
|
3
|
+
import { SessionUnavailableError } from "./adapters/sessionFailure.js";
|
|
4
|
+
/** Provider-backed exact-session probe. Codex attaches with thread/resume and never starts a turn. */
|
|
5
|
+
export async function resolveProviderSessionAvailability(ref, options = {}) {
|
|
6
|
+
const cwd = options.cwd ?? ref.cwd;
|
|
7
|
+
const configRoot = options.configRoot ?? ref.configRoot;
|
|
8
|
+
if (ref.provider === "claude") {
|
|
9
|
+
return probeClaudeSession(ref, { cwd, configRoot, workspaceIdentity: options.workspaceIdentity, now: options.now });
|
|
10
|
+
}
|
|
11
|
+
if (ref.source === "legacy-inferred") {
|
|
12
|
+
return { version: 1, status: "unknown", checkedAt: (options.now ?? new Date()).toISOString(), reason: "legacy-unscoped", detail: "legacy Codex thread IDs cannot be proven exact without an observed scoped binding", sessionRef: ref };
|
|
13
|
+
}
|
|
14
|
+
const adapterOptions = {
|
|
15
|
+
cwd,
|
|
16
|
+
configRoot,
|
|
17
|
+
runtimeExecutable: options.runtimeExecutable,
|
|
18
|
+
runtimePhase: "recovery",
|
|
19
|
+
resumeSessionRef: ref,
|
|
20
|
+
sessionRole: ref.role,
|
|
21
|
+
sessionStream: ref.stream,
|
|
22
|
+
sessionGeneration: ref.generation,
|
|
23
|
+
workspaceIdentity: options.workspaceIdentity,
|
|
24
|
+
ticketId: ref.ticketId,
|
|
25
|
+
deliveryUnitId: ref.deliveryUnitId,
|
|
26
|
+
sandboxMode: "read-only",
|
|
27
|
+
permission: async () => ({ behavior: "deny", message: "session availability probes never run tools" }),
|
|
28
|
+
};
|
|
29
|
+
const adapter = new CodexAdapter(adapterOptions);
|
|
30
|
+
try {
|
|
31
|
+
return await adapter.validateSession();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return { version: 1, status: "unknown", checkedAt: (options.now ?? new Date()).toISOString(), reason: "probe-failed", detail: error instanceof Error ? error.message : String(error), sessionRef: ref };
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await adapter.close().catch(() => { });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Create a validated exact adapter for callers that need one after a successful probe. */
|
|
41
|
+
export async function createValidatedProviderAdapter(ref, options) {
|
|
42
|
+
const scoped = { ...options, resumeSessionRef: ref, sessionRole: ref.role, sessionStream: ref.stream, sessionGeneration: ref.generation };
|
|
43
|
+
if (ref.provider === "claude")
|
|
44
|
+
return ClaudeAdapter.create(scoped);
|
|
45
|
+
const adapter = new CodexAdapter(scoped);
|
|
46
|
+
const availability = await adapter.validateSession();
|
|
47
|
+
if (availability.status !== "available") {
|
|
48
|
+
await adapter.close().catch(() => { });
|
|
49
|
+
throw new SessionUnavailableError({
|
|
50
|
+
runtime: "codex", phase: "preflight", dispatchState: "not-sent",
|
|
51
|
+
executable: options.runtimeExecutable ?? "codex", cwd: options.cwd,
|
|
52
|
+
diagnostics: availability.detail ?? `Codex session ${ref.sessionId} is ${availability.status}`,
|
|
53
|
+
availability,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return adapter;
|
|
57
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ConfigurableAgentRole, ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
2
|
+
export interface ProviderSessionScope {
|
|
3
|
+
provider: "claude" | "codex";
|
|
4
|
+
cwd: string;
|
|
5
|
+
configRoot: string;
|
|
6
|
+
role?: ConfigurableAgentRole;
|
|
7
|
+
stream?: string;
|
|
8
|
+
workspaceIdentity?: string;
|
|
9
|
+
ticketId?: string;
|
|
10
|
+
deliveryUnitId?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface CreateProviderSessionRefInput extends ProviderSessionScope {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
generation?: number;
|
|
15
|
+
source?: ProviderSessionRefV1["source"];
|
|
16
|
+
createdAt?: string;
|
|
17
|
+
validatedAt?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Resolve symlinks for an existing location and otherwise return an absolute normalized path. */
|
|
20
|
+
export declare function canonicalSessionPath(path: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Stable for one Git worktree lifetime. The token lives in that worktree's
|
|
23
|
+
* administrative Git directory (not in the checked-out tree), so normal edits
|
|
24
|
+
* and commits do not change it while removal/recreation cannot inherit it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function captureWorkspaceIdentity(cwd: string): string | undefined;
|
|
27
|
+
export declare function createProviderSessionRef(input: CreateProviderSessionRefInput): ProviderSessionRefV1;
|
|
28
|
+
/** Identity used by accounting, compaction, settings acknowledgements, and mutation leases. */
|
|
29
|
+
export declare function providerSessionKey(ref: ProviderSessionRefV1): string;
|
|
30
|
+
export declare function validateProviderSessionScope(ref: ProviderSessionRefV1, requested: ProviderSessionScope, now?: Date): SessionAvailabilityV1;
|
|
31
|
+
export declare function latestSessionBinding(bindings: readonly ProviderSessionRefV1[] | undefined, role: ConfigurableAgentRole, sessionId?: string): ProviderSessionRefV1 | undefined;
|
|
32
|
+
export declare function resolveUniqueSessionBinding(bindings: readonly ProviderSessionRefV1[] | undefined, sessionId: string): ProviderSessionRefV1 | undefined;
|
|
33
|
+
export declare function upsertSessionBinding(bindings: readonly ProviderSessionRefV1[] | undefined, ref: ProviderSessionRefV1): ProviderSessionRefV1[];
|