@astrosheep/keiyaku 0.1.76 → 0.1.78
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 +88 -96
- package/build/.tsbuildinfo +1 -1
- package/build/config/apply-argument-descriptions.js +1 -1
- package/build/config/base-rules.js +14 -7
- package/build/config/dotenv.js +17 -11
- package/build/config/keiyaku-home.js +9 -0
- package/build/config/settings.js +41 -24
- package/build/config/term-presets/resolver.js +0 -3
- package/build/errno.js +3 -0
- package/build/flow-error.js +2 -0
- package/build/generated/version.js +1 -1
- package/build/git/diff/constants.js +1 -0
- package/build/git/diff/filter.js +3 -18
- package/build/git/diff/parsers.js +149 -61
- package/build/git/diff/preview.js +16 -2
- package/build/git/diff/read.js +32 -20
- package/build/git/snapshot.js +5 -24
- package/build/git/worktree.js +5 -4
- package/build/mcp/responses.js +3 -2
- package/build/mcp/server.js +61 -69
- package/build/protocol/draft-artifacts.js +2 -1
- package/build/protocol/file-guards.js +2 -1
- package/build/protocol/markdown/lex.js +52 -14
- package/build/protocol/markdown/normalization.js +3 -2
- package/build/protocol/markdown/parser.js +2 -2
- package/build/protocol/response-history.js +44 -5
- package/build/protocol/status-previews.js +20 -8
- package/build/protocol/summon-draft.js +3 -2
- package/build/protocol/summon-input.js +1 -0
- package/build/protocol/trace.js +1 -1
- package/build/tools/amend/index.js +11 -21
- package/build/tools/ask/index.js +11 -18
- package/build/tools/ask/persist.js +60 -37
- package/build/tools/ask/run.js +17 -7
- package/build/tools/create-handler.js +31 -0
- package/build/tools/drive/index.js +11 -24
- package/build/tools/drive/run.js +9 -5
- package/build/tools/petition/claim-gates.js +38 -9
- package/build/tools/petition/claim.js +20 -2
- package/build/tools/petition/forfeit.js +4 -1
- package/build/tools/petition/index.js +43 -58
- package/build/tools/petition/run.js +12 -0
- package/build/tools/round/head-guard.js +10 -0
- package/build/tools/round/incremental-diff.js +6 -2
- package/build/tools/round/report.js +24 -2
- package/build/tools/round/run.js +6 -0
- package/build/tools/round/worktree.js +6 -2
- package/build/tools/status/index.js +11 -24
- package/build/tools/status/read.js +6 -4
- package/build/tools/summon/index.js +17 -27
- package/build/tools/summon/run.js +21 -18
- package/package.json +6 -6
- package/build/git/diff/stat.js +0 -9
|
@@ -7,8 +7,10 @@ import { runAsk } from "./run.js";
|
|
|
7
7
|
import { assertKeiyakuProtocolFiles } from "../../protocol/file-guards.js";
|
|
8
8
|
import { persistResponseHistory } from "../../protocol/response-history.js";
|
|
9
9
|
import { getCurrentBranch } from "../../git/branches.js";
|
|
10
|
+
import { MISSING_HEAD_PATTERNS, NOT_GIT_REPOSITORY_PATTERNS, errorContainsAnyPattern } from "../../git/core.js";
|
|
10
11
|
import { getActiveKeiyakuGitState } from "../../git/keiyaku-state.js";
|
|
11
12
|
import { addFiles, commit, getLatestCommitHash } from "../../git/staging.js";
|
|
13
|
+
import { isErrnoException } from "../../errno.js";
|
|
12
14
|
const ASK_TRACE_COMMIT_PREFIX = "ask: ";
|
|
13
15
|
const ASK_TRACE_COMMIT_SUMMARY_MAX_CHARS = 50;
|
|
14
16
|
const ASK_TRACE_IGNORE_FLOW_ERRORS = new Set([
|
|
@@ -17,6 +19,24 @@ const ASK_TRACE_IGNORE_FLOW_ERRORS = new Set([
|
|
|
17
19
|
"MISSING_PROTOCOL_FILES",
|
|
18
20
|
"NOT_ACTIVE_KEIYAKU_BRANCH",
|
|
19
21
|
]);
|
|
22
|
+
function isMissingPathError(error) {
|
|
23
|
+
return isErrnoException(error) && error.code === "ENOENT";
|
|
24
|
+
}
|
|
25
|
+
function isBenignHistoryBranchLookupError(error) {
|
|
26
|
+
return ((isFlowError(error) && error.code === "DETACHED_HEAD") ||
|
|
27
|
+
isMissingPathError(error) ||
|
|
28
|
+
errorContainsAnyPattern(error, NOT_GIT_REPOSITORY_PATTERNS));
|
|
29
|
+
}
|
|
30
|
+
function isBenignHistoryCommitLookupError(error) {
|
|
31
|
+
return (isMissingPathError(error) ||
|
|
32
|
+
errorContainsAnyPattern(error, NOT_GIT_REPOSITORY_PATTERNS) ||
|
|
33
|
+
errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS));
|
|
34
|
+
}
|
|
35
|
+
function isBenignAskTracePersistenceError(error) {
|
|
36
|
+
return ((isFlowError(error) && ASK_TRACE_IGNORE_FLOW_ERRORS.has(error.code)) ||
|
|
37
|
+
isMissingPathError(error) ||
|
|
38
|
+
errorContainsAnyPattern(error, NOT_GIT_REPOSITORY_PATTERNS));
|
|
39
|
+
}
|
|
20
40
|
function buildAskTraceCommitMessage(request) {
|
|
21
41
|
const summary = request.replaceAll(/\s+/g, " ").trim().slice(0, ASK_TRACE_COMMIT_SUMMARY_MAX_CHARS);
|
|
22
42
|
return `${ASK_TRACE_COMMIT_PREFIX}${summary}`;
|
|
@@ -24,22 +44,28 @@ function buildAskTraceCommitMessage(request) {
|
|
|
24
44
|
export async function runAskAndPersist(input) {
|
|
25
45
|
const result = await runAsk(input);
|
|
26
46
|
let responsePath;
|
|
47
|
+
let historyBranch;
|
|
27
48
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
historyBranch = undefined;
|
|
34
|
-
}
|
|
35
|
-
let commit;
|
|
36
|
-
try {
|
|
37
|
-
commit = await getLatestCommitHash(input.cwd);
|
|
49
|
+
historyBranch = await getCurrentBranch(input.cwd);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (!isBenignHistoryBranchLookupError(error)) {
|
|
53
|
+
throw error;
|
|
38
54
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
historyBranch = undefined;
|
|
56
|
+
}
|
|
57
|
+
let commitHash;
|
|
58
|
+
try {
|
|
59
|
+
commitHash = await getLatestCommitHash(input.cwd);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (!isBenignHistoryCommitLookupError(error)) {
|
|
63
|
+
throw error;
|
|
42
64
|
}
|
|
65
|
+
// Ask can run outside git repos; history should still be persisted.
|
|
66
|
+
commitHash = undefined;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
43
69
|
responsePath = await persistResponseHistory({
|
|
44
70
|
cwd: input.cwd,
|
|
45
71
|
tool: "ask",
|
|
@@ -51,7 +77,7 @@ export async function runAskAndPersist(input) {
|
|
|
51
77
|
sessionId: result.sessionId,
|
|
52
78
|
subagentName: result.identity,
|
|
53
79
|
branch: historyBranch,
|
|
54
|
-
commit,
|
|
80
|
+
commit: commitHash,
|
|
55
81
|
});
|
|
56
82
|
}
|
|
57
83
|
catch (historyError) {
|
|
@@ -69,29 +95,6 @@ export async function runAskAndPersist(input) {
|
|
|
69
95
|
summary: result.summary,
|
|
70
96
|
historyPath: responsePath,
|
|
71
97
|
});
|
|
72
|
-
try {
|
|
73
|
-
const activeState = await getActiveKeiyakuGitState(input.cwd);
|
|
74
|
-
if (activeState) {
|
|
75
|
-
await assertKeiyakuProtocolFiles(input.cwd);
|
|
76
|
-
try {
|
|
77
|
-
await addFiles(input.cwd, TRACE_FILE);
|
|
78
|
-
await commit(input.cwd, buildAskTraceCommitMessage(input.request));
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
-
logWarn(`Failed to commit ask trace update: ${message}`, { cwd: input.cwd, section: "script" });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
if (isFlowError(error) && ASK_TRACE_IGNORE_FLOW_ERRORS.has(error.code)) {
|
|
88
|
-
// Non-active keiyaku or missing protocol files should not block ask.
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
-
logWarn(`Unexpected ask trace path error: ${message}`, { cwd: input.cwd, section: "script" });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
98
|
}
|
|
96
99
|
catch (askTraceError) {
|
|
97
100
|
const askTraceErrorMessage = askTraceError instanceof Error ? askTraceError.message : String(askTraceError);
|
|
@@ -99,6 +102,26 @@ export async function runAskAndPersist(input) {
|
|
|
99
102
|
cwd: input.cwd,
|
|
100
103
|
section: "script",
|
|
101
104
|
});
|
|
105
|
+
if (!isBenignAskTracePersistenceError(askTraceError)) {
|
|
106
|
+
logWarn(`Failed to append ask trace entry: ${askTraceErrorMessage}`, {
|
|
107
|
+
cwd: input.cwd,
|
|
108
|
+
section: "script",
|
|
109
|
+
});
|
|
110
|
+
throw askTraceError;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const activeState = await getActiveKeiyakuGitState(input.cwd);
|
|
115
|
+
if (activeState) {
|
|
116
|
+
await assertKeiyakuProtocolFiles(input.cwd);
|
|
117
|
+
await addFiles(input.cwd, TRACE_FILE);
|
|
118
|
+
await commit(input.cwd, buildAskTraceCommitMessage(input.request));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!isBenignAskTracePersistenceError(error)) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
102
125
|
}
|
|
103
126
|
return { result, responsePath };
|
|
104
127
|
}
|
package/build/tools/ask/run.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { KEIYAKU_FILE, TRACE_FILE } from "../../keiyaku.js";
|
|
4
|
+
import { isErrnoException } from "../../errno.js";
|
|
4
5
|
import { requireText } from "../../flow-error.js";
|
|
5
6
|
import { selectSubagent } from "../../agents/selector.js";
|
|
6
7
|
import { runSubagentWithResult } from "../../agents/round-runner.js";
|
|
8
|
+
import { NOT_GIT_REPOSITORY_PATTERNS, errorContainsAnyPattern } from "../../git/core.js";
|
|
7
9
|
import { getActiveKeiyakuGitState } from "../../git/keiyaku-state.js";
|
|
8
10
|
import { createWorkspaceSnapshot, getChangedFilesBetweenSnapshots } from "../../git/snapshot.js";
|
|
9
11
|
import { MAX_DIFF_LINES_PER_FILE } from "../../git/diff/constants.js";
|
|
@@ -14,13 +16,16 @@ import { readBaseRules } from "../../config/base-rules.js";
|
|
|
14
16
|
import { resolveIncrementalDiffMode } from "../../config/incremental-diff-mode.js";
|
|
15
17
|
import { resolveAskResponsePath } from "../../protocol/response-history.js";
|
|
16
18
|
import { INCREMENTAL_DIFF_MAX_CHARS } from "../../keiyaku.js";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
function isBenignAskGitStateError(error) {
|
|
20
|
+
return (isErrnoException(error) && error.code === "ENOENT") || errorContainsAnyPattern(error, NOT_GIT_REPOSITORY_PATTERNS);
|
|
21
|
+
}
|
|
22
|
+
export async function renderSnapshotDiff(cwd, beforeSha, afterSha, mode) {
|
|
23
|
+
const snapshotTarget = [beforeSha, afterSha];
|
|
24
|
+
if (mode === "stat") {
|
|
25
|
+
const statDiff = await readDiff(cwd, snapshotTarget, "stat", { maxChars: INCREMENTAL_DIFF_MAX_CHARS.stat });
|
|
21
26
|
return statDiff ?? "No incremental diff available.";
|
|
22
27
|
}
|
|
23
|
-
const patchDiff = await readDiff(cwd,
|
|
28
|
+
const patchDiff = await readDiff(cwd, snapshotTarget, "patch", {
|
|
24
29
|
maxChars: INCREMENTAL_DIFF_MAX_CHARS.targeted,
|
|
25
30
|
maxLinesPerFile: MAX_DIFF_LINES_PER_FILE,
|
|
26
31
|
});
|
|
@@ -35,7 +40,7 @@ async function readAskProtocolReferences(cwd) {
|
|
|
35
40
|
return { keiyaku, trace };
|
|
36
41
|
}
|
|
37
42
|
catch (error) {
|
|
38
|
-
if (error
|
|
43
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
39
44
|
return undefined;
|
|
40
45
|
}
|
|
41
46
|
throw error;
|
|
@@ -46,7 +51,12 @@ export async function runAsk(input) {
|
|
|
46
51
|
requireText("title", input.title);
|
|
47
52
|
const request = requireText("request", input.request);
|
|
48
53
|
const referenceRules = await readBaseRules(cwd);
|
|
49
|
-
const activeState = await getActiveKeiyakuGitState(cwd).catch(() =>
|
|
54
|
+
const activeState = await getActiveKeiyakuGitState(cwd).catch((error) => {
|
|
55
|
+
if (isBenignAskGitStateError(error)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
});
|
|
50
60
|
const protocolReferences = activeState ? await readAskProtocolReferences(cwd) : undefined;
|
|
51
61
|
const prompt = buildAskPrompt(request, input.context, referenceRules, protocolReferences);
|
|
52
62
|
const beforeSnapshot = await createWorkspaceSnapshot(cwd);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { appendDebugLog } from "../telemetry/debug-log.js";
|
|
2
|
+
import { handleToolError } from "../mcp/tool-errors.js";
|
|
3
|
+
const TOOL_LOG_SECTION = "script";
|
|
4
|
+
function resolveWorkingDirectory(cwd) {
|
|
5
|
+
return typeof cwd === "string" && cwd.length > 0 ? cwd : process.cwd();
|
|
6
|
+
}
|
|
7
|
+
export function createToolHandler(config) {
|
|
8
|
+
return async (input, extra) => {
|
|
9
|
+
const normalizedInput = { ...input, cwd: resolveWorkingDirectory(input.cwd) };
|
|
10
|
+
try {
|
|
11
|
+
appendDebugLog(`${config.logLabel} cwd=${normalizedInput.cwd}`, {
|
|
12
|
+
cwd: normalizedInput.cwd,
|
|
13
|
+
section: TOOL_LOG_SECTION,
|
|
14
|
+
});
|
|
15
|
+
const result = await config.run(normalizedInput, extra);
|
|
16
|
+
const doneLog = typeof config.doneLog === "function" ? config.doneLog(result) : config.doneLog;
|
|
17
|
+
appendDebugLog(doneLog, {
|
|
18
|
+
cwd: normalizedInput.cwd,
|
|
19
|
+
section: TOOL_LOG_SECTION,
|
|
20
|
+
});
|
|
21
|
+
return config.buildResponse(result, normalizedInput);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return handleToolError({
|
|
25
|
+
error,
|
|
26
|
+
cwd: normalizedInput.cwd,
|
|
27
|
+
logLabel: config.logLabel,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -1,35 +1,22 @@
|
|
|
1
|
-
import { appendDebugLog } from "../../telemetry/debug-log.js";
|
|
2
1
|
import { driveServant } from "./run.js";
|
|
3
2
|
import { buildDriveResponse, } from "../../mcp/responses.js";
|
|
4
|
-
import {
|
|
3
|
+
import { createToolHandler } from "../create-handler.js";
|
|
5
4
|
export function createDriveHandler() {
|
|
6
|
-
return
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
});
|
|
13
|
-
const outcome = await driveServant({
|
|
14
|
-
cwd: workingDir,
|
|
5
|
+
return createToolHandler({
|
|
6
|
+
logLabel: "tool drive",
|
|
7
|
+
doneLog: (outcome) => `tool drive success round=${outcome.round}`,
|
|
8
|
+
async run({ title, directive, context, tier, cwd }, extra) {
|
|
9
|
+
return driveServant({
|
|
10
|
+
cwd,
|
|
15
11
|
title,
|
|
16
12
|
directive,
|
|
17
13
|
context,
|
|
18
14
|
tier,
|
|
19
15
|
signal: extra.signal,
|
|
20
16
|
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
section: "script",
|
|
24
|
-
});
|
|
17
|
+
},
|
|
18
|
+
buildResponse(outcome, { tier }) {
|
|
25
19
|
return buildDriveResponse(outcome, { tier });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return handleToolError({
|
|
29
|
-
error: err,
|
|
30
|
-
cwd: workingDir,
|
|
31
|
-
logLabel: "tool drive",
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
};
|
|
20
|
+
},
|
|
21
|
+
});
|
|
35
22
|
}
|
package/build/tools/drive/run.js
CHANGED
|
@@ -23,6 +23,7 @@ import { appendDebugLog } from "../../telemetry/debug-log.js";
|
|
|
23
23
|
import { isDraftArtifactPath } from "../../protocol/draft-artifacts.js";
|
|
24
24
|
import { getUntrackedFiles } from "../../git/worktree.js";
|
|
25
25
|
import { renderIncrementalDiff } from "../round/incremental-diff.js";
|
|
26
|
+
import { assertHeadUnchanged } from "../round/head-guard.js";
|
|
26
27
|
const DRIVE_CANCELLATION_NOTE = "Round execution canceled.";
|
|
27
28
|
const DRIVE_CANCELLATION_REASON = "canceled";
|
|
28
29
|
function normalizeSectionTitle(title) {
|
|
@@ -67,9 +68,10 @@ function buildCancelledRevertCommitMessage(titleToken, round, attemptCommit) {
|
|
|
67
68
|
function buildCancelledTraceCommitMessage(titleToken, round) {
|
|
68
69
|
return buildRoundCommitMessage(titleToken, round, DRIVE_CANCELLATION_REASON);
|
|
69
70
|
}
|
|
70
|
-
async function stageAbortAttemptFiles(cwd) {
|
|
71
|
+
async function stageAbortAttemptFiles(cwd, preRunUntracked) {
|
|
71
72
|
await addUpdatedFiles(cwd);
|
|
72
|
-
const
|
|
73
|
+
const before = new Set(preRunUntracked);
|
|
74
|
+
const untracked = (await getUntrackedFiles(cwd)).filter((filePath) => !isDraftArtifactPath(filePath) && !before.has(filePath));
|
|
73
75
|
if (untracked.length > 0) {
|
|
74
76
|
await addFiles(cwd, untracked);
|
|
75
77
|
}
|
|
@@ -80,11 +82,11 @@ async function commitCancellationTraceOnly(cwd, title, round) {
|
|
|
80
82
|
await commitChanges(cwd, buildCancelledTraceCommitMessage(title, round));
|
|
81
83
|
await getLatestCommitHash(cwd);
|
|
82
84
|
}
|
|
83
|
-
async function commitAndRevertCancelledRoundAttempt(cwd, title, round) {
|
|
85
|
+
export async function commitAndRevertCancelledRoundAttempt(cwd, title, round, preRunUntracked) {
|
|
84
86
|
await appendRoundSystemNote(cwd, round, DRIVE_CANCELLATION_NOTE);
|
|
85
87
|
let attemptCommit;
|
|
86
88
|
try {
|
|
87
|
-
await stageAbortAttemptFiles(cwd);
|
|
89
|
+
await stageAbortAttemptFiles(cwd, preRunUntracked);
|
|
88
90
|
await commitChanges(cwd, buildCancelledAttemptCommitMessage(title, round));
|
|
89
91
|
attemptCommit = await getLatestCommitHash(cwd);
|
|
90
92
|
}
|
|
@@ -142,15 +144,17 @@ export async function driveServant(input) {
|
|
|
142
144
|
traceState = computeTraceState(traceContent);
|
|
143
145
|
}
|
|
144
146
|
const plan = buildIteratePlan(title, traceState, traceContent, keiyakuContent, goal, normalizedDirective, context, baseRules);
|
|
147
|
+
const headBeforeRound = await getLatestCommitHash(cwd);
|
|
145
148
|
const preRunUntracked = await getUntrackedFiles(cwd);
|
|
146
149
|
const { roundSummary, rawSummary, commit, identity, sessionId } = await runAndRecordRound(cwd, title, plan.targetRound, plan.prompt, {
|
|
147
150
|
signal,
|
|
148
151
|
tier,
|
|
149
152
|
failureMode: "round_specific",
|
|
150
153
|
preRunUntracked,
|
|
154
|
+
beforePersist: () => assertHeadUnchanged(cwd, headBeforeRound, "drive"),
|
|
151
155
|
beforeRoundWrite: () => appendDirective(cwd, plan.targetRound, plan.directiveReason),
|
|
152
156
|
onAbort: async () => {
|
|
153
|
-
await commitAndRevertCancelledRoundAttempt(cwd, title, plan.targetRound);
|
|
157
|
+
await commitAndRevertCancelledRoundAttempt(cwd, title, plan.targetRound, preRunUntracked);
|
|
154
158
|
},
|
|
155
159
|
});
|
|
156
160
|
const summary = renderToolRoundSummary(roundSummary);
|
|
@@ -33,16 +33,33 @@ const REVIEW_REJECTION_MESSAGE = "CLAIM rejected by independent reviewer.";
|
|
|
33
33
|
const REVIEW_REJECTION_AUDIT_PREFIX = "Independent reviewer rejected CLAIM.";
|
|
34
34
|
const REVIEW_RESPONSE_FALLBACK = "(no output)";
|
|
35
35
|
const REVIEW_SKIPPED_RESULT = { pass: true };
|
|
36
|
+
const REVIEW_ATTEMPT_SECTION_TITLE = "## Review Attempt";
|
|
36
37
|
const REVIEW_PROMPT_LINES = [
|
|
37
38
|
"You are an independent reviewer.",
|
|
38
|
-
"Evaluate whether the submitted work
|
|
39
|
-
"You have full access to the repository. Read
|
|
39
|
+
"Evaluate whether the submitted work fulfills the keiyaku.",
|
|
40
|
+
"You have full access to the repository. Read source files as needed to verify the changes in context.",
|
|
41
|
+
"Do not run tests or commands - a separate verification step handles that.",
|
|
42
|
+
"",
|
|
43
|
+
"## Workflow Context",
|
|
44
|
+
"- KEIYAKU.md and KEIYAKU_TRACE.md are workflow protocol files that will be removed before merge. Ignore them.",
|
|
45
|
+
"- Commit messages in this branch are intermediate; the branch will be squash-merged. Do not evaluate commit hygiene.",
|
|
40
46
|
"",
|
|
41
47
|
"## Output Rules",
|
|
42
|
-
"- If the work
|
|
43
|
-
"- If not:
|
|
44
|
-
"
|
|
48
|
+
"- If the work fulfills the keiyaku: begin with `PASS`, optionally followed by observations.",
|
|
49
|
+
"- If not: begin with `REJECTED`, then state what is wrong and why.",
|
|
50
|
+
" Be specific. Reference files and lines. Do not suggest fixes.",
|
|
45
51
|
];
|
|
52
|
+
function buildReviewAttemptBlock(reviewNumber) {
|
|
53
|
+
if (reviewNumber === 1) {
|
|
54
|
+
return [REVIEW_ATTEMPT_SECTION_TITLE, "This is review attempt 1."];
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
REVIEW_ATTEMPT_SECTION_TITLE,
|
|
58
|
+
`This is review attempt ${reviewNumber}.`,
|
|
59
|
+
"The prior review was rejected.",
|
|
60
|
+
"Focus on whether the previous concerns have been addressed.",
|
|
61
|
+
];
|
|
62
|
+
}
|
|
46
63
|
function buildCommandmentFailureMessage(violationTitle, score, threshold, definition, driveCommandName) {
|
|
47
64
|
return `${violationTitle} score too low (${score} < ${threshold})\n${definition}\nIdentify what needs to change to meet this standard. Then use \`${driveCommandName}\` to implement those improvements in the code. Score edits without new implementation evidence are treated as oath violation.`;
|
|
48
65
|
}
|
|
@@ -83,6 +100,8 @@ function buildReviewerPrompt(input) {
|
|
|
83
100
|
const lines = [
|
|
84
101
|
...REVIEW_PROMPT_LINES,
|
|
85
102
|
"",
|
|
103
|
+
...buildReviewAttemptBlock(input.reviewNumber),
|
|
104
|
+
"",
|
|
86
105
|
"## Keiyaku",
|
|
87
106
|
input.keiyakuContent,
|
|
88
107
|
"",
|
|
@@ -223,18 +242,26 @@ export function createAgentReviewGate() {
|
|
|
223
242
|
claimPlea: input.plea,
|
|
224
243
|
baseRef: baseBranch,
|
|
225
244
|
headRef,
|
|
245
|
+
reviewNumber,
|
|
226
246
|
diffStat: diffStat ?? "No diff.",
|
|
227
247
|
fullDiff: fullDiff ?? "No diff.",
|
|
228
248
|
priorFindings: priorReview?.result === "rejected" ? priorReview.findings : undefined,
|
|
229
249
|
});
|
|
230
250
|
const responsePathResolution = getConfig().review.resumeSession && priorReview?.historyPath
|
|
231
|
-
? await resolveAskResponsePath({
|
|
251
|
+
? await resolveAskResponsePath({
|
|
252
|
+
cwd,
|
|
253
|
+
responsePath: priorReview.historyPath,
|
|
254
|
+
allowedTools: ["review"],
|
|
255
|
+
onArtifactTypeMismatch: "return-null",
|
|
256
|
+
})
|
|
232
257
|
: undefined;
|
|
233
258
|
const subagentResult = await runSubagentExec(REVIEWER_ROLE, prompt, cwd, {
|
|
234
259
|
signal: input.signal,
|
|
235
260
|
responsePath: responsePathResolution?.sessionId,
|
|
236
261
|
});
|
|
237
|
-
const
|
|
262
|
+
const raw = subagentResult.finalMessage.trim();
|
|
263
|
+
const passed = raw.startsWith(REVIEW_PASS_TOKEN);
|
|
264
|
+
const observations = passed ? raw.slice(REVIEW_PASS_TOKEN.length).trim() || undefined : undefined;
|
|
238
265
|
const responsePath = await persistResponseHistory({
|
|
239
266
|
cwd,
|
|
240
267
|
tool: "review",
|
|
@@ -248,14 +275,15 @@ export function createAgentReviewGate() {
|
|
|
248
275
|
});
|
|
249
276
|
const review = {
|
|
250
277
|
review: reviewNumber,
|
|
251
|
-
result:
|
|
278
|
+
result: passed ? "passed" : "rejected",
|
|
252
279
|
historyPath: responsePath,
|
|
253
|
-
findings:
|
|
280
|
+
findings: passed ? observations : raw || REVIEW_RESPONSE_FALLBACK,
|
|
254
281
|
};
|
|
255
282
|
if (review.result === "passed") {
|
|
256
283
|
return {
|
|
257
284
|
pass: true,
|
|
258
285
|
review,
|
|
286
|
+
reviewHeadBeforeWrite: headRef,
|
|
259
287
|
};
|
|
260
288
|
}
|
|
261
289
|
return {
|
|
@@ -264,6 +292,7 @@ export function createAgentReviewGate() {
|
|
|
264
292
|
message: REVIEW_REJECTION_MESSAGE,
|
|
265
293
|
auditFailures: [`${REVIEW_REJECTION_AUDIT_PREFIX} See Review ${review.review}.`],
|
|
266
294
|
review,
|
|
295
|
+
reviewHeadBeforeWrite: headRef,
|
|
267
296
|
};
|
|
268
297
|
},
|
|
269
298
|
};
|
|
@@ -13,6 +13,7 @@ import { appendReview, computeTraceState, readTraceContent, renderReviewSection,
|
|
|
13
13
|
import { assertKeiyakuProtocolFiles } from "../../protocol/file-guards.js";
|
|
14
14
|
import { assertCleanWorkingTree } from "../round/worktree.js";
|
|
15
15
|
import { buildClaimGatePipeline, collectClaimScores, } from "./claim-gates.js";
|
|
16
|
+
import { assertHeadUnchanged } from "../round/head-guard.js";
|
|
16
17
|
function buildMergeMessage(title, plea, keiyakuContent, reportContent) {
|
|
17
18
|
return `keiyaku(${title}): done\n\nPlea: ${plea}\n\n---\n${keiyakuContent}\n---\n${reportContent}\n---\n`;
|
|
18
19
|
}
|
|
@@ -23,6 +24,9 @@ async function throwClaimGateFailure(failure, input, context, dependencies) {
|
|
|
23
24
|
const { cwd, titleToken } = context;
|
|
24
25
|
const scores = "dangerouslyBypassGates" in input && input.dangerouslyBypassGates ? undefined : collectClaimScores(input);
|
|
25
26
|
if (failure.review) {
|
|
27
|
+
if (failure.reviewHeadBeforeWrite) {
|
|
28
|
+
await assertHeadUnchanged(cwd, failure.reviewHeadBeforeWrite, "petition claim review");
|
|
29
|
+
}
|
|
26
30
|
await appendReview(cwd, failure.review);
|
|
27
31
|
}
|
|
28
32
|
if (failure.auditFailures && scores) {
|
|
@@ -38,6 +42,7 @@ async function runClaimGatePipelineOrThrow(input, context, dependencies) {
|
|
|
38
42
|
const driveCommandName = resolveTermPreset().tools.drive.name;
|
|
39
43
|
const { verdict, verificationCommands } = await dependencies.resolveClaimGateConfig(context.cwd);
|
|
40
44
|
let review;
|
|
45
|
+
let reviewHeadBeforeWrite;
|
|
41
46
|
for (const gate of buildClaimGatePipeline()) {
|
|
42
47
|
const result = await gate.evaluate({
|
|
43
48
|
cwd: context.cwd,
|
|
@@ -51,12 +56,13 @@ async function runClaimGatePipelineOrThrow(input, context, dependencies) {
|
|
|
51
56
|
});
|
|
52
57
|
if (result.review) {
|
|
53
58
|
review = result.review;
|
|
59
|
+
reviewHeadBeforeWrite = result.reviewHeadBeforeWrite;
|
|
54
60
|
}
|
|
55
61
|
if (!result.pass) {
|
|
56
62
|
await throwClaimGateFailure(result, input, context, dependencies);
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
|
-
return { scores, review };
|
|
65
|
+
return { scores, review, reviewHeadBeforeWrite };
|
|
60
66
|
}
|
|
61
67
|
export async function claimKeiyaku(input, context, dependencies) {
|
|
62
68
|
const { cwd, baseBranch, keiyakuBranch, titleToken } = context;
|
|
@@ -66,7 +72,7 @@ export async function claimKeiyaku(input, context, dependencies) {
|
|
|
66
72
|
const claimTitle = input.title;
|
|
67
73
|
const claimPlea = input.plea;
|
|
68
74
|
const claimResult = "dangerouslyBypassGates" in input && input.dangerouslyBypassGates
|
|
69
|
-
? { review: undefined, scores: undefined, bypassedByArchitect: true }
|
|
75
|
+
? { review: undefined, reviewHeadBeforeWrite: undefined, scores: undefined, bypassedByArchitect: true }
|
|
70
76
|
: {
|
|
71
77
|
...(await runClaimGatePipelineOrThrow(input, { cwd, titleToken }, {
|
|
72
78
|
...dependencies,
|
|
@@ -80,6 +86,17 @@ export async function claimKeiyaku(input, context, dependencies) {
|
|
|
80
86
|
logInfo(`[CLAIM] Collecting diff preview against base '${baseBranch}'`, { cwd, section: "script", progressOnly: true });
|
|
81
87
|
logInfo("[CLAIM] Reading keiyaku protocol files", { cwd, section: "script", progressOnly: true });
|
|
82
88
|
const keiyakuContent = await fs.readFile(path.join(cwd, KEIYAKU_FILE), "utf-8");
|
|
89
|
+
if (claimResult.review && claimResult.reviewHeadBeforeWrite) {
|
|
90
|
+
await assertHeadUnchanged(cwd, claimResult.reviewHeadBeforeWrite, "petition claim review");
|
|
91
|
+
await appendReview(cwd, claimResult.review);
|
|
92
|
+
try {
|
|
93
|
+
await dependencies.commitPassedReviewAuditTrail(cwd);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const warning = `[CLAIM] Failed to persist passed review audit trail: ${error instanceof Error ? error.message : String(error)}`;
|
|
97
|
+
logWarn(warning, { cwd, section: "script" });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
83
100
|
const reportTraceContent = claimResult.review ? `${traceContent}${renderReviewSection(claimResult.review)}` : traceContent;
|
|
84
101
|
const reportContent = `${reportTraceContent}${renderVerdictSection("ACCEPTED", {
|
|
85
102
|
scores: claimResult.scores,
|
|
@@ -114,6 +131,7 @@ export async function claimKeiyaku(input, context, dependencies) {
|
|
|
114
131
|
return {
|
|
115
132
|
status: "success",
|
|
116
133
|
result: "merged",
|
|
134
|
+
review: claimResult.review,
|
|
117
135
|
round,
|
|
118
136
|
currentBranch: baseBranch,
|
|
119
137
|
baseBranch,
|
|
@@ -16,6 +16,9 @@ function buildForfeitSafetyStashMessage(titleToken) {
|
|
|
16
16
|
function buildForfeitRecoveryHint(stashRef) {
|
|
17
17
|
return `${FORFEIT_SAFETY_STASH_HINT_PREFIX}: ${stashRef}`;
|
|
18
18
|
}
|
|
19
|
+
function buildArchiveTag(titleToken, now = Date.now()) {
|
|
20
|
+
return `${KEIYAKU_ARCHIVE_TAG_PREFIX}${titleToken}-${now}`;
|
|
21
|
+
}
|
|
19
22
|
async function restoreForfeitSafetySnapshot(cwd, keiyakuBranch, stashRef) {
|
|
20
23
|
const hints = [];
|
|
21
24
|
try {
|
|
@@ -38,7 +41,7 @@ async function restoreForfeitSafetySnapshot(cwd, keiyakuBranch, stashRef) {
|
|
|
38
41
|
}
|
|
39
42
|
export async function forfeitKeiyaku(context) {
|
|
40
43
|
const { cwd, baseBranch, keiyakuBranch, titleToken, draftSnapshot } = context;
|
|
41
|
-
const archiveTag =
|
|
44
|
+
const archiveTag = buildArchiveTag(titleToken);
|
|
42
45
|
let round = 0;
|
|
43
46
|
try {
|
|
44
47
|
const trace = await fs.readFile(path.join(cwd, TRACE_FILE), "utf-8");
|