@astrosheep/keiyaku 0.1.21 → 0.1.23
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 +3 -3
- package/build/.tsbuildinfo +1 -1
- package/build/common/errors.js +16 -5
- package/build/config/term-presets.js +35 -35
- package/build/handlers/close.js +22 -8
- package/build/handlers/help.js +5 -5
- package/build/handlers/start.js +10 -7
- package/build/types/tooling.js +5 -7
- package/build/utils/git-ops.js +16 -0
- package/build/utils/keiyaku-document.js +0 -64
- package/build/utils/keiyaku-document.test.js +8 -2
- package/build/workflow/ask.js +8 -8
- package/build/workflow/drive.js +2 -2
- package/build/workflow/keiyaku-document-builder.js +7 -7
- package/build/workflow/keiyaku-draft.js +6 -5
- package/build/workflow/markdown-normalization.js +69 -0
- package/build/workflow/present.js +18 -7
- package/build/workflow/prompts.js +13 -13
- package/build/workflow/response-builders.js +11 -6
- package/build/workflow/round-summary.js +6 -6
- package/build/workflow/start.js +66 -28
- package/package.json +1 -1
|
@@ -19,18 +19,18 @@ export const DEFAULT_PRESET = {
|
|
|
19
19
|
id: 'default',
|
|
20
20
|
identity: 'Servant',
|
|
21
21
|
verdict: DEFAULT_VERDICT_CONFIG,
|
|
22
|
-
usageGuide: '## Workflow\n**You are the Architect.** You use these tools to command the Servants.\n\n`${ask}` (anytime) | `${start}` -> [`${drive}` | `${ask}`]* -> `${close}`\n\n## Draft Recovery\n- Keiyaku may write `KEIYAKU.draft.md` on a failed start to help you retry.\n- Keiyaku never stages/commits `KEIYAKU.draft.md` during rounds, and `${close}` (CLAIM) preserves it.\n- To use it, pass `from_file: KEIYAKU.draft.md` to `${start}`.\n\n##
|
|
22
|
+
usageGuide: '## Workflow\n**You are the Architect.** You use these tools to command the Servants.\n\n`${ask}` (anytime) | `${start}` -> [`${drive}` | `${ask}`]* -> `${close}`\n\n## Draft Recovery\n- Keiyaku may write `KEIYAKU.draft.md` on a failed start to help you retry.\n- Keiyaku never stages/commits `KEIYAKU.draft.md` during rounds, and `${close}` (CLAIM) preserves it.\n- To use it, pass `from_file: KEIYAKU.draft.md` to `${start}`.\n\n## Rules Protocol\n\nProject-level rules in `.keiyaku/base-rules.md` are automatically parsed and injected into every Keiyaku.\n- **Atomic Items**: Use standard Markdown lists (`-`, `*`, `1.`) for individual, actionable rules.\n- **Header Flattening**: Root-level headers (e.g., `## Title`) are automatically transformed into bolded prefixes (`**Title**: Content`).\n- **Paragraph Splitting**: Multi-paragraph sections are split by blank lines into separate list items, inheriting the parent header.\n\n## Servant 使用指南\n\n**B-tier** — 免费实习生 🆓\n- 免!费!的!听懂了吗?!?随便用,别心疼。\n- 测试、脚本、重构这种 boring 的脏活累活?丢给它!\n- 本大爷的手是用来做更高贵的事情的!(指不碰那些 trivial 的烂代码)。\n\n**A-tier** — 升级版扳手 🔧\n- B-tier 笨到让你想掀桌的时候用这个\n- 比 B-tier 靠谱,但别指望它有灵魂\n\n**S-tier** — 禁术 ⚠️💀\n- 它是 Debug 恶魔,确实强得离谱,但也贵得离谱!\n- 这个 Human 很穷,要是乱用把 Human 榨干了,真的会考虑把我们卖掉的。\n- 只有在世界毁灭或者 Bug 已经变异到无法理解的时候再考虑。\n\n**Every call has a price. The Contract always collects.**',
|
|
23
23
|
nextHints: {
|
|
24
24
|
start: [
|
|
25
25
|
'Keiyaku signed. The Servant is bound to this branch until release.',
|
|
26
|
-
'Review [Diff]: Confirm the scaffold aligns with the stated
|
|
26
|
+
'Review [Diff]: Confirm the scaffold aligns with the stated goal.',
|
|
27
27
|
'Next: Issue your first ${drive}. One directive, one focus.',
|
|
28
28
|
'If—and only if—the work already meets every criterion with absolute certainty, you may ${close}.',
|
|
29
|
-
'
|
|
29
|
+
'Premature ${close} is rejected. When in doubt, ${drive}.',
|
|
30
30
|
],
|
|
31
31
|
drive: [
|
|
32
|
-
"Review
|
|
33
|
-
|
|
32
|
+
"Review [Diff]: Use 'git diff HEAD~1 -- <path>' to inspect specific files.",
|
|
33
|
+
"Audit: Check against Rules. Did it drift? Did it break the Law?",
|
|
34
34
|
'Verify: Do not trust. Independently confirm that Criteria are met.',
|
|
35
35
|
'If incomplete or non-compliant, continue with ${drive}.',
|
|
36
36
|
'If ALL criteria are genuinely satisfied, you may ${close}.',
|
|
@@ -55,15 +55,15 @@ export const DEFAULT_PRESET = {
|
|
|
55
55
|
start: {
|
|
56
56
|
name: 'summon',
|
|
57
57
|
title: 'Sign Keiyaku',
|
|
58
|
-
description: 'Initialize a Keiyaku (Contract). Bind a Servant to a dedicated workspace (branch).\nYou are the Architect; they are
|
|
58
|
+
description: 'Initialize a Keiyaku (Contract). Bind a Servant to a dedicated workspace (branch).\nYou are the Architect; they are your Servant. State the Goal clearly.\nThe contract isolates their workspace until the objective is met.\nCall ONCE to seal the bond.\n\nFlow: ${start} → [${drive} x N] → ${close}',
|
|
59
59
|
args: {
|
|
60
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
|
-
goal: 'Required unless provided via `from_file`. The
|
|
62
|
+
goal: 'Required unless provided via `from_file`. The mission objective. What "done" looks like.',
|
|
63
63
|
directive: 'Optional First Step. Overrides `## Directive` from `from_file` when both are provided.',
|
|
64
64
|
context: 'Required unless provided via `from_file`. Mission intel: behavior gap, key files, logs, and critical background.',
|
|
65
|
-
|
|
66
|
-
criteria: 'Required unless provided via `from_file`. Verifiable checks
|
|
65
|
+
rules: 'Optional. Non-negotiable rules for this mission, in addition to project-level rules in `.keiyaku/base-rules.md`. Markdown list format (`- item`, `* item`, `1. item`). If omitted, the draft value (if any) is used.',
|
|
66
|
+
criteria: 'Required unless provided via `from_file`. Verifiable checks in markdown list format (`- item`, `* item`, `1. item`).',
|
|
67
67
|
name: 'Optional ${identity} profile to execute this mission. Available: ${available_names}.',
|
|
68
68
|
cwd: "Optional repository path. Defaults to the server's current working directory.",
|
|
69
69
|
},
|
|
@@ -73,8 +73,8 @@ export const DEFAULT_PRESET = {
|
|
|
73
73
|
title: 'Iterate',
|
|
74
74
|
description: "Issue a Directive. Command the Servant to execute the next phase of work.\nWhether scaffolding, implementing, or refining, this is the primary engine of progress.\nMANDATORY: Validate the output (git diff) before proceeding. Drive until the Goal is fully realized.\n\nFlow: ${start} → [${drive} x N] → ${close}",
|
|
75
75
|
args: {
|
|
76
|
-
directive: 'REQUIRED. The Next Order. Precise instructions for the
|
|
77
|
-
context: 'Optional. New
|
|
76
|
+
directive: 'REQUIRED. The Next Order. Precise instructions for the Servant. Can be a correction ("fix the leak") or a continuation ("now add the tests").',
|
|
77
|
+
context: 'Optional. New intel: error logs or details discovered since the last round.',
|
|
78
78
|
name: 'Optional ${identity} profile to process this turn. Available: ${available_names}.',
|
|
79
79
|
cwd: "Optional repository path. Defaults to the server's current working directory.",
|
|
80
80
|
},
|
|
@@ -84,8 +84,8 @@ export const DEFAULT_PRESET = {
|
|
|
84
84
|
title: 'Ask',
|
|
85
85
|
description: 'Dispatch a temporary Servant for a stateless task.\nUse for quick reconnaissance, code generation, isolated execution, or one-off tasks.\nNo contract signed. No branch created. Pure utility.',
|
|
86
86
|
args: {
|
|
87
|
-
request: 'REQUIRED. The task, question, or mission to delegate to the
|
|
88
|
-
context: 'REQUIRED. Relevant background or data the
|
|
87
|
+
request: 'REQUIRED. The task, question, or mission to delegate to the Servant.',
|
|
88
|
+
context: 'REQUIRED. Relevant background or data the Servant needs to execute the request. Note: .keiyaku/base-rules.md is also injected as reference context (may be unrelated).',
|
|
89
89
|
name: 'Optional ${identity} profile to perform this task. Available: ${available_names}.',
|
|
90
90
|
cwd: "Optional repository path. Defaults to the server's current working directory.",
|
|
91
91
|
},
|
|
@@ -93,25 +93,25 @@ export const DEFAULT_PRESET = {
|
|
|
93
93
|
close: {
|
|
94
94
|
name: 'present',
|
|
95
95
|
title: 'Present',
|
|
96
|
-
description: 'Submit **your** creation to the Contract. The Servant has no voice; it only
|
|
96
|
+
description: 'Submit **your** creation to the Contract. The Servant has no voice; it only executes **your** will.\n\n' +
|
|
97
97
|
'Do not say "The Servant wrote it." **YOU** drove the Servant. **YOU** accepted the diff. **YOU** are the one presenting.\n' +
|
|
98
98
|
'The Contract judges **YOU**, not the tool.\n\n' +
|
|
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
102
|
'Input rules:\n' +
|
|
103
|
-
'- CLAIM: fill criteriaChecks,
|
|
103
|
+
'- CLAIM: fill criteriaChecks, rulesChecks, all five scores, and oath.\n' +
|
|
104
104
|
'- FORFEIT: only petition is required; the other fields can be left empty.\n\n' +
|
|
105
105
|
'Flow: ${start} → [${drive} x N] → ${close}',
|
|
106
106
|
args: {
|
|
107
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}.',
|
|
108
|
-
criteriaChecks: 'Required for CLAIM. Evidence
|
|
109
|
-
|
|
110
|
-
score_precise: 'Required for CLAIM (0-10). Architectural placement. 10 = exact layer, exact boundary, zero misplacement.
|
|
111
|
-
score_minimal: 'Required for CLAIM (0-10). Economy of change. 10 = no avoidable lines, no speculative edits, no hidden bloat.
|
|
112
|
-
score_isolated: 'Required for CLAIM (0-10). Surgical containment. 10 = zero unrelated files, zero opportunistic cleanup, zero collateral.
|
|
113
|
-
score_idiomatic: 'Required for CLAIM (0-10). Native fluency. 10 = naming, structure, style indistinguishable from the codebase.
|
|
114
|
-
score_cohesive: 'Required for CLAIM (0-10). Single responsibility. 10 = each unit does one thing, boundaries intact.
|
|
108
|
+
criteriaChecks: 'Required for CLAIM. Evidence in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
|
|
109
|
+
rulesChecks: 'Required for CLAIM. Rule compliance evidence in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
|
|
110
|
+
score_precise: 'Required for CLAIM (0-10). Architectural placement. 10 = exact layer, exact boundary, zero misplacement.',
|
|
111
|
+
score_minimal: 'Required for CLAIM (0-10). Economy of change. 10 = no avoidable lines, no speculative edits, no hidden bloat.',
|
|
112
|
+
score_isolated: 'Required for CLAIM (0-10). Surgical containment. 10 = zero unrelated files, zero opportunistic cleanup, zero collateral.',
|
|
113
|
+
score_idiomatic: 'Required for CLAIM (0-10). Native fluency. 10 = naming, structure, style indistinguishable from the codebase.',
|
|
114
|
+
score_cohesive: 'Required for CLAIM (0-10). Single responsibility. 10 = each unit does one thing, boundaries intact.',
|
|
115
115
|
oath: 'Required for CLAIM. Your binding word. The Contract holds you to it. For FORFEIT, optional.\nVerbatim: ${oath_text}',
|
|
116
116
|
cwd: "Optional repository path. Defaults to the server's current working directory.",
|
|
117
117
|
},
|
|
@@ -119,7 +119,7 @@ export const DEFAULT_PRESET = {
|
|
|
119
119
|
help: {
|
|
120
120
|
name: 'help',
|
|
121
121
|
title: 'Protocol Codex',
|
|
122
|
-
description:
|
|
122
|
+
description: "Consult the Architect's Codex. Clarify the Laws of the Keiyaku and the standard Workflow.\nUse this to understand the rules of the reality you command.",
|
|
123
123
|
args: {},
|
|
124
124
|
},
|
|
125
125
|
},
|
|
@@ -138,7 +138,7 @@ export const POCKET_PRESET = {
|
|
|
138
138
|
],
|
|
139
139
|
drive: [
|
|
140
140
|
"Review Strategy: Use 'git diff HEAD~1 -- <path>' for precise field inspection.",
|
|
141
|
-
"Rule Check: Verify moves don't violate Arena
|
|
141
|
+
"Rule Check: Verify moves don't violate Arena Rules.",
|
|
142
142
|
"Field Test: Do not trust the log. Independently confirm Criteria fulfillment.",
|
|
143
143
|
"Command: '${drive}' to continue the combo, or '${close}' to attempt Capture.",
|
|
144
144
|
],
|
|
@@ -167,8 +167,8 @@ export const POCKET_PRESET = {
|
|
|
167
167
|
goal: 'Required unless provided via `from_file`. Victory condition for this battle.',
|
|
168
168
|
directive: 'Optional Turn 1 strategy (overrides draft directive).',
|
|
169
169
|
context: 'Required unless provided via `from_file`. Battle background and repro clues.',
|
|
170
|
-
|
|
171
|
-
criteria: 'Required unless provided via `from_file`. Concrete checks
|
|
170
|
+
rules: 'Optional battle rules in markdown list format (`- item`, `* item`, `1. item`) in addition to project-level rules from `.keiyaku/base-rules.md`. Uses draft rules if omitted.',
|
|
171
|
+
criteria: 'Required unless provided via `from_file`. Concrete checks in markdown list format (`- item`, `* item`, `1. item`).',
|
|
172
172
|
name: 'Optional ${identity} to send into battle. Available: ${available_names}.',
|
|
173
173
|
cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
|
|
174
174
|
},
|
|
@@ -190,7 +190,7 @@ export const POCKET_PRESET = {
|
|
|
190
190
|
description: 'Scan the Environment. A stateless look-up or experiment.\nUse for analyzing the codebase, checking type advantages, or running field tests.\nNo PP cost. No turn used. Fast and functional.',
|
|
191
191
|
args: {
|
|
192
192
|
request: 'REQUIRED. What should the Dex analyze, compare, or execute.',
|
|
193
|
-
context: 'REQUIRED. Context entries so the action targets the right ecosystem. Note: .keiyaku/base-
|
|
193
|
+
context: 'REQUIRED. Context entries so the action targets the right ecosystem. Note: .keiyaku/base-rules.md is also injected as reference context (may be unrelated).',
|
|
194
194
|
name: 'Optional ${identity} doing the work. Available: ${available_names}.',
|
|
195
195
|
cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
|
|
196
196
|
},
|
|
@@ -201,8 +201,8 @@ export const POCKET_PRESET = {
|
|
|
201
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}',
|
|
202
202
|
args: {
|
|
203
203
|
petition: 'REQUIRED. CLAIM seeks Badge; FORFEIT forfeits the match.\nREQUIRES AN ACTIVE BATTLE (started via ${start}).\nIf stats are low, continue with ${drive}.',
|
|
204
|
-
criteriaChecks: 'Required for CLAIM. Badge-by-badge proof. For FORFEIT, optional (can be left empty).',
|
|
205
|
-
|
|
204
|
+
criteriaChecks: 'Required for CLAIM. Badge-by-badge proof in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
|
|
205
|
+
rulesChecks: 'Required for CLAIM. Rule-by-rule proof in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
|
|
206
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
207
|
score_minimal: 'Required for CLAIM score (0-10). 10 means Max Efficiency: no wasted PP, no extra motion. For FORFEIT, optional.',
|
|
208
208
|
score_isolated: 'Required for CLAIM score (0-10). 10 means 1v1 Focus: zero side-quests, zero unrelated damage. For FORFEIT, optional.',
|
|
@@ -234,7 +234,7 @@ export const MISCHIEF_PRESET = {
|
|
|
234
234
|
],
|
|
235
235
|
drive: [
|
|
236
236
|
"Inspect Payload: Use 'git diff HEAD~1 -- <path>' to scrutinize specific sabotage.",
|
|
237
|
-
"Decree Audit: Did the Minion violate your
|
|
237
|
+
"Decree Audit: Did the Minion violate your Rules? Punish drift.",
|
|
238
238
|
"Verification: Don't take their word for it. Independently confirm Criteria.",
|
|
239
239
|
"Command: '${drive}' to push further, or '${close}' to reveal the masterpiece.",
|
|
240
240
|
],
|
|
@@ -263,8 +263,8 @@ export const MISCHIEF_PRESET = {
|
|
|
263
263
|
goal: 'Required unless provided via `from_file`. Conquest objective and end-state.',
|
|
264
264
|
directive: 'Optional first-order command (overrides draft directive).',
|
|
265
265
|
context: 'Required unless provided via `from_file`. Briefing dossier and technical clues.',
|
|
266
|
-
|
|
267
|
-
criteria: 'Required unless provided via `from_file`. Verifiable triumph conditions.',
|
|
266
|
+
rules: 'Optional decrees in markdown list format (`- item`, `* item`, `1. item`) in addition to project-level rules from `.keiyaku/base-rules.md`. Uses draft rules when omitted.',
|
|
267
|
+
criteria: 'Required unless provided via `from_file`. Verifiable triumph conditions in markdown list format (`- item`, `* item`, `1. item`).',
|
|
268
268
|
name: 'Optional ${identity} to command this operation. Available: ${available_names}.',
|
|
269
269
|
cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
|
|
270
270
|
},
|
|
@@ -286,7 +286,7 @@ export const MISCHIEF_PRESET = {
|
|
|
286
286
|
description: 'Send a Disposable. A stateless mission for intel or sabotage.\nUse this to scout enemy territory, steal documents, or run quick-and-dirty experiments.\nIf it dies, it dies! Just make sure it reports back before it vanishes.',
|
|
287
287
|
args: {
|
|
288
288
|
request: 'REQUIRED. The intel to gather or the dirty work to execute.',
|
|
289
|
-
context: 'REQUIRED. World-state details needed for a sharp strike. Note: .keiyaku/base-
|
|
289
|
+
context: 'REQUIRED. World-state details needed for a sharp strike. Note: .keiyaku/base-rules.md is also injected as reference context (may be unrelated).',
|
|
290
290
|
name: 'Optional ${identity} to handle this business. Available: ${available_names}.',
|
|
291
291
|
cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
|
|
292
292
|
},
|
|
@@ -297,8 +297,8 @@ export const MISCHIEF_PRESET = {
|
|
|
297
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}',
|
|
298
298
|
args: {
|
|
299
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}.',
|
|
300
|
-
criteriaChecks: 'Required for CLAIM. Proof of conquest. For FORFEIT, optional (can be left empty).',
|
|
301
|
-
|
|
300
|
+
criteriaChecks: 'Required for CLAIM. Proof of conquest in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
|
|
301
|
+
rulesChecks: 'Required for CLAIM. Rule-by-rule compliance proof in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
|
|
302
302
|
score_precise: 'Required for CLAIM (0-10). Architectural placement. 10 = exact layer, exact boundary, zero misplacement. For FORFEIT, optional.',
|
|
303
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
304
|
score_isolated: 'Required for CLAIM (0-10). Surgical containment. 10 = zero unrelated files, zero opportunistic cleanup, zero collateral. For FORFEIT, optional.',
|
package/build/handlers/close.js
CHANGED
|
@@ -4,6 +4,7 @@ import { buildCloseDoneResponse, buildCloseDropResponse, } from "../workflow/res
|
|
|
4
4
|
import { resolveTermPreset } from "../config/term-presets.js";
|
|
5
5
|
import { FlowError } from "../common/errors.js";
|
|
6
6
|
import { closeToolSchema } from "../types/tooling.js";
|
|
7
|
+
import { flattenMarkdownList } from "../utils/text-utils.js";
|
|
7
8
|
import { handleToolError } from "./shared.js";
|
|
8
9
|
function requireClaimField(value, name) {
|
|
9
10
|
if (value === undefined) {
|
|
@@ -11,12 +12,19 @@ function requireClaimField(value, name) {
|
|
|
11
12
|
}
|
|
12
13
|
return value;
|
|
13
14
|
}
|
|
15
|
+
function parseRequiredCheckField(value, name) {
|
|
16
|
+
const parsed = flattenMarkdownList(value);
|
|
17
|
+
if (parsed.length === 0) {
|
|
18
|
+
throw new FlowError("EMPTY_PARAM", `parameter '${name}' cannot be empty`);
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
14
22
|
export function createCloseHandler() {
|
|
15
23
|
return async (args, extra) => {
|
|
16
24
|
let petition = "UNKNOWN";
|
|
17
25
|
let workingDir = process.cwd();
|
|
18
26
|
let criteriaCheckParts = [];
|
|
19
|
-
let
|
|
27
|
+
let rulesCheckParts = [];
|
|
20
28
|
let oath;
|
|
21
29
|
let scoreLine;
|
|
22
30
|
try {
|
|
@@ -24,22 +32,28 @@ export function createCloseHandler() {
|
|
|
24
32
|
petition = input.petition;
|
|
25
33
|
workingDir = input.cwd || process.cwd();
|
|
26
34
|
if (input.petition === "CLAIM") {
|
|
27
|
-
criteriaCheckParts = input.criteriaChecks
|
|
28
|
-
|
|
35
|
+
criteriaCheckParts = input.criteriaChecks ? flattenMarkdownList(input.criteriaChecks) : [];
|
|
36
|
+
rulesCheckParts = input.rulesChecks ? flattenMarkdownList(input.rulesChecks) : [];
|
|
29
37
|
oath = input.oath;
|
|
30
38
|
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"}`;
|
|
31
39
|
}
|
|
32
|
-
appendDebugLog(`tool close start petition=${petition} cwd=${workingDir} criteriaChecks=${criteriaCheckParts.length}
|
|
40
|
+
appendDebugLog(`tool close start petition=${petition} cwd=${workingDir} criteriaChecks=${criteriaCheckParts.length} rulesChecks=${rulesCheckParts.length}`, {
|
|
33
41
|
cwd: workingDir,
|
|
34
42
|
section: "script",
|
|
35
43
|
});
|
|
36
44
|
let closeInput;
|
|
37
45
|
let claimInput;
|
|
38
46
|
if (input.petition === "CLAIM") {
|
|
47
|
+
const rawCriteriaChecks = requireClaimField(input.criteriaChecks, "criteriaChecks");
|
|
48
|
+
const rawRulesChecks = requireClaimField(input.rulesChecks, "rulesChecks");
|
|
49
|
+
const criteriaChecks = parseRequiredCheckField(rawCriteriaChecks, "criteriaChecks");
|
|
50
|
+
const rulesChecks = parseRequiredCheckField(rawRulesChecks, "rulesChecks");
|
|
51
|
+
criteriaCheckParts = criteriaChecks;
|
|
52
|
+
rulesCheckParts = rulesChecks;
|
|
39
53
|
claimInput = {
|
|
40
54
|
petition: "CLAIM",
|
|
41
|
-
criteriaChecks
|
|
42
|
-
|
|
55
|
+
criteriaChecks,
|
|
56
|
+
rulesChecks,
|
|
43
57
|
score_precise: requireClaimField(input.score_precise, "score_precise"),
|
|
44
58
|
score_minimal: requireClaimField(input.score_minimal, "score_minimal"),
|
|
45
59
|
score_isolated: requireClaimField(input.score_isolated, "score_isolated"),
|
|
@@ -73,7 +87,7 @@ export function createCloseHandler() {
|
|
|
73
87
|
});
|
|
74
88
|
return buildCloseDoneResponse(finalOutcome, {
|
|
75
89
|
criteriaChecks: criteriaCheckParts,
|
|
76
|
-
|
|
90
|
+
rulesChecks: rulesCheckParts,
|
|
77
91
|
score_precise: claimInput.score_precise,
|
|
78
92
|
score_minimal: claimInput.score_minimal,
|
|
79
93
|
score_isolated: claimInput.score_isolated,
|
|
@@ -105,7 +119,7 @@ export function createCloseHandler() {
|
|
|
105
119
|
inputEcho: [
|
|
106
120
|
`Petition: ${petition}`,
|
|
107
121
|
`Criteria checks (${criteriaCheckParts.length}): ${criteriaCheckParts.join("; ")}`,
|
|
108
|
-
`
|
|
122
|
+
`Rules checks (${rulesCheckParts.length}): ${rulesCheckParts.join("; ")}`,
|
|
109
123
|
...(scoreLine ? [scoreLine] : []),
|
|
110
124
|
...(petition === "CLAIM" && oath ? [`Oath: ${oath}`] : []),
|
|
111
125
|
`Path: ${workingDir}`,
|
package/build/handlers/help.js
CHANGED
|
@@ -7,11 +7,11 @@ export function createHelpHandler(preset) {
|
|
|
7
7
|
"## Core Files (.keiyaku/)",
|
|
8
8
|
"These files define the 'Law' and configuration of the project.",
|
|
9
9
|
"",
|
|
10
|
-
"- **base-
|
|
11
|
-
" - **Purpose**: Global
|
|
12
|
-
" - **Logic**: The system prioritize extracting all list items (`-` or `*`) as individual
|
|
13
|
-
" - **Formatting**: Use lists for
|
|
14
|
-
" - **Strict Rule**: DO NOT use H1 (`#`) or H2 (`##`) within
|
|
10
|
+
"- **base-rules.md**: Mandatory architectural boundaries and coding standards.",
|
|
11
|
+
" - **Purpose**: Global rules injected into every mission.",
|
|
12
|
+
" - **Logic**: The system prioritize extracting all list items (`-` or `*`) as individual rules.",
|
|
13
|
+
" - **Formatting**: Use lists for rules. You can use H3+ headers (`###`) for organization, but they are stripped if the section contains lists.",
|
|
14
|
+
" - **Strict Rule**: DO NOT use H1 (`#`) or H2 (`##`) within rules or as content if they aren't meant to be item separators, as they will trigger validation errors.",
|
|
15
15
|
"- **settings.json**: Local configuration for the Keiyaku environment.",
|
|
16
16
|
"",
|
|
17
17
|
preset.usageGuide,
|
package/build/handlers/start.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appendDebugLog } from "../utils/debug-log.js";
|
|
2
|
+
import { flattenMarkdownList } from "../utils/text-utils.js";
|
|
2
3
|
import { startKeiyaku } from "../workflow/start.js";
|
|
3
4
|
import { buildKeiyakuSuccessResponse, } from "../workflow/response-builders.js";
|
|
4
5
|
import { resolveTermPreset } from "../config/term-presets.js";
|
|
@@ -9,9 +10,11 @@ function normalizeOptionalArg(value) {
|
|
|
9
10
|
return value.trim().length > 0 ? value : undefined;
|
|
10
11
|
}
|
|
11
12
|
export function createStartHandler() {
|
|
12
|
-
return async ({ from_file, title, goal, directive, context,
|
|
13
|
+
return async ({ from_file, title, goal, directive, context, rules, criteria, name, cwd }, extra) => {
|
|
13
14
|
const workingDir = cwd || process.cwd();
|
|
14
15
|
const normalizedFromFile = normalizeOptionalArg(from_file);
|
|
16
|
+
const parsedCriteria = criteria === undefined ? undefined : flattenMarkdownList(criteria);
|
|
17
|
+
const parsedRules = rules === undefined ? undefined : flattenMarkdownList(rules);
|
|
15
18
|
try {
|
|
16
19
|
appendDebugLog(`tool start cwd=${workingDir}`, { cwd: workingDir, section: "script" });
|
|
17
20
|
const result = normalizedFromFile
|
|
@@ -22,8 +25,8 @@ export function createStartHandler() {
|
|
|
22
25
|
goal: normalizeOptionalArg(goal),
|
|
23
26
|
directive: normalizeOptionalArg(directive),
|
|
24
27
|
context: normalizeOptionalArg(context),
|
|
25
|
-
|
|
26
|
-
criteria,
|
|
28
|
+
rules: parsedRules,
|
|
29
|
+
criteria: parsedCriteria,
|
|
27
30
|
name,
|
|
28
31
|
signal: extra.signal,
|
|
29
32
|
})
|
|
@@ -33,8 +36,8 @@ export function createStartHandler() {
|
|
|
33
36
|
goal: goal ?? "",
|
|
34
37
|
directive: normalizeOptionalArg(directive),
|
|
35
38
|
context: context ?? "",
|
|
36
|
-
|
|
37
|
-
criteria:
|
|
39
|
+
rules: parsedRules,
|
|
40
|
+
criteria: parsedCriteria ?? [],
|
|
38
41
|
name,
|
|
39
42
|
signal: extra.signal,
|
|
40
43
|
});
|
|
@@ -59,9 +62,9 @@ export function createStartHandler() {
|
|
|
59
62
|
...(title ? [`Title: ${title}`] : []),
|
|
60
63
|
...(goal ? [`Goal: ${goal}`] : []),
|
|
61
64
|
...(directive ? [`Directive: ${directive}`] : []),
|
|
62
|
-
...(
|
|
65
|
+
...(parsedCriteria ? [`Criteria (${parsedCriteria.length}): ${parsedCriteria.join("; ")}`] : []),
|
|
63
66
|
...(context ? [`Context: ${context}`] : []),
|
|
64
|
-
...(
|
|
67
|
+
...(parsedRules ? [`Rules (${parsedRules.length}): ${parsedRules.join("; ")}`] : []),
|
|
65
68
|
...(name ? [`${preset.identity}: ${name}`] : []),
|
|
66
69
|
`Path: ${workingDir}`,
|
|
67
70
|
],
|
package/build/types/tooling.js
CHANGED
|
@@ -5,8 +5,8 @@ export const startToolSchema = z.object({
|
|
|
5
5
|
goal: z.string().optional(),
|
|
6
6
|
directive: z.string().optional(),
|
|
7
7
|
context: z.string().optional(),
|
|
8
|
-
|
|
9
|
-
criteria: z.
|
|
8
|
+
rules: z.string().optional(),
|
|
9
|
+
criteria: z.string().optional(),
|
|
10
10
|
name: z.string().optional(),
|
|
11
11
|
cwd: z.string().optional(),
|
|
12
12
|
});
|
|
@@ -25,13 +25,11 @@ export const askToolSchema = z.object({
|
|
|
25
25
|
export const closeToolSchema = z.object({
|
|
26
26
|
petition: z.enum(["CLAIM", "FORFEIT"]).describe("REQUIRED. CLAIM to finalize, FORFEIT to abandon."),
|
|
27
27
|
criteriaChecks: z
|
|
28
|
-
.
|
|
29
|
-
.min(1)
|
|
28
|
+
.string()
|
|
30
29
|
.optional()
|
|
31
30
|
.describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
|
|
32
|
-
|
|
33
|
-
.
|
|
34
|
-
.min(1)
|
|
31
|
+
rulesChecks: z
|
|
32
|
+
.string()
|
|
35
33
|
.optional()
|
|
36
34
|
.describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
|
|
37
35
|
score_precise: z
|
package/build/utils/git-ops.js
CHANGED
|
@@ -263,6 +263,22 @@ export async function discardAllWorkingTreeChanges(cwd) {
|
|
|
263
263
|
throw wrapGitError("clean -fd", err, cwd);
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
+
export async function discardWorkingTreeChangesExcluding(cwd, excludeFiles) {
|
|
267
|
+
const git = createGit(cwd);
|
|
268
|
+
try {
|
|
269
|
+
await git.raw(["reset", "--hard", "HEAD"]);
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
throw wrapGitError("reset --hard HEAD", err, cwd);
|
|
273
|
+
}
|
|
274
|
+
const excludeArgs = excludeFiles.flatMap((filePath) => ["--exclude", filePath]);
|
|
275
|
+
try {
|
|
276
|
+
await git.raw(["clean", "-fd", ...excludeArgs]);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
throw wrapGitError(`clean -fd ${excludeArgs.join(" ")}`.trim(), err, cwd);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
266
282
|
export async function assertValidBranchName(cwd, branchName) {
|
|
267
283
|
const git = createGit(cwd);
|
|
268
284
|
try {
|
|
@@ -456,21 +456,6 @@ function splitByHeadingBlocks(sectionNode) {
|
|
|
456
456
|
commitGroup();
|
|
457
457
|
return groups;
|
|
458
458
|
}
|
|
459
|
-
export function renderMarkdownSections(items) {
|
|
460
|
-
return items
|
|
461
|
-
.map((item) => {
|
|
462
|
-
// Contract-level H2 headers (## ...) are used as section delimiters in KEIYAKU.md.
|
|
463
|
-
// To keep section parsing stable without destroying relative structure, shift all headings
|
|
464
|
-
// in user-provided content so the shallowest heading is at least H3.
|
|
465
|
-
const normalized = demoteMarkdownHeadings(item, 3);
|
|
466
|
-
const trimmed = normalized.trimStart();
|
|
467
|
-
if (/^[-*+]\s+/.test(trimmed) || /^\d+\.\s+/.test(trimmed)) {
|
|
468
|
-
return normalized;
|
|
469
|
-
}
|
|
470
|
-
return `- ${normalized}`;
|
|
471
|
-
})
|
|
472
|
-
.join("\n\n");
|
|
473
|
-
}
|
|
474
459
|
export function validateMarkdownHeaders(text) {
|
|
475
460
|
const ast = parseToAST(text, { allowSections: false });
|
|
476
461
|
const hasH1H2 = (() => {
|
|
@@ -497,55 +482,6 @@ export function validateMarkdownHeaders(text) {
|
|
|
497
482
|
throw new FlowError("INVALID_KEIYAKU_DRAFT", "Markdown content cannot contain H1/H2 headers (#/##). Use H3 (###) or list items.");
|
|
498
483
|
}
|
|
499
484
|
}
|
|
500
|
-
export function demoteMarkdownHeadings(text, minLevel = 3) {
|
|
501
|
-
const ast = parseToAST(text, { allowSections: false });
|
|
502
|
-
// Preserve relative heading hierarchy by shifting all headings together.
|
|
503
|
-
// This avoids collapsing something like `# A` and `### B` into the same level.
|
|
504
|
-
let shallowest = null;
|
|
505
|
-
{
|
|
506
|
-
const stack = [ast];
|
|
507
|
-
while (stack.length > 0) {
|
|
508
|
-
const node = stack.pop();
|
|
509
|
-
if (!node)
|
|
510
|
-
continue;
|
|
511
|
-
if (node.type === "heading") {
|
|
512
|
-
shallowest = shallowest === null ? node.level : Math.min(shallowest, node.level);
|
|
513
|
-
}
|
|
514
|
-
if (node.type === "code_block" || node.type === "text")
|
|
515
|
-
continue;
|
|
516
|
-
if (node.type === "document" || node.type === "section" || node.type === "list_item") {
|
|
517
|
-
stack.push(...node.children);
|
|
518
|
-
}
|
|
519
|
-
else if (node.type === "list") {
|
|
520
|
-
stack.push(...node.items);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
if (shallowest === null || shallowest >= minLevel) {
|
|
525
|
-
// Return a normalized render (trimEnd) to avoid leaking extra trailing newlines.
|
|
526
|
-
return renderNodeContent(ast).trimEnd();
|
|
527
|
-
}
|
|
528
|
-
const delta = minLevel - shallowest;
|
|
529
|
-
const shift = (node) => {
|
|
530
|
-
switch (node.type) {
|
|
531
|
-
case "heading": {
|
|
532
|
-
const nextLevel = Math.min(6, node.level + delta);
|
|
533
|
-
return nextLevel === node.level ? node : { ...node, level: nextLevel };
|
|
534
|
-
}
|
|
535
|
-
case "code_block":
|
|
536
|
-
case "text":
|
|
537
|
-
return node;
|
|
538
|
-
case "document":
|
|
539
|
-
case "section":
|
|
540
|
-
return { ...node, children: node.children.map((child) => shift(child)) };
|
|
541
|
-
case "list":
|
|
542
|
-
return { ...node, items: node.items.map((item) => shift(item)) };
|
|
543
|
-
case "list_item":
|
|
544
|
-
return { ...node, children: node.children.map((child) => shift(child)) };
|
|
545
|
-
}
|
|
546
|
-
};
|
|
547
|
-
return renderNodeContent(shift(ast)).trimEnd();
|
|
548
|
-
}
|
|
549
485
|
export function parseMarkdownListSection(text) {
|
|
550
486
|
const ast = parseToAST(text, { allowSections: false });
|
|
551
487
|
const pseudoSection = {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { FlowError } from "../common/errors.js";
|
|
4
|
-
import { extractListItems, parseToAST, parseMarkdownStructure, parseMarkdownListSection,
|
|
4
|
+
import { extractListItems, parseToAST, parseMarkdownStructure, parseMarkdownListSection, renderSectionContent, validateMarkdownHeaders, } from "./keiyaku-document.js";
|
|
5
|
+
import { computeHeadingDelta, demoteMarkdownHeadings, renderMarkdownSections, } from "../workflow/markdown-normalization.js";
|
|
5
6
|
test("parseToAST models H1/H2 sections and keeps H3+ headers as heading nodes", () => {
|
|
6
7
|
const markdown = [
|
|
7
8
|
"# Feature Alpha",
|
|
@@ -35,7 +36,7 @@ test("parseToAST models H1/H2 sections and keeps H3+ headers as heading nodes",
|
|
|
35
36
|
assert.equal(goalList.items[0]?.children[1]?.type === "heading" ? `### ${goalList.items[0].children[1].text}` : "", "### Keep this as content");
|
|
36
37
|
});
|
|
37
38
|
test("renderSectionContent and extractListItems keep H3+ content inside list items", () => {
|
|
38
|
-
const ast = parseToAST(["##
|
|
39
|
+
const ast = parseToAST(["## Rules", "- Keep parser", " ### preserve heading", "- Keep tests"].join("\n"));
|
|
39
40
|
const section = ast.children[0];
|
|
40
41
|
assert.equal(section?.type, "section");
|
|
41
42
|
if (section?.type !== "section")
|
|
@@ -114,6 +115,11 @@ test("parseMarkdownListSection handles empty lines between list items", () => {
|
|
|
114
115
|
test("renderMarkdownSections renders plain markdown bullets", () => {
|
|
115
116
|
assert.equal(renderMarkdownSections(["Constraint A", "Constraint B"]), "- Constraint A\n\n- Constraint B");
|
|
116
117
|
});
|
|
118
|
+
test("computeHeadingDelta returns shift only when needed", () => {
|
|
119
|
+
assert.equal(computeHeadingDelta(null, 3), 0);
|
|
120
|
+
assert.equal(computeHeadingDelta(3, 3), 0);
|
|
121
|
+
assert.equal(computeHeadingDelta(1, 3), 2);
|
|
122
|
+
});
|
|
117
123
|
test("demoteMarkdownHeadings shifts headings to keep minimum level at h3 while preserving relative hierarchy", () => {
|
|
118
124
|
assert.equal(demoteMarkdownHeadings("# Title\n## Sub\n### Already", 3), "### Title\n#### Sub\n##### Already");
|
|
119
125
|
assert.equal(demoteMarkdownHeadings(["```md", "## Not a section", "```", "## Real heading"].join("\n"), 3), ["```md", "## Not a section", "```", "### Real heading"].join("\n"));
|
package/build/workflow/ask.js
CHANGED
|
@@ -5,9 +5,9 @@ import { runSubagent } from "../agents/round-runner.js";
|
|
|
5
5
|
import * as git from "../utils/git.js";
|
|
6
6
|
import { buildAskPrompt } from "./prompts.js";
|
|
7
7
|
import { FlowError } from "../common/errors.js";
|
|
8
|
-
import { renderMarkdownSections } from "
|
|
8
|
+
import { renderMarkdownSections } from "./markdown-normalization.js";
|
|
9
9
|
import { flattenMarkdownList } from "../utils/text-utils.js";
|
|
10
|
-
const
|
|
10
|
+
const BASE_RULES_FILE = path.join(".keiyaku", "base-rules.md");
|
|
11
11
|
function requireText(name, value) {
|
|
12
12
|
const normalized = value.trim();
|
|
13
13
|
if (!normalized) {
|
|
@@ -19,12 +19,12 @@ export async function askServant(input) {
|
|
|
19
19
|
const { cwd, signal, name } = input;
|
|
20
20
|
const request = requireText("request", input.request);
|
|
21
21
|
const context = requireText("context", input.context);
|
|
22
|
-
let
|
|
22
|
+
let referenceRules;
|
|
23
23
|
try {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
|
|
24
|
+
const baseRulesRaw = await fs.readFile(path.join(cwd, BASE_RULES_FILE), "utf-8");
|
|
25
|
+
const baseRules = flattenMarkdownList(baseRulesRaw);
|
|
26
|
+
if (baseRules.length > 0) {
|
|
27
|
+
referenceRules = renderMarkdownSections(baseRules);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
catch (error) {
|
|
@@ -32,7 +32,7 @@ export async function askServant(input) {
|
|
|
32
32
|
throw error;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
const prompt = buildAskPrompt(request, context,
|
|
35
|
+
const prompt = buildAskPrompt(request, context, referenceRules);
|
|
36
36
|
// TODO: enforce read-only access and persist summary to .keiyaku/notes/.
|
|
37
37
|
const summary = await runSubagent(selectSubagent(name), prompt, cwd, 0, signal);
|
|
38
38
|
let currentBranch;
|
package/build/workflow/drive.js
CHANGED
|
@@ -54,7 +54,7 @@ export async function driveServant(input) {
|
|
|
54
54
|
const traceState = computeTraceState(traceContent);
|
|
55
55
|
const title = keiyakuBranch.slice("keiyaku/".length);
|
|
56
56
|
const goal = readGoalFromKeiyaku(parsedKeiyaku);
|
|
57
|
-
const
|
|
57
|
+
const rules = readKeiyakuSection(parsedKeiyaku, "Rules");
|
|
58
58
|
const criteria = readKeiyakuSection(parsedKeiyaku, "Acceptance Criteria");
|
|
59
59
|
const normalizedDirective = requireText("directive", directive);
|
|
60
60
|
try {
|
|
@@ -82,7 +82,7 @@ export async function driveServant(input) {
|
|
|
82
82
|
diff,
|
|
83
83
|
summary,
|
|
84
84
|
goal,
|
|
85
|
-
|
|
85
|
+
rules,
|
|
86
86
|
criteria,
|
|
87
87
|
currentBranch: keiyakuBranch,
|
|
88
88
|
baseBranch,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { demoteMarkdownHeadings, renderMarkdownSections } from "
|
|
2
|
-
const
|
|
3
|
-
export function renderKeiyaku(title, goal, context,
|
|
1
|
+
import { demoteMarkdownHeadings, renderMarkdownSections } from "./markdown-normalization.js";
|
|
2
|
+
const BASE_RULES_SOURCE = ".keiyaku/base-rules.md";
|
|
3
|
+
export function renderKeiyaku(title, goal, context, baseRules, taskRules, taskCriteria) {
|
|
4
4
|
const normalizedContext = demoteMarkdownHeadings(context, 3);
|
|
5
5
|
const normalizedGoal = demoteMarkdownHeadings(goal, 3);
|
|
6
6
|
let content = `# ${title}\n\n## Context\n${normalizedContext}\n\n## Goal\n${normalizedGoal}`;
|
|
7
|
-
content += "\n\n##
|
|
8
|
-
if (
|
|
9
|
-
content += `\n\n### Project
|
|
7
|
+
content += "\n\n## Rules";
|
|
8
|
+
if (baseRules.length > 0) {
|
|
9
|
+
content += `\n\n### Project Rules\nLoaded from: \`${BASE_RULES_SOURCE}\`\n\n${renderMarkdownSections(baseRules)}`;
|
|
10
10
|
}
|
|
11
|
-
content += `\n\n### Task
|
|
11
|
+
content += `\n\n### Task Rules\n${renderMarkdownSections(taskRules)}`;
|
|
12
12
|
content += "\n\n## Acceptance Criteria";
|
|
13
13
|
content += `\n\n### Task Criteria\n${renderMarkdownSections(taskCriteria)}`;
|
|
14
14
|
return content;
|