@astrosheep/keiyaku 0.1.17 → 0.1.19
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/common/errors.js +7 -47
- package/build/config/term-presets.js +30 -27
- package/build/handlers/close.js +47 -16
- package/build/handlers/shared.js +1 -1
- package/build/index.js +3 -1
- package/build/types/tooling.js +50 -18
- package/build/utils/git-ops.js +30 -1
- package/build/utils/text-utils.js +13 -3
- package/build/workflow/contract.js +11 -2
- package/build/workflow/drive.js +3 -0
- package/build/workflow/present.js +38 -10
- package/build/workflow/response-builders.js +19 -5
- package/build/workflow/start.js +8 -1
- package/package.json +1 -1
package/build/common/errors.js
CHANGED
|
@@ -70,57 +70,17 @@ function hintForFlowCode(code, message) {
|
|
|
70
70
|
return `\`${preset.tools.start.name}\` requires a clean slate. Remove existing \`KEIYAKU.md\` before starting.`;
|
|
71
71
|
case "DRAFT_FILE_EXISTS":
|
|
72
72
|
return `Existing \`KEIYAKU.draft.md\` detected. Use it via \`from_file\` or delete it before starting.`;
|
|
73
|
+
case "DETACHED_HEAD":
|
|
74
|
+
return "Repository is in detached HEAD state. Switch to a branch and retry.";
|
|
75
|
+
case "INVALID_CLOSE_PETITION":
|
|
76
|
+
return "Invalid close petition. Use `CLAIM` or `FORFEIT`.";
|
|
77
|
+
case "INTERNAL_STATE":
|
|
78
|
+
return message;
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
|
-
|
|
76
|
-
{ code: "NOT_GIT_REPO", patterns: ["is not a git repository"] },
|
|
77
|
-
{ code: "ACTIVE_KEIYAKU_EXISTS", patterns: ["active keiyaku already exists"] },
|
|
78
|
-
{ code: "EXISTING_KEIYAKU_BRANCH_FOUND", patterns: ["existing keiyaku branch found"] },
|
|
79
|
-
{ code: "EMPTY_PARAM", patterns: ["cannot be empty"] },
|
|
80
|
-
{ code: "DIRTY_WORKTREE", patterns: ["working tree has uncommitted changes"] },
|
|
81
|
-
{ code: "NOT_ACTIVE_KEIYAKU_BRANCH", patterns: ["current branch is not an active keiyaku branch"] },
|
|
82
|
-
{ code: "MISSING_KEIYAKU_BASE", patterns: ["is missing base metadata"] },
|
|
83
|
-
{ code: "MISSING_PROTOCOL_FILES", patterns: ["missing protocol files"] },
|
|
84
|
-
{ code: "DONE_MERGE_CONFLICT", patterns: ["DONE merge conflict"] },
|
|
85
|
-
{ code: "CLOSE_QUALITY_GATE_FAILED", patterns: ["God's Wrath: CLAIM denied"] },
|
|
86
|
-
{
|
|
87
|
-
code: "OATH_MISMATCH",
|
|
88
|
-
patterns: [
|
|
89
|
-
"requires the sacred oath to exactly equal",
|
|
90
|
-
"requires oath to exactly match configured value",
|
|
91
|
-
"requires oath to match configured value. If template contains",
|
|
92
|
-
"To declare DONE, you must solemnly swear the sacred oath.",
|
|
93
|
-
"To declare DONE, oath mismatch.",
|
|
94
|
-
"Oath mismatch.",
|
|
95
|
-
],
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
code: "SUBAGENT_DID_NOT_ADVANCE_ROUND",
|
|
99
|
-
patterns: ["subagent did not advance round", "did not append KEIYAKU_TRACE"],
|
|
100
|
-
},
|
|
101
|
-
{ code: "ROUND_SUBAGENT_FAILED", patterns: ["failed during subagent execution"] },
|
|
102
|
-
{ code: "INVALID_BRANCH_TITLE", patterns: ["cannot be converted to a valid branch name"] },
|
|
103
|
-
{ code: "UNKNOWN_SUBAGENT", patterns: ["Unknown subagent"] },
|
|
104
|
-
{ code: "FROM_FILE_NOT_FOUND", patterns: ["from_file path does not exist"] },
|
|
105
|
-
{ code: "INVALID_KEIYAKU_DRAFT", patterns: ["invalid keiyaku draft"] },
|
|
106
|
-
{ code: "KEIYAKU_FILE_EXISTS", patterns: ["pre-flight failed: KEIYAKU.md already exists"] },
|
|
107
|
-
{ code: "DRAFT_FILE_EXISTS", patterns: ["pre-flight failed: KEIYAKU.draft.md exists but from_file does not target it"] },
|
|
108
|
-
];
|
|
109
|
-
function inferFlowCodeFromMessage(message) {
|
|
110
|
-
for (const entry of MESSAGE_HINT_PATTERNS) {
|
|
111
|
-
if (entry.patterns.some((pattern) => message.includes(pattern))) {
|
|
112
|
-
return entry.code;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
export function pickHintFromError(err, message) {
|
|
81
|
+
export function pickHintFromError(err) {
|
|
118
82
|
if (isFlowError(err)) {
|
|
119
83
|
return hintForFlowCode(err.code, err.message);
|
|
120
84
|
}
|
|
121
|
-
const inferredCode = inferFlowCodeFromMessage(message);
|
|
122
|
-
if (inferredCode) {
|
|
123
|
-
return hintForFlowCode(inferredCode, message);
|
|
124
|
-
}
|
|
125
85
|
return "Review the error details, fix the issue, and retry.";
|
|
126
86
|
}
|
|
@@ -57,7 +57,7 @@ export const DEFAULT_PRESET = {
|
|
|
57
57
|
title: 'Sign Keiyaku',
|
|
58
58
|
description: 'Initialize a Keiyaku (Contract). Bind a Servant to a dedicated workspace (branch).\nYou are the Architect; they are the Instrument. Define the Goal and Scope clearly.\nThe contract isolates their existence until the objective is met.\nCall ONCE to seal the bond.\n\nFlow: ${start} → [${drive} x N] → ${close}',
|
|
59
59
|
args: {
|
|
60
|
-
from_file: "Optional markdown draft path. Loads `# title` + required sections from file (relative to cwd unless absolute).",
|
|
60
|
+
from_file: "Optional markdown draft path. Loads `# title` + required sections from file (relative to cwd unless absolute). Note: this file is auto-ignored by dirty-tree checks.",
|
|
61
61
|
title: 'Required unless provided via `from_file`. A concise codename for this hunt.',
|
|
62
62
|
goal: 'Required unless provided via `from_file`. The Kill Condition for this mission.',
|
|
63
63
|
directive: 'Optional First Step. Overrides `## Directive` from `from_file` when both are provided.',
|
|
@@ -99,17 +99,20 @@ export const DEFAULT_PRESET = {
|
|
|
99
99
|
'If there is a bug, **YOU** put it there.\n' +
|
|
100
100
|
'If it is messy, **YOU** left it there.\n\n' +
|
|
101
101
|
'Stand by your work. If uncertain, return to ${drive}. Premature claims are not forgiven.\n\n' +
|
|
102
|
+
'Input rules:\n' +
|
|
103
|
+
'- CLAIM: fill criteriaChecks, constraintsChecks, all five scores, and oath.\n' +
|
|
104
|
+
'- FORFEIT: only petition is required; the other fields can be left empty.\n\n' +
|
|
102
105
|
'Flow: ${start} → [${drive} x N] → ${close}',
|
|
103
106
|
args: {
|
|
104
107
|
petition: 'REQUIRED. CLAIM declares fulfillment; FORFEIT concedes failure.\nREQUIRES AN ACTIVE KEIYAKU (started via ${start}).\nIf any score wavers, do not claim—return to ${drive}.',
|
|
105
|
-
criteriaChecks: '
|
|
106
|
-
constraintsChecks: '
|
|
107
|
-
score_precise: '
|
|
108
|
-
score_minimal: '
|
|
109
|
-
score_isolated: '
|
|
110
|
-
score_idiomatic: '
|
|
111
|
-
score_cohesive: '
|
|
112
|
-
oath: 'Required for CLAIM. Your binding word. The Contract holds you to it.\nVerbatim: ${oath_text}',
|
|
108
|
+
criteriaChecks: 'Required for CLAIM. Evidence that each criterion is met. For FORFEIT, optional (can be left empty).',
|
|
109
|
+
constraintsChecks: 'Required for CLAIM. Evidence that each constraint stayed compliant. For FORFEIT, optional (can be left empty).',
|
|
110
|
+
score_precise: 'Required for CLAIM (0-10). Architectural placement. 10 = exact layer, exact boundary, zero misplacement. For FORFEIT, optional.',
|
|
111
|
+
score_minimal: 'Required for CLAIM (0-10). Economy of change. 10 = no avoidable lines, no speculative edits, no hidden bloat. For FORFEIT, optional.',
|
|
112
|
+
score_isolated: 'Required for CLAIM (0-10). Surgical containment. 10 = zero unrelated files, zero opportunistic cleanup, zero collateral. For FORFEIT, optional.',
|
|
113
|
+
score_idiomatic: 'Required for CLAIM (0-10). Native fluency. 10 = naming, structure, style indistinguishable from the codebase. For FORFEIT, optional.',
|
|
114
|
+
score_cohesive: 'Required for CLAIM (0-10). Single responsibility. 10 = each unit does one thing, boundaries intact. For FORFEIT, optional.',
|
|
115
|
+
oath: 'Required for CLAIM. Your binding word. The Contract holds you to it. For FORFEIT, optional.\nVerbatim: ${oath_text}',
|
|
113
116
|
cwd: "Optional repository path. Defaults to the server's current working directory.",
|
|
114
117
|
},
|
|
115
118
|
},
|
|
@@ -159,7 +162,7 @@ export const POCKET_PRESET = {
|
|
|
159
162
|
title: 'I Choose You!',
|
|
160
163
|
description: 'Initialize the Battle Phase. Send a Critter (Servant) into a dedicated Arena (branch).\nYou are the Trainer; they are the Fighter. Define the Victory Condition (Goal) clearly.\nThe battle continues in this Arena until the Badge is won.\nCall ONCE to start the encounter.\n\nFlow: ${start} → [${drive} x N] → ${close}',
|
|
161
164
|
args: {
|
|
162
|
-
from_file: 'Optional battle plan markdown path with title + sections.',
|
|
165
|
+
from_file: 'Optional battle plan markdown path with title + sections. This file is auto-ignored by dirty-tree checks.',
|
|
163
166
|
title: 'Required unless provided via `from_file`. Battle card title for this encounter.',
|
|
164
167
|
goal: 'Required unless provided via `from_file`. Victory condition for this battle.',
|
|
165
168
|
directive: 'Optional Turn 1 strategy (overrides draft directive).',
|
|
@@ -198,14 +201,14 @@ export const POCKET_PRESET = {
|
|
|
198
201
|
description: 'Attempt Capture. Present the weakened target for League Inspection.\nWARNING: The League (System) checks every Badge. If you attempt to `CLAIM` with fainted code, Disqualification (FORFEIT) is immediate.\nOnly throw the Ball when the target is 100% ready.\n\nFlow: ${start} → [${drive} x N] → ${close}',
|
|
199
202
|
args: {
|
|
200
203
|
petition: 'REQUIRED. CLAIM seeks Badge; FORFEIT forfeits the match.\nREQUIRES AN ACTIVE BATTLE (started via ${start}).\nIf stats are low, continue with ${drive}.',
|
|
201
|
-
criteriaChecks: '
|
|
202
|
-
constraintsChecks: '
|
|
203
|
-
score_precise: '
|
|
204
|
-
score_minimal: '
|
|
205
|
-
score_isolated: '
|
|
206
|
-
score_idiomatic: "
|
|
207
|
-
score_cohesive: '
|
|
208
|
-
oath: "Trainer's Honor Code. Required for CLAIM. Verbatim: ${oath_text}",
|
|
204
|
+
criteriaChecks: 'Required for CLAIM. Badge-by-badge proof. For FORFEIT, optional (can be left empty).',
|
|
205
|
+
constraintsChecks: 'Required for CLAIM. Rule-by-rule proof that constraints were respected. For FORFEIT, optional (can be left empty).',
|
|
206
|
+
score_precise: 'Required for CLAIM score (0-10). 10 means a Critical Hit: exact layer, exact target, zero meaningful misplacement. For FORFEIT, optional.',
|
|
207
|
+
score_minimal: 'Required for CLAIM score (0-10). 10 means Max Efficiency: no wasted PP, no extra motion. For FORFEIT, optional.',
|
|
208
|
+
score_isolated: 'Required for CLAIM score (0-10). 10 means 1v1 Focus: zero side-quests, zero unrelated damage. For FORFEIT, optional.',
|
|
209
|
+
score_idiomatic: "Required for CLAIM score (0-10). 10 means STAB (Same Type Attack Bonus): perfectly matches the codebase style. For FORFEIT, optional.",
|
|
210
|
+
score_cohesive: 'Required for CLAIM score (0-10). 10 means Team Synergy: action serves one purpose with clean boundaries. For FORFEIT, optional.',
|
|
211
|
+
oath: "Trainer's Honor Code. Required for CLAIM. For FORFEIT, optional. Verbatim: ${oath_text}",
|
|
209
212
|
cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
|
|
210
213
|
},
|
|
211
214
|
},
|
|
@@ -255,7 +258,7 @@ export const MISCHIEF_PRESET = {
|
|
|
255
258
|
title: 'Oi!',
|
|
256
259
|
description: "Initiate Grand Scheme. Summon a Minion to the Lair (branch).\nYou are the Mastermind; they are the Henchman. Define the Conquest (Goal) with dominance.\nThe minion is bound to this Lair until the plot is realized.\nCall ONCE to start the machine.\n\nFlow: ${start} → [${drive} x N] → ${close}",
|
|
257
260
|
args: {
|
|
258
|
-
from_file: 'Optional scheme markdown path with title + sectioned objectives.',
|
|
261
|
+
from_file: 'Optional scheme markdown path with title + sectioned objectives. This file is auto-ignored by dirty-tree checks.',
|
|
259
262
|
title: 'Required unless provided via `from_file`. Operation codename.',
|
|
260
263
|
goal: 'Required unless provided via `from_file`. Conquest objective and end-state.',
|
|
261
264
|
directive: 'Optional first-order command (overrides draft directive).',
|
|
@@ -294,14 +297,14 @@ export const MISCHIEF_PRESET = {
|
|
|
294
297
|
description: 'The Final Reveal. Present your Masterpiece to the Dark Council (System).\nWARNING: The Council destroys failure. If you `CLAIM` with weak plans, you will be eaten (FORFEIT).\nOnly throw the switch when the Doomsday Device is 100% operational.\n\nFlow: ${start} → [${drive} x N] → ${close}',
|
|
295
298
|
args: {
|
|
296
299
|
petition: 'REQUIRED. CLAIM demands rule; FORFEIT admits defeat.\nREQUIRES AN ACTIVE SCHEME (started via ${start}).\nIf the plan is weak, improve it with ${drive}.',
|
|
297
|
-
criteriaChecks: '
|
|
298
|
-
constraintsChecks: '
|
|
299
|
-
score_precise: '
|
|
300
|
-
score_minimal: '
|
|
301
|
-
score_isolated: '
|
|
302
|
-
score_idiomatic: '
|
|
303
|
-
score_cohesive: '
|
|
304
|
-
oath: "Mastermind's Vow. Required for CLAIM. Verbatim: ${oath_text}",
|
|
300
|
+
criteriaChecks: 'Required for CLAIM. Proof of conquest. For FORFEIT, optional (can be left empty).',
|
|
301
|
+
constraintsChecks: 'Required for CLAIM. Constraint-by-constraint compliance proof. For FORFEIT, optional (can be left empty).',
|
|
302
|
+
score_precise: 'Required for CLAIM (0-10). Architectural placement. 10 = exact layer, exact boundary, zero misplacement. For FORFEIT, optional.',
|
|
303
|
+
score_minimal: 'Required for CLAIM (0-10). Economy of change. 10 = no avoidable lines, no speculative edits, no hidden bloat. For FORFEIT, optional.',
|
|
304
|
+
score_isolated: 'Required for CLAIM (0-10). Surgical containment. 10 = zero unrelated files, zero opportunistic cleanup, zero collateral. For FORFEIT, optional.',
|
|
305
|
+
score_idiomatic: 'Required for CLAIM (0-10). Native fluency. 10 = naming, structure, style indistinguishable from the codebase. For FORFEIT, optional.',
|
|
306
|
+
score_cohesive: 'Required for CLAIM (0-10). Single responsibility. 10 = each unit does one thing, boundaries intact. For FORFEIT, optional.',
|
|
307
|
+
oath: "Mastermind's Vow. Required for CLAIM. For FORFEIT, optional. Verbatim: ${oath_text}",
|
|
305
308
|
cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
|
|
306
309
|
},
|
|
307
310
|
},
|
package/build/handlers/close.js
CHANGED
|
@@ -2,8 +2,15 @@ import { appendDebugLog } from "../utils/debug-log.js";
|
|
|
2
2
|
import { presentWork } from "../workflow/present.js";
|
|
3
3
|
import { buildCloseDoneResponse, buildCloseDropResponse, } from "../workflow/response-builders.js";
|
|
4
4
|
import { resolveTermPreset } from "../config/term-presets.js";
|
|
5
|
+
import { FlowError } from "../common/errors.js";
|
|
5
6
|
import { closeToolSchema } from "../types/tooling.js";
|
|
6
7
|
import { handleToolError } from "./shared.js";
|
|
8
|
+
function requireClaimField(value, name) {
|
|
9
|
+
if (value === undefined) {
|
|
10
|
+
throw new FlowError("EMPTY_PARAM", `parameter '${name}' is required when petition=CLAIM`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
7
14
|
export function createCloseHandler() {
|
|
8
15
|
return async (args, extra) => {
|
|
9
16
|
let petition = "UNKNOWN";
|
|
@@ -17,23 +24,47 @@ export function createCloseHandler() {
|
|
|
17
24
|
petition = input.petition;
|
|
18
25
|
workingDir = input.cwd || process.cwd();
|
|
19
26
|
if (input.petition === "CLAIM") {
|
|
20
|
-
criteriaCheckParts = input.criteriaChecks;
|
|
21
|
-
constraintsCheckParts = input.constraintsChecks;
|
|
27
|
+
criteriaCheckParts = input.criteriaChecks ?? [];
|
|
28
|
+
constraintsCheckParts = input.constraintsChecks ?? [];
|
|
22
29
|
oath = input.oath;
|
|
23
|
-
scoreLine = `Scores: precise=${input.score_precise} minimal=${input.score_minimal} isolated=${input.score_isolated} idiomatic=${input.score_idiomatic} cohesive=${input.score_cohesive}`;
|
|
30
|
+
scoreLine = `Scores: precise=${input.score_precise ?? "MISSING"} minimal=${input.score_minimal ?? "MISSING"} isolated=${input.score_isolated ?? "MISSING"} idiomatic=${input.score_idiomatic ?? "MISSING"} cohesive=${input.score_cohesive ?? "MISSING"}`;
|
|
24
31
|
}
|
|
25
32
|
appendDebugLog(`tool close start petition=${petition} cwd=${workingDir} criteriaChecks=${criteriaCheckParts.length} constraintsChecks=${constraintsCheckParts.length}`, {
|
|
26
33
|
cwd: workingDir,
|
|
27
34
|
section: "script",
|
|
28
35
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
cwd: workingDir,
|
|
32
|
-
signal: extra.signal,
|
|
33
|
-
});
|
|
36
|
+
let closeInput;
|
|
37
|
+
let claimInput;
|
|
34
38
|
if (input.petition === "CLAIM") {
|
|
39
|
+
claimInput = {
|
|
40
|
+
petition: "CLAIM",
|
|
41
|
+
criteriaChecks: requireClaimField(input.criteriaChecks, "criteriaChecks"),
|
|
42
|
+
constraintsChecks: requireClaimField(input.constraintsChecks, "constraintsChecks"),
|
|
43
|
+
score_precise: requireClaimField(input.score_precise, "score_precise"),
|
|
44
|
+
score_minimal: requireClaimField(input.score_minimal, "score_minimal"),
|
|
45
|
+
score_isolated: requireClaimField(input.score_isolated, "score_isolated"),
|
|
46
|
+
score_idiomatic: requireClaimField(input.score_idiomatic, "score_idiomatic"),
|
|
47
|
+
score_cohesive: requireClaimField(input.score_cohesive, "score_cohesive"),
|
|
48
|
+
oath: requireClaimField(input.oath, "oath"),
|
|
49
|
+
cwd: workingDir,
|
|
50
|
+
signal: extra.signal,
|
|
51
|
+
};
|
|
52
|
+
closeInput = claimInput;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
closeInput = {
|
|
56
|
+
petition: "FORFEIT",
|
|
57
|
+
cwd: workingDir,
|
|
58
|
+
signal: extra.signal,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const outcome = await presentWork(closeInput);
|
|
62
|
+
if (input.petition === "CLAIM") {
|
|
63
|
+
if (!claimInput) {
|
|
64
|
+
throw new FlowError("INTERNAL_STATE", "Unexpected CLAIM input shape");
|
|
65
|
+
}
|
|
35
66
|
if (!("result" in outcome) || outcome.result !== "merged") {
|
|
36
|
-
throw new
|
|
67
|
+
throw new FlowError("INTERNAL_STATE", "Unexpected CLAIM outcome shape");
|
|
37
68
|
}
|
|
38
69
|
const finalOutcome = outcome;
|
|
39
70
|
appendDebugLog(`tool close CLAIM success branch=${finalOutcome.currentBranch} base=${finalOutcome.baseBranch}`, {
|
|
@@ -43,17 +74,17 @@ export function createCloseHandler() {
|
|
|
43
74
|
return buildCloseDoneResponse(finalOutcome, {
|
|
44
75
|
criteriaChecks: criteriaCheckParts,
|
|
45
76
|
constraintsChecks: constraintsCheckParts,
|
|
46
|
-
score_precise:
|
|
47
|
-
score_minimal:
|
|
48
|
-
score_isolated:
|
|
49
|
-
score_idiomatic:
|
|
50
|
-
score_cohesive:
|
|
51
|
-
oath:
|
|
77
|
+
score_precise: claimInput.score_precise,
|
|
78
|
+
score_minimal: claimInput.score_minimal,
|
|
79
|
+
score_isolated: claimInput.score_isolated,
|
|
80
|
+
score_idiomatic: claimInput.score_idiomatic,
|
|
81
|
+
score_cohesive: claimInput.score_cohesive,
|
|
82
|
+
oath: claimInput.oath,
|
|
52
83
|
cwd: workingDir,
|
|
53
84
|
});
|
|
54
85
|
}
|
|
55
86
|
if (!("result" in outcome) || outcome.result !== "dropped") {
|
|
56
|
-
throw new
|
|
87
|
+
throw new FlowError("INTERNAL_STATE", "Unexpected FORFEIT outcome shape");
|
|
57
88
|
}
|
|
58
89
|
const finalOutcome = outcome;
|
|
59
90
|
appendDebugLog(`tool close FORFEIT success branch=${finalOutcome.currentBranch} base=${finalOutcome.baseBranch}`, {
|
package/build/handlers/shared.js
CHANGED
|
@@ -17,7 +17,7 @@ export function classifyToolError(error) {
|
|
|
17
17
|
export function handleToolError(input) {
|
|
18
18
|
const message = asMessage(input.error);
|
|
19
19
|
appendDebugLog(`${input.logLabel} error: ${message}`, { cwd: input.cwd, section: "script" });
|
|
20
|
-
const hint = pickHintFromError(input.error
|
|
20
|
+
const hint = pickHintFromError(input.error);
|
|
21
21
|
const { errorType, errorCode } = classifyToolError(input.error);
|
|
22
22
|
return buildToolErrorResponse({
|
|
23
23
|
tool: input.toolName,
|
package/build/index.js
CHANGED
|
@@ -41,7 +41,9 @@ function registerTools(server) {
|
|
|
41
41
|
const askPreset = renderedPreset.tools.ask;
|
|
42
42
|
const closePreset = renderedPreset.tools.close;
|
|
43
43
|
const helpPreset = renderedPreset.tools.help;
|
|
44
|
-
const dynamicCloseSchema = closeToolSchema
|
|
44
|
+
const dynamicCloseSchema = applyArgumentDescriptions(closeToolSchema, {
|
|
45
|
+
...closePreset.args,
|
|
46
|
+
});
|
|
45
47
|
const dynamicStartSchema = applyArgumentDescriptions(startToolSchema, {
|
|
46
48
|
...startPreset.args,
|
|
47
49
|
});
|
package/build/types/tooling.js
CHANGED
|
@@ -22,22 +22,54 @@ export const askToolSchema = z.object({
|
|
|
22
22
|
name: z.string().optional(),
|
|
23
23
|
cwd: z.string().optional(),
|
|
24
24
|
});
|
|
25
|
-
export const closeToolSchema = z.
|
|
26
|
-
z.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
25
|
+
export const closeToolSchema = z.object({
|
|
26
|
+
petition: z.enum(["CLAIM", "FORFEIT"]).describe("REQUIRED. CLAIM to finalize, FORFEIT to abandon."),
|
|
27
|
+
criteriaChecks: z
|
|
28
|
+
.array(z.string().trim().min(1))
|
|
29
|
+
.min(1)
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
|
|
32
|
+
constraintsChecks: z
|
|
33
|
+
.array(z.string().trim().min(1))
|
|
34
|
+
.min(1)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
|
|
37
|
+
score_precise: z
|
|
38
|
+
.number()
|
|
39
|
+
.min(0)
|
|
40
|
+
.max(10)
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
|
|
43
|
+
score_minimal: z
|
|
44
|
+
.number()
|
|
45
|
+
.min(0)
|
|
46
|
+
.max(10)
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
|
|
49
|
+
score_isolated: z
|
|
50
|
+
.number()
|
|
51
|
+
.min(0)
|
|
52
|
+
.max(10)
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
|
|
55
|
+
score_idiomatic: z
|
|
56
|
+
.number()
|
|
57
|
+
.min(0)
|
|
58
|
+
.max(10)
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
|
|
61
|
+
score_cohesive: z
|
|
62
|
+
.number()
|
|
63
|
+
.min(0)
|
|
64
|
+
.max(10)
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
|
|
67
|
+
oath: z
|
|
68
|
+
.string()
|
|
69
|
+
.trim()
|
|
70
|
+
.min(1)
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
|
|
73
|
+
cwd: z.string().optional(),
|
|
74
|
+
});
|
|
43
75
|
export const helpToolSchema = z.object({});
|
package/build/utils/git-ops.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { simpleGit } from "simple-git";
|
|
2
|
+
import { FlowError } from "../common/errors.js";
|
|
2
3
|
import { appendDebugBlock } from "./debug-log.js";
|
|
3
4
|
const DEFAULT_GIT_TIMEOUT_MS = 15 * 1000;
|
|
4
5
|
function readPositiveIntEnv(name, fallback) {
|
|
@@ -80,7 +81,7 @@ export async function getCurrentBranch(cwd) {
|
|
|
80
81
|
throw wrapGitError("status", err, cwd);
|
|
81
82
|
}
|
|
82
83
|
if (!status.current) {
|
|
83
|
-
throw new
|
|
84
|
+
throw new FlowError("DETACHED_HEAD", "Detached HEAD or no branch found");
|
|
84
85
|
}
|
|
85
86
|
return status.current;
|
|
86
87
|
}
|
|
@@ -234,6 +235,34 @@ export async function getDirtyPaths(cwd) {
|
|
|
234
235
|
const files = await getDirtyFiles(cwd);
|
|
235
236
|
return Array.from(new Set(files.map(f => f.path)));
|
|
236
237
|
}
|
|
238
|
+
export async function isPathTracked(cwd, filePath) {
|
|
239
|
+
const git = createGit(cwd);
|
|
240
|
+
let output;
|
|
241
|
+
try {
|
|
242
|
+
output = await git.raw(["ls-files", "--", filePath]);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
throw wrapGitError(`ls-files -- ${filePath}`, err, cwd);
|
|
246
|
+
}
|
|
247
|
+
return output
|
|
248
|
+
.split(/\r?\n/)
|
|
249
|
+
.some((line) => line.trim() === filePath);
|
|
250
|
+
}
|
|
251
|
+
export async function discardAllWorkingTreeChanges(cwd) {
|
|
252
|
+
const git = createGit(cwd);
|
|
253
|
+
try {
|
|
254
|
+
await git.raw(["reset", "--hard", "HEAD"]);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
throw wrapGitError("reset --hard HEAD", err, cwd);
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
await git.raw(["clean", "-fd"]);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
throw wrapGitError("clean -fd", err, cwd);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
237
266
|
export async function assertValidBranchName(cwd, branchName) {
|
|
238
267
|
const git = createGit(cwd);
|
|
239
268
|
try {
|
|
@@ -8,17 +8,27 @@ function isPlainObject(value) {
|
|
|
8
8
|
export function flattenMarkdownList(text) {
|
|
9
9
|
const ast = parseToAST(text, { allowSections: false });
|
|
10
10
|
const items = [];
|
|
11
|
-
|
|
11
|
+
const headingStack = [];
|
|
12
12
|
const pushItem = (content) => {
|
|
13
13
|
const trimmed = content.trim();
|
|
14
14
|
if (!trimmed)
|
|
15
15
|
return;
|
|
16
|
-
const
|
|
16
|
+
const headingPath = headingStack.map((entry) => entry.title);
|
|
17
|
+
const prefix = headingPath.length > 0 ? `**${headingPath.join(" > ")}**: ` : "";
|
|
17
18
|
items.push(`${prefix}${trimmed}`);
|
|
18
19
|
};
|
|
20
|
+
const updateHeadingPath = (level, rawText) => {
|
|
21
|
+
const title = rawText.trim().replace(/^\d+\.\s*/, "");
|
|
22
|
+
if (!title)
|
|
23
|
+
return;
|
|
24
|
+
while (headingStack.length > 0 && headingStack[headingStack.length - 1].level >= level) {
|
|
25
|
+
headingStack.pop();
|
|
26
|
+
}
|
|
27
|
+
headingStack.push({ level, title });
|
|
28
|
+
};
|
|
19
29
|
for (const node of ast.children) {
|
|
20
30
|
if (node.type === "heading") {
|
|
21
|
-
|
|
31
|
+
updateHeadingPath(node.level, node.text);
|
|
22
32
|
continue;
|
|
23
33
|
}
|
|
24
34
|
if (node.type === "list") {
|
|
@@ -6,6 +6,7 @@ import { FlowError } from "../common/errors.js";
|
|
|
6
6
|
import * as git from "../utils/git.js";
|
|
7
7
|
import { resolveOath } from "./oath.js";
|
|
8
8
|
export { resolveOath };
|
|
9
|
+
const DIRTY_WORKTREE_LIST_LIMIT = 10;
|
|
9
10
|
export async function ensureKeiyakuFiles(cwd) {
|
|
10
11
|
const keiyakuPath = path.join(cwd, KEIYAKU_FILE);
|
|
11
12
|
const tracePath = path.join(cwd, TRACE_FILE);
|
|
@@ -36,13 +37,21 @@ export async function assertCleanWorkingTree(cwd, ignorePatterns) {
|
|
|
36
37
|
.getDirtyFiles(cwd)
|
|
37
38
|
.then((files) => files.filter((file) => !ignoredPaths.has(normalizePathForDirtyMatch(cwd, file.path))));
|
|
38
39
|
if (dirtyFiles.length > 0) {
|
|
39
|
-
const
|
|
40
|
+
const shown = dirtyFiles.slice(0, DIRTY_WORKTREE_LIST_LIMIT);
|
|
41
|
+
const list = shown
|
|
40
42
|
.map((f) => {
|
|
41
43
|
const status = `${f.index}${f.working_dir}`;
|
|
42
44
|
return ` ${status} ${f.path}`;
|
|
43
45
|
})
|
|
44
46
|
.join("\n");
|
|
45
|
-
|
|
47
|
+
const overflowCount = dirtyFiles.length - shown.length;
|
|
48
|
+
const overflowLine = overflowCount > 0 ? `\n ... (+${overflowCount} more)` : "";
|
|
49
|
+
const ignoredList = ignoredPaths.size > 0
|
|
50
|
+
? `\n\nIgnored uncommitted paths (not blocking):\n${Array.from(ignoredPaths)
|
|
51
|
+
.map((pathValue) => ` - ${pathValue}`)
|
|
52
|
+
.join("\n")}`
|
|
53
|
+
: "";
|
|
54
|
+
throw new FlowError("DIRTY_WORKTREE", `Uncommitted changes detected in working tree:\n${list}${overflowLine}${ignoredList}\n\nPlease commit or stash blocking changes before proceeding.`);
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
export async function buildNoActiveKeiyakuGuidance(cwd, toolName) {
|
package/build/workflow/drive.js
CHANGED
|
@@ -55,6 +55,7 @@ export async function driveServant(input) {
|
|
|
55
55
|
await appendReview(cwd, plan.targetRound, plan.reviewReason);
|
|
56
56
|
await git.addFiles(cwd, TRACE_FILE);
|
|
57
57
|
await git.commit(cwd, `keiyaku(${title}): iterate round ${plan.targetRound}`);
|
|
58
|
+
const commit = await git.getLatestCommitHash(cwd);
|
|
58
59
|
const { roundSummary } = await runAndRecordRound(cwd, title, plan.targetRound, plan.prompt, {
|
|
59
60
|
signal,
|
|
60
61
|
name,
|
|
@@ -63,6 +64,7 @@ export async function driveServant(input) {
|
|
|
63
64
|
await appendRoundSystemNote(cwd, plan.targetRound, "Subagent execution cancelled by user/client.");
|
|
64
65
|
await git.addFiles(cwd, TRACE_FILE);
|
|
65
66
|
await git.commit(cwd, `keiyaku(${title}): round ${plan.targetRound} cancelled`);
|
|
67
|
+
await git.getLatestCommitHash(cwd);
|
|
66
68
|
},
|
|
67
69
|
});
|
|
68
70
|
const summary = renderRoundSummary(roundSummary, TOOL_DEFAULT_POLICY);
|
|
@@ -77,6 +79,7 @@ export async function driveServant(input) {
|
|
|
77
79
|
criteria,
|
|
78
80
|
currentBranch: keiyakuBranch,
|
|
79
81
|
baseBranch,
|
|
82
|
+
commit,
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
catch (err) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { KEIYAKU_FILE, TRACE_FILE } from "../common/constants.js";
|
|
3
|
+
import { KEIYAKU_DRAFT_FILE, KEIYAKU_DRAFT_NEW_FILE, KEIYAKU_FILE, TRACE_FILE } from "../common/constants.js";
|
|
4
4
|
import { resolveTermPreset } from "../config/term-presets.js";
|
|
5
5
|
import { appendDebugLog } from "../utils/debug-log.js";
|
|
6
6
|
import { FlowError, wrapFlowError } from "../common/errors.js";
|
|
@@ -95,6 +95,27 @@ function requireChecks(name, values) {
|
|
|
95
95
|
}
|
|
96
96
|
return values;
|
|
97
97
|
}
|
|
98
|
+
async function removeClaimProtocolFiles(cwd) {
|
|
99
|
+
await fs.unlink(path.join(cwd, KEIYAKU_FILE));
|
|
100
|
+
await fs.unlink(path.join(cwd, TRACE_FILE));
|
|
101
|
+
const draftCandidates = [KEIYAKU_DRAFT_FILE, KEIYAKU_DRAFT_NEW_FILE];
|
|
102
|
+
for (const draftPath of draftCandidates) {
|
|
103
|
+
const tracked = await git.isPathTracked(cwd, draftPath);
|
|
104
|
+
if (tracked)
|
|
105
|
+
continue;
|
|
106
|
+
try {
|
|
107
|
+
await fs.unlink(path.join(cwd, draftPath));
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error?.code !== "ENOENT") {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function formatCloseDiffSummary(stats, baseBranch) {
|
|
117
|
+
return `Range ${baseBranch}...HEAD | Files ${stats.filesChanged} | +${stats.insertions} / -${stats.deletions}`;
|
|
118
|
+
}
|
|
98
119
|
export async function presentWork(input) {
|
|
99
120
|
const { cwd } = input;
|
|
100
121
|
const isRepo = await git.isGitRepo(cwd);
|
|
@@ -120,8 +141,12 @@ export async function presentWork(input) {
|
|
|
120
141
|
catch {
|
|
121
142
|
round = 0;
|
|
122
143
|
}
|
|
123
|
-
await
|
|
144
|
+
const dirtyFiles = await git.getDirtyFiles(cwd);
|
|
145
|
+
const droppedChanges = dirtyFiles.map((file) => `${file.index}${file.working_dir} ${file.path}`);
|
|
124
146
|
try {
|
|
147
|
+
if (droppedChanges.length > 0) {
|
|
148
|
+
await git.discardAllWorkingTreeChanges(cwd);
|
|
149
|
+
}
|
|
125
150
|
await git.checkoutBranch(cwd, baseBranch);
|
|
126
151
|
await git.deleteBranch(cwd, keiyakuBranch, true);
|
|
127
152
|
await git.clearKeiyakuBase(cwd, keiyakuBranch);
|
|
@@ -136,7 +161,10 @@ export async function presentWork(input) {
|
|
|
136
161
|
currentBranch: baseBranch,
|
|
137
162
|
baseBranch,
|
|
138
163
|
deletedBranch: keiyakuBranch,
|
|
139
|
-
diff:
|
|
164
|
+
diff: droppedChanges.length > 0
|
|
165
|
+
? `Forfeited without merge. Dropped ${droppedChanges.length} local change(s).`
|
|
166
|
+
: "Forfeited without merge.",
|
|
167
|
+
droppedChanges,
|
|
140
168
|
};
|
|
141
169
|
}
|
|
142
170
|
await ensureKeiyakuFiles(cwd);
|
|
@@ -169,7 +197,7 @@ export async function presentWork(input) {
|
|
|
169
197
|
if (!oathMatches(input.oath, expectedOath)) {
|
|
170
198
|
throw new FlowError("OATH_MISMATCH", `Oath mismatch. Correct oath: ${expectedOath}`);
|
|
171
199
|
}
|
|
172
|
-
await assertCleanWorkingTree(cwd);
|
|
200
|
+
await assertCleanWorkingTree(cwd, [KEIYAKU_DRAFT_FILE, KEIYAKU_DRAFT_NEW_FILE]);
|
|
173
201
|
try {
|
|
174
202
|
const invokeDiffLog = `[CLAIM] Collecting diff preview against base '${baseBranch}'`;
|
|
175
203
|
console.error(invokeDiffLog);
|
|
@@ -179,14 +207,14 @@ export async function presentWork(input) {
|
|
|
179
207
|
appendDebugLog(invokeReadLog, { cwd, section: "script" });
|
|
180
208
|
const keiyakuContent = await fs.readFile(path.join(cwd, KEIYAKU_FILE), "utf-8");
|
|
181
209
|
const message = buildMergeMessage(title, keiyakuContent, traceContent);
|
|
210
|
+
const diffStats = await git.getDiffStats(cwd, baseBranch);
|
|
211
|
+
const diff = formatCloseDiffSummary(diffStats, baseBranch);
|
|
182
212
|
const invokeCleanupLog = "[CLAIM] Removing protocol files and creating cleanup commit";
|
|
183
213
|
console.error(invokeCleanupLog);
|
|
184
214
|
appendDebugLog(invokeCleanupLog, { cwd, section: "script" });
|
|
185
|
-
await
|
|
186
|
-
await fs.unlink(path.join(cwd, TRACE_FILE));
|
|
215
|
+
await removeClaimProtocolFiles(cwd);
|
|
187
216
|
await git.addFiles(cwd, "-A");
|
|
188
217
|
await git.commit(cwd, `keiyaku(${title}): cleanup`);
|
|
189
|
-
const diff = await git.getDiffPreviewText(cwd, baseBranch);
|
|
190
218
|
const invokeCheckoutLog = `[CLAIM] Checking out base branch '${baseBranch}'`;
|
|
191
219
|
console.error(invokeCheckoutLog);
|
|
192
220
|
appendDebugLog(invokeCheckoutLog, { cwd, section: "script" });
|
|
@@ -195,7 +223,7 @@ export async function presentWork(input) {
|
|
|
195
223
|
console.error(invokeMergeLog);
|
|
196
224
|
appendDebugLog(invokeMergeLog, { cwd, section: "script" });
|
|
197
225
|
await git.merge(cwd, keiyakuBranch, message);
|
|
198
|
-
const
|
|
226
|
+
const commit = await git.getLatestCommitHash(cwd);
|
|
199
227
|
const invokeFinalizeLog = `[CLAIM] Deleting merged branch '${keiyakuBranch}' and clearing metadata`;
|
|
200
228
|
console.error(invokeFinalizeLog);
|
|
201
229
|
appendDebugLog(invokeFinalizeLog, { cwd, section: "script" });
|
|
@@ -208,7 +236,7 @@ export async function presentWork(input) {
|
|
|
208
236
|
round,
|
|
209
237
|
currentBranch: baseBranch,
|
|
210
238
|
baseBranch,
|
|
211
|
-
|
|
239
|
+
commit,
|
|
212
240
|
mergedInto: baseBranch,
|
|
213
241
|
deletedBranch: keiyakuBranch,
|
|
214
242
|
diff,
|
|
@@ -222,5 +250,5 @@ export async function presentWork(input) {
|
|
|
222
250
|
throw wrapFlowError(`execute CLAIM (merge ${keiyakuBranch} into ${baseBranch})`, err);
|
|
223
251
|
}
|
|
224
252
|
}
|
|
225
|
-
throw new
|
|
253
|
+
throw new FlowError("INVALID_CLOSE_PETITION", `unsupported close petition: ${petition}`);
|
|
226
254
|
}
|
|
@@ -135,8 +135,12 @@ export function buildKeiyakuSuccessResponse(result, input) {
|
|
|
135
135
|
...formatMaybe("Path", input.cwd, 300),
|
|
136
136
|
...formatMaybe("Current Branch", result.currentBranch, 200),
|
|
137
137
|
...formatMaybe("Base Branch", result.baseBranch, 200),
|
|
138
|
+
...formatMaybe("Commit", result.commit, 100),
|
|
138
139
|
];
|
|
139
|
-
const
|
|
140
|
+
const summaryLine = result.commit
|
|
141
|
+
? `Created branch '${result.currentBranch}' (base: '${result.baseBranch}') [${result.commit}].`
|
|
142
|
+
: `Created branch '${result.currentBranch}' (base: '${result.baseBranch}').`;
|
|
143
|
+
const text = assembleResponse(`◆ Started (Round ${result.round})`, summaryLine, [summarySection, constraintsSection, diffSection].filter((section) => section !== null), infoLines);
|
|
140
144
|
return {
|
|
141
145
|
content: [{ type: "text", text }],
|
|
142
146
|
structuredContent: buildSuccessStructuredContent(getStartToolName(), resultData),
|
|
@@ -154,8 +158,12 @@ export function buildDriveResponse(result, input) {
|
|
|
154
158
|
...formatMaybe("Path", input.cwd, 300),
|
|
155
159
|
...formatMaybe("Current Branch", result.currentBranch, 200),
|
|
156
160
|
...formatMaybe("Base Branch", result.baseBranch, 200),
|
|
161
|
+
...formatMaybe("Commit", result.commit, 100),
|
|
157
162
|
];
|
|
158
|
-
const
|
|
163
|
+
const summaryLine = result.commit
|
|
164
|
+
? `Updated branch '${result.currentBranch}' [${result.commit}].`
|
|
165
|
+
: `Updated branch '${result.currentBranch}'.`;
|
|
166
|
+
const text = assembleResponse(`◆ Driven (Round ${result.round})`, summaryLine, [summarySection, goalSection, constraintsSection, criteriaSection, diffSection].filter((section) => section !== null), infoLines);
|
|
159
167
|
return {
|
|
160
168
|
content: [{ type: "text", text }],
|
|
161
169
|
structuredContent: buildSuccessStructuredContent(getDriveToolName(), resultData),
|
|
@@ -181,7 +189,7 @@ export function buildCloseDoneResponse(result, input) {
|
|
|
181
189
|
const infoLines = [
|
|
182
190
|
...formatMaybe("Path", input.cwd, 300),
|
|
183
191
|
...formatMaybe("Result", result.result, 100),
|
|
184
|
-
...formatMaybe("
|
|
192
|
+
...formatMaybe("Commit", result.commit, 100),
|
|
185
193
|
...formatMaybe("Merged Into", result.mergedInto, 200),
|
|
186
194
|
...formatMaybe("Deleted Branch", result.deletedBranch, 200),
|
|
187
195
|
...formatMaybe("Current Branch", result.currentBranch, 200),
|
|
@@ -191,7 +199,9 @@ export function buildCloseDoneResponse(result, input) {
|
|
|
191
199
|
`Scores: precise=${input.score_precise}/10 minimal=${input.score_minimal}/10 isolated=${input.score_isolated}/10 idiomatic=${input.score_idiomatic}/10 cohesive=${input.score_cohesive}/10`,
|
|
192
200
|
...formatMaybe("Oath", input.oath, 220),
|
|
193
201
|
];
|
|
194
|
-
const text = assembleResponse("✓ Keiyaku Fulfilled (CLAIM)",
|
|
202
|
+
const text = assembleResponse("✓ Keiyaku Fulfilled (CLAIM)", result.commit
|
|
203
|
+
? `Merged '${result.deletedBranch}' into '${result.mergedInto}' [${result.commit}]. Deleted feature branch.`
|
|
204
|
+
: `Merged '${result.deletedBranch}' into '${result.mergedInto}'. Deleted feature branch.`, [typeof diffSection === "string" ? null : diffSection].filter((section) => section !== null), infoLines);
|
|
195
205
|
return {
|
|
196
206
|
content: [{ type: "text", text }],
|
|
197
207
|
structuredContent: buildSuccessStructuredContent(closeToolName, resultData),
|
|
@@ -200,13 +210,17 @@ export function buildCloseDoneResponse(result, input) {
|
|
|
200
210
|
export function buildCloseDropResponse(result, input) {
|
|
201
211
|
const { status: _status, ...resultData } = result;
|
|
202
212
|
const closeToolName = getCloseToolName();
|
|
213
|
+
const droppedChanges = result.droppedChanges ?? [];
|
|
214
|
+
const warningSection = buildSection("Warning", formatList("Dropped Local Changes", droppedChanges, { maxItems: 20, maxItemChars: 220 }));
|
|
203
215
|
const infoLines = [
|
|
204
216
|
...formatMaybe("Path", input.cwd, 300),
|
|
205
217
|
...formatMaybe("Deleted Branch", result.deletedBranch, 200),
|
|
206
218
|
...formatMaybe("Current Branch", result.currentBranch, 200),
|
|
207
219
|
...formatMaybe("Base Branch", result.baseBranch, 200),
|
|
208
220
|
];
|
|
209
|
-
const text = assembleResponse("✗ Keiyaku Forfeited (FORFEIT)",
|
|
221
|
+
const text = assembleResponse("✗ Keiyaku Forfeited (FORFEIT)", droppedChanges.length > 0
|
|
222
|
+
? `Deleted '${result.deletedBranch}'. Switched back to '${result.baseBranch}'. Dropped ${droppedChanges.length} local change(s).`
|
|
223
|
+
: `Deleted '${result.deletedBranch}'. Switched back to '${result.baseBranch}'.`, [warningSection].filter((section) => section !== null), infoLines);
|
|
210
224
|
return {
|
|
211
225
|
content: [{ type: "text", text }],
|
|
212
226
|
structuredContent: buildSuccessStructuredContent(closeToolName, resultData),
|
package/build/workflow/start.js
CHANGED
|
@@ -15,6 +15,7 @@ import { runAndRecordRound } from "./round.js";
|
|
|
15
15
|
const BASE_CONSTRAINTS_FILE = path.join(".keiyaku", "base-constraints.md");
|
|
16
16
|
const ACTIVE_KEIYAKU_PREVIEW_MAX_CHARS = 8000;
|
|
17
17
|
const KNOWN_SECTIONS = new Set(["goal", "directive", "context", "constraints", "criteria", "acceptance criteria"]);
|
|
18
|
+
const INTERNAL_START_DIRTY_ALLOWLIST = [];
|
|
18
19
|
function normalizeSectionTitle(title) {
|
|
19
20
|
return title.trim().toLowerCase().replace(/\s+/g, " ");
|
|
20
21
|
}
|
|
@@ -268,6 +269,7 @@ function normalizeTitleForBranch(title) {
|
|
|
268
269
|
}
|
|
269
270
|
async function resolveStartInput(cwd, input) {
|
|
270
271
|
const fromFile = normalizeOptionalText(input.from_file);
|
|
272
|
+
const dirtyAllowlist = [...INTERNAL_START_DIRTY_ALLOWLIST];
|
|
271
273
|
const provided = {
|
|
272
274
|
title: normalizeOptionalText(input.title),
|
|
273
275
|
goal: normalizeOptionalText(input.goal),
|
|
@@ -297,6 +299,7 @@ async function resolveStartInput(cwd, input) {
|
|
|
297
299
|
criteria: normalizeMarkdownListItems("criteria", provided.criteria),
|
|
298
300
|
constraints: normalizeMarkdownListItems("constraints", provided.constraints ?? []),
|
|
299
301
|
fromFile: undefined,
|
|
302
|
+
dirtyAllowlist,
|
|
300
303
|
};
|
|
301
304
|
}
|
|
302
305
|
const draftPath = path.isAbsolute(fromFile) ? fromFile : path.join(cwd, fromFile);
|
|
@@ -337,6 +340,7 @@ async function resolveStartInput(cwd, input) {
|
|
|
337
340
|
criteria: normalizeMarkdownListItems("criteria", criteria),
|
|
338
341
|
constraints: normalizeMarkdownListItems("constraints", constraints),
|
|
339
342
|
fromFile,
|
|
343
|
+
dirtyAllowlist,
|
|
340
344
|
};
|
|
341
345
|
}
|
|
342
346
|
async function readBaseConstraints(cwd) {
|
|
@@ -431,7 +435,8 @@ export async function startKeiyaku(input) {
|
|
|
431
435
|
appendDebugLog(branchWarning, { cwd, section: "script" });
|
|
432
436
|
}
|
|
433
437
|
await assertStartPreFlight(cwd, resolved.fromFile);
|
|
434
|
-
|
|
438
|
+
const dirtyAllowlist = Array.from(new Set([...(resolved.fromFile ? [resolved.fromFile] : []), ...resolved.dirtyAllowlist]));
|
|
439
|
+
await assertCleanWorkingTree(cwd, dirtyAllowlist.length > 0 ? dirtyAllowlist : undefined);
|
|
435
440
|
baseBranch = await git.getCurrentBranch(cwd);
|
|
436
441
|
const branchToken = normalizeTitleForBranch(resolved.title);
|
|
437
442
|
keiyakuBranch = `keiyaku/${branchToken}`;
|
|
@@ -454,6 +459,7 @@ export async function startKeiyaku(input) {
|
|
|
454
459
|
await fs.writeFile(path.join(cwd, TRACE_FILE), "# Keiyaku Trace\n");
|
|
455
460
|
await git.addFiles(cwd, [KEIYAKU_FILE, TRACE_FILE]);
|
|
456
461
|
await git.commit(cwd, `keiyaku(${branchToken}): open`);
|
|
462
|
+
const commit = await git.getLatestCommitHash(cwd);
|
|
457
463
|
const prompt = buildStartPrompt(resolved.title, resolved.goal, resolved.directive);
|
|
458
464
|
const { roundSummary } = await runAndRecordRound(cwd, branchToken, 1, prompt, {
|
|
459
465
|
signal,
|
|
@@ -480,6 +486,7 @@ export async function startKeiyaku(input) {
|
|
|
480
486
|
constraints: constraintsSection,
|
|
481
487
|
currentBranch: keiyakuBranch,
|
|
482
488
|
baseBranch,
|
|
489
|
+
commit,
|
|
483
490
|
};
|
|
484
491
|
}
|
|
485
492
|
catch (err) {
|