@astrosheep/keiyaku 1.0.0 → 1.0.2
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 +73 -56
- package/build/.tsbuildinfo +1 -1
- package/build/agents/call-terms_v2.js +103 -0
- package/build/agents/harness/activity-values.js +54 -0
- package/build/agents/harness/event-channel.js +55 -20
- package/build/agents/harness/events.js +47 -1
- package/build/agents/harness/execution-handle.js +292 -61
- package/build/agents/harness/index.js +103 -121
- package/build/agents/harness/pump.js +36 -47
- package/build/agents/harness/runtime.js +3 -0
- package/build/agents/launch-snapshot_v2.js +348 -0
- package/build/agents/opencode-sdk.js +42 -169
- package/build/agents/providers/claude-agent-sdk.js +240 -79
- package/build/agents/providers/codex-app-server.js +280 -72
- package/build/agents/providers/codex-sdk.js +159 -41
- package/build/agents/providers/event-type-tail.js +16 -0
- package/build/agents/providers/opencode-sdk.js +165 -59
- package/build/agents/providers/pi.js +254 -42
- package/build/agents/selector_v2.js +122 -0
- package/build/cli/commands/akuma.js +120 -42
- package/build/cli/completion.js +6 -7
- package/build/cli/flags.js +36 -10
- package/build/cli/help.js +42 -26
- package/build/cli/index.js +225 -93
- package/build/cli/parse.js +194 -75
- package/build/cli/projection-address.js +11 -0
- package/build/cli/render/address.js +88 -0
- package/build/cli/render/arc.js +10 -10
- package/build/cli/render/call.js +59 -0
- package/build/cli/render/compact-text.js +3 -0
- package/build/cli/render/errors.js +4 -2
- package/build/cli/render/format.js +6 -0
- package/build/cli/render/line-width.js +70 -0
- package/build/cli/render/misc.js +7 -4
- package/build/cli/render/petition.js +4 -3
- package/build/cli/render/projection-activity.js +201 -0
- package/build/cli/render/shared.js +46 -12
- package/build/cli/render/status.js +59 -10
- package/build/cli/render/tool-ledger-rollup.js +74 -0
- package/build/cli/render/tool-presentation.js +97 -0
- package/build/cli/render/wait.js +229 -0
- package/build/cli/skills-install.js +42 -24
- package/build/cli/subagent-guard.js +1 -1
- package/build/config/akuma-loader_v2.js +222 -0
- package/build/config/env-keys.js +0 -4
- package/build/config/env.js +0 -6
- package/build/config/settings.js +325 -12
- package/build/core/addressing.js +224 -0
- package/build/core/amend.js +30 -47
- package/build/core/arc.js +68 -60
- package/build/core/bind.js +105 -25
- package/build/core/call-persist.js +69 -0
- package/build/core/call.js +658 -0
- package/build/core/claim.js +72 -44
- package/build/core/command-io.js +19 -19
- package/build/core/context.js +11 -36
- package/build/core/draft.js +2 -3
- package/build/core/entry.js +101 -6
- package/build/core/execution-coordinate.js +62 -0
- package/build/core/execution-pact.js +141 -0
- package/build/core/forfeit.js +2 -3
- package/build/core/hints.js +10 -10
- package/build/core/ids.js +6 -0
- package/build/core/ledger.js +16 -1
- package/build/core/petition-claim-gates.js +14 -12
- package/build/core/petition-claim.js +1 -0
- package/build/core/petition-run.js +6 -2
- package/build/core/petition.js +63 -21
- package/build/core/places.js +106 -13
- package/build/core/projection/generation.js +412 -0
- package/build/core/projection/heart.js +316 -0
- package/build/core/projection/identity.js +81 -0
- package/build/core/projection/leash.js +87 -0
- package/build/core/projection/mint.js +154 -0
- package/build/core/projection-activity.js +219 -0
- package/build/core/projection-coordinate.js +35 -0
- package/build/core/projection-core.js +69 -463
- package/build/core/projection-generation-continuation.js +31 -0
- package/build/core/projection-generation-execution.js +148 -0
- package/build/core/projection-generation-identity.js +17 -0
- package/build/core/projection-generation-launcher.js +90 -0
- package/build/core/projection-generation-process.js +43 -0
- package/build/core/projection-generation-runner.js +144 -0
- package/build/core/projection-generation-runtime.js +15 -0
- package/build/core/projection-generation-store.js +707 -0
- package/build/core/projection-identity.js +11 -0
- package/build/core/projection-life-observer.js +87 -0
- package/build/core/projection-life-protocol.js +95 -0
- package/build/core/projection-mint.js +96 -24
- package/build/core/projection-runner-lock.js +230 -0
- package/build/core/projection-status.js +207 -107
- package/build/core/projection-tells.js +401 -0
- package/build/core/projection-wait.js +155 -0
- package/build/core/projection-wake.js +109 -231
- package/build/core/queue_v2.js +342 -0
- package/build/core/registry.js +6 -3
- package/build/core/render.js +63 -1
- package/build/core/renew.js +80 -53
- package/build/core/scope.js +186 -137
- package/build/core/seal.js +16 -19
- package/build/core/status.js +88 -25
- package/build/core/stored-agent-event.js +101 -0
- package/build/core/transcripts.js +214 -228
- package/build/core/verdict.js +27 -15
- package/build/core/worktree-bootstrap.js +187 -0
- package/build/core/worktree-path.js +72 -18
- package/build/flow-error.js +4 -6
- package/build/generated/version.js +1 -1
- package/build/git/branches.js +6 -1
- package/build/index.js +8 -5
- package/build/keiyaku.js +1 -3
- package/package.json +6 -5
- package/skills/keiyaku/SKILL.md +6 -47
- package/skills/keiyaku-akuma/SKILL.md +75 -0
- package/build/agents/effective-policy.js +0 -32
- package/build/agents/index.js +0 -114
- package/build/agents/selector.js +0 -28
- package/build/cli/render/summon.js +0 -17
- package/build/cli/render/tell.js +0 -11
- package/build/config/akuma-loader.js +0 -268
- package/build/core/queue.js +0 -73
- package/build/core/summon-persist.js +0 -73
- package/build/core/summon.js +0 -377
package/build/core/scope.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { createGit, wrapGitError } from "../git/core.js";
|
|
2
6
|
export class ScopePatternError extends Error {
|
|
3
7
|
code = "INVALID_SCOPE_PATTERN";
|
|
4
8
|
constructor(message) {
|
|
@@ -6,158 +10,203 @@ export class ScopePatternError extends Error {
|
|
|
6
10
|
this.name = "ScopePatternError";
|
|
7
11
|
}
|
|
8
12
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Gitignore trailing spaces are ignored unless escaped with a backslash.
|
|
15
|
+
* Preserve `\ ` (and longer odd-backslash runs) while stripping only unescaped
|
|
16
|
+
* trailing SPACE characters. Tabs and other trailing whitespace stay as written.
|
|
17
|
+
*/
|
|
18
|
+
function stripUnescapedTrailingSpaces(value) {
|
|
19
|
+
let end = value.length;
|
|
20
|
+
while (end > 0 && value[end - 1] === " ") {
|
|
21
|
+
let backslashes = 0;
|
|
22
|
+
for (let index = end - 2; index >= 0 && value[index] === "\\"; index -= 1) {
|
|
23
|
+
backslashes += 1;
|
|
24
|
+
}
|
|
25
|
+
if (backslashes % 2 === 1)
|
|
26
|
+
break;
|
|
27
|
+
end -= 1;
|
|
16
28
|
}
|
|
29
|
+
return value.slice(0, end);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Drop cosmetic framing whitespace without destroying escaped trailing spaces.
|
|
33
|
+
* Leading whitespace is always cosmetic for scope entry; trailing SPACE follows
|
|
34
|
+
* gitignore escape rules.
|
|
35
|
+
*/
|
|
36
|
+
function canonicalizeScopePatternInput(pattern) {
|
|
37
|
+
return stripUnescapedTrailingSpaces(pattern.replace(/^\s+/, ""));
|
|
17
38
|
}
|
|
39
|
+
/** True when a raw Scope Add / list line has no pattern after gitignore blank rules. */
|
|
40
|
+
export function isBlankScopePatternLine(line) {
|
|
41
|
+
return canonicalizeScopePatternInput(line).length === 0;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Canonicalize a scope gitignore pattern for exclude matching and persistence.
|
|
45
|
+
* Strips only a cosmetic leading `./` (including after `!`); preserves meaningful
|
|
46
|
+
* leading `/` anchoring, gitignore backslash escapes, and escaped trailing spaces.
|
|
47
|
+
*/
|
|
18
48
|
export function normalizeScopePattern(pattern) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
throw new ScopePatternError("scope pattern cannot be empty");
|
|
49
|
+
if (pattern.includes("\0") || pattern.includes("\n") || pattern.includes("\r")) {
|
|
50
|
+
throw new ScopePatternError("scope pattern must fit on one line and cannot contain NUL");
|
|
22
51
|
}
|
|
23
|
-
|
|
24
|
-
if (trimmed
|
|
25
|
-
|
|
52
|
+
const trimmed = canonicalizeScopePatternInput(pattern);
|
|
53
|
+
if (!trimmed)
|
|
54
|
+
throw new ScopePatternError("scope pattern cannot be empty");
|
|
55
|
+
const negative = trimmed.startsWith("!");
|
|
56
|
+
let body = negative ? trimmed.slice(1) : trimmed;
|
|
57
|
+
while (body.startsWith("./")) {
|
|
58
|
+
body = body.slice(2);
|
|
26
59
|
}
|
|
27
|
-
|
|
60
|
+
if (!body)
|
|
61
|
+
throw new ScopePatternError("scope pattern cannot be empty");
|
|
62
|
+
return negative ? `!${body}` : body;
|
|
28
63
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Normalize a path to Git diff form for exact waiver comparison:
|
|
66
|
+
* either slash convention, collapse separators, and strip cosmetic `./` or `/`.
|
|
67
|
+
* Leading and trailing spaces are significant path bytes and must remain unchanged.
|
|
68
|
+
*/
|
|
69
|
+
export function normalizeScopePath(filePath) {
|
|
70
|
+
let normalized = filePath.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
71
|
+
while (normalized.startsWith("./")) {
|
|
72
|
+
normalized = normalized.slice(2);
|
|
33
73
|
}
|
|
74
|
+
normalized = normalized.replace(/^\/+/, "");
|
|
75
|
+
if (!normalized || normalized === ".")
|
|
76
|
+
throw new ScopePatternError("scope path cannot be empty");
|
|
77
|
+
if (normalized.includes("\0"))
|
|
78
|
+
throw new ScopePatternError("scope path cannot contain NUL");
|
|
34
79
|
return normalized;
|
|
35
80
|
}
|
|
36
|
-
function
|
|
37
|
-
return
|
|
81
|
+
function runCheckIgnore(gitDir, workTree, cwd, paths) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const child = spawn("git", [
|
|
84
|
+
`--git-dir=${gitDir}`,
|
|
85
|
+
`--work-tree=${workTree}`,
|
|
86
|
+
"check-ignore",
|
|
87
|
+
"--no-index",
|
|
88
|
+
"-z",
|
|
89
|
+
"--stdin",
|
|
90
|
+
], {
|
|
91
|
+
cwd,
|
|
92
|
+
env: {
|
|
93
|
+
...process.env,
|
|
94
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
95
|
+
GCM_INTERACTIVE: "Never",
|
|
96
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
97
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
98
|
+
LC_ALL: "C",
|
|
99
|
+
},
|
|
100
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
101
|
+
});
|
|
102
|
+
const stdout = [];
|
|
103
|
+
const stderr = [];
|
|
104
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
105
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
106
|
+
child.stdin.on("error", reject);
|
|
107
|
+
child.on("error", reject);
|
|
108
|
+
child.on("close", (code) => {
|
|
109
|
+
if (code !== 0 && code !== 1) {
|
|
110
|
+
reject(wrapGitError("check-ignore --no-index -z --stdin", {
|
|
111
|
+
message: `git check-ignore exited with status ${code ?? "unknown"}`,
|
|
112
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
113
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
114
|
+
}, cwd));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
resolve(new Set(Buffer.concat(stdout)
|
|
118
|
+
.toString("utf8")
|
|
119
|
+
.split("\0")
|
|
120
|
+
.filter(Boolean)));
|
|
121
|
+
});
|
|
122
|
+
child.stdin.end(`${paths.join("\0")}\0`);
|
|
123
|
+
});
|
|
38
124
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
125
|
+
export async function pathsMatchingScope(cwd, patterns, filePaths) {
|
|
126
|
+
const normalizedPatterns = patterns.map(normalizeScopePattern);
|
|
127
|
+
const normalizedPaths = filePaths.map(normalizeScopePath);
|
|
128
|
+
if (normalizedPatterns.length === 0 || normalizedPaths.length === 0)
|
|
129
|
+
return new Set();
|
|
130
|
+
const temporaryRoot = await fs.mkdtemp(path.join(os.tmpdir(), "keiyaku-scope-"));
|
|
131
|
+
const gitDir = path.join(temporaryRoot, "scope.git");
|
|
132
|
+
const workTree = path.join(temporaryRoot, "worktree");
|
|
133
|
+
try {
|
|
134
|
+
await Promise.all([fs.mkdir(gitDir), fs.mkdir(workTree)]);
|
|
135
|
+
await createGit(gitDir).init(true);
|
|
136
|
+
await fs.writeFile(path.join(gitDir, "info", "exclude"), `${normalizedPatterns.join("\n")}\n`, "utf8");
|
|
137
|
+
return await runCheckIgnore(gitDir, workTree, cwd, normalizedPaths);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await fs.rm(temporaryRoot, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function isNegativeScopePattern(pattern) {
|
|
144
|
+
return normalizeScopePattern(pattern).startsWith("!");
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Parse only the subset whose path language can be proved statically: literal
|
|
148
|
+
* segments, `*` segments, and `**` segments. Anything using richer gitignore
|
|
149
|
+
* syntax is left to Git and produces no advisory warning.
|
|
150
|
+
*/
|
|
151
|
+
function staticScopeSegments(pattern) {
|
|
152
|
+
const positive = normalizeScopePattern(pattern);
|
|
153
|
+
if (positive.startsWith("!") || positive.startsWith("#"))
|
|
154
|
+
return null;
|
|
155
|
+
const body = positive.startsWith("/") ? positive.slice(1) : positive;
|
|
156
|
+
if (!body || body.endsWith("/"))
|
|
157
|
+
return null;
|
|
158
|
+
const parsed = [];
|
|
159
|
+
for (const segment of body.split("/")) {
|
|
160
|
+
if (!segment || segment === "." || segment === "..")
|
|
161
|
+
return null;
|
|
162
|
+
if (segment === "*")
|
|
163
|
+
parsed.push({ kind: "one" });
|
|
164
|
+
else if (segment === "**")
|
|
165
|
+
parsed.push({ kind: "many" });
|
|
166
|
+
else if (/[*?\[\]\\]/.test(segment))
|
|
167
|
+
return null;
|
|
168
|
+
else
|
|
169
|
+
parsed.push({ kind: "literal", value: segment });
|
|
170
|
+
}
|
|
171
|
+
return parsed;
|
|
41
172
|
}
|
|
42
|
-
function
|
|
43
|
-
if (
|
|
173
|
+
function ordinarySegmentsIntersect(a, b) {
|
|
174
|
+
if (a.kind === "many" || b.kind === "many")
|
|
44
175
|
return true;
|
|
45
|
-
if (
|
|
176
|
+
if (a.kind === "one" || b.kind === "one")
|
|
46
177
|
return true;
|
|
47
|
-
|
|
48
|
-
for (const char of patternSegment) {
|
|
49
|
-
if (char === "*") {
|
|
50
|
-
regexSource += ".*";
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
regexSource += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
54
|
-
}
|
|
55
|
-
regexSource += "$";
|
|
56
|
-
return new RegExp(regexSource).test(pathSegment);
|
|
178
|
+
return a.value === b.value;
|
|
57
179
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
180
|
+
/** Exact non-empty intersection for the finite segment-language subset above. */
|
|
181
|
+
function staticSegmentLanguagesOverlap(a, b) {
|
|
182
|
+
const pending = [[0, 0]];
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
while (pending.length > 0) {
|
|
185
|
+
const [aIndex, bIndex] = pending.pop();
|
|
61
186
|
const key = `${aIndex}:${bIndex}`;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
else if (aIndex < a.length && bIndex < b.length && a[aIndex] === b[bIndex]) {
|
|
80
|
-
result = visit(aIndex + 1, bIndex + 1);
|
|
81
|
-
}
|
|
82
|
-
memo.set(key, result);
|
|
83
|
-
return result;
|
|
84
|
-
};
|
|
85
|
-
return visit(0, 0);
|
|
86
|
-
}
|
|
87
|
-
function canPatternMatchEmpty(segments, index) {
|
|
88
|
-
for (let cursor = index; cursor < segments.length; cursor += 1) {
|
|
89
|
-
if (segments[cursor] !== "**")
|
|
90
|
-
return false;
|
|
187
|
+
if (seen.has(key))
|
|
188
|
+
continue;
|
|
189
|
+
seen.add(key);
|
|
190
|
+
if (aIndex === a.length && bIndex === b.length)
|
|
191
|
+
return true;
|
|
192
|
+
const aSegment = a[aIndex];
|
|
193
|
+
const bSegment = b[bIndex];
|
|
194
|
+
if (aSegment?.kind === "many")
|
|
195
|
+
pending.push([aIndex + 1, bIndex]);
|
|
196
|
+
if (bSegment?.kind === "many")
|
|
197
|
+
pending.push([aIndex, bIndex + 1]);
|
|
198
|
+
if (!aSegment || !bSegment || !ordinarySegmentsIntersect(aSegment, bSegment))
|
|
199
|
+
continue;
|
|
200
|
+
pending.push([
|
|
201
|
+
aSegment.kind === "many" ? aIndex : aIndex + 1,
|
|
202
|
+
bSegment.kind === "many" ? bIndex : bIndex + 1,
|
|
203
|
+
]);
|
|
91
204
|
}
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
export function pathMatches(pattern, path) {
|
|
95
|
-
const patternSegments = splitPattern(pattern);
|
|
96
|
-
const pathSegments = splitPath(path);
|
|
97
|
-
const memo = new Map();
|
|
98
|
-
const visit = (patternIndex, pathIndex) => {
|
|
99
|
-
const key = `${patternIndex}:${pathIndex}`;
|
|
100
|
-
const cached = memo.get(key);
|
|
101
|
-
if (cached !== undefined)
|
|
102
|
-
return cached;
|
|
103
|
-
let result = false;
|
|
104
|
-
if (patternIndex === patternSegments.length && pathIndex === pathSegments.length) {
|
|
105
|
-
result = true;
|
|
106
|
-
}
|
|
107
|
-
else if (patternIndex === patternSegments.length) {
|
|
108
|
-
result = false;
|
|
109
|
-
}
|
|
110
|
-
else if (patternSegments[patternIndex] === "**") {
|
|
111
|
-
result =
|
|
112
|
-
visit(patternIndex + 1, pathIndex) ||
|
|
113
|
-
(pathIndex < pathSegments.length && visit(patternIndex, pathIndex + 1));
|
|
114
|
-
}
|
|
115
|
-
else if (pathIndex < pathSegments.length) {
|
|
116
|
-
result =
|
|
117
|
-
segmentMatchesPattern(patternSegments[patternIndex], pathSegments[pathIndex]) &&
|
|
118
|
-
visit(patternIndex + 1, pathIndex + 1);
|
|
119
|
-
}
|
|
120
|
-
memo.set(key, result);
|
|
121
|
-
return result;
|
|
122
|
-
};
|
|
123
|
-
return visit(0, 0);
|
|
205
|
+
return false;
|
|
124
206
|
}
|
|
207
|
+
/** Advisory only: negative patterns are exclusions, and are intentionally ignored here. */
|
|
125
208
|
export function patternsOverlap(a, b) {
|
|
126
|
-
const aSegments =
|
|
127
|
-
const bSegments =
|
|
128
|
-
|
|
129
|
-
const visit = (aIndex, bIndex) => {
|
|
130
|
-
const key = `${aIndex}:${bIndex}`;
|
|
131
|
-
const cached = memo.get(key);
|
|
132
|
-
if (cached !== undefined)
|
|
133
|
-
return cached;
|
|
134
|
-
let result = false;
|
|
135
|
-
if (aIndex === aSegments.length && bIndex === bSegments.length) {
|
|
136
|
-
result = true;
|
|
137
|
-
}
|
|
138
|
-
else if (aIndex === aSegments.length) {
|
|
139
|
-
result = canPatternMatchEmpty(bSegments, bIndex);
|
|
140
|
-
}
|
|
141
|
-
else if (bIndex === bSegments.length) {
|
|
142
|
-
result = canPatternMatchEmpty(aSegments, aIndex);
|
|
143
|
-
}
|
|
144
|
-
else if (aSegments[aIndex] === "**") {
|
|
145
|
-
result =
|
|
146
|
-
visit(aIndex + 1, bIndex) ||
|
|
147
|
-
(bSegments[bIndex] !== "**" && visit(aIndex, bIndex + 1));
|
|
148
|
-
}
|
|
149
|
-
else if (bSegments[bIndex] === "**") {
|
|
150
|
-
result =
|
|
151
|
-
visit(aIndex, bIndex + 1) ||
|
|
152
|
-
(aSegments[aIndex] !== "**" && visit(aIndex + 1, bIndex));
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
result =
|
|
156
|
-
segmentPatternsOverlap(aSegments[aIndex], bSegments[bIndex]) &&
|
|
157
|
-
visit(aIndex + 1, bIndex + 1);
|
|
158
|
-
}
|
|
159
|
-
memo.set(key, result);
|
|
160
|
-
return result;
|
|
161
|
-
};
|
|
162
|
-
return visit(0, 0);
|
|
209
|
+
const aSegments = staticScopeSegments(a);
|
|
210
|
+
const bSegments = staticScopeSegments(b);
|
|
211
|
+
return aSegments !== null && bSegments !== null && staticSegmentLanguagesOverlap(aSegments, bSegments);
|
|
163
212
|
}
|
package/build/core/seal.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createGit, wrapGitError } from "../git/core.js";
|
|
|
5
5
|
import { KEIYAKU_FILE } from "../keiyaku.js";
|
|
6
6
|
import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
|
|
7
7
|
import { renderContractView } from "./render.js";
|
|
8
|
-
import {
|
|
8
|
+
import { normalizeScopePath, pathsMatchingScope } from "./scope.js";
|
|
9
9
|
import { deriveContractState, isTerminalState } from "./status.js";
|
|
10
10
|
function findBind(entries) {
|
|
11
11
|
const bind = entries.find((entry) => entry.kind === "bind");
|
|
@@ -46,14 +46,8 @@ export function effectiveScope(entries) {
|
|
|
46
46
|
for (const entry of entries) {
|
|
47
47
|
if (entry.kind !== "amend" || !entry.data.scopeDelta)
|
|
48
48
|
continue;
|
|
49
|
-
const remove = new Set(entry.data.scopeDelta.remove);
|
|
50
|
-
for (let index = scope.length - 1; index >= 0; index -= 1) {
|
|
51
|
-
if (remove.has(scope[index]))
|
|
52
|
-
scope.splice(index, 1);
|
|
53
|
-
}
|
|
54
49
|
for (const pattern of entry.data.scopeDelta.add) {
|
|
55
|
-
|
|
56
|
-
scope.push(pattern);
|
|
50
|
+
scope.push(pattern);
|
|
57
51
|
}
|
|
58
52
|
}
|
|
59
53
|
return scope;
|
|
@@ -82,9 +76,8 @@ async function revList(cwd, from, to) {
|
|
|
82
76
|
async function changedPaths(cwd, from, to) {
|
|
83
77
|
const git = createGit(cwd);
|
|
84
78
|
try {
|
|
85
|
-
return (await git.raw(["diff", "--name-only", `${from}..${to}`]))
|
|
86
|
-
.split(
|
|
87
|
-
.map((line) => line.trim())
|
|
79
|
+
return (await git.raw(["diff", "--name-only", "-z", `${from}..${to}`]))
|
|
80
|
+
.split("\0")
|
|
88
81
|
.filter(Boolean);
|
|
89
82
|
}
|
|
90
83
|
catch (error) {
|
|
@@ -109,8 +102,10 @@ async function diffstat(cwd, from, to) {
|
|
|
109
102
|
throw wrapGitError(`diff --numstat ${from}..${to}`, error, cwd);
|
|
110
103
|
}
|
|
111
104
|
}
|
|
112
|
-
function scopeViolations(paths, scope) {
|
|
113
|
-
|
|
105
|
+
async function scopeViolations(cwd, paths, scope) {
|
|
106
|
+
const auditedPaths = paths.filter((changedPath) => changedPath !== KEIYAKU_FILE);
|
|
107
|
+
const matched = await pathsMatchingScope(cwd, scope, auditedPaths);
|
|
108
|
+
return auditedPaths.filter((changedPath) => !matched.has(changedPath));
|
|
114
109
|
}
|
|
115
110
|
function assertOperable(contractId, entries) {
|
|
116
111
|
const state = deriveContractState(entries);
|
|
@@ -121,7 +116,7 @@ function assertOperable(contractId, entries) {
|
|
|
121
116
|
throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot seal from state ${state.state}`);
|
|
122
117
|
}
|
|
123
118
|
}
|
|
124
|
-
export async function captureSeal(cwd, entries, head = "HEAD") {
|
|
119
|
+
export async function captureSeal(cwd, entries, head = "HEAD", waivePaths = []) {
|
|
125
120
|
const open = findOpenArc(entries);
|
|
126
121
|
if (!open)
|
|
127
122
|
return null;
|
|
@@ -133,9 +128,11 @@ export async function captureSeal(cwd, entries, head = "HEAD") {
|
|
|
133
128
|
diffstat(cwd, oldFence, newFence),
|
|
134
129
|
]);
|
|
135
130
|
const scope = effectiveScope(entries);
|
|
136
|
-
const violations = scopeViolations(paths, scope);
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
const violations = await scopeViolations(cwd, paths, scope);
|
|
132
|
+
const waivedPaths = new Set(waivePaths.map(normalizeScopePath));
|
|
133
|
+
const unwaived = violations.filter((violation) => !waivedPaths.has(violation));
|
|
134
|
+
if (unwaived.length > 0) {
|
|
135
|
+
throw new FlowError("INTERNAL_STATE", `scope audit failed for contract ${open.contract}: paths ${unwaived.join(", ")} outside ${scope.join(", ")}`);
|
|
139
136
|
}
|
|
140
137
|
return {
|
|
141
138
|
arc: open.data.arc,
|
|
@@ -143,7 +140,7 @@ export async function captureSeal(cwd, entries, head = "HEAD") {
|
|
|
143
140
|
newFence,
|
|
144
141
|
receipts,
|
|
145
142
|
diffstat: stat,
|
|
146
|
-
scopeAudit: { violations, waived:
|
|
143
|
+
scopeAudit: { violations, waived: violations.length > 0 },
|
|
147
144
|
};
|
|
148
145
|
}
|
|
149
146
|
async function renderUpdatedContract(ledgerCwd, renderCwd, contractId) {
|
|
@@ -159,7 +156,7 @@ export async function sealOpenArc(input) {
|
|
|
159
156
|
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
|
|
160
157
|
}
|
|
161
158
|
assertOperable(input.contractId, ledger.entries);
|
|
162
|
-
const capture = input.capture ?? await captureSeal(input.cwd, ledger.entries);
|
|
159
|
+
const capture = input.capture ?? await captureSeal(input.cwd, ledger.entries, "HEAD", input.waivePaths);
|
|
163
160
|
if (!capture)
|
|
164
161
|
return null;
|
|
165
162
|
const at = new Date(input.nowMs).toISOString();
|
package/build/core/status.js
CHANGED
|
@@ -3,12 +3,22 @@ import * as path from "node:path";
|
|
|
3
3
|
import { assertSettingsKnobUsable, loadKeiyakuSettings, } from "../config/settings.js";
|
|
4
4
|
import { FlowError } from "../flow-error.js";
|
|
5
5
|
import { SETTINGS_FILE } from "../keiyaku.js";
|
|
6
|
+
import { isGitRepo } from "../git/branches.js";
|
|
6
7
|
import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../git/core.js";
|
|
7
|
-
import {
|
|
8
|
+
import { fullBranchRefSchema } from "./entry.js";
|
|
9
|
+
import { listContractIds, readLedger } from "./ledger.js";
|
|
8
10
|
import { readProjectionStatusBoard } from "./projection-status.js";
|
|
9
|
-
import {
|
|
11
|
+
import { deriveQueueReadModel, queueMembership, } from "./queue_v2.js";
|
|
10
12
|
import { findOpenArc, getMaxArc, receiptsAfterFence } from "./seal.js";
|
|
11
13
|
import { stableRepoRoot } from "./worktree-path.js";
|
|
14
|
+
/** Prefix bare branch names and validate the full ref. Sole status/bind normalizer. */
|
|
15
|
+
export function normalizeTargetRef(raw) {
|
|
16
|
+
const ref = raw.startsWith("refs/heads/") ? raw : `refs/heads/${raw}`;
|
|
17
|
+
if (!fullBranchRefSchema.safeParse(ref).success) {
|
|
18
|
+
throw new FlowError("INVALID_TARGET", `invalid target ref '${raw}' — pass a branch name or refs/heads/<name>`);
|
|
19
|
+
}
|
|
20
|
+
return ref;
|
|
21
|
+
}
|
|
12
22
|
function transitionFromEntry(current, entry) {
|
|
13
23
|
if (current === "claimed" || current === "forfeited" || current === "corrupt") {
|
|
14
24
|
return "corrupt";
|
|
@@ -16,6 +26,9 @@ function transitionFromEntry(current, entry) {
|
|
|
16
26
|
switch (entry.kind) {
|
|
17
27
|
case "bind":
|
|
18
28
|
return "corrupt";
|
|
29
|
+
case "bootstrap":
|
|
30
|
+
// Placement legality is owned exclusively by deriveContractState.
|
|
31
|
+
return "corrupt";
|
|
19
32
|
case "arc":
|
|
20
33
|
return current === "bound" || current === "active" ? "active" : "corrupt";
|
|
21
34
|
case "seal":
|
|
@@ -41,9 +54,20 @@ export function deriveContractState(entries) {
|
|
|
41
54
|
if (entries.length === 0 || entries[0]?.kind !== "bind") {
|
|
42
55
|
return { state: "corrupt", at: 0, reason: "ledger must start with bind" };
|
|
43
56
|
}
|
|
57
|
+
const bind = entries[0];
|
|
44
58
|
let state = "bound";
|
|
45
59
|
for (let index = 1; index < entries.length; index += 1) {
|
|
46
60
|
const entry = entries[index];
|
|
61
|
+
if (entry.kind === "bootstrap") {
|
|
62
|
+
if (index !== 1 || state !== "bound") {
|
|
63
|
+
return { state: "corrupt", at: index + 1, reason: "bootstrap must immediately follow bind" };
|
|
64
|
+
}
|
|
65
|
+
if (!bind.data.bootstrap) {
|
|
66
|
+
return { state: "corrupt", at: index + 1, reason: "bootstrap without bind plan" };
|
|
67
|
+
}
|
|
68
|
+
// Legal bootstrap is a durable fact and leaves the derived state bound.
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
47
71
|
const next = transitionFromEntry(state, entry);
|
|
48
72
|
if (next === "corrupt") {
|
|
49
73
|
return { state: "corrupt", at: index + 1, reason: `invalid ${entry.kind} after ${state}` };
|
|
@@ -52,6 +76,14 @@ export function deriveContractState(entries) {
|
|
|
52
76
|
}
|
|
53
77
|
return { state };
|
|
54
78
|
}
|
|
79
|
+
export function deriveBootstrapDiagnostics(entries) {
|
|
80
|
+
const bind = entries.find((entry) => entry.kind === "bind");
|
|
81
|
+
if (!bind?.data.bootstrap)
|
|
82
|
+
return [];
|
|
83
|
+
if (entries.some((entry) => entry.kind === "bootstrap"))
|
|
84
|
+
return [];
|
|
85
|
+
return [{ kind: "plan-without-result" }];
|
|
86
|
+
}
|
|
55
87
|
export function isTerminalState(state) {
|
|
56
88
|
return state.state === "claimed" || state.state === "forfeited";
|
|
57
89
|
}
|
|
@@ -79,20 +111,16 @@ export function latestBase(entries) {
|
|
|
79
111
|
}
|
|
80
112
|
return entries.find((entry) => entry.kind === "bind")?.data.base;
|
|
81
113
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
throw wrapGitError(`for-each-ref ${refPrefix}`, error, cwd);
|
|
95
|
-
}
|
|
114
|
+
/** Commission settlement target from the bind entry. Schema guarantees refs/heads/<name>. */
|
|
115
|
+
export function commissionTarget(entries) {
|
|
116
|
+
const bind = findBindEntry(entries);
|
|
117
|
+
if (!bind)
|
|
118
|
+
return undefined;
|
|
119
|
+
const ref = bind.data.target;
|
|
120
|
+
return {
|
|
121
|
+
ref,
|
|
122
|
+
branch: ref.slice("refs/heads/".length),
|
|
123
|
+
};
|
|
96
124
|
}
|
|
97
125
|
async function resolveCommit(cwd, ref) {
|
|
98
126
|
const git = createGit(cwd);
|
|
@@ -366,8 +394,19 @@ async function computeClaimReconciliation(cwd, defaultBranch) {
|
|
|
366
394
|
}
|
|
367
395
|
return { defaultBranch: defaultBranch.branch, head: defaultBranch.head, unclaimedSegments: segments, receiptIssues: issues };
|
|
368
396
|
}
|
|
369
|
-
export async function readKanshiBoard(cwd, now) {
|
|
397
|
+
export async function readKanshiBoard(cwd, now, opts) {
|
|
398
|
+
const targetFilter = opts?.target ?? null;
|
|
370
399
|
const loadedSettings = await loadKeiyakuSettings(cwd);
|
|
400
|
+
if (!(await isGitRepo(cwd))) {
|
|
401
|
+
return {
|
|
402
|
+
contracts: [],
|
|
403
|
+
projections: null,
|
|
404
|
+
queueLength: 0,
|
|
405
|
+
diseases: loadedSettings.diseases,
|
|
406
|
+
claimReconciliation: null,
|
|
407
|
+
targetFilter,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
371
410
|
// Status is a pure reader: use the same resolver, but retain its ability to
|
|
372
411
|
// open when the defaultBranch knob itself is diseased.
|
|
373
412
|
let defaultBranch = null;
|
|
@@ -377,16 +416,16 @@ export async function readKanshiBoard(cwd, now) {
|
|
|
377
416
|
catch {
|
|
378
417
|
defaultBranch = null;
|
|
379
418
|
}
|
|
380
|
-
const
|
|
419
|
+
const contractIds = await listContractIds(cwd);
|
|
381
420
|
const terminalById = new Map();
|
|
382
421
|
const ledgers = [];
|
|
383
|
-
const
|
|
384
|
-
const queuePositions = queuePositionsFromEntries(queueEntries);
|
|
422
|
+
const readableLedgers = [];
|
|
385
423
|
for (const id of contractIds) {
|
|
386
424
|
try {
|
|
387
425
|
const ledger = await readLedger(cwd, id);
|
|
388
426
|
if (ledger) {
|
|
389
427
|
ledgers.push(ledger);
|
|
428
|
+
readableLedgers.push(ledger);
|
|
390
429
|
terminalById.set(id, isTerminalState(deriveContractState(ledger.entries)));
|
|
391
430
|
}
|
|
392
431
|
}
|
|
@@ -394,9 +433,15 @@ export async function readKanshiBoard(cwd, now) {
|
|
|
394
433
|
ledgers.push({ id, error: error instanceof Error ? error : new Error(String(error)) });
|
|
395
434
|
}
|
|
396
435
|
}
|
|
436
|
+
const queueModel = deriveQueueReadModel(readableLedgers);
|
|
437
|
+
const boardQueueLength = queueModel.targetQueues.reduce((sum, queue) => sum + queue.ordered.length, 0);
|
|
397
438
|
const contracts = [];
|
|
439
|
+
// Readable-ledger commission targets retained for post-assembly filter only.
|
|
440
|
+
const derivedTargetById = new Map();
|
|
398
441
|
for (const item of ledgers) {
|
|
399
442
|
if ("error" in item) {
|
|
443
|
+
// Unreadable ledgers have no derivable target and stay under every filter.
|
|
444
|
+
derivedTargetById.set(item.id, undefined);
|
|
400
445
|
contracts.push({
|
|
401
446
|
id: item.id,
|
|
402
447
|
slug: contractSlugFromId(item.id),
|
|
@@ -408,10 +453,13 @@ export async function readKanshiBoard(cwd, now) {
|
|
|
408
453
|
baseDrift: { state: "unknown", label: "n/a (corrupt ledger)" },
|
|
409
454
|
after: [],
|
|
410
455
|
corruptReason: item.error.message,
|
|
456
|
+
bootstrapDiagnostics: [],
|
|
457
|
+
queueDiagnostics: [],
|
|
411
458
|
});
|
|
412
459
|
continue;
|
|
413
460
|
}
|
|
414
461
|
const bind = findBindEntry(item.entries);
|
|
462
|
+
derivedTargetById.set(item.contractId, commissionTarget(item.entries)?.ref);
|
|
415
463
|
const base = latestBase(item.entries);
|
|
416
464
|
const state = deriveContractState(item.entries);
|
|
417
465
|
if (isTerminalState(state))
|
|
@@ -442,15 +490,18 @@ export async function readKanshiBoard(cwd, now) {
|
|
|
442
490
|
const sealForecast = openArc ? `seals a${openArc.data.arc} (${receiptCount} receipts), ` : "";
|
|
443
491
|
nextStep.hint = `run: keiyaku @${item.contractId} renew — ${sealForecast}follows ${baseDrift.defaultBranch} ▲${baseDrift.behindBy}`;
|
|
444
492
|
}
|
|
493
|
+
const membership = queueMembership(queueModel, item.contractId);
|
|
494
|
+
const queuePosition = membership?.position ?? null;
|
|
495
|
+
const targetQueueLength = membership?.queueLength ?? 0;
|
|
445
496
|
contracts.push({
|
|
446
497
|
id: item.contractId,
|
|
447
498
|
slug: contractSlugFromId(item.contractId),
|
|
448
499
|
state,
|
|
449
|
-
queuePosition
|
|
500
|
+
queuePosition,
|
|
450
501
|
queueHeadStalled: isQueueHeadStall({
|
|
451
502
|
entries: item.entries,
|
|
452
|
-
queuePosition
|
|
453
|
-
queueLength:
|
|
503
|
+
queuePosition,
|
|
504
|
+
queueLength: targetQueueLength,
|
|
454
505
|
state,
|
|
455
506
|
}),
|
|
456
507
|
arcs: getMaxArc(item.entries),
|
|
@@ -461,16 +512,28 @@ export async function readKanshiBoard(cwd, now) {
|
|
|
461
512
|
base,
|
|
462
513
|
baseDrift,
|
|
463
514
|
after,
|
|
515
|
+
bootstrapDiagnostics: deriveBootstrapDiagnostics(item.entries),
|
|
516
|
+
queueDiagnostics: membership ? [...membership.diagnostics] : [],
|
|
464
517
|
});
|
|
465
518
|
}
|
|
519
|
+
const filteredContracts = targetFilter === null
|
|
520
|
+
? contracts
|
|
521
|
+
: contracts.filter((contract) => {
|
|
522
|
+
const derived = derivedTargetById.get(contract.id);
|
|
523
|
+
return derived === undefined || derived === targetFilter;
|
|
524
|
+
});
|
|
525
|
+
const queueLength = targetFilter === null
|
|
526
|
+
? boardQueueLength
|
|
527
|
+
: (queueModel.targetQueues.find((queue) => queue.target === targetFilter)?.ordered.length ?? 0);
|
|
466
528
|
const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
|
|
467
529
|
return {
|
|
468
|
-
contracts,
|
|
530
|
+
contracts: filteredContracts,
|
|
469
531
|
projections: readProjectionStatusBoard(cwd, now),
|
|
470
|
-
queueLength
|
|
532
|
+
queueLength,
|
|
471
533
|
defaultBranch: defaultBranch?.branch,
|
|
472
534
|
defaultHead: defaultBranch?.head,
|
|
473
535
|
diseases: loadedSettings.diseases,
|
|
474
536
|
claimReconciliation,
|
|
537
|
+
targetFilter,
|
|
475
538
|
};
|
|
476
539
|
}
|