@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.
Files changed (53) hide show
  1. package/README.md +88 -96
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/config/apply-argument-descriptions.js +1 -1
  4. package/build/config/base-rules.js +14 -7
  5. package/build/config/dotenv.js +17 -11
  6. package/build/config/keiyaku-home.js +9 -0
  7. package/build/config/settings.js +41 -24
  8. package/build/config/term-presets/resolver.js +0 -3
  9. package/build/errno.js +3 -0
  10. package/build/flow-error.js +2 -0
  11. package/build/generated/version.js +1 -1
  12. package/build/git/diff/constants.js +1 -0
  13. package/build/git/diff/filter.js +3 -18
  14. package/build/git/diff/parsers.js +149 -61
  15. package/build/git/diff/preview.js +16 -2
  16. package/build/git/diff/read.js +32 -20
  17. package/build/git/snapshot.js +5 -24
  18. package/build/git/worktree.js +5 -4
  19. package/build/mcp/responses.js +3 -2
  20. package/build/mcp/server.js +61 -69
  21. package/build/protocol/draft-artifacts.js +2 -1
  22. package/build/protocol/file-guards.js +2 -1
  23. package/build/protocol/markdown/lex.js +52 -14
  24. package/build/protocol/markdown/normalization.js +3 -2
  25. package/build/protocol/markdown/parser.js +2 -2
  26. package/build/protocol/response-history.js +44 -5
  27. package/build/protocol/status-previews.js +20 -8
  28. package/build/protocol/summon-draft.js +3 -2
  29. package/build/protocol/summon-input.js +1 -0
  30. package/build/protocol/trace.js +1 -1
  31. package/build/tools/amend/index.js +11 -21
  32. package/build/tools/ask/index.js +11 -18
  33. package/build/tools/ask/persist.js +60 -37
  34. package/build/tools/ask/run.js +17 -7
  35. package/build/tools/create-handler.js +31 -0
  36. package/build/tools/drive/index.js +11 -24
  37. package/build/tools/drive/run.js +9 -5
  38. package/build/tools/petition/claim-gates.js +38 -9
  39. package/build/tools/petition/claim.js +20 -2
  40. package/build/tools/petition/forfeit.js +4 -1
  41. package/build/tools/petition/index.js +43 -58
  42. package/build/tools/petition/run.js +12 -0
  43. package/build/tools/round/head-guard.js +10 -0
  44. package/build/tools/round/incremental-diff.js +6 -2
  45. package/build/tools/round/report.js +24 -2
  46. package/build/tools/round/run.js +6 -0
  47. package/build/tools/round/worktree.js +6 -2
  48. package/build/tools/status/index.js +11 -24
  49. package/build/tools/status/read.js +6 -4
  50. package/build/tools/summon/index.js +17 -27
  51. package/build/tools/summon/run.js +21 -18
  52. package/package.json +6 -6
  53. package/build/git/diff/stat.js +0 -9
@@ -1,9 +1,8 @@
1
- import { appendDebugLog } from "../../telemetry/debug-log.js";
2
1
  import { petitionKeiyaku } from "./run.js";
3
2
  import { buildPetitionClaimResponse, buildPetitionForfeitResponse, } from "../../mcp/responses.js";
4
3
  import { FlowError } from "../../flow-error.js";
5
4
  import { petitionToolSchema } from "../schema.js";
6
- import { handleToolError } from "../../mcp/tool-errors.js";
5
+ import { createToolHandler } from "../create-handler.js";
7
6
  function requireClaimField(value, name) {
8
7
  if (value === undefined) {
9
8
  throw new FlowError("EMPTY_PARAM", `parameter '${name}' is required when intent=CLAIM`);
@@ -11,23 +10,22 @@ function requireClaimField(value, name) {
11
10
  return value;
12
11
  }
13
12
  export function createPetitionHandler() {
14
- return async (args, extra) => {
15
- let intent = "UNKNOWN";
16
- let workingDir = process.cwd();
17
- try {
13
+ return createToolHandler({
14
+ logLabel: "tool close",
15
+ doneLog: (result) => `tool close ${result.intent} success branch=${result.outcome.currentBranch} base=${result.outcome.baseBranch}`,
16
+ async run(args, extra) {
18
17
  const input = petitionToolSchema.parse(args);
19
- intent = input.intent;
20
- workingDir = input.cwd || process.cwd();
18
+ const cwd = input.cwd ?? args.cwd;
21
19
  let closeInput;
22
- let claimInput;
23
20
  if (input.intent === "CLAIM") {
21
+ let claimInput;
24
22
  if (input.dangerouslyBypassGates) {
25
23
  claimInput = {
26
24
  intent: "CLAIM",
27
25
  title: requireClaimField(input.title, "title"),
28
26
  plea: requireClaimField(input.plea, "plea"),
29
27
  dangerouslyBypassGates: true,
30
- cwd: workingDir,
28
+ cwd,
31
29
  signal: extra.signal,
32
30
  };
33
31
  }
@@ -41,66 +39,53 @@ export function createPetitionHandler() {
41
39
  containment: requireClaimField(input.containment, "containment"),
42
40
  idiomatic: requireClaimField(input.idiomatic, "idiomatic"),
43
41
  oath: requireClaimField(input.oath, "oath"),
44
- cwd: workingDir,
42
+ cwd,
45
43
  signal: extra.signal,
46
44
  };
47
45
  }
48
46
  closeInput = claimInput;
49
- }
50
- else {
51
- if (input.dangerouslyBypassGates) {
52
- throw new FlowError("EMPTY_PARAM", "Parameter 'dangerouslyBypassGates' can only be used when intent=CLAIM.");
53
- }
54
- closeInput = {
55
- intent: "FORFEIT",
56
- cwd: workingDir,
57
- signal: extra.signal,
47
+ const outcome = await petitionKeiyaku(closeInput);
48
+ return {
49
+ intent: "CLAIM",
50
+ input: claimInput,
51
+ outcome,
58
52
  };
59
53
  }
60
- appendDebugLog(`tool close start intent=${intent} cwd=${workingDir}`, {
61
- cwd: workingDir,
62
- section: "script",
63
- });
64
- const outcome = await petitionKeiyaku(closeInput);
65
- if (input.intent === "CLAIM") {
66
- if (!claimInput) {
67
- throw new FlowError("INTERNAL_STATE", "Unexpected CLAIM input shape");
68
- }
69
- if (!("result" in outcome) || outcome.result !== "merged") {
54
+ if (input.dangerouslyBypassGates) {
55
+ throw new FlowError("EMPTY_PARAM", "Parameter 'dangerouslyBypassGates' can only be used when intent=CLAIM.");
56
+ }
57
+ closeInput = {
58
+ intent: "FORFEIT",
59
+ cwd,
60
+ signal: extra.signal,
61
+ };
62
+ return {
63
+ intent: "FORFEIT",
64
+ outcome: await petitionKeiyaku(closeInput),
65
+ };
66
+ },
67
+ buildResponse(result) {
68
+ if (result.intent === "CLAIM") {
69
+ if (!("result" in result.outcome) || result.outcome.result !== "merged") {
70
70
  throw new FlowError("INTERNAL_STATE", "Unexpected CLAIM outcome shape");
71
71
  }
72
- const finalOutcome = outcome;
73
- appendDebugLog(`tool close CLAIM success branch=${finalOutcome.currentBranch} base=${finalOutcome.baseBranch}`, {
74
- cwd: workingDir,
75
- section: "script",
76
- });
72
+ const finalOutcome = result.outcome;
77
73
  return buildPetitionClaimResponse(finalOutcome, {
78
- title: claimInput.title,
79
- plea: claimInput.plea,
80
- dangerouslyBypassGates: "dangerouslyBypassGates" in claimInput && claimInput.dangerouslyBypassGates === true,
81
- placement: "placement" in claimInput ? claimInput.placement : undefined,
82
- exactness: "exactness" in claimInput ? claimInput.exactness : undefined,
83
- containment: "containment" in claimInput ? claimInput.containment : undefined,
84
- idiomatic: "idiomatic" in claimInput ? claimInput.idiomatic : undefined,
85
- oath: "oath" in claimInput ? claimInput.oath : undefined,
74
+ title: result.input.title,
75
+ plea: result.input.plea,
76
+ dangerouslyBypassGates: "dangerouslyBypassGates" in result.input && result.input.dangerouslyBypassGates === true,
77
+ placement: "placement" in result.input ? result.input.placement : undefined,
78
+ exactness: "exactness" in result.input ? result.input.exactness : undefined,
79
+ containment: "containment" in result.input ? result.input.containment : undefined,
80
+ idiomatic: "idiomatic" in result.input ? result.input.idiomatic : undefined,
81
+ oath: "oath" in result.input ? result.input.oath : undefined,
86
82
  });
87
83
  }
88
- if (!("result" in outcome) || outcome.result !== "dropped") {
84
+ if (!("result" in result.outcome) || result.outcome.result !== "dropped") {
89
85
  throw new FlowError("INTERNAL_STATE", "Unexpected FORFEIT outcome shape");
90
86
  }
91
- const finalOutcome = outcome;
92
- appendDebugLog(`tool close FORFEIT success branch=${finalOutcome.currentBranch} base=${finalOutcome.baseBranch}`, {
93
- cwd: workingDir,
94
- section: "script",
95
- });
87
+ const finalOutcome = result.outcome;
96
88
  return buildPetitionForfeitResponse(finalOutcome, {});
97
- }
98
- catch (err) {
99
- return handleToolError({
100
- error: err,
101
- cwd: workingDir,
102
- logLabel: "tool close",
103
- });
104
- }
105
- };
89
+ },
90
+ });
106
91
  }
@@ -14,6 +14,7 @@ import { claimKeiyaku } from "./claim.js";
14
14
  import { forfeitKeiyaku } from "./forfeit.js";
15
15
  const VERDICT_DENIED_CODE = "VERDICT_DENIED";
16
16
  const DIMENSION_KEY_SET = new Set(CLAIM_DIMENSIONS);
17
+ const REVIEW_AUDIT_COMMIT_MESSAGE = "keiyaku: review audit trail";
17
18
  function clampThreshold(value, fallback) {
18
19
  if (typeof value !== "number" || !Number.isFinite(value))
19
20
  return fallback;
@@ -88,6 +89,16 @@ async function appendDeniedVerdictWithAuditCommit(cwd, titleToken, scores, check
88
89
  logWarn(warning, { cwd, section: "script" });
89
90
  }
90
91
  }
92
+ async function commitPassedReviewAuditTrail(cwd) {
93
+ try {
94
+ await addFiles(cwd, [TRACE_FILE]);
95
+ await commit(cwd, REVIEW_AUDIT_COMMIT_MESSAGE);
96
+ }
97
+ catch (error) {
98
+ const warning = `[CLAIM] Failed to commit passed review audit trail: ${error instanceof Error ? error.message : String(error)}`;
99
+ logWarn(warning, { cwd, section: "script" });
100
+ }
101
+ }
91
102
  async function loadCloseContext(cwd) {
92
103
  const isRepo = await isGitRepo(cwd);
93
104
  if (!isRepo) {
@@ -118,6 +129,7 @@ export async function petitionKeiyaku(input) {
118
129
  return claimKeiyaku(input, context, {
119
130
  resolveClaimGateConfig,
120
131
  appendDeniedVerdictWithAuditCommit,
132
+ commitPassedReviewAuditTrail,
121
133
  });
122
134
  }
123
135
  throw new FlowError("INVALID_CLOSE_INTENT", `unsupported close intent: ${intent}`);
@@ -0,0 +1,10 @@
1
+ import { FlowError } from "../../flow-error.js";
2
+ import { getLatestCommitHash } from "../../git/staging.js";
3
+ const CONCURRENT_MODIFICATION_CODE = "CONCURRENT_MODIFICATION";
4
+ export async function assertHeadUnchanged(cwd, expectedHead, operation) {
5
+ const currentHead = await getLatestCommitHash(cwd);
6
+ if (currentHead === expectedHead) {
7
+ return;
8
+ }
9
+ throw new FlowError(CONCURRENT_MODIFICATION_CODE, `concurrent ${operation} modification detected: repository HEAD changed from ${expectedHead} to ${currentHead} while the ${operation} was running`);
10
+ }
@@ -1,5 +1,6 @@
1
1
  import { MAX_DIFF_LINES_PER_FILE, TARGETED_DIFF_WARNING } from "../../git/diff/constants.js";
2
- import { capTargetedDiffText, filterDiffByCoordinates } from "../../git/diff/filter.js";
2
+ import { filterDiffByCoordinates } from "../../git/diff/filter.js";
3
+ import { truncateTextWithFooter } from "../../git/diff/parsers.js";
3
4
  import { readDiff } from "../../git/diff/read.js";
4
5
  export async function renderIncrementalDiff({ cwd, range, mode, maxChars, coordinates, }) {
5
6
  const statDiff = await readDiff(cwd, range, "stat", { maxChars: maxChars.stat });
@@ -24,5 +25,8 @@ export async function renderIncrementalDiff({ cwd, range, mode, maxChars, coordi
24
25
  if (!targetedPatch) {
25
26
  return `${statDiff}\n\n${TARGETED_DIFF_WARNING}`;
26
27
  }
27
- return `${statDiff}\n\n${capTargetedDiffText(targetedPatch, maxChars.targeted)}`;
28
+ const cappedTargetedPatch = targetedPatch.length <= maxChars.targeted
29
+ ? targetedPatch
30
+ : truncateTextWithFooter(targetedPatch, maxChars.targeted, targetedPatch.length - maxChars.targeted);
31
+ return `${statDiff}\n\n${cappedTargetedPatch}`;
28
32
  }
@@ -28,6 +28,18 @@ const SECTION_TITLE_TO_KEY = {
28
28
  "follow ups": "followUps",
29
29
  followups: "followUps",
30
30
  };
31
+ const SECTION_KEY_TO_TITLE = {
32
+ outcome: "Outcome",
33
+ changes: "Changes",
34
+ testResults: "Test Results",
35
+ aestheticsGap: "Aesthetics Gap",
36
+ blindspots: "Blindspots",
37
+ criteriaCheck: "Criteria Check",
38
+ rulesCheck: "Rules Check",
39
+ followUps: "Follow-ups",
40
+ diffCoordinates: "Diff Coordinates",
41
+ };
42
+ const MISSING_SECTION_WARNING_PREFIX = "> ⚠️ Missing section: ";
31
43
  function createEmptySections() {
32
44
  return {
33
45
  outcome: null,
@@ -77,6 +89,11 @@ function toKnownSection(sectionMarkdown) {
77
89
  function shouldRenderSection(key, policy) {
78
90
  return policy.allowedSectionKeys.includes(key);
79
91
  }
92
+ function renderMissingSectionWarnings(missingSections, policy) {
93
+ return missingSections
94
+ .filter((key) => shouldRenderSection(key, policy))
95
+ .map((key) => `${MISSING_SECTION_WARNING_PREFIX}${SECTION_KEY_TO_TITLE[key]}`);
96
+ }
80
97
  export function parseRoundSummary(raw) {
81
98
  const sections = parseMarkdownSections(raw);
82
99
  if (sections.length === 0) {
@@ -124,10 +141,15 @@ export function renderRoundSummary(roundSummary, policy = TOOL_DEFAULT_POLICY) {
124
141
  return [entry.section.markdown.trim()];
125
142
  })
126
143
  .filter((entry) => entry.length > 0);
127
- if (renderedEntries.length === 0) {
144
+ const missingSectionWarnings = renderMissingSectionWarnings(roundSummary.missingSections, policy);
145
+ const unknownEntries = roundSummary.entries
146
+ .flatMap((entry) => (entry.kind === "unknown" ? [entry.markdown.trim()] : []))
147
+ .filter((entry) => entry.length > 0);
148
+ const renderedOutput = [...renderedEntries, ...missingSectionWarnings, ...unknownEntries];
149
+ if (renderedOutput.length === 0) {
128
150
  return "";
129
151
  }
130
- return renderedEntries.join("\n\n").trim();
152
+ return renderedOutput.join("\n\n").trim();
131
153
  }
132
154
  export function renderToolRoundSummary(roundSummary) {
133
155
  return renderRoundSummary(roundSummary, TOOL_DEFAULT_POLICY);
@@ -127,6 +127,9 @@ export async function runAndRecordRound(cwd, titleToken, round, prompt, options)
127
127
  const summary = options.failureMode === "round_specific"
128
128
  ? `Round ${round} completed with subagent execution failure recorded in trace.`
129
129
  : "Round 1 completed with subagent execution failure recorded in trace.";
130
+ if (options.beforePersist) {
131
+ await options.beforePersist();
132
+ }
130
133
  if (options.beforeRoundWrite) {
131
134
  await options.beforeRoundWrite();
132
135
  }
@@ -137,6 +140,9 @@ export async function runAndRecordRound(cwd, titleToken, round, prompt, options)
137
140
  hints: buildSubagentFailureHints(err),
138
141
  });
139
142
  }
143
+ if (options.beforePersist) {
144
+ await options.beforePersist();
145
+ }
140
146
  if (options.beforeRoundWrite) {
141
147
  await options.beforeRoundWrite();
142
148
  }
@@ -1,6 +1,6 @@
1
1
  import { formatArtifactDirectories, KEIYAKU_ARTIFACT_DIRS } from "../../keiyaku.js";
2
2
  import { FlowError } from "../../flow-error.js";
3
- import { getDirtyFiles, renderDirtyFileStatusLine } from "../../git/worktree.js";
3
+ import { DIRTY_FILE_CATEGORY, getDirtyFiles, renderDirtyFileStatusLine } from "../../git/worktree.js";
4
4
  import * as path from "path";
5
5
  const DIRTY_WORKTREE_LIST_LIMIT = 10;
6
6
  const KEIYAKU_ARTIFACT_DIR_PREFIX = ".keiyaku/";
@@ -39,8 +39,12 @@ export function filterDirtyFilesByAllowlist(cwd, dirtyFiles, ignorePatterns) {
39
39
  });
40
40
  return { dirtyFiles: filteredDirtyFiles, ignoredPaths };
41
41
  }
42
+ export function filterBlockingDirtyFiles(dirtyFiles) {
43
+ return dirtyFiles.filter((file) => file.category !== DIRTY_FILE_CATEGORY.untracked);
44
+ }
42
45
  export async function assertCleanWorkingTree(cwd, ignorePatterns) {
43
- const { dirtyFiles } = filterDirtyFilesByAllowlist(cwd, await getDirtyFiles(cwd), ignorePatterns);
46
+ const blockingDirtyFiles = filterBlockingDirtyFiles(await getDirtyFiles(cwd));
47
+ const { dirtyFiles } = filterDirtyFilesByAllowlist(cwd, blockingDirtyFiles, ignorePatterns);
44
48
  if (dirtyFiles.length > 0) {
45
49
  const hasDirtyKeiyakuArtifacts = dirtyFiles.some((file) => normalizePathForDirtyMatch(cwd, file.path).startsWith(KEIYAKU_ARTIFACT_DIR_PREFIX));
46
50
  const shown = dirtyFiles.slice(0, DIRTY_WORKTREE_LIST_LIMIT);
@@ -1,34 +1,21 @@
1
- import { appendDebugLog } from "../../telemetry/debug-log.js";
2
1
  import { resolveTermPreset } from "../../config/term-presets/resolver.js";
3
2
  import { readKeiyakuStatus } from "./read.js";
4
3
  import { buildStatusResponse } from "../../mcp/responses.js";
5
- import { handleToolError } from "../../mcp/tool-errors.js";
4
+ import { createToolHandler } from "../create-handler.js";
6
5
  export function createStatusHandler() {
7
- return async ({ cwd }, _extra) => {
8
- const workingDir = cwd || process.cwd();
9
- const preset = resolveTermPreset();
10
- try {
11
- appendDebugLog(`tool status start cwd=${workingDir}`, {
12
- cwd: workingDir,
13
- section: "script",
14
- });
15
- const outcome = await readKeiyakuStatus({ cwd: workingDir });
16
- appendDebugLog(`tool status success active=${outcome.active} round=${outcome.round}`, {
17
- cwd: workingDir,
18
- section: "script",
19
- });
6
+ return createToolHandler({
7
+ logLabel: "tool status",
8
+ doneLog: (outcome) => `tool status success active=${outcome.active} round=${outcome.round}`,
9
+ async run({ cwd }) {
10
+ return readKeiyakuStatus({ cwd });
11
+ },
12
+ buildResponse(outcome) {
13
+ const preset = resolveTermPreset();
20
14
  return buildStatusResponse(outcome, {
21
15
  start: preset.tools.start.name,
22
16
  drive: preset.tools.drive.name,
23
17
  close: preset.tools.close.name,
24
18
  });
25
- }
26
- catch (err) {
27
- return handleToolError({
28
- error: err,
29
- cwd: workingDir,
30
- logLabel: "tool status",
31
- });
32
- }
33
- };
19
+ },
20
+ });
34
21
  }
@@ -1,13 +1,14 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { formatArtifactDirectories, KEIYAKU_ARTIFACT_DIRS, KEIYAKU_BRANCH_PREFIX, KEIYAKU_DRAFT_DIR, KEIYAKU_FILE, TRACE_FILE, } from "../../keiyaku.js";
4
+ import { isErrnoException } from "../../errno.js";
4
5
  import { FlowError } from "../../flow-error.js";
5
6
  import { resolveTermPreset } from "../../config/term-presets/resolver.js";
6
7
  import { getCurrentBranch, isGitRepo, listLocalKeiyakuBranches } from "../../git/branches.js";
7
8
  import { getKeiyakuBase } from "../../git/keiyaku-state.js";
8
9
  import { getDirtyFiles, getUntrackedFiles, isPathTracked, renderDirtyFileStatusLine } from "../../git/worktree.js";
9
10
  import { computeTraceState } from "../../protocol/trace.js";
10
- import { filterDirtyFilesByAllowlist } from "../round/worktree.js";
11
+ import { filterBlockingDirtyFiles, filterDirtyFilesByAllowlist } from "../round/worktree.js";
11
12
  import { isDraftArtifactPath, readLatestDraftArtifact } from "../../protocol/draft-artifacts.js";
12
13
  import { extractDocumentPreviewBySections, extractLastRoundSummary, toStatusPreview } from "../../protocol/status-previews.js";
13
14
  const STATUS_BRANCH_CANDIDATE_LIMIT = 3;
@@ -17,7 +18,7 @@ async function readOptionalFile(cwd, fileName) {
17
18
  return await fs.readFile(path.join(cwd, fileName), "utf-8");
18
19
  }
19
20
  catch (error) {
20
- if (error?.code === "ENOENT") {
21
+ if (isErrnoException(error) && error.code === "ENOENT") {
21
22
  return null;
22
23
  }
23
24
  throw error;
@@ -29,7 +30,7 @@ async function isPresentFile(cwd, fileName) {
29
30
  return stats.isFile();
30
31
  }
31
32
  catch (error) {
32
- if (error?.code === "ENOENT") {
33
+ if (isErrnoException(error) && error.code === "ENOENT") {
33
34
  return false;
34
35
  }
35
36
  throw error;
@@ -170,7 +171,8 @@ export async function readKeiyakuStatus(input) {
170
171
  const [dirtyFiles, untrackedPaths] = await Promise.all([getDirtyFiles(cwd), getUntrackedFiles(cwd)]);
171
172
  const rawDirtyPaths = Array.from(new Set(dirtyFiles.map((file) => file.path)));
172
173
  const rawArtifactPaths = Array.from(new Set([...rawDirtyPaths, ...untrackedPaths]));
173
- const startBlockingDirtyFiles = filterDirtyFilesByAllowlist(cwd, dirtyFiles, []).dirtyFiles;
174
+ const blockingDirtyFiles = filterBlockingDirtyFiles(dirtyFiles);
175
+ const startBlockingDirtyFiles = filterDirtyFilesByAllowlist(cwd, blockingDirtyFiles, []).dirtyFiles;
174
176
  const roundBlockingDirtyFiles = filterDirtyFilesByAllowlist(cwd, startBlockingDirtyFiles, []).dirtyFiles;
175
177
  const dirtyPaths = Array.from(new Set(startBlockingDirtyFiles.map((file) => file.path)));
176
178
  const dirtyStatusLines = startBlockingDirtyFiles.map((file) => renderDirtyFileStatusLine(file));
@@ -1,23 +1,22 @@
1
- import { appendDebugLog } from "../../telemetry/debug-log.js";
2
1
  import { summonKeiyaku } from "./run.js";
3
2
  import { buildSummonSuccessResponse, } from "../../mcp/responses.js";
4
- import { handleToolError } from "../../mcp/tool-errors.js";
3
+ import { createToolHandler } from "../create-handler.js";
5
4
  function normalizeOptionalArg(value) {
6
5
  if (value === undefined)
7
6
  return undefined;
8
7
  return value.trim().length > 0 ? value : undefined;
9
8
  }
10
9
  export function createSummonHandler() {
11
- return async ({ from_draft, draft_only, title, goal, context, rules, criteria, cwd }, extra) => {
12
- const workingDir = cwd || process.cwd();
13
- const normalizedFromDraft = normalizeOptionalArg(from_draft);
14
- const parsedCriteria = normalizeOptionalArg(criteria);
15
- const parsedRules = normalizeOptionalArg(rules);
16
- try {
17
- appendDebugLog(`tool start cwd=${workingDir}`, { cwd: workingDir, section: "script" });
18
- const result = normalizedFromDraft
19
- ? await summonKeiyaku({
20
- cwd: workingDir,
10
+ return createToolHandler({
11
+ logLabel: "tool start",
12
+ doneLog: (result) => `tool start success branch=${result.currentBranch} base=${result.baseBranch} round=${result.round}`,
13
+ async run({ from_draft, draft_only, title, goal, context, rules, criteria, cwd }, extra) {
14
+ const normalizedFromDraft = normalizeOptionalArg(from_draft);
15
+ const parsedCriteria = normalizeOptionalArg(criteria);
16
+ const parsedRules = normalizeOptionalArg(rules);
17
+ return normalizedFromDraft
18
+ ? summonKeiyaku({
19
+ cwd,
21
20
  from_draft: normalizedFromDraft,
22
21
  draft_only,
23
22
  title: normalizeOptionalArg(title),
@@ -27,8 +26,8 @@ export function createSummonHandler() {
27
26
  criteria: parsedCriteria,
28
27
  signal: extra.signal,
29
28
  })
30
- : await summonKeiyaku({
31
- cwd: workingDir,
29
+ : summonKeiyaku({
30
+ cwd,
32
31
  draft_only,
33
32
  title: title ?? "",
34
33
  goal: goal ?? "",
@@ -37,18 +36,9 @@ export function createSummonHandler() {
37
36
  criteria: parsedCriteria ?? "",
38
37
  signal: extra.signal,
39
38
  });
40
- appendDebugLog(`tool start success branch=${result.currentBranch} base=${result.baseBranch} round=${result.round}`, {
41
- cwd: workingDir,
42
- section: "script",
43
- });
39
+ },
40
+ buildResponse(result) {
44
41
  return buildSummonSuccessResponse(result);
45
- }
46
- catch (err) {
47
- return handleToolError({
48
- error: err,
49
- cwd: workingDir,
50
- logLabel: "tool start",
51
- });
52
- }
53
- };
42
+ },
43
+ });
54
44
  }
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
3
  import { KEIYAKU_BRANCH_PREFIX, KEIYAKU_FILE, TRACE_FILE, } from "../../keiyaku.js";
4
+ import { isErrnoException } from "../../errno.js";
4
5
  import { logInfo, logWarn } from "../../telemetry/logger.js";
5
6
  import { FlowError } from "../../flow-error.js";
6
7
  import { assertValidBranchName, checkoutBranch, createAndCheckoutBranch, deleteBranch, getCurrentBranch, hasLocalBranch, isGitRepo, listLocalKeiyakuBranches } from "../../git/branches.js";
@@ -30,7 +31,7 @@ async function fileExists(filePath) {
30
31
  return true;
31
32
  }
32
33
  catch (error) {
33
- if (error?.code === "ENOENT") {
34
+ if (isErrnoException(error) && error.code === "ENOENT") {
34
35
  return false;
35
36
  }
36
37
  throw error;
@@ -101,10 +102,28 @@ async function initKeiyakuBranch(cwd, resolved, keiyakuContent) {
101
102
  }
102
103
  async function finalizeSummonKeiyaku(input, resolved, setup, baseRules) {
103
104
  const { cwd } = input;
104
- const { branchToken, baseBranch, commit, existingBranches, keiyakuBranch } = setup;
105
+ const { branchToken, baseBranch, existingBranches, keiyakuBranch } = setup;
105
106
  const identity = selectSubagent().displayName;
106
107
  const summary = SUMMON_READY_SUMMARY;
107
108
  const diff = SUMMON_READY_DIFF;
109
+ let commit = setup.commit;
110
+ let consumedFromDraft;
111
+ if (resolved.fromDraft && resolved.fromDraftPath) {
112
+ const trackedFromDraftPath = resolveGitRelativePath(cwd, resolved.fromDraftPath);
113
+ const shouldCommitDeletion = trackedFromDraftPath !== null && (await isPathTracked(cwd, trackedFromDraftPath));
114
+ await fs.unlink(resolved.fromDraftPath);
115
+ if (shouldCommitDeletion && trackedFromDraftPath) {
116
+ await addFiles(cwd, trackedFromDraftPath);
117
+ await commitChanges(cwd, `keiyaku(${branchToken}): ${SUMMON_CONSUME_FROM_DRAFT_COMMIT_SUFFIX}`);
118
+ commit = await getLatestCommitHash(cwd);
119
+ }
120
+ consumedFromDraft = resolved.fromDraft;
121
+ logInfo(`[keiyaku] Consumed from_draft and deleted source draft: ${resolved.fromDraft}`, {
122
+ cwd,
123
+ section: "script",
124
+ progressOnly: true,
125
+ });
126
+ }
108
127
  let responsePath;
109
128
  try {
110
129
  responsePath = await persistResponseHistory({
@@ -128,22 +147,6 @@ async function finalizeSummonKeiyaku(input, resolved, setup, baseRules) {
128
147
  section: "script",
129
148
  });
130
149
  }
131
- let consumedFromDraft;
132
- if (resolved.fromDraft && resolved.fromDraftPath) {
133
- const trackedFromDraftPath = resolveGitRelativePath(cwd, resolved.fromDraftPath);
134
- const shouldCommitDeletion = trackedFromDraftPath !== null && (await isPathTracked(cwd, trackedFromDraftPath));
135
- await fs.unlink(resolved.fromDraftPath);
136
- if (shouldCommitDeletion && trackedFromDraftPath) {
137
- await addFiles(cwd, trackedFromDraftPath);
138
- await commitChanges(cwd, `keiyaku(${branchToken}): ${SUMMON_CONSUME_FROM_DRAFT_COMMIT_SUFFIX}`);
139
- }
140
- consumedFromDraft = resolved.fromDraft;
141
- logInfo(`[keiyaku] Consumed from_draft and deleted source draft: ${resolved.fromDraft}`, {
142
- cwd,
143
- section: "script",
144
- progressOnly: true,
145
- });
146
- }
147
150
  return {
148
151
  status: "success",
149
152
  round: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "0.1.76",
3
+ "version": "0.1.78",
4
4
  "description": "MCP server for running iterative keiyaku workflows with Codex subagents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,14 +39,14 @@
39
39
  "release": "npm version patch && npm publish"
40
40
  },
41
41
  "dependencies": {
42
- "@modelcontextprotocol/sdk": "*",
42
+ "@modelcontextprotocol/sdk": "1.26.0",
43
43
  "@openai/codex-sdk": "^0.113.0",
44
44
  "simple-git": "^3.21.0",
45
- "zod": "*"
45
+ "zod": "4.3.6"
46
46
  },
47
47
  "devDependencies": {
48
- "@types/node": "*",
49
- "tsx": "*",
50
- "typescript": "*"
48
+ "@types/node": "25.2.2",
49
+ "tsx": "4.21.0",
50
+ "typescript": "5.9.3"
51
51
  }
52
52
  }
@@ -1,9 +0,0 @@
1
- import { truncateTextWithFooter } from "./parsers.js";
2
- export function renderStatDiffPreviewText(output, maxChars) {
3
- const trimmed = output.trim();
4
- if (!trimmed)
5
- return "No diff.";
6
- if (trimmed.length <= maxChars)
7
- return trimmed;
8
- return truncateTextWithFooter(trimmed, maxChars, trimmed.length - maxChars);
9
- }