@astrosheep/keiyaku 2.9.5 → 2.9.6
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/build/.tsbuildinfo +1 -1
- package/build/agents/harness/outcome.js +10 -4
- package/build/agents/harness/projection.js +8 -7
- package/build/agents/harness/router.js +2 -1
- package/build/agents/providers/claude-agent-sdk/adapter.js +13 -1
- package/build/agents/providers/claude-agent-sdk/registration.js +2 -1
- package/build/agents/providers/codex-app-server/adapter.js +11 -10
- package/build/agents/providers/codex-app-server/registration.js +2 -1
- package/build/agents/providers/opencode-sdk/adapter.js +7 -0
- package/build/agents/providers/opencode-sdk/registration.js +2 -1
- package/build/agents/providers/pi/adapter.js +10 -0
- package/build/agents/providers/pi/registration.js +3 -1
- package/build/cli/completion.js +9 -2
- package/build/cli/flags.js +3 -2
- package/build/cli/render/errors.js +18 -1
- package/build/cli/render/format.js +5 -2
- package/build/core/audit/candidate.js +48 -0
- package/build/core/audit/coordinates.js +180 -0
- package/build/core/audit/evidence.js +158 -0
- package/build/core/audit/facade.js +42 -0
- package/build/core/audit/readiness.js +4 -0
- package/build/core/audit/report.js +38 -0
- package/build/core/audit.js +1 -650
- package/build/core/task/query.js +65 -23
- package/build/generated/version.js +2 -2
- package/package.json +1 -1
- package/skills/keiyaku-workflow/SKILL.md +6 -0
|
@@ -3,13 +3,17 @@ import { SUBAGENT_SESSION_VERSION, } from "../session.js";
|
|
|
3
3
|
import { OUTCOME_ERROR_VERSION, SubagentFailure, } from "../types.js";
|
|
4
4
|
import { isProviderTerminalError, } from "./provider-adapter.js";
|
|
5
5
|
import { renderCapturedText } from "./runtime.js";
|
|
6
|
-
export function createOutcomeState(maxCaptureChars) {
|
|
6
|
+
export function createOutcomeState(maxCaptureChars, admittedSession) {
|
|
7
7
|
return {
|
|
8
8
|
outputBuffer: new StringTailBuffer(maxCaptureChars),
|
|
9
9
|
diagnosticsBuffer: new StringTailBuffer(maxCaptureChars),
|
|
10
10
|
warnings: [],
|
|
11
|
+
...(admittedSession ? { session: admittedSession } : {}),
|
|
11
12
|
};
|
|
12
13
|
}
|
|
14
|
+
export function recordSessionAdmission(state, session) {
|
|
15
|
+
state.session = session;
|
|
16
|
+
}
|
|
13
17
|
function mintOutcome(draft) {
|
|
14
18
|
return draft;
|
|
15
19
|
}
|
|
@@ -29,7 +33,6 @@ export function processEvent(state, event) {
|
|
|
29
33
|
return;
|
|
30
34
|
case "done":
|
|
31
35
|
state.doneEvent = event;
|
|
32
|
-
state.session = event.session;
|
|
33
36
|
if (event.finalMessage !== undefined) {
|
|
34
37
|
state.finalMessageCandidate = event.finalMessage;
|
|
35
38
|
}
|
|
@@ -173,13 +176,16 @@ export function failWithError(shared, state, error, finalMessage, diagnostics =
|
|
|
173
176
|
warnings: [...state.warnings],
|
|
174
177
|
});
|
|
175
178
|
}
|
|
176
|
-
export function completeOutcome(
|
|
179
|
+
export function completeOutcome(state, finalMessage) {
|
|
180
|
+
if (!state.session) {
|
|
181
|
+
throw new Error("Cannot complete an outcome before session admission");
|
|
182
|
+
}
|
|
177
183
|
return mintOutcome({
|
|
178
184
|
status: "completed",
|
|
179
185
|
finalMessage,
|
|
180
186
|
output: renderCapturedText(state.outputBuffer),
|
|
181
187
|
diagnostics: renderCapturedText(state.diagnosticsBuffer),
|
|
182
|
-
session,
|
|
188
|
+
session: state.session,
|
|
183
189
|
warnings: [...state.warnings],
|
|
184
190
|
});
|
|
185
191
|
}
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { FlowError } from "../../flow-error.js";
|
|
3
3
|
import { createEventChannel } from "./event-channel.js";
|
|
4
4
|
import { overflowMarkerFromDropped, prepareAgentEventForPersistence, } from "./execution-handle.js";
|
|
5
|
-
import { classifyFailure, classifySettlementFailure, completeOutcome, createOutcomeState, failFromFailEvent, failWithError, processEvent, renderOutcomeCapturedText, } from "./outcome.js";
|
|
5
|
+
import { classifyFailure, classifySettlementFailure, completeOutcome, createOutcomeState, failFromFailEvent, failWithError, processEvent, recordSessionAdmission, renderOutcomeCapturedText, } from "./outcome.js";
|
|
6
6
|
import { startLiveProjection } from "./router.js";
|
|
7
7
|
import { createAbortError } from "./runtime.js";
|
|
8
8
|
const POLICY_BUDGET_CLEANUP_GRACE_MS = 5_000;
|
|
@@ -198,7 +198,7 @@ function deliverDriverEvent(shared, state, live, event, refreshIdleTimeout, know
|
|
|
198
198
|
processEvent(state, event);
|
|
199
199
|
}
|
|
200
200
|
async function settleProjection(shared) {
|
|
201
|
-
const state = createOutcomeState(shared.terms.maxCaptureChars);
|
|
201
|
+
const state = createOutcomeState(shared.terms.maxCaptureChars, shared.reviveTerms?.session);
|
|
202
202
|
let idleTimedOut = false;
|
|
203
203
|
let transportFailure;
|
|
204
204
|
let policyBudgetExceeded = false;
|
|
@@ -245,7 +245,6 @@ async function settleProjection(shared) {
|
|
|
245
245
|
resolvePolicyBudget();
|
|
246
246
|
}, shared.terms.executionTimeoutMs);
|
|
247
247
|
executionTimeout.unref?.();
|
|
248
|
-
refreshIdleTimeout();
|
|
249
248
|
const requestLiveAbort = () => {
|
|
250
249
|
if (shared.explicitlyAborted)
|
|
251
250
|
return;
|
|
@@ -257,7 +256,10 @@ async function settleProjection(shared) {
|
|
|
257
256
|
shared.lifecycleAbort.signal.addEventListener("abort", requestLiveAbort, { once: true });
|
|
258
257
|
const providerSettlement = (async () => {
|
|
259
258
|
try {
|
|
260
|
-
const live = await startLiveProjection(shared.terms, shared.lifecycleAbort.signal, shared.reviveTerms, refreshIdleTimeout, shared.observer)
|
|
259
|
+
const live = await startLiveProjection(shared.terms, shared.lifecycleAbort.signal, shared.reviveTerms, refreshIdleTimeout, shared.observer, (admission) => {
|
|
260
|
+
recordSessionAdmission(state, admission.session);
|
|
261
|
+
refreshIdleTimeout();
|
|
262
|
+
});
|
|
261
263
|
liveDriver = live;
|
|
262
264
|
shared.onLive(live);
|
|
263
265
|
resolveForceWaiters(shared, live);
|
|
@@ -324,8 +326,7 @@ async function settleProjection(shared) {
|
|
|
324
326
|
completionOutcome?.finalMessage ??
|
|
325
327
|
state.finalMessageCandidate ??
|
|
326
328
|
renderOutcomeCapturedText(state.outputBuffer).trim();
|
|
327
|
-
|
|
328
|
-
if (!session) {
|
|
329
|
+
if (!state.session) {
|
|
329
330
|
return failWithError(shared, state, {
|
|
330
331
|
code: "SUBAGENT_EXEC_ERROR",
|
|
331
332
|
message: `${shared.terms.provider} exec ended without a session handle`,
|
|
@@ -333,7 +334,7 @@ async function settleProjection(shared) {
|
|
|
333
334
|
exitCode: null,
|
|
334
335
|
}, finalMessage);
|
|
335
336
|
}
|
|
336
|
-
return completeOutcome(
|
|
337
|
+
return completeOutcome(state, finalMessage);
|
|
337
338
|
}
|
|
338
339
|
catch (error) {
|
|
339
340
|
if (error instanceof FlowError) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getProviderRegistration } from "../providers/registry.js";
|
|
2
|
-
export async function startLiveProjection(terms, signal, reviveTerms, onIngress, observer) {
|
|
2
|
+
export async function startLiveProjection(terms, signal, reviveTerms, onIngress, observer, onSessionAdmission) {
|
|
3
3
|
const onSyncCheckpoint = observer
|
|
4
4
|
? (control, checkpoint) => {
|
|
5
5
|
observer.onCheckpoint(control, checkpoint);
|
|
@@ -19,6 +19,7 @@ export async function startLiveProjection(terms, signal, reviveTerms, onIngress,
|
|
|
19
19
|
reviveTerms,
|
|
20
20
|
onSyncCheckpoint,
|
|
21
21
|
onDeliveryOpportunity,
|
|
22
|
+
onSessionAdmission,
|
|
22
23
|
onIngress,
|
|
23
24
|
observer,
|
|
24
25
|
});
|
|
@@ -198,6 +198,7 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
|
|
|
198
198
|
queryActive = true;
|
|
199
199
|
notifyActive();
|
|
200
200
|
let resumedSessionId = sessionId;
|
|
201
|
+
let admittedSessionId;
|
|
201
202
|
let finalMessage;
|
|
202
203
|
let latestAssistantMessageModel;
|
|
203
204
|
// Provider acceptance / pre-terminal retry evidence for no-turn classification.
|
|
@@ -207,7 +208,18 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
|
|
|
207
208
|
const seenToolUses = new Map();
|
|
208
209
|
for await (const event of query) {
|
|
209
210
|
builder.ingress(CLAUDE_AGENT_SDK_PROVIDER, event.type, event, claudeIngressDisposition(event.type), () => {
|
|
210
|
-
|
|
211
|
+
const providerSessionId = extractResumeToken(event);
|
|
212
|
+
resumedSessionId = providerSessionId ?? resumedSessionId;
|
|
213
|
+
if (providerSessionId && providerSessionId !== admittedSessionId) {
|
|
214
|
+
admittedSessionId = providerSessionId;
|
|
215
|
+
options.onSessionAdmission?.({
|
|
216
|
+
session: {
|
|
217
|
+
provider: CLAUDE_AGENT_SDK_PROVIDER,
|
|
218
|
+
version: SUBAGENT_SESSION_VERSION,
|
|
219
|
+
handle: { sessionId: providerSessionId },
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
211
223
|
if (isClaudeApiRetryEvent(event)) {
|
|
212
224
|
lastPreTerminalEvidenceWasRetry = true;
|
|
213
225
|
builder.emit({ type: "warning", warning: mapClaudeApiRetryWarning(event) });
|
|
@@ -179,7 +179,7 @@ export const claudeAgentSdkRegistration = {
|
|
|
179
179
|
return loader ? { ...terms, loader } : terms;
|
|
180
180
|
},
|
|
181
181
|
nativePolicyOwnership: () => null,
|
|
182
|
-
startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onIngress }) => {
|
|
182
|
+
startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onSessionAdmission, onIngress }) => {
|
|
183
183
|
if (terms.provider !== CLAUDE_AGENT_SDK_PROVIDER) {
|
|
184
184
|
throw new Error(`startLive expected ${CLAUDE_AGENT_SDK_PROVIDER}`);
|
|
185
185
|
}
|
|
@@ -197,6 +197,7 @@ export const claudeAgentSdkRegistration = {
|
|
|
197
197
|
systemPrompt: claudeTerms.systemPrompt,
|
|
198
198
|
onSyncCheckpoint,
|
|
199
199
|
onEvidence: claudeTerms.onEvidence,
|
|
200
|
+
onSessionAdmission,
|
|
200
201
|
onIngress,
|
|
201
202
|
initialTurn: claudeTerms.initialTurn,
|
|
202
203
|
loader: claudeTerms.loader,
|
|
@@ -304,6 +304,15 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
|
|
|
304
304
|
builder.emit({ type: "fail", message });
|
|
305
305
|
throw createProviderFailure(message);
|
|
306
306
|
}
|
|
307
|
+
const admittedSession = {
|
|
308
|
+
provider: "codex-app-server",
|
|
309
|
+
version: SUBAGENT_SESSION_VERSION,
|
|
310
|
+
handle: {
|
|
311
|
+
threadId: activeThreadId,
|
|
312
|
+
...(serverCodexHome ? { codexHome: serverCodexHome } : {}),
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
options.onSessionAdmission?.({ session: admittedSession });
|
|
307
316
|
if (options.onEvidence)
|
|
308
317
|
emitProviderEvidence(options.onEvidence, { type: "session", provider: CODEX_APP_SERVER_PROVIDER, threadId: activeThreadId }, builderDiagnostic);
|
|
309
318
|
// Policy A defaults, but omit when config.* already owns that surface (thread/start config wins).
|
|
@@ -351,17 +360,9 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
|
|
|
351
360
|
throw createProviderFailure(message);
|
|
352
361
|
}
|
|
353
362
|
// Successful turn/completed is consumption evidence; interrupt ack alone is not terminal proof.
|
|
354
|
-
const session = {
|
|
355
|
-
provider: "codex-app-server",
|
|
356
|
-
version: SUBAGENT_SESSION_VERSION,
|
|
357
|
-
handle: {
|
|
358
|
-
threadId: activeThreadId,
|
|
359
|
-
...(serverCodexHome ? { codexHome: serverCodexHome } : {}),
|
|
360
|
-
},
|
|
361
|
-
};
|
|
362
363
|
const finalMessage = turnState.lastAgentMessage;
|
|
363
|
-
builder.emit({ type: "done", finalMessage, session });
|
|
364
|
-
return { finalMessage, session };
|
|
364
|
+
builder.emit({ type: "done", finalMessage, session: admittedSession });
|
|
365
|
+
return { finalMessage, session: admittedSession };
|
|
365
366
|
}
|
|
366
367
|
finally {
|
|
367
368
|
activeTurnCompletion = undefined;
|
|
@@ -212,7 +212,7 @@ export const codexAppServerRegistration = {
|
|
|
212
212
|
webSearch: Object.freeze(webSearch),
|
|
213
213
|
});
|
|
214
214
|
},
|
|
215
|
-
startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onDeliveryOpportunity, onIngress, }) => {
|
|
215
|
+
startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onDeliveryOpportunity, onSessionAdmission, onIngress, }) => {
|
|
216
216
|
if (terms.provider !== CODEX_APP_SERVER_PROVIDER) {
|
|
217
217
|
throw new Error(`startLive expected ${CODEX_APP_SERVER_PROVIDER}`);
|
|
218
218
|
}
|
|
@@ -231,6 +231,7 @@ export const codexAppServerRegistration = {
|
|
|
231
231
|
onSyncCheckpoint,
|
|
232
232
|
onDeliveryOpportunity,
|
|
233
233
|
onEvidence: codexTerms.onEvidence,
|
|
234
|
+
onSessionAdmission,
|
|
234
235
|
onIngress,
|
|
235
236
|
initialTurn: codexTerms.initialTurn,
|
|
236
237
|
};
|
|
@@ -264,6 +264,13 @@ export async function startOpencode(prompt, cwd, options = {}) {
|
|
|
264
264
|
builder.emit({ type: "fail", message });
|
|
265
265
|
throw createProviderFailure(message);
|
|
266
266
|
}
|
|
267
|
+
options.onSessionAdmission?.({
|
|
268
|
+
session: {
|
|
269
|
+
provider: OPENCODE_SDK_PROVIDER,
|
|
270
|
+
version: SUBAGENT_SESSION_VERSION,
|
|
271
|
+
handle: { sessionId: activeSessionId },
|
|
272
|
+
},
|
|
273
|
+
});
|
|
267
274
|
idleMonitor = new OpencodeIdleMonitor(sdk.client.v2.session, activeSessionId, combined.signal);
|
|
268
275
|
eventController = createOpencodeEventController({
|
|
269
276
|
builder,
|
|
@@ -104,7 +104,7 @@ export const opencodeSdkRegistration = {
|
|
|
104
104
|
return loader ? { ...terms, loader } : terms;
|
|
105
105
|
},
|
|
106
106
|
nativePolicyOwnership: () => null,
|
|
107
|
-
startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onDeliveryOpportunity, onIngress, }) => {
|
|
107
|
+
startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onDeliveryOpportunity, onSessionAdmission, onIngress, }) => {
|
|
108
108
|
if (terms.provider !== OPENCODE_SDK_PROVIDER) {
|
|
109
109
|
throw new Error(`startLive expected ${OPENCODE_SDK_PROVIDER}`);
|
|
110
110
|
}
|
|
@@ -118,6 +118,7 @@ export const opencodeSdkRegistration = {
|
|
|
118
118
|
onSyncCheckpoint,
|
|
119
119
|
onDeliveryOpportunity,
|
|
120
120
|
onEvidence: opencodeTerms.onEvidence,
|
|
121
|
+
onSessionAdmission,
|
|
121
122
|
onIngress,
|
|
122
123
|
initialTurn: opencodeTerms.initialTurn,
|
|
123
124
|
loader: opencodeTerms.loader,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FlowError } from "../../../flow-error.js";
|
|
2
2
|
import { appendDebugLog } from "../../../telemetry/debug-log.js";
|
|
3
|
+
import { SUBAGENT_SESSION_VERSION, } from "../../session.js";
|
|
3
4
|
import { createProjectionDriver } from "../../harness/execution-handle.js";
|
|
4
5
|
import { createProviderFailure, toProviderTerminalError, } from "../../harness/provider-adapter.js";
|
|
5
6
|
import { createAbortError, emitProviderEvidence, sha256Utf8 } from "../../harness/runtime.js";
|
|
@@ -103,6 +104,15 @@ async function openPiProjectionDriver(prompt, cwd, options) {
|
|
|
103
104
|
...(resourceLoader ? { resourceLoader } : {}),
|
|
104
105
|
});
|
|
105
106
|
const session = created.session;
|
|
107
|
+
if (session.sessionFile) {
|
|
108
|
+
options.onSessionAdmission?.({
|
|
109
|
+
session: {
|
|
110
|
+
provider: PI_PROVIDER,
|
|
111
|
+
version: SUBAGENT_SESSION_VERSION,
|
|
112
|
+
handle: { sessionId: session.sessionId, sessionFile: session.sessionFile },
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
106
116
|
if (options.onEvidence)
|
|
107
117
|
emitProviderEvidence(options.onEvidence, { type: "session", provider: PI_PROVIDER, sessionId: session.sessionId, ...(session.sessionFile ? { sessionFile: session.sessionFile } : {}) }, reportEvidenceDiagnostic);
|
|
108
118
|
session.setSteeringMode("one-at-a-time");
|
|
@@ -109,7 +109,7 @@ export const piRegistration = {
|
|
|
109
109
|
return loader ? { ...terms, loader } : terms;
|
|
110
110
|
},
|
|
111
111
|
nativePolicyOwnership: () => null,
|
|
112
|
-
startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onIngress }) => {
|
|
112
|
+
startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onSessionAdmission, onIngress }) => {
|
|
113
113
|
if (terms.provider !== PI_PROVIDER) {
|
|
114
114
|
throw new Error(`startLive expected ${PI_PROVIDER}`);
|
|
115
115
|
}
|
|
@@ -125,6 +125,7 @@ export const piRegistration = {
|
|
|
125
125
|
appendSystemPromptOverride: piTerms.appendSystemPromptOverride,
|
|
126
126
|
onSyncCheckpoint,
|
|
127
127
|
onEvidence: piTerms.onEvidence,
|
|
128
|
+
onSessionAdmission,
|
|
128
129
|
onIngress,
|
|
129
130
|
initialTurn: piTerms.initialTurn,
|
|
130
131
|
loader: piTerms.loader,
|
|
@@ -137,6 +138,7 @@ export const piRegistration = {
|
|
|
137
138
|
appendSystemPromptOverride: piTerms.appendSystemPromptOverride,
|
|
138
139
|
onSyncCheckpoint,
|
|
139
140
|
onEvidence: piTerms.onEvidence,
|
|
141
|
+
onSessionAdmission,
|
|
140
142
|
onIngress,
|
|
141
143
|
initialTurn: piTerms.initialTurn,
|
|
142
144
|
loader: piTerms.loader,
|
package/build/cli/completion.js
CHANGED
|
@@ -2,6 +2,7 @@ import { assertSettingsKnobUsable } from "../config/settings/disease.js";
|
|
|
2
2
|
import { loadKeiyakuSettings } from "../config/settings/loader.js";
|
|
3
3
|
import { loadAkumaCatalog } from "../config/akuma-loader.js";
|
|
4
4
|
import { listContractIds } from "../core/ledger.js";
|
|
5
|
+
import { TASK_FILTER_COMPLETION } from "../core/task/query.js";
|
|
5
6
|
import { allCommandMetadata, getCommandMetadata } from "./commands/metadata.js";
|
|
6
7
|
function topLevelCommands() {
|
|
7
8
|
return allCommandMetadata()
|
|
@@ -46,6 +47,9 @@ export async function renderCompletionCandidates(input) {
|
|
|
46
47
|
if (input.previous === "task") {
|
|
47
48
|
return filterPrefix(subcommandsFor("task"), word).join("\n");
|
|
48
49
|
}
|
|
50
|
+
if (input.previous === "--filter") {
|
|
51
|
+
return filterPrefix([TASK_FILTER_COMPLETION], word).join("\n");
|
|
52
|
+
}
|
|
49
53
|
return filterPrefix(topLevelCommands(), word).join("\n");
|
|
50
54
|
}
|
|
51
55
|
export function renderCompletionScript(shell = "bash") {
|
|
@@ -54,7 +58,7 @@ export function renderCompletionScript(shell = "bash") {
|
|
|
54
58
|
"#compdef keiyaku",
|
|
55
59
|
"_keiyaku() {",
|
|
56
60
|
" local -a candidates",
|
|
57
|
-
" candidates=(${(f)\"$(keiyaku completion --complete --word
|
|
61
|
+
" candidates=(${(f)\"$(keiyaku completion --complete --word=\"${words[CURRENT]}\" --previous=\"${words[CURRENT-1]}\" 2>/dev/null)\"})",
|
|
58
62
|
" compadd -- $candidates",
|
|
59
63
|
"}",
|
|
60
64
|
"_keiyaku \"$@\"",
|
|
@@ -65,7 +69,10 @@ export function renderCompletionScript(shell = "bash") {
|
|
|
65
69
|
" local cur prev",
|
|
66
70
|
" cur=\"${COMP_WORDS[COMP_CWORD]}\"",
|
|
67
71
|
" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"",
|
|
68
|
-
" COMPREPLY=(
|
|
72
|
+
" COMPREPLY=()",
|
|
73
|
+
" while IFS= read -r candidate; do",
|
|
74
|
+
" COMPREPLY+=(\"$candidate\")",
|
|
75
|
+
" done < <(keiyaku completion --complete --word=\"$cur\" --previous=\"$prev\" 2>/dev/null)",
|
|
69
76
|
"}",
|
|
70
77
|
"complete -F _keiyaku_completion keiyaku",
|
|
71
78
|
].join("\n");
|
package/build/cli/flags.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ACTOR_IDENTITY } from "../keiyaku.js";
|
|
2
|
+
import { TASK_FILTER_GRAMMAR } from "../core/task/query.js";
|
|
2
3
|
export const HELP_FLAGS = new Set(["--help", "-h"]);
|
|
3
4
|
export const FLAG_SPECS = {
|
|
4
5
|
cwd: {
|
|
@@ -178,8 +179,8 @@ export const FLAG_SPECS = {
|
|
|
178
179
|
},
|
|
179
180
|
filter: {
|
|
180
181
|
kind: "value",
|
|
181
|
-
placeholder:
|
|
182
|
-
description: "Filter structured task facts. Repeat for AND; comma
|
|
182
|
+
placeholder: TASK_FILTER_GRAMMAR,
|
|
183
|
+
description: "Filter structured task facts. Repeat --filter for AND; comma values or | terms are OR.",
|
|
183
184
|
},
|
|
184
185
|
sort: {
|
|
185
186
|
kind: "value",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asMessage, isFlowError, pickHintsFromError } from "../../flow-error.js";
|
|
1
|
+
import { asMessage, isFlowError, pickHintsFromError, } from "../../flow-error.js";
|
|
2
2
|
import { assertSettingsKnobUsable } from "../../config/settings/disease.js";
|
|
3
3
|
import { loadKeiyakuSettings } from "../../config/settings/loader.js";
|
|
4
4
|
import { loadAkumaCatalog } from "../../config/akuma-loader.js";
|
|
@@ -6,6 +6,20 @@ import { appendDebugLog } from "../../telemetry/debug-log.js";
|
|
|
6
6
|
import { ACTOR_IDENTITY, TOOL_NAMES, BUILTIN_AGENT_NAMES } from "../../keiyaku.js";
|
|
7
7
|
import { buildErrorResponse } from "./misc.js";
|
|
8
8
|
import { addressCarrierFromError } from "../../core/addressing.js";
|
|
9
|
+
import { shellQuote } from "./format.js";
|
|
10
|
+
const TASK_FILTER_GRAMMAR_EXAMPLE = "title~audit,scope";
|
|
11
|
+
function formatTaskFilterExpression(terms) {
|
|
12
|
+
return terms.map((term) => `${term.field}${term.operator}${term.values.join(",")}`).join("|");
|
|
13
|
+
}
|
|
14
|
+
function renderInvalidTaskFilterMessage(facts) {
|
|
15
|
+
const guidance = facts.recoveredTerms.length === 0
|
|
16
|
+
? `Grammar example: keiyaku task ls --filter ${shellQuote(TASK_FILTER_GRAMMAR_EXAMPLE)}`
|
|
17
|
+
: `Paste-ready equivalent for parsed terms: keiyaku task ls --filter ${shellQuote(formatTaskFilterExpression(facts.recoveredTerms))}`;
|
|
18
|
+
return `invalid task filter: ${facts.detail}. Valid fields: ${facts.fields.join(", ")}. `
|
|
19
|
+
+ `Valid operators: ${facts.operators.join(", ")}. `
|
|
20
|
+
+ `Repeat --filter for AND; use commas for same-field OR or | between complete filters. `
|
|
21
|
+
+ guidance;
|
|
22
|
+
}
|
|
9
23
|
function renderScopeViolation(evidence) {
|
|
10
24
|
if (evidence.outcome === "no-positive-match") {
|
|
11
25
|
return ` ${evidence.path}\n no positive pattern matched`;
|
|
@@ -35,6 +49,9 @@ export function renderScopeAuditMessage(scope, evidence) {
|
|
|
35
49
|
].join("\n");
|
|
36
50
|
}
|
|
37
51
|
function renderErrorMessage(error) {
|
|
52
|
+
if (isFlowError(error) && error.facts?.kind === "invalid_task_filter") {
|
|
53
|
+
return renderInvalidTaskFilterMessage(error.facts);
|
|
54
|
+
}
|
|
38
55
|
if (isFlowError(error) && error.facts?.kind === "scope_violation") {
|
|
39
56
|
return renderScopeAuditMessage(error.facts.scope, error.facts.violations);
|
|
40
57
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { getConfig } from "../../config/env.js";
|
|
2
2
|
import { SECTION_ICONS } from "./response-style.js";
|
|
3
|
-
|
|
3
|
+
// Command-copy output is POSIX shell syntax: close the quote, emit a literal
|
|
4
|
+
// apostrophe, then reopen the quote. Other shell dialects are not supported.
|
|
5
|
+
const POSIX_SINGLE_QUOTE_ESCAPE = "'\\''";
|
|
6
|
+
/** Quote a POSIX shell argument only when the token is not already a single safe word. */
|
|
4
7
|
export function shellQuote(value) {
|
|
5
8
|
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
|
|
6
9
|
return value;
|
|
7
|
-
return `'${value.replace(/'/g,
|
|
10
|
+
return `'${value.replace(/'/g, POSIX_SINGLE_QUOTE_ESCAPE)}'`;
|
|
8
11
|
}
|
|
9
12
|
export function formatAgeMs(ms) {
|
|
10
13
|
if (ms === undefined)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { FlowError, asMessage } from "../../flow-error.js";
|
|
5
|
+
import { createGit, wrapGitError } from "../../git/core.js";
|
|
6
|
+
import { readStructuredDiff } from "../../git/diff/structured.js";
|
|
7
|
+
import { rewriteRenewBranch } from "../renew.js";
|
|
8
|
+
async function resolveObject(cwd, revision, kind) {
|
|
9
|
+
try {
|
|
10
|
+
return (await createGit(cwd).raw(["rev-parse", "--verify", `${revision}^{${kind}}`])).trim();
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw wrapGitError(`rev-parse ${revision}^{${kind}}`, error, cwd);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function createAuditCandidate(input) {
|
|
17
|
+
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "keiyaku-audit-"));
|
|
18
|
+
const git = createGit(input.root);
|
|
19
|
+
try {
|
|
20
|
+
await git.raw(["clone", "--quiet", "--no-hardlinks", "--no-checkout", input.root, cwd]);
|
|
21
|
+
const candidateGit = createGit(cwd);
|
|
22
|
+
await candidateGit.raw(["config", "user.name", "Keiyaku Audit"]);
|
|
23
|
+
await candidateGit.raw(["config", "user.email", "audit@keiyaku.invalid"]);
|
|
24
|
+
await candidateGit.raw(["checkout", "--quiet", "-B", `keiyaku/${input.contractId}`, input.frozen.coordinates.delivery.head]);
|
|
25
|
+
if (input.frozen.base === input.frozen.coordinates.target.head)
|
|
26
|
+
return { cwd, rehearsal: { status: "fresh" }, candidate: { base: input.frozen.coordinates.target.head, head: input.frozen.coordinates.delivery.head, tree: input.frozen.coordinates.delivery.tree, rewritten: false } };
|
|
27
|
+
try {
|
|
28
|
+
await rewriteRenewBranch({ cwd, contractId: input.contractId, oldHead: input.frozen.coordinates.delivery.head, recoveryRef: "refs/keiyaku/audit/disposable", newBase: input.frozen.coordinates.target.head, oldBase: input.frozen.base });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof FlowError && error.code === "CLOSE_MERGE_CONFLICT" && error.facts?.kind === "renew_conflict")
|
|
32
|
+
return { cwd, rehearsal: { status: "conflict", paths: [...error.facts.conflictPaths], pathCapture: error.facts.conflictPathCapture.status === "captured" ? "captured" : "failed", ...(error.facts.conflictPathCapture.status === "failed" ? { diagnostic: error.facts.conflictPathCapture.diagnostic } : {}) }, candidate: null };
|
|
33
|
+
return { cwd, rehearsal: { status: "unknown", cause: { code: error instanceof FlowError ? error.code : "INTERNAL_STATE", message: asMessage(error) } }, candidate: null };
|
|
34
|
+
}
|
|
35
|
+
const [head, tree] = await Promise.all([resolveObject(cwd, "HEAD", "commit"), resolveObject(cwd, "HEAD", "tree")]);
|
|
36
|
+
return { cwd, rehearsal: { status: "clean", oldBase: input.frozen.base, newBase: input.frozen.coordinates.target.head, oldHead: input.frozen.coordinates.delivery.head }, candidate: { base: input.frozen.coordinates.target.head, head, tree, rewritten: true } };
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return { cwd, rehearsal: { status: "unknown", cause: { code: error instanceof FlowError ? error.code : "INTERNAL_STATE", message: asMessage(error) } }, candidate: null };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function disposeAuditCandidate(candidate) { await fs.rm(candidate.cwd, { recursive: true, force: true }); }
|
|
43
|
+
export async function readAuditCandidateDiff(candidateResult) {
|
|
44
|
+
const candidate = candidateResult.candidate;
|
|
45
|
+
if (!candidate)
|
|
46
|
+
return null;
|
|
47
|
+
return readStructuredDiff(candidateResult.cwd, candidate.base, candidate.head, { excludePaths: [".keiyaku/KEIYAKU.md"] });
|
|
48
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { FlowError, asMessage } from "../../flow-error.js";
|
|
5
|
+
import { createGit, wrapGitError } from "../../git/core.js";
|
|
6
|
+
import { readLedger } from "../ledger.js";
|
|
7
|
+
import { hasStrandableProjectionGeneration } from "../projection/index.js";
|
|
8
|
+
import { readRenewRecoverySession } from "../renew-session.js";
|
|
9
|
+
import { effectiveScope, findOpenArc, getFence, getMaxArc, receiptsAfterFence } from "../seal.js";
|
|
10
|
+
import { queueMembership, readQueueReadModel } from "../settlement/queue.js";
|
|
11
|
+
import { markdownSourceSections } from "../scope.js";
|
|
12
|
+
import { normalizeSectionTitle } from "../markdown/titles.js";
|
|
13
|
+
import { commissionTarget, latestBase } from "../status/drift.js";
|
|
14
|
+
import { deriveContractState, isTerminalState } from "../status/lifecycle.js";
|
|
15
|
+
import { resolveContractDeliveryCwd } from "../worktree-path.js";
|
|
16
|
+
import { KEIYAKU_FILE } from "../../keiyaku.js";
|
|
17
|
+
async function resolveObject(cwd, revision, kind) {
|
|
18
|
+
const git = createGit(cwd);
|
|
19
|
+
try {
|
|
20
|
+
return (await git.raw(["rev-parse", "--verify", `${revision}^{${kind}}`])).trim();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw wrapGitError(`rev-parse ${revision}^{${kind}}`, error, cwd);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function appendedScopeTerms(entries) {
|
|
27
|
+
return entries.flatMap((entry) => entry.kind === "amend" && entry.data.scopeDelta ? entry.data.scopeDelta.add : []);
|
|
28
|
+
}
|
|
29
|
+
export function effectiveCriteria(entries) {
|
|
30
|
+
const bind = entries.find((entry) => entry.kind === "bind");
|
|
31
|
+
if (!bind)
|
|
32
|
+
return [];
|
|
33
|
+
const criteria = [...bind.data.checks];
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (entry.kind !== "amend")
|
|
36
|
+
continue;
|
|
37
|
+
for (const section of markdownSourceSections(entry.data.amendment)) {
|
|
38
|
+
if (section.level !== 2 || normalizeSectionTitle(section.title) !== "checks")
|
|
39
|
+
continue;
|
|
40
|
+
for (const line of entry.data.amendment.slice(section.contentStart, section.end).split(/\r?\n/)) {
|
|
41
|
+
const match = line.match(/^\s*-\s+(.+)$/);
|
|
42
|
+
if (match)
|
|
43
|
+
criteria.push(match[1].trim());
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return criteria;
|
|
48
|
+
}
|
|
49
|
+
async function pinLedgerCoordinates(input) {
|
|
50
|
+
const ledger = await readLedger(input.root, input.contractId);
|
|
51
|
+
if (!ledger)
|
|
52
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
|
|
53
|
+
const bind = ledger.entries.find((entry) => entry.kind === "bind");
|
|
54
|
+
const target = commissionTarget(ledger.entries);
|
|
55
|
+
const base = latestBase(ledger.entries);
|
|
56
|
+
if (!bind || !target || !base)
|
|
57
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} is missing pinned audit coordinates`);
|
|
58
|
+
const [deliveryHead, deliveryTree, targetHead, targetTree] = await Promise.all([
|
|
59
|
+
resolveObject(input.root, input.deliveryRef, "commit"), resolveObject(input.root, input.deliveryRef, "tree"),
|
|
60
|
+
resolveObject(input.root, target.ref, "commit"), resolveObject(input.root, target.ref, "tree"),
|
|
61
|
+
]);
|
|
62
|
+
return {
|
|
63
|
+
criteria: effectiveCriteria(ledger.entries), scope: effectiveScope(ledger.entries), appendScope: appendedScopeTerms(ledger.entries), base, entries: ledger.entries,
|
|
64
|
+
coordinates: { ledger: { head: ledger.head }, delivery: { ref: input.deliveryRef, head: deliveryHead, tree: deliveryTree }, target: { ref: target.ref, head: targetHead, tree: targetTree } },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async function deliveryResidue(cwd) {
|
|
68
|
+
const fields = (await createGit(cwd).raw(["status", "--porcelain=v1", "-z", "--untracked-files=all"])).split("\0");
|
|
69
|
+
const staged = new Set();
|
|
70
|
+
const unstaged = new Set();
|
|
71
|
+
const untracked = new Set();
|
|
72
|
+
for (let index = 0; index < fields.length; index += 1) {
|
|
73
|
+
const field = fields[index];
|
|
74
|
+
if (!field)
|
|
75
|
+
continue;
|
|
76
|
+
const xy = field.slice(0, 2);
|
|
77
|
+
const filePath = field.slice(3);
|
|
78
|
+
if (filePath === KEIYAKU_FILE || filePath.startsWith(".keiyaku/recovery/")) {
|
|
79
|
+
if (xy[0] === "R" || xy[0] === "C")
|
|
80
|
+
index += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (xy === "??")
|
|
84
|
+
untracked.add(filePath);
|
|
85
|
+
else {
|
|
86
|
+
if (xy[0] !== " ")
|
|
87
|
+
staged.add(filePath);
|
|
88
|
+
if (xy[1] !== " ")
|
|
89
|
+
unstaged.add(filePath);
|
|
90
|
+
if (xy[0] === "R" || xy[0] === "C")
|
|
91
|
+
index += 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const tracked = new Set([...staged, ...unstaged]);
|
|
95
|
+
return { tracked: [...tracked], staged: [...staged], unstaged: [...unstaged], untracked: [...untracked] };
|
|
96
|
+
}
|
|
97
|
+
function residueKey(residue) {
|
|
98
|
+
return JSON.stringify({ tracked: [...residue.tracked].sort(), staged: [...residue.staged].sort(), unstaged: [...residue.unstaged].sort(), untracked: [...residue.untracked].sort() });
|
|
99
|
+
}
|
|
100
|
+
async function residueFingerprint(cwd, residue) {
|
|
101
|
+
const git = createGit(cwd);
|
|
102
|
+
const digest = createHash("sha256").update(residueKey(residue));
|
|
103
|
+
const tracked = [...residue.tracked].sort();
|
|
104
|
+
if (tracked.length > 0)
|
|
105
|
+
digest.update(await git.raw(["diff", "--no-color", "--no-ext-diff", "--binary", "HEAD", "--", ...tracked]));
|
|
106
|
+
for (const pathname of [...residue.untracked].sort()) {
|
|
107
|
+
const absolute = path.join(cwd, pathname);
|
|
108
|
+
const stat = await fs.lstat(absolute);
|
|
109
|
+
digest.update(pathname).update("\0").update(String(stat.mode)).update("\0");
|
|
110
|
+
if (stat.isSymbolicLink())
|
|
111
|
+
digest.update(await fs.readlink(absolute));
|
|
112
|
+
else
|
|
113
|
+
digest.update(await fs.readFile(absolute));
|
|
114
|
+
digest.update("\0");
|
|
115
|
+
}
|
|
116
|
+
return digest.digest("hex");
|
|
117
|
+
}
|
|
118
|
+
async function deliveryCoordinate(cwd) {
|
|
119
|
+
const [head, tree, residue] = await Promise.all([resolveObject(cwd, "HEAD", "commit"), resolveObject(cwd, "HEAD", "tree"), deliveryResidue(cwd)]);
|
|
120
|
+
return { head, tree, residue, residueFingerprint: await residueFingerprint(cwd, residue) };
|
|
121
|
+
}
|
|
122
|
+
async function readReadiness(input) {
|
|
123
|
+
const bind = input.frozen.entries.find((entry) => entry.kind === "bind");
|
|
124
|
+
const lifecycle = deriveContractState(input.frozen.entries);
|
|
125
|
+
const [residue, looseCommits, session, queue, liveProjection, dependencies] = await Promise.all([
|
|
126
|
+
deliveryResidue(input.deliveryCwd), receiptsAfterFence(input.root, input.frozen.entries, input.frozen.coordinates.delivery.head), readRenewRecoverySession(input.root, input.contractId), readQueueReadModel(input.root),
|
|
127
|
+
hasStrandableProjectionGeneration({ cwd: input.root, commissionId: input.contractId, deliveryWorktree: input.deliveryCwd, nowMs: Date.now() }),
|
|
128
|
+
Promise.all((bind.data.after ?? []).map(async (id) => { const ledger = await readLedger(input.root, id); const state = ledger ? deriveContractState(ledger.entries) : { state: "missing" }; return { id, satisfied: ledger !== null && isTerminalState(state), state: state.state }; })),
|
|
129
|
+
]);
|
|
130
|
+
const membership = queueMembership(queue, input.contractId);
|
|
131
|
+
return { deliveryResidue: residue, arc: { open: findOpenArc(input.frozen.entries)?.data.arc ?? (getMaxArc(input.frozen.entries) === 0 ? 0 : null), max: getMaxArc(input.frozen.entries), fence: getFence(input.frozen.entries), looseCommits }, renewSession: { open: session !== null }, rewriteGuard: { liveProjection }, dependencies, petition: { state: lifecycle.state, queue: membership ? { position: membership.position, length: membership.queueLength, head: membership.queueHead, diagnostics: membership.diagnostics.map((item) => item.kind) } : null } };
|
|
132
|
+
}
|
|
133
|
+
export async function pinAuditSnapshot(input) {
|
|
134
|
+
const contractId = input.address.binding.commissionId;
|
|
135
|
+
const root = input.address.repo.stableRoot;
|
|
136
|
+
const deliveryRef = `refs/heads/keiyaku/${contractId}`;
|
|
137
|
+
return pinLedgerCoordinates({ root, contractId, deliveryRef });
|
|
138
|
+
}
|
|
139
|
+
export function initialCoordinateRace(input) {
|
|
140
|
+
return { which: "delivery-worktree", pinned: input.frozen.coordinates.delivery, observed: input.observed };
|
|
141
|
+
}
|
|
142
|
+
export async function pinAuditCoordinates(input) {
|
|
143
|
+
const contractId = input.address.binding.commissionId;
|
|
144
|
+
const root = input.address.repo.stableRoot;
|
|
145
|
+
const frozen = await pinAuditSnapshot({ address: input.address });
|
|
146
|
+
const deliveryCwd = await resolveContractDeliveryCwd(input.cwd, contractId);
|
|
147
|
+
const coordinate = await deliveryCoordinate(deliveryCwd);
|
|
148
|
+
if (coordinate.head !== frozen.coordinates.delivery.head || coordinate.tree !== frozen.coordinates.delivery.tree)
|
|
149
|
+
throw new Error(`registered delivery worktree ${coordinate.head}/${coordinate.tree} does not match pinned delivery ref ${frozen.coordinates.delivery.head}/${frozen.coordinates.delivery.tree}`);
|
|
150
|
+
const readiness = await readReadiness({ root, deliveryCwd, contractId, frozen });
|
|
151
|
+
return { ...frozen, deliveryCwd, deliveryCoordinate: coordinate, readiness };
|
|
152
|
+
}
|
|
153
|
+
export async function rereadCoordinateRace(input) {
|
|
154
|
+
const contractId = input.address.binding.commissionId;
|
|
155
|
+
const root = input.address.repo.stableRoot;
|
|
156
|
+
const deliveryRef = `refs/heads/keiyaku/${contractId}`;
|
|
157
|
+
let after;
|
|
158
|
+
try {
|
|
159
|
+
after = await pinLedgerCoordinates({ root, contractId, deliveryRef });
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
return { which: "ledger", pinned: input.frozen.coordinates.ledger, observed: asMessage(error) };
|
|
163
|
+
}
|
|
164
|
+
if (after.coordinates.ledger.head !== input.frozen.coordinates.ledger.head)
|
|
165
|
+
return { which: "ledger", pinned: input.frozen.coordinates.ledger, observed: after.coordinates.ledger };
|
|
166
|
+
if (after.coordinates.delivery.head !== input.frozen.coordinates.delivery.head || after.coordinates.delivery.tree !== input.frozen.coordinates.delivery.tree)
|
|
167
|
+
return { which: "delivery", pinned: input.frozen.coordinates.delivery, observed: after.coordinates.delivery };
|
|
168
|
+
if (after.coordinates.target.head !== input.frozen.coordinates.target.head || after.coordinates.target.tree !== input.frozen.coordinates.target.tree)
|
|
169
|
+
return { which: "target", pinned: input.frozen.coordinates.target, observed: after.coordinates.target };
|
|
170
|
+
try {
|
|
171
|
+
const deliveryCwd = await resolveContractDeliveryCwd(input.cwd, contractId);
|
|
172
|
+
const observed = await deliveryCoordinate(deliveryCwd);
|
|
173
|
+
if (path.resolve(deliveryCwd) !== path.resolve(input.frozen.deliveryCwd) || observed.head !== input.frozen.deliveryCoordinate.head || observed.tree !== input.frozen.deliveryCoordinate.tree || observed.residueFingerprint !== input.frozen.deliveryCoordinate.residueFingerprint)
|
|
174
|
+
return { which: "delivery-worktree", pinned: { cwd: input.frozen.deliveryCwd, ...input.frozen.deliveryCoordinate }, observed: { cwd: deliveryCwd, ...observed } };
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
return { which: "delivery-worktree", pinned: { cwd: input.frozen.deliveryCwd, ...input.frozen.deliveryCoordinate }, observed: asMessage(error) };
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|