@astrosheep/keiyaku 0.1.47 → 0.1.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/.tsbuildinfo +1 -1
- package/build/agents/round-runner.js +1 -1
- package/build/agents/selector.js +1 -1
- package/build/common/constants.js +6 -1
- package/build/common/errors.js +1 -1
- package/build/common/response-style.js +4 -1
- package/build/config/term-presets/constants.js +24 -0
- package/build/config/term-presets/default-preset.js +119 -0
- package/build/config/term-presets/index.js +5 -0
- package/build/config/term-presets/mischief-preset.js +105 -0
- package/build/config/term-presets/pocket-preset.js +105 -0
- package/build/config/term-presets/resolver.js +52 -0
- package/build/config/term-presets/types.js +1 -0
- package/build/handlers/ask.js +10 -120
- package/build/handlers/close.js +2 -9
- package/build/handlers/drive.js +1 -15
- package/build/handlers/shared.js +1 -2
- package/build/handlers/start.js +0 -17
- package/build/handlers/status.js +2 -6
- package/build/index.js +2 -2
- package/build/types/git-diff.js +1 -0
- package/build/utils/ask-history.js +75 -0
- package/build/utils/draft.js +22 -0
- package/build/utils/git-diff/constants.js +9 -0
- package/build/utils/git-diff/filter.js +70 -0
- package/build/utils/git-diff/incremental.js +111 -0
- package/build/utils/git-diff/index.js +3 -0
- package/build/utils/git-diff/parsers.js +160 -0
- package/build/utils/git-diff/preview.js +157 -0
- package/build/utils/git-diff/stat.js +27 -0
- package/build/utils/git-diff/types.js +1 -0
- package/build/utils/git-ops.js +9 -0
- package/build/utils/git.js +1 -1
- package/build/utils/keiyaku-document/index.js +13 -0
- package/build/utils/keiyaku-document/lex.js +178 -0
- package/build/utils/keiyaku-document/parser.js +242 -0
- package/build/utils/keiyaku-document/render.js +68 -0
- package/build/utils/keiyaku-document/sections.js +105 -0
- package/build/utils/keiyaku-document/types.js +6 -0
- package/build/utils/keiyaku-document.test.js +12 -1
- package/build/utils/trace.js +6 -7
- package/build/workflow/ask-execution.js +81 -0
- package/build/workflow/drive.js +10 -5
- package/build/workflow/iterate-plan.js +1 -1
- package/build/workflow/keiyaku-draft.js +1 -1
- package/build/workflow/{contract.js → keiyaku.js} +2 -2
- package/build/workflow/markdown-normalization.js +3 -2
- package/build/workflow/present.js +68 -13
- package/build/workflow/response-builders.js +75 -44
- package/build/workflow/response-meta.js +15 -0
- package/build/workflow/response-renderer.js +2 -13
- package/build/workflow/round-summary.js +1 -1
- package/build/workflow/start.js +8 -3
- package/build/workflow/status.js +129 -62
- package/package.json +1 -1
- package/build/config/term-presets.js +0 -398
- package/build/utils/git-diff.js +0 -519
- package/build/utils/keiyaku-document.js +0 -539
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function renderListItemFirstLine(item) {
|
|
2
|
+
const content = renderNodeContent(item);
|
|
3
|
+
if (!content) {
|
|
4
|
+
return `${" ".repeat(item.indent)}${item.marker}`;
|
|
5
|
+
}
|
|
6
|
+
const [firstLine, ...rest] = content.split("\n");
|
|
7
|
+
const prefix = `${" ".repeat(item.indent)}${item.marker}${firstLine.length > 0 ? " " : ""}${firstLine}`;
|
|
8
|
+
if (rest.length === 0) {
|
|
9
|
+
return prefix;
|
|
10
|
+
}
|
|
11
|
+
return [prefix, ...rest].join("\n");
|
|
12
|
+
}
|
|
13
|
+
export function renderBlock(node) {
|
|
14
|
+
switch (node.type) {
|
|
15
|
+
case "section": {
|
|
16
|
+
const header = `${"#".repeat(node.level)} ${node.title}`;
|
|
17
|
+
const body = renderNodeContent(node);
|
|
18
|
+
return body ? `${header}\n${body}` : header;
|
|
19
|
+
}
|
|
20
|
+
case "list":
|
|
21
|
+
return node.items.map((item) => renderListItemFirstLine(item)).join("\n");
|
|
22
|
+
case "code_block":
|
|
23
|
+
return node.lines.join("\n");
|
|
24
|
+
case "blockquote":
|
|
25
|
+
return node.lines.map((line) => `${node.marker}${line}`).join("\n");
|
|
26
|
+
case "text":
|
|
27
|
+
return node.value;
|
|
28
|
+
case "heading":
|
|
29
|
+
return `${"#".repeat(node.level)} ${node.text}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function renderNodeContent(node) {
|
|
33
|
+
switch (node.type) {
|
|
34
|
+
case "document":
|
|
35
|
+
return node.children.map((child) => renderBlock(child)).join("\n");
|
|
36
|
+
case "section":
|
|
37
|
+
return node.children.map((child) => renderBlock(child)).join("\n");
|
|
38
|
+
case "list":
|
|
39
|
+
return renderBlock(node);
|
|
40
|
+
case "list_item":
|
|
41
|
+
return node.children.map((child) => renderBlock(child)).join("\n");
|
|
42
|
+
case "code_block":
|
|
43
|
+
return node.lines.join("\n");
|
|
44
|
+
case "blockquote":
|
|
45
|
+
return node.lines.map((line) => `${node.marker}${line}`).join("\n");
|
|
46
|
+
case "text":
|
|
47
|
+
return node.value;
|
|
48
|
+
case "heading":
|
|
49
|
+
return `${"#".repeat(node.level)} ${node.text}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function renderSectionContent(sectionNode) {
|
|
53
|
+
return sectionNode.children.map((child) => renderBlock(child)).join("\n");
|
|
54
|
+
}
|
|
55
|
+
export function extractListItems(sectionNode) {
|
|
56
|
+
const items = [];
|
|
57
|
+
for (const child of sectionNode.children) {
|
|
58
|
+
if (child.type !== "list")
|
|
59
|
+
continue;
|
|
60
|
+
for (const item of child.items) {
|
|
61
|
+
const rendered = renderNodeContent(item).trimEnd();
|
|
62
|
+
if (rendered.trim().length > 0) {
|
|
63
|
+
items.push(rendered);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return items;
|
|
68
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { parseToAST } from "./parser.js";
|
|
2
|
+
import { extractListItems, renderBlock, renderSectionContent } from "./render.js";
|
|
3
|
+
import { KeiyakuParseError } from "./types.js";
|
|
4
|
+
function normalizeSectionTitle(title) {
|
|
5
|
+
return title.trim().toLowerCase().replace(/\s+/g, " ");
|
|
6
|
+
}
|
|
7
|
+
function splitByHeadingBlocks(sectionNode) {
|
|
8
|
+
const groups = [];
|
|
9
|
+
let currentGroup = [];
|
|
10
|
+
const commitGroup = () => {
|
|
11
|
+
if (currentGroup.length === 0)
|
|
12
|
+
return;
|
|
13
|
+
const rendered = currentGroup.map((node) => renderBlock(node)).join("\n").trim();
|
|
14
|
+
if (rendered.length > 0) {
|
|
15
|
+
groups.push(rendered);
|
|
16
|
+
}
|
|
17
|
+
currentGroup = [];
|
|
18
|
+
};
|
|
19
|
+
for (const child of sectionNode.children) {
|
|
20
|
+
if (child.type === "heading" && child.level >= 2) {
|
|
21
|
+
commitGroup();
|
|
22
|
+
currentGroup = [child];
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
currentGroup.push(child);
|
|
26
|
+
}
|
|
27
|
+
commitGroup();
|
|
28
|
+
return groups;
|
|
29
|
+
}
|
|
30
|
+
export function hasTopLevelHeaders(text) {
|
|
31
|
+
const ast = parseToAST(text, { allowSections: false });
|
|
32
|
+
return (() => {
|
|
33
|
+
const stack = [ast];
|
|
34
|
+
while (stack.length > 0) {
|
|
35
|
+
const node = stack.pop();
|
|
36
|
+
if (!node)
|
|
37
|
+
continue;
|
|
38
|
+
if (node.type === "heading" && node.level <= 2)
|
|
39
|
+
return true;
|
|
40
|
+
// We intentionally do not inspect code block lines for headings.
|
|
41
|
+
if (node.type === "code_block" || node.type === "text")
|
|
42
|
+
continue;
|
|
43
|
+
if (node.type === "document" || node.type === "section" || node.type === "list_item") {
|
|
44
|
+
stack.push(...node.children);
|
|
45
|
+
}
|
|
46
|
+
else if (node.type === "list") {
|
|
47
|
+
stack.push(...node.items);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
})();
|
|
52
|
+
}
|
|
53
|
+
export function parseMarkdownListSection(text) {
|
|
54
|
+
const ast = parseToAST(text, { allowSections: false });
|
|
55
|
+
const pseudoSection = {
|
|
56
|
+
type: "section",
|
|
57
|
+
level: 2,
|
|
58
|
+
title: "",
|
|
59
|
+
children: ast.children,
|
|
60
|
+
};
|
|
61
|
+
const listItems = extractListItems(pseudoSection);
|
|
62
|
+
if (listItems.length > 0) {
|
|
63
|
+
return listItems;
|
|
64
|
+
}
|
|
65
|
+
const nonHeadingSection = {
|
|
66
|
+
type: "section",
|
|
67
|
+
level: 2,
|
|
68
|
+
title: "",
|
|
69
|
+
children: pseudoSection.children.filter((child) => child.type !== "heading"),
|
|
70
|
+
};
|
|
71
|
+
const fallback = renderSectionContent(nonHeadingSection).trim();
|
|
72
|
+
return fallback ? [fallback] : [];
|
|
73
|
+
}
|
|
74
|
+
export function parseMarkdownSections(text) {
|
|
75
|
+
const ast = parseToAST(text, { allowSections: false });
|
|
76
|
+
const pseudoSection = {
|
|
77
|
+
type: "section",
|
|
78
|
+
level: 2,
|
|
79
|
+
title: "",
|
|
80
|
+
children: ast.children,
|
|
81
|
+
};
|
|
82
|
+
return splitByHeadingBlocks(pseudoSection);
|
|
83
|
+
}
|
|
84
|
+
export function parseMarkdownStructure(content) {
|
|
85
|
+
const ast = parseToAST(content);
|
|
86
|
+
const sections = new Map();
|
|
87
|
+
let title;
|
|
88
|
+
for (const node of ast.children) {
|
|
89
|
+
if (node.type !== "section")
|
|
90
|
+
continue;
|
|
91
|
+
if (node.level === 1 && !title) {
|
|
92
|
+
title = node.title.trim() || undefined;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (node.level !== 2)
|
|
96
|
+
continue;
|
|
97
|
+
const normalizedSection = normalizeSectionTitle(node.title);
|
|
98
|
+
if (sections.has(normalizedSection)) {
|
|
99
|
+
throw new KeiyakuParseError(`invalid keiyaku draft: duplicate section header '${normalizedSection}'`);
|
|
100
|
+
}
|
|
101
|
+
const body = renderSectionContent(node).trim();
|
|
102
|
+
sections.set(normalizedSection, body ? body.split("\n") : []);
|
|
103
|
+
}
|
|
104
|
+
return { title, sections };
|
|
105
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { extractListItems, hasTopLevelHeaders, KeiyakuParseError, parseToAST, parseMarkdownStructure, parseMarkdownListSection, renderSectionContent, } from "./keiyaku-document.js";
|
|
3
|
+
import { extractListItems, hasTopLevelHeaders, KeiyakuParseError, parseToAST, parseMarkdownStructure, parseMarkdownListSection, renderSectionContent, } from "./keiyaku-document/index.js";
|
|
4
4
|
import { computeHeadingDelta, demoteMarkdownHeadings, renderMarkdownSections, } from "../workflow/markdown-normalization.js";
|
|
5
5
|
test("parseToAST models H1/H2 sections and keeps H3+ headers as heading nodes", () => {
|
|
6
6
|
const markdown = [
|
|
@@ -55,6 +55,17 @@ test("parseToAST keeps fenced code blocks as structured nodes", () => {
|
|
|
55
55
|
assert.equal(section.children[0]?.type === "code_block" ? section.children[0].lines.join("\n") : "", "```md\n## Not A Section\n```");
|
|
56
56
|
assert.equal(section.children[1]?.type === "text" ? section.children[1].value : "", "After fence.");
|
|
57
57
|
});
|
|
58
|
+
test("parseToAST keeps blockquote blocks as structured nodes", () => {
|
|
59
|
+
const markdown = ["## Summary", "> ## Outcome", "> Implemented parser.", ">", "> ## Changes", "> - Added tests."].join("\n");
|
|
60
|
+
const ast = parseToAST(markdown);
|
|
61
|
+
const section = ast.children[0];
|
|
62
|
+
assert.equal(section?.type, "section");
|
|
63
|
+
if (section?.type !== "section")
|
|
64
|
+
return;
|
|
65
|
+
assert.equal(section.children[0]?.type, "blockquote");
|
|
66
|
+
assert.equal(section.children[0]?.type === "blockquote" ? section.children[0].value : "", "## Outcome\nImplemented parser.\n\n## Changes\n- Added tests.");
|
|
67
|
+
assert.equal(section.children[0]?.type === "blockquote" ? renderSectionContent(section) : "", "> ## Outcome\n> Implemented parser.\n> \n> ## Changes\n> - Added tests.");
|
|
68
|
+
});
|
|
58
69
|
test("parseMarkdownListSection flattens list items and ignores source headers", () => {
|
|
59
70
|
const markdown = [
|
|
60
71
|
"## Group A",
|
package/build/utils/trace.js
CHANGED
|
@@ -2,10 +2,12 @@ import * as fs from "fs/promises";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { TRACE_FILE } from "../common/constants.js";
|
|
4
4
|
import { RESPONSE_MARKERS } from "../common/response-style.js";
|
|
5
|
-
import { parseToAST } from "./keiyaku-document.js";
|
|
5
|
+
import { parseToAST } from "./keiyaku-document/index.js";
|
|
6
6
|
const DIFF_COORDINATES_SECTION_PREFIX = "Diff Coordinates";
|
|
7
7
|
const DIFF_COORDINATES_FENCE = "```keiyaku-coords";
|
|
8
8
|
const DIFF_COORDINATE_LINE_RE = /^(.*):(\d+)(?:-(\d+))?$/;
|
|
9
|
+
const BLOCKQUOTE_PREFIX = "> ";
|
|
10
|
+
const EMPTY_BLOCKQUOTE_LINE = ">";
|
|
9
11
|
async function fileExists(cwd, filepath) {
|
|
10
12
|
try {
|
|
11
13
|
await fs.access(path.join(cwd, filepath));
|
|
@@ -33,6 +35,7 @@ export async function appendReview(cwd, round, reason) {
|
|
|
33
35
|
export async function appendRoundReport(cwd, report) {
|
|
34
36
|
const summaryText = report.summary.trim().length > 0 ? report.summary : "Unknown error.";
|
|
35
37
|
const tracePath = path.join(cwd, TRACE_FILE);
|
|
38
|
+
const quotedSummary = summaryText.split(/\r?\n/).map((line) => (line.length > 0 ? `${BLOCKQUOTE_PREFIX}${line}` : EMPTY_BLOCKQUOTE_LINE));
|
|
36
39
|
const lines = [
|
|
37
40
|
"",
|
|
38
41
|
"---",
|
|
@@ -41,9 +44,7 @@ export async function appendRoundReport(cwd, report) {
|
|
|
41
44
|
"### Status",
|
|
42
45
|
report.status,
|
|
43
46
|
"### Summary",
|
|
44
|
-
|
|
45
|
-
summaryText,
|
|
46
|
-
"```",
|
|
47
|
+
...quotedSummary,
|
|
47
48
|
];
|
|
48
49
|
if (report.errorMessage) {
|
|
49
50
|
lines.push("### Error", report.errorMessage);
|
|
@@ -62,9 +63,7 @@ export async function appendRoundSystemNote(cwd, round, note) {
|
|
|
62
63
|
"### Status",
|
|
63
64
|
"FAILED",
|
|
64
65
|
"### Summary",
|
|
65
|
-
|
|
66
|
-
"Subagent execution cancelled by user/client.",
|
|
67
|
-
"```",
|
|
66
|
+
`${BLOCKQUOTE_PREFIX}Subagent execution cancelled by user/client.`,
|
|
68
67
|
"### Error",
|
|
69
68
|
note,
|
|
70
69
|
"",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { TRACE_FILE } from "../common/constants.js";
|
|
2
|
+
import { appendAsk } from "../utils/trace.js";
|
|
3
|
+
import { appendDebugLog } from "../utils/debug-log.js";
|
|
4
|
+
import { logWarn } from "../utils/logger.js";
|
|
5
|
+
import { runAsk } from "./ask.js";
|
|
6
|
+
import { assertKeiyakuProtocolFiles } from "./keiyaku.js";
|
|
7
|
+
import { persistAskHistory } from "../utils/ask-history.js";
|
|
8
|
+
import * as git from "../utils/git.js";
|
|
9
|
+
const ASK_TRACE_COMMIT_PREFIX = "ask: ";
|
|
10
|
+
const ASK_TRACE_COMMIT_SUMMARY_MAX_CHARS = 50;
|
|
11
|
+
function buildAskTraceCommitMessage(request) {
|
|
12
|
+
const summary = request.replaceAll(/\s+/g, " ").trim().slice(0, ASK_TRACE_COMMIT_SUMMARY_MAX_CHARS);
|
|
13
|
+
return `${ASK_TRACE_COMMIT_PREFIX}${summary}`;
|
|
14
|
+
}
|
|
15
|
+
export async function runAskAndPersist(input) {
|
|
16
|
+
const result = await runAsk(input);
|
|
17
|
+
let warning;
|
|
18
|
+
let savedTo;
|
|
19
|
+
try {
|
|
20
|
+
let commit;
|
|
21
|
+
try {
|
|
22
|
+
commit = await git.getLatestCommitHash(input.cwd);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Ask can run outside git repos; history should still be persisted.
|
|
26
|
+
commit = undefined;
|
|
27
|
+
}
|
|
28
|
+
savedTo = await persistAskHistory({
|
|
29
|
+
cwd: input.cwd,
|
|
30
|
+
request: input.request,
|
|
31
|
+
context: input.context,
|
|
32
|
+
summary: result.summary,
|
|
33
|
+
sessionId: input.sessionId,
|
|
34
|
+
branch: result.currentBranch,
|
|
35
|
+
commit,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (historyError) {
|
|
39
|
+
const historyErrorMessage = historyError instanceof Error ? historyError.message : String(historyError);
|
|
40
|
+
appendDebugLog(`tool ask history logging failed: ${historyErrorMessage}`, {
|
|
41
|
+
cwd: input.cwd,
|
|
42
|
+
section: "script",
|
|
43
|
+
});
|
|
44
|
+
warning = `Failed to persist ask history: ${historyErrorMessage}`;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await appendAsk(input.cwd, {
|
|
48
|
+
request: input.request,
|
|
49
|
+
context: input.context,
|
|
50
|
+
summary: result.summary,
|
|
51
|
+
diffStats: result.diffStats,
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
const activeState = await git.getActiveKeiyakuGitState(input.cwd);
|
|
55
|
+
if (activeState) {
|
|
56
|
+
await assertKeiyakuProtocolFiles(input.cwd);
|
|
57
|
+
try {
|
|
58
|
+
await git.addFiles(input.cwd, TRACE_FILE);
|
|
59
|
+
await git.commit(input.cwd, buildAskTraceCommitMessage(input.request));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
const commitWarning = `Failed to commit ask trace update: ${message}`;
|
|
64
|
+
logWarn(commitWarning, { cwd: input.cwd, section: "script" });
|
|
65
|
+
warning = warning ? `${warning}\n${commitWarning}` : commitWarning;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Non-active keiyaku or missing protocol files should not block ask.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (askTraceError) {
|
|
74
|
+
const askTraceErrorMessage = askTraceError instanceof Error ? askTraceError.message : String(askTraceError);
|
|
75
|
+
appendDebugLog(`tool ask trace logging failed: ${askTraceErrorMessage}`, {
|
|
76
|
+
cwd: input.cwd,
|
|
77
|
+
section: "script",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return { result, warning, savedTo };
|
|
81
|
+
}
|
package/build/workflow/drive.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { KEIYAKU_BRANCH_PREFIX, KEIYAKU_DRAFT_FILE, KEIYAKU_FILE, TRACE_FILE } from "../common/constants.js";
|
|
3
|
+
import { INCREMENTAL_DIFF_MAX_CHARS, KEIYAKU_BRANCH_PREFIX, KEIYAKU_DRAFT_FILE, KEIYAKU_FILE, TRACE_FILE, } from "../common/constants.js";
|
|
4
4
|
import { FlowError, requireText, wrapFlowError } from "../common/errors.js";
|
|
5
5
|
import { appendRoundSystemNote, appendReview, computeTraceState, parseDiffCoordinates, readTraceContent, } from "../utils/trace.js";
|
|
6
6
|
import * as git from "../utils/git.js";
|
|
7
|
-
import { KeiyakuParseError, parseMarkdownStructure } from "../utils/keiyaku-document.js";
|
|
7
|
+
import { KeiyakuParseError, parseMarkdownStructure } from "../utils/keiyaku-document/index.js";
|
|
8
8
|
import { logWarn } from "../utils/logger.js";
|
|
9
9
|
import { renderToolRoundSummary } from "./round-summary.js";
|
|
10
|
-
import { assertCleanWorkingTree,
|
|
10
|
+
import { assertCleanWorkingTree, assertKeiyakuProtocolFiles, buildNoActiveKeiyakuGuidance } from "./keiyaku.js";
|
|
11
11
|
import { buildRoundCommitMessage, runAndRecordRound } from "./round.js";
|
|
12
12
|
import { resolveIncrementalDiffMode } from "../config/incremental-diff-mode.js";
|
|
13
13
|
import { buildIteratePlan } from "./iterate-plan.js";
|
|
14
14
|
import { readBaseRules } from "./base-rules.js";
|
|
15
|
+
import { resolveWorkflowHints } from "./response-meta.js";
|
|
15
16
|
function normalizeSectionTitle(title) {
|
|
16
17
|
return title.trim().toLowerCase().replace(/\s+/g, " ");
|
|
17
18
|
}
|
|
@@ -57,7 +58,7 @@ export async function driveServant(input) {
|
|
|
57
58
|
throw new FlowError("MISSING_KEIYAKU_BASE", `branch ${keiyakuBranch} is missing base metadata; cannot continue drive`);
|
|
58
59
|
}
|
|
59
60
|
await assertCleanWorkingTree(cwd, [KEIYAKU_DRAFT_FILE]);
|
|
60
|
-
await
|
|
61
|
+
await assertKeiyakuProtocolFiles(cwd);
|
|
61
62
|
const keiyakuContent = await fs.readFile(path.join(cwd, KEIYAKU_FILE), "utf-8");
|
|
62
63
|
const parsedKeiyaku = parseKeiyakuStructure(keiyakuContent);
|
|
63
64
|
const traceContent = await readTraceContent(cwd);
|
|
@@ -94,7 +95,10 @@ export async function driveServant(input) {
|
|
|
94
95
|
coordinates = undefined;
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
|
-
const diff = await git.getIncrementalDiff(cwd, incrementalDiffMode,
|
|
98
|
+
const diff = await git.getIncrementalDiff(cwd, incrementalDiffMode, {
|
|
99
|
+
coordinates,
|
|
100
|
+
maxChars: INCREMENTAL_DIFF_MAX_CHARS,
|
|
101
|
+
});
|
|
98
102
|
return {
|
|
99
103
|
status: "success",
|
|
100
104
|
round: plan.targetRound,
|
|
@@ -103,6 +107,7 @@ export async function driveServant(input) {
|
|
|
103
107
|
currentBranch: keiyakuBranch,
|
|
104
108
|
baseBranch,
|
|
105
109
|
commit,
|
|
110
|
+
meta: { hints: resolveWorkflowHints("drive") },
|
|
106
111
|
};
|
|
107
112
|
}
|
|
108
113
|
catch (err) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ITERATE_PRIOR_ROUND_SUMMARY_COUNT } from "../common/constants.js";
|
|
2
2
|
import { FlowError } from "../common/errors.js";
|
|
3
|
-
import { parseToAST, renderSectionContent } from "../utils/keiyaku-document.js";
|
|
3
|
+
import { parseToAST, renderSectionContent } from "../utils/keiyaku-document/index.js";
|
|
4
4
|
import { buildIteratePrompt } from "./prompts.js";
|
|
5
5
|
function readRoundSectionNumber(title) {
|
|
6
6
|
const match = /^round\s+(\d+)$/i.exec(title.trim());
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FlowError } from "../common/errors.js";
|
|
2
|
-
import { KeiyakuParseError, parseToAST, renderSectionContent, } from "../utils/keiyaku-document.js";
|
|
2
|
+
import { KeiyakuParseError, parseToAST, renderSectionContent, } from "../utils/keiyaku-document/index.js";
|
|
3
3
|
import { computeHeadingDelta } from "./markdown-normalization.js";
|
|
4
4
|
const KNOWN_DRAFT_SECTIONS = new Set([
|
|
5
5
|
"goal",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { resolveTermPreset } from "../config/term-presets.js";
|
|
3
|
+
import { resolveTermPreset } from "../config/term-presets/index.js";
|
|
4
4
|
import { KEIYAKU_FILE, TRACE_FILE } from "../common/constants.js";
|
|
5
5
|
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
9
|
const DIRTY_WORKTREE_LIST_LIMIT = 10;
|
|
10
|
-
export async function
|
|
10
|
+
export async function assertKeiyakuProtocolFiles(cwd) {
|
|
11
11
|
const keiyakuPath = path.join(cwd, KEIYAKU_FILE);
|
|
12
12
|
const tracePath = path.join(cwd, TRACE_FILE);
|
|
13
13
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseToAST, renderNodeContent, } from "../utils/keiyaku-document.js";
|
|
1
|
+
import { parseToAST, renderNodeContent, } from "../utils/keiyaku-document/index.js";
|
|
2
2
|
export function computeHeadingDelta(shallowest, minLevel) {
|
|
3
3
|
if (shallowest === null || shallowest >= minLevel) {
|
|
4
4
|
return 0;
|
|
@@ -19,7 +19,7 @@ export function demoteMarkdownHeadings(text, minLevel = 3) {
|
|
|
19
19
|
if (node.type === "heading") {
|
|
20
20
|
shallowest = shallowest === null ? node.level : Math.min(shallowest, node.level);
|
|
21
21
|
}
|
|
22
|
-
if (node.type === "code_block" || node.type === "text")
|
|
22
|
+
if (node.type === "code_block" || node.type === "blockquote" || node.type === "text")
|
|
23
23
|
continue;
|
|
24
24
|
if (node.type === "document" || node.type === "section" || node.type === "list_item") {
|
|
25
25
|
stack.push(...node.children);
|
|
@@ -41,6 +41,7 @@ export function demoteMarkdownHeadings(text, minLevel = 3) {
|
|
|
41
41
|
return nextLevel === node.level ? node : { ...node, level: nextLevel };
|
|
42
42
|
}
|
|
43
43
|
case "code_block":
|
|
44
|
+
case "blockquote":
|
|
44
45
|
case "text":
|
|
45
46
|
return node;
|
|
46
47
|
case "document":
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
3
4
|
import { KEIYAKU_ARCHIVE_TAG_PREFIX, KEIYAKU_BRANCH_PREFIX, KEIYAKU_DRAFT_FILE, KEIYAKU_FILE, TRACE_FILE, } from "../common/constants.js";
|
|
4
|
-
import { resolveTermPreset } from "../config/term-presets.js";
|
|
5
|
+
import { resolveTermPreset } from "../config/term-presets/index.js";
|
|
5
6
|
import { logInfo, logWarn } from "../utils/logger.js";
|
|
6
7
|
import { FlowError, wrapFlowError } from "../common/errors.js";
|
|
8
|
+
import { readDraftSnapshot, restoreDraftSnapshot } from "../utils/draft.js";
|
|
7
9
|
import * as git from "../utils/git.js";
|
|
8
10
|
import { appendVerdict, computeTraceState, readTraceContent } from "../utils/trace.js";
|
|
9
11
|
import { oathMatches } from "./oath.js";
|
|
10
|
-
import { assertCleanWorkingTree,
|
|
12
|
+
import { assertCleanWorkingTree, assertKeiyakuProtocolFiles, buildNoActiveKeiyakuGuidance, resolveOath } from "./keiyaku.js";
|
|
11
13
|
const DIMENSIONS = [
|
|
12
14
|
"placement",
|
|
13
15
|
"exactness",
|
|
@@ -32,6 +34,10 @@ const DIMENSION_STANDARDS = {
|
|
|
32
34
|
const VERDICT_DENIED_CODE = "VERDICT_DENIED";
|
|
33
35
|
const VERDICT_SETTINGS_FILE = path.join(".keiyaku", "settings.json");
|
|
34
36
|
const DIMENSION_KEY_SET = new Set(DIMENSIONS);
|
|
37
|
+
const CLAIM_VERIFICATION_COMMANDS = [
|
|
38
|
+
{ command: "npm", args: ["test"], label: "npm test" },
|
|
39
|
+
{ command: "npm", args: ["run", "test:typecheck"], label: "npm run test:typecheck" },
|
|
40
|
+
];
|
|
35
41
|
function clampThreshold(value, fallback) {
|
|
36
42
|
if (typeof value !== "number" || !Number.isFinite(value))
|
|
37
43
|
return fallback;
|
|
@@ -118,6 +124,58 @@ function collectScores(input) {
|
|
|
118
124
|
cohesive: input.cohesive,
|
|
119
125
|
};
|
|
120
126
|
}
|
|
127
|
+
async function runCommand(options) {
|
|
128
|
+
return await new Promise((resolve, reject) => {
|
|
129
|
+
const child = spawn(options.command, options.args, {
|
|
130
|
+
cwd: options.cwd,
|
|
131
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
132
|
+
env: process.env,
|
|
133
|
+
});
|
|
134
|
+
let stdout = "";
|
|
135
|
+
let stderr = "";
|
|
136
|
+
child.stdout.on("data", (chunk) => {
|
|
137
|
+
stdout += String(chunk);
|
|
138
|
+
});
|
|
139
|
+
child.stderr.on("data", (chunk) => {
|
|
140
|
+
stderr += String(chunk);
|
|
141
|
+
});
|
|
142
|
+
child.on("error", reject);
|
|
143
|
+
child.on("close", (code) => {
|
|
144
|
+
resolve({
|
|
145
|
+
code: typeof code === "number" ? code : -1,
|
|
146
|
+
stdout,
|
|
147
|
+
stderr,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function clipCommandOutput(value, maxChars = 1200) {
|
|
153
|
+
const trimmed = value.trim();
|
|
154
|
+
if (!trimmed)
|
|
155
|
+
return "(no output)";
|
|
156
|
+
if (trimmed.length <= maxChars)
|
|
157
|
+
return trimmed;
|
|
158
|
+
return `${trimmed.slice(0, maxChars)}\n… (+${trimmed.length - maxChars} chars)`;
|
|
159
|
+
}
|
|
160
|
+
async function runClaimVerificationGate(cwd) {
|
|
161
|
+
for (const command of CLAIM_VERIFICATION_COMMANDS) {
|
|
162
|
+
logInfo(`[CLAIM] Running verification: ${command.label}`, { cwd, section: "script", progressOnly: true });
|
|
163
|
+
const result = await runCommand({
|
|
164
|
+
cwd,
|
|
165
|
+
command: command.command,
|
|
166
|
+
args: command.args,
|
|
167
|
+
});
|
|
168
|
+
if (result.code !== 0) {
|
|
169
|
+
throw new FlowError("CLOSE_QUALITY_GATE_FAILED", `CLAIM blocked: verification failed at '${command.label}' (exit ${result.code}).`, {
|
|
170
|
+
suggestion: [
|
|
171
|
+
`Fix failures from '${command.label}', then retry CLAIM.`,
|
|
172
|
+
`stdout:\n${clipCommandOutput(result.stdout)}`,
|
|
173
|
+
`stderr:\n${clipCommandOutput(result.stderr)}`,
|
|
174
|
+
].join("\n\n"),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
121
179
|
export function buildCommandmentFailureMessage(violationTitle, score, threshold, definition, driveCommandName) {
|
|
122
180
|
return `${violationTitle} score too low (${score} < ${threshold})\n${definition}\nIdentify what needs to change to meet this standard. Then use \`${driveCommandName}\` to implement those improvements in the code. Score edits without new implementation evidence are treated as oath violation.`;
|
|
123
181
|
}
|
|
@@ -155,14 +213,7 @@ export async function presentWork(input) {
|
|
|
155
213
|
}
|
|
156
214
|
const title = keiyakuBranch.slice(KEIYAKU_BRANCH_PREFIX.length);
|
|
157
215
|
const petition = input.petition;
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
const stat = await fs.stat(path.join(cwd, KEIYAKU_DRAFT_FILE));
|
|
161
|
-
draftKept = stat.isFile();
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
// ignored
|
|
165
|
-
}
|
|
216
|
+
const draftSnapshot = await readDraftSnapshot(cwd);
|
|
166
217
|
if (petition === "FORFEIT") {
|
|
167
218
|
const archiveTag = `${KEIYAKU_ARCHIVE_TAG_PREFIX}${title}`;
|
|
168
219
|
let round = 0;
|
|
@@ -181,6 +232,9 @@ export async function presentWork(input) {
|
|
|
181
232
|
await git.discardAllWorkingTreeChanges(cwd);
|
|
182
233
|
}
|
|
183
234
|
await git.checkoutBranch(cwd, baseBranch);
|
|
235
|
+
if (draftSnapshot) {
|
|
236
|
+
await restoreDraftSnapshot(cwd, draftSnapshot);
|
|
237
|
+
}
|
|
184
238
|
await git.deleteBranch(cwd, keiyakuBranch, true);
|
|
185
239
|
await git.clearKeiyakuBase(cwd, keiyakuBranch);
|
|
186
240
|
}
|
|
@@ -199,10 +253,10 @@ export async function presentWork(input) {
|
|
|
199
253
|
? `Forfeited without merge. Dropped ${droppedChanges.length} local change(s).`
|
|
200
254
|
: "Forfeited without merge.",
|
|
201
255
|
droppedChanges,
|
|
202
|
-
|
|
256
|
+
draftPath: draftSnapshot?.path ?? null,
|
|
203
257
|
};
|
|
204
258
|
}
|
|
205
|
-
await
|
|
259
|
+
await assertKeiyakuProtocolFiles(cwd);
|
|
206
260
|
let traceContent = await readTraceContent(cwd);
|
|
207
261
|
if (petition === "CLAIM") {
|
|
208
262
|
const driveCommandName = resolveTermPreset().tools.drive.name;
|
|
@@ -239,6 +293,7 @@ export async function presentWork(input) {
|
|
|
239
293
|
await appendDeniedVerdictWithAuditCommit(cwd, title, scores, ["Oath mismatch."]);
|
|
240
294
|
throw oathMismatchError;
|
|
241
295
|
}
|
|
296
|
+
await runClaimVerificationGate(cwd);
|
|
242
297
|
await assertCleanWorkingTree(cwd, [KEIYAKU_DRAFT_FILE]);
|
|
243
298
|
try {
|
|
244
299
|
const invokeDiffLog = `[CLAIM] Collecting diff preview against base '${baseBranch}'`;
|
|
@@ -282,7 +337,7 @@ export async function presentWork(input) {
|
|
|
282
337
|
mergedInto: baseBranch,
|
|
283
338
|
deletedBranch: keiyakuBranch,
|
|
284
339
|
diff,
|
|
285
|
-
|
|
340
|
+
draftPath: draftSnapshot?.path ?? null,
|
|
286
341
|
};
|
|
287
342
|
}
|
|
288
343
|
catch (err) {
|