@astrosheep/keiyaku 2.9.6 → 2.9.8
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/harness/event-persistence.js +7 -5
- package/build/agents/harness/events.js +3 -2
- package/build/agents/harness/outcome.js +10 -0
- package/build/agents/providers/codex-app-server/adapter.js +6 -1
- package/build/agents/providers/codex-app-server/session.js +8 -7
- package/build/agents/selector.js +12 -1
- package/build/cli/commands/akuma/view/handler.js +3 -11
- package/build/cli/commands/contract/amend/handler.js +1 -1
- package/build/cli/commands/contract/amend/meta.js +4 -4
- package/build/cli/commands/contract/bind/handler.js +10 -4
- package/build/cli/commands/contract/bind/meta.js +6 -6
- package/build/cli/commands/contract/petition/handler.js +16 -3
- package/build/cli/commands/contract/petition/meta.js +4 -4
- package/build/cli/commands/metadata.js +3 -2
- package/build/cli/commands/projection/status/handler.js +5 -4
- package/build/cli/commands/projection/status/meta.js +2 -2
- package/build/cli/commands/projection/tell/handler.js +10 -2
- package/build/cli/commands/projection/tell/meta.js +3 -3
- package/build/cli/commands/task/add/meta.js +9 -1
- package/build/cli/commands/task/catalog.js +2 -0
- package/build/cli/commands/task/log/handler.js +12 -0
- package/build/cli/commands/task/log/meta.js +8 -0
- package/build/cli/commands/task/shared.js +2 -1
- package/build/cli/completion.js +8 -0
- package/build/cli/flags.js +8 -0
- package/build/cli/index.js +10 -6
- package/build/cli/parse-flags.js +6 -0
- package/build/cli/parse-metadata.js +1 -1
- package/build/cli/render/kanshi.js +2 -2
- package/build/cli/render/line-width.js +33 -0
- package/build/cli/render/path-prefix-compaction.js +131 -0
- package/build/cli/render/petition.js +4 -0
- package/build/cli/render/projection-activity.js +103 -21
- package/build/cli/render/shared.js +38 -15
- package/build/cli/render/status.js +8 -5
- package/build/cli/render/success-response.js +2 -0
- package/build/cli/render/tool-presentation.js +3 -3
- package/build/cli/render/wait.js +68 -48
- package/build/cli/subagent-guard.js +3 -0
- package/build/cli/types.js +1 -1
- package/build/config/settings/disease.js +4 -4
- package/build/config/settings/loader.js +44 -21
- package/build/core/addressing.js +40 -9
- package/build/core/amend.js +21 -5
- package/build/core/bind.js +111 -6
- package/build/core/call/context.js +19 -3
- package/build/core/call/execution.js +43 -20
- package/build/core/draft.js +1 -1
- package/build/core/ledger-batch.js +194 -0
- package/build/core/projection/generation/database.js +43 -2
- package/build/core/projection/generation/model.js +24 -3
- package/build/core/projection/generation/projection-generation-continuation.js +75 -10
- package/build/core/projection/generation/projection-generation-execution.js +46 -37
- package/build/core/projection/generation/projection-generation-launcher.js +148 -20
- package/build/core/projection/generation/projection-generation-process.js +3 -1
- package/build/core/projection/generation/projection-generation-runner.js +145 -66
- package/build/core/projection/generation/projection-generation-runtime.js +93 -11
- package/build/core/projection/generation/store.js +26 -1
- package/build/core/projection/generation/transitions.js +193 -14
- package/build/core/projection/index.js +5 -5
- package/build/core/projection/projection-core.js +1 -1
- package/build/core/projection/projection-kill.js +53 -10
- package/build/core/projection/projection-life-protocol.js +10 -0
- package/build/core/projection/projection-runner-lock.js +177 -37
- package/build/core/projection/projection-status.js +143 -55
- package/build/core/projection/projection-wait.js +53 -15
- package/build/core/projection/projection-wake.js +177 -46
- package/build/core/projection/tell/database.js +18 -0
- package/build/core/projection/tell/model.js +1 -0
- package/build/core/projection/tell/store.js +102 -55
- package/build/core/registry.js +82 -69
- package/build/core/scope.js +9 -9
- package/build/core/status/board.js +42 -4
- package/build/core/status/drift.js +21 -5
- package/build/core/status/ledger-batch.js +1 -158
- package/build/core/task/index.js +2 -2
- package/build/core/task/task-contract.js +18 -0
- package/build/core/task/task-git-runtime.js +8 -10
- package/build/core/task/task-git-store.js +55 -3
- package/build/core/worktree-path.js +39 -25
- package/build/flow-error.js +1 -1
- package/build/generated/version.js +2 -2
- package/build/git/refs.js +47 -1
- package/package.json +1 -1
- package/skills/keiyaku-akuma/SKILL.md +28 -0
- package/skills/keiyaku-workflow/SKILL.md +142 -18
package/build/core/registry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FlowError } from "../flow-error.js";
|
|
2
2
|
import { createGit, wrapGitError } from "../git/core.js";
|
|
3
3
|
import { readLedger } from "./ledger.js";
|
|
4
|
-
import { appendRefLogWithRetry } from "./ref-log.js";
|
|
4
|
+
import { appendRefLogWithRetry, readRefLog } from "./ref-log.js";
|
|
5
5
|
import { patternsOverlap, scopePathEvidence } from "./scope.js";
|
|
6
6
|
import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
|
|
7
7
|
export const REGISTRY_REF = "refs/keiyaku/registry";
|
|
@@ -39,11 +39,9 @@ async function activeRegistrations(cwd, entries) {
|
|
|
39
39
|
}
|
|
40
40
|
return active;
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
let overlaps = [];
|
|
44
|
-
let trackedPaths;
|
|
42
|
+
async function trackedRepositoryPaths(cwd) {
|
|
45
43
|
try {
|
|
46
|
-
|
|
44
|
+
return (await createGit(cwd).raw(["ls-files", "-z"]))
|
|
47
45
|
.split("\0")
|
|
48
46
|
.filter(Boolean)
|
|
49
47
|
.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
@@ -51,76 +49,91 @@ export async function registerScope(cwd, input) {
|
|
|
51
49
|
catch (error) {
|
|
52
50
|
throw wrapGitError("ls-files -z", error, cwd);
|
|
53
51
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
}
|
|
53
|
+
async function scopeRegistrationAdmission(cwd, input, entries, trackedPaths) {
|
|
54
|
+
const nextOverlaps = [];
|
|
55
|
+
const active = await activeRegistrations(cwd, entries);
|
|
56
|
+
if (input.exclusive && active.length > 0) {
|
|
57
|
+
const blocking = active.find((existing) => existing.contract !== input.contractId) ?? active[0];
|
|
58
|
+
throw new FlowError("ACTIVE_KEIYAKU_EXISTS", `exclusive bind rejected: active contract ${blocking.contract} already exists`);
|
|
59
|
+
}
|
|
60
|
+
for (const existing of active) {
|
|
61
|
+
if (existing.contract === input.contractId)
|
|
62
|
+
continue;
|
|
63
|
+
if (existing.tree === input.tree) {
|
|
64
|
+
throw new FlowError("ACTIVE_KEIYAKU_EXISTS", [
|
|
65
|
+
`tree is already occupied by active contract ${existing.contract}`,
|
|
66
|
+
`Run \`keiyaku forfeit @${existing.contract}\` to free this tree.`,
|
|
67
|
+
].join(" "));
|
|
60
68
|
}
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
const [nextEvidence, existingEvidence] = await Promise.all([
|
|
70
|
+
scopePathEvidence(cwd, input.scope, trackedPaths),
|
|
71
|
+
scopePathEvidence(cwd, existing.scope, trackedPaths),
|
|
72
|
+
]);
|
|
73
|
+
const existingByPath = new Map(existingEvidence.map((evidence) => [evidence.path, evidence]));
|
|
74
|
+
const concreteByPatterns = new Map();
|
|
75
|
+
for (const next of nextEvidence) {
|
|
76
|
+
const prior = existingByPath.get(next.path);
|
|
77
|
+
if (!next.matched || !prior?.matched || !next.decisivePattern || !prior.decisivePattern)
|
|
63
78
|
continue;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
for (const nextPattern of input.scope) {
|
|
103
|
-
for (const existingPattern of existing.scope) {
|
|
104
|
-
if (patternsOverlap(nextPattern, existingPattern)) {
|
|
105
|
-
const [nextPatternEvidence, existingPatternEvidence] = await Promise.all([
|
|
106
|
-
scopePathEvidence(cwd, [nextPattern], trackedPaths),
|
|
107
|
-
scopePathEvidence(cwd, [existingPattern], trackedPaths),
|
|
108
|
-
]);
|
|
109
|
-
const existingPatternByPath = new Map(existingPatternEvidence.map((evidence) => [evidence.path, evidence]));
|
|
110
|
-
const hasCurrentWitness = nextPatternEvidence.some((evidence) => (evidence.matched && existingPatternByPath.get(evidence.path)?.matched));
|
|
111
|
-
if (!hasCurrentWitness) {
|
|
112
|
-
nextOverlaps.push({
|
|
113
|
-
contracts: [input.contractId, existing.contract],
|
|
114
|
-
patterns: [nextPattern, existingPattern],
|
|
115
|
-
kind: "potential",
|
|
116
|
-
paths: { preview: [], count: 0, truncated: false },
|
|
117
|
-
});
|
|
118
|
-
}
|
|
79
|
+
const key = `${next.decisivePattern.index}:${prior.decisivePattern.index}`;
|
|
80
|
+
const group = concreteByPatterns.get(key) ?? {
|
|
81
|
+
next: next.decisivePattern.pattern,
|
|
82
|
+
existing: prior.decisivePattern.pattern,
|
|
83
|
+
paths: [],
|
|
84
|
+
};
|
|
85
|
+
group.paths.push(next.path);
|
|
86
|
+
concreteByPatterns.set(key, group);
|
|
87
|
+
}
|
|
88
|
+
for (const concrete of concreteByPatterns.values()) {
|
|
89
|
+
const preview = concrete.paths.slice(0, SCOPE_OVERLAP_PATH_PREVIEW_LIMIT);
|
|
90
|
+
nextOverlaps.push({
|
|
91
|
+
contracts: [input.contractId, existing.contract],
|
|
92
|
+
patterns: [concrete.next, concrete.existing],
|
|
93
|
+
kind: "concrete",
|
|
94
|
+
paths: {
|
|
95
|
+
preview,
|
|
96
|
+
count: concrete.paths.length,
|
|
97
|
+
truncated: preview.length < concrete.paths.length,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
for (const nextPattern of input.scope) {
|
|
102
|
+
for (const existingPattern of existing.scope) {
|
|
103
|
+
if (patternsOverlap(nextPattern, existingPattern)) {
|
|
104
|
+
const [nextPatternEvidence, existingPatternEvidence] = await Promise.all([
|
|
105
|
+
scopePathEvidence(cwd, [nextPattern], trackedPaths),
|
|
106
|
+
scopePathEvidence(cwd, [existingPattern], trackedPaths),
|
|
107
|
+
]);
|
|
108
|
+
const existingPatternByPath = new Map(existingPatternEvidence.map((evidence) => [evidence.path, evidence]));
|
|
109
|
+
const hasCurrentWitness = nextPatternEvidence.some((evidence) => (evidence.matched && existingPatternByPath.get(evidence.path)?.matched));
|
|
110
|
+
if (!hasCurrentWitness) {
|
|
111
|
+
nextOverlaps.push({
|
|
112
|
+
contracts: [input.contractId, existing.contract],
|
|
113
|
+
patterns: [nextPattern, existingPattern],
|
|
114
|
+
kind: "potential",
|
|
115
|
+
paths: { preview: [], count: 0, truncated: false },
|
|
116
|
+
});
|
|
119
117
|
}
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
|
-
|
|
121
|
+
}
|
|
122
|
+
return nextOverlaps;
|
|
123
|
+
}
|
|
124
|
+
/** Read-only bind admission: validate exclusivity and classify scope overlap. */
|
|
125
|
+
export async function previewScopeRegistration(cwd, input) {
|
|
126
|
+
const [entries, trackedPaths] = await Promise.all([
|
|
127
|
+
readRefLog(cwd, REGISTRY_REF, parseRegistryEntry),
|
|
128
|
+
trackedRepositoryPaths(cwd),
|
|
129
|
+
]);
|
|
130
|
+
return await scopeRegistrationAdmission(cwd, input, entries, trackedPaths);
|
|
131
|
+
}
|
|
132
|
+
export async function registerScope(cwd, input) {
|
|
133
|
+
let overlaps = [];
|
|
134
|
+
const trackedPaths = await trackedRepositoryPaths(cwd);
|
|
135
|
+
const result = await appendRefLogWithRetry(cwd, REGISTRY_REF, registrySubject, parseRegistryEntry, async (entries) => {
|
|
136
|
+
overlaps = await scopeRegistrationAdmission(cwd, input, entries, trackedPaths);
|
|
124
137
|
return {
|
|
125
138
|
v: 1,
|
|
126
139
|
kind: "register",
|
package/build/core/scope.js
CHANGED
|
@@ -38,17 +38,17 @@ export function isBlankScopePatternLine(line) {
|
|
|
38
38
|
}
|
|
39
39
|
/** Parse Keiyaku's source-line Scope opener, not a normalized Markdown node. */
|
|
40
40
|
export function parseScopeFenceOpening(line) {
|
|
41
|
-
const match = line.match(/^(`{3,})$/);
|
|
42
|
-
return match ? { length: match[1].length } : null;
|
|
41
|
+
const match = line.match(/^(`{3,}|~{3,})$/);
|
|
42
|
+
return match ? { delimiter: match[1][0], length: match[1].length } : null;
|
|
43
43
|
}
|
|
44
|
-
/** Keiyaku closer: column-zero
|
|
44
|
+
/** Keiyaku closer: matching column-zero delimiter, at least opener length, then whitespace. */
|
|
45
45
|
export function isScopeFenceClosing(line, fence) {
|
|
46
|
-
const match = line.match(/^(
|
|
47
|
-
return Boolean(match && match[1].length >= fence.length);
|
|
46
|
+
const match = line.match(/^(`+|~+)[ \t]*$/);
|
|
47
|
+
return Boolean(match && match[1][0] === fence.delimiter && match[1].length >= fence.length);
|
|
48
48
|
}
|
|
49
49
|
function fencedScopeError(heading, detail) {
|
|
50
50
|
return new FlowError("INVALID_KEIYAKU_DRAFT", `${heading} must contain exactly one fenced gitignore-subset block; ${detail}. ` +
|
|
51
|
-
`Paste-ready canonical example:\n\n${heading}\n
|
|
51
|
+
`Paste-ready canonical example:\n\n${heading}\n~~~\nfoo\nbar\n~~~`);
|
|
52
52
|
}
|
|
53
53
|
/** Validate and normalize the one fenced block that constitutes a Scope body. */
|
|
54
54
|
export function validateScopeFragmentLines(lines, options) {
|
|
@@ -152,14 +152,14 @@ export function markdownSourceSections(markdown) {
|
|
|
152
152
|
return { ...heading, end: next?.start ?? markdown.length };
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
-
/** Render canonical Scope bytes with a fence that content cannot close. */
|
|
155
|
+
/** Render canonical Scope bytes with a tilde fence that content cannot close. */
|
|
156
156
|
export function renderScopeFragment(patterns) {
|
|
157
157
|
let longestRun = 0;
|
|
158
158
|
for (const pattern of patterns) {
|
|
159
|
-
for (const run of pattern.match(
|
|
159
|
+
for (const run of pattern.match(/~+/g) ?? [])
|
|
160
160
|
longestRun = Math.max(longestRun, run.length);
|
|
161
161
|
}
|
|
162
|
-
const fence = "
|
|
162
|
+
const fence = "~".repeat(Math.max(3, longestRun + 1));
|
|
163
163
|
return `${fence}\n${patterns.join("\n")}\n${fence}`;
|
|
164
164
|
}
|
|
165
165
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { availableParallelism } from "node:os";
|
|
1
2
|
import { loadKeiyakuSettings } from "../../config/settings/loader.js";
|
|
2
3
|
import { observeGitRepository } from "../../git/branches.js";
|
|
3
|
-
import {
|
|
4
|
+
import { readExactProjectionStatus, readOperationalProjectionStatusBoard, } from "../projection/index.js";
|
|
4
5
|
import { deriveQueueReadModel, queueMembership, } from "../settlement/index.js";
|
|
5
6
|
import { findOpenArc, getMaxArc, receiptsAfterFence } from "../seal.js";
|
|
6
7
|
import { openTaskStore, readTaskBoard, taskRepositoryFromStore, taskUpdatedAt, } from "../task/index.js";
|
|
@@ -8,6 +9,9 @@ import { deriveBootstrapDiagnostics, deriveContractState, hasCurrentPetitionVerd
|
|
|
8
9
|
import { commissionTarget, evaluateBaseDrift, latestBase, resolveDefaultBranchFromSettings, } from "./drift.js";
|
|
9
10
|
import { computeClaimReconciliation, } from "./reconciliation.js";
|
|
10
11
|
import { readStatusLedgers } from "./ledger-batch.js";
|
|
12
|
+
// Git object reads benefit from overlap, but a hard cap avoids process and disk contention.
|
|
13
|
+
const STATUS_DRIFT_MAX_CONCURRENCY = 8;
|
|
14
|
+
const STATUS_DRIFT_CONCURRENCY = Math.min(STATUS_DRIFT_MAX_CONCURRENCY, availableParallelism());
|
|
11
15
|
function compareTaskStatusRows(left, right) {
|
|
12
16
|
const leftUpdated = taskUpdatedAt(left);
|
|
13
17
|
const rightUpdated = taskUpdatedAt(right);
|
|
@@ -165,6 +169,23 @@ function deriveLastActiveAge(entries, now) {
|
|
|
165
169
|
lastActiveMs: Math.max(0, now - atMs),
|
|
166
170
|
};
|
|
167
171
|
}
|
|
172
|
+
export async function evaluateDistinctBaseDrifts(cwd, bases, defaultBranch, evaluate = evaluateBaseDrift) {
|
|
173
|
+
const uniqueBases = [...new Set(bases)];
|
|
174
|
+
const results = new Map();
|
|
175
|
+
let nextIndex = 0;
|
|
176
|
+
const worker = async () => {
|
|
177
|
+
while (nextIndex < uniqueBases.length) {
|
|
178
|
+
const index = nextIndex++;
|
|
179
|
+
const base = uniqueBases[index];
|
|
180
|
+
if (base === undefined)
|
|
181
|
+
return;
|
|
182
|
+
results.set(base, await evaluate(cwd, base, defaultBranch));
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const workerCount = Math.min(STATUS_DRIFT_CONCURRENCY, uniqueBases.length);
|
|
186
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
187
|
+
return results;
|
|
188
|
+
}
|
|
168
189
|
export async function readKanshiBoard(cwd, now, opts) {
|
|
169
190
|
const targetFilter = opts?.target ?? null;
|
|
170
191
|
const loadedSettings = await loadKeiyakuSettings(cwd);
|
|
@@ -173,9 +194,14 @@ export async function readKanshiBoard(cwd, now, opts) {
|
|
|
173
194
|
if (repository.capability === "unknown")
|
|
174
195
|
throw repository.error;
|
|
175
196
|
if (repository.capability !== "available" || repository.coordinate !== "repository") {
|
|
197
|
+
const exactProjection = opts?.projectionId === undefined
|
|
198
|
+
? undefined
|
|
199
|
+
: await readExactProjectionStatus(cwd, now, opts.projectionId);
|
|
176
200
|
return {
|
|
177
201
|
contracts: [],
|
|
178
|
-
projections:
|
|
202
|
+
projections: opts?.projectionId === undefined
|
|
203
|
+
? await readOperationalProjectionStatusBoard(cwd, now)
|
|
204
|
+
: exactProjection ? { rows: [exactProjection] } : null,
|
|
179
205
|
queueLength: 0,
|
|
180
206
|
diseases: loadedSettings.diseases,
|
|
181
207
|
claimReconciliation: null,
|
|
@@ -203,6 +229,13 @@ export async function readKanshiBoard(cwd, now, opts) {
|
|
|
203
229
|
}
|
|
204
230
|
const queueModel = deriveQueueReadModel(readableLedgers);
|
|
205
231
|
const boardQueueLength = queueModel.targetQueues.reduce((sum, queue) => sum + queue.ordered.length, 0);
|
|
232
|
+
const baseDrifts = await evaluateDistinctBaseDrifts(cwd, readableLedgers.flatMap((item) => {
|
|
233
|
+
const state = deriveContractState(item.entries);
|
|
234
|
+
return isTerminalState(state) ? [] : (() => {
|
|
235
|
+
const base = latestBase(item.entries);
|
|
236
|
+
return base ? [base] : [];
|
|
237
|
+
})();
|
|
238
|
+
}), defaultBranch);
|
|
206
239
|
const contracts = [];
|
|
207
240
|
// Readable-ledger commission targets retained for post-assembly filter only.
|
|
208
241
|
const derivedTargetById = new Map();
|
|
@@ -236,7 +269,7 @@ export async function readKanshiBoard(cwd, now, opts) {
|
|
|
236
269
|
const after = (bind?.data.after ?? []).map((id) => ({ id, terminal: terminalById.get(id) ?? false }));
|
|
237
270
|
const currentArc = deriveCurrentArc(item.entries, bind?.data.objective, state);
|
|
238
271
|
const baseDrift = base
|
|
239
|
-
?
|
|
272
|
+
? baseDrifts.get(base) ?? { state: "unknown", label: "n/a (base drift unavailable)" }
|
|
240
273
|
: { state: "unknown", label: "n/a (no default branch)" };
|
|
241
274
|
const nextStep = deriveNextStep({
|
|
242
275
|
id: item.contractId,
|
|
@@ -296,9 +329,14 @@ export async function readKanshiBoard(cwd, now, opts) {
|
|
|
296
329
|
? boardQueueLength
|
|
297
330
|
: (queueModel.targetQueues.find((queue) => queue.target === targetFilter)?.ordered.length ?? 0);
|
|
298
331
|
const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
|
|
332
|
+
const exactProjection = opts?.projectionId === undefined
|
|
333
|
+
? undefined
|
|
334
|
+
: await readExactProjectionStatus(cwd, now, opts.projectionId);
|
|
299
335
|
return {
|
|
300
336
|
contracts: filteredContracts,
|
|
301
|
-
projections:
|
|
337
|
+
projections: opts?.projectionId === undefined
|
|
338
|
+
? await readOperationalProjectionStatusBoard(cwd, now)
|
|
339
|
+
: exactProjection ? { rows: [exactProjection] } : null,
|
|
302
340
|
queueLength,
|
|
303
341
|
defaultBranch: defaultBranch?.branch,
|
|
304
342
|
defaultHead: defaultBranch?.head,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { assertSettingsKnobUsable } from "../../config/settings/disease.js";
|
|
2
2
|
import { loadKeiyakuSettings } from "../../config/settings/loader.js";
|
|
3
3
|
import { FlowError } from "../../flow-error.js";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveLocalBranchHead } from "../../git/refs.js";
|
|
5
5
|
import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../../git/core.js";
|
|
6
6
|
function findBindEntry(entries) {
|
|
7
7
|
return entries.find((entry) => entry.kind === "bind");
|
|
@@ -49,10 +49,26 @@ export async function resolveDefaultBranchFromSettings(cwd, loaded, rejectDiseas
|
|
|
49
49
|
assertSettingsKnobUsable(loaded, "defaultBranch");
|
|
50
50
|
const override = loaded.knobs?.defaultBranch;
|
|
51
51
|
if (override) {
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
const resolved = await resolveLocalBranchHead(cwd, override);
|
|
53
|
+
if (!resolved) {
|
|
54
|
+
const source = loaded.knobSources.defaultBranch;
|
|
55
|
+
if (source === undefined) {
|
|
56
|
+
throw new Error("admitted defaultBranch has no settings source coordinate");
|
|
57
|
+
}
|
|
58
|
+
throw new FlowError("INVALID_SETTINGS", `${source.coordinate} defaultBranch '${override}' does not resolve`, {
|
|
59
|
+
facts: {
|
|
60
|
+
kind: "invalid_settings",
|
|
61
|
+
diseases: [{
|
|
62
|
+
source: source.source,
|
|
63
|
+
coordinate: source.coordinate,
|
|
64
|
+
knob: "defaultBranch",
|
|
65
|
+
reason: `defaultBranch '${override}' does not resolve as a local branch`,
|
|
66
|
+
}],
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const branch = resolved.ref.slice("refs/heads/".length);
|
|
71
|
+
return { branch, head: resolved.head };
|
|
56
72
|
}
|
|
57
73
|
const git = createGit(cwd);
|
|
58
74
|
for (const branch of ["main", "master"]) {
|
|
@@ -1,158 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { GitStreamingBatchFramingError, GitStreamingBatchResponseEndedError, openGitStreamingBatch, } from "../../git/streaming-batch.js";
|
|
3
|
-
import { parseLedgerEntryCommitMessage } from "../entry.js";
|
|
4
|
-
import { ACTIVE_LEDGER_REF_PREFIX, } from "../ledger.js";
|
|
5
|
-
const MAX_LEDGER_COMMIT_BYTES = 1024 * 1024;
|
|
6
|
-
async function runStatusGit(cwd, args) {
|
|
7
|
-
const result = await runGitProcess(cwd, args);
|
|
8
|
-
if (result.error || result.status !== 0) {
|
|
9
|
-
throw wrapGitError(args.join(" "), {
|
|
10
|
-
message: result.error?.message
|
|
11
|
-
?? `git exited with status ${result.status ?? "unknown"}`,
|
|
12
|
-
stderr: result.stderr,
|
|
13
|
-
stdout: result.stdout,
|
|
14
|
-
}, cwd);
|
|
15
|
-
}
|
|
16
|
-
return Buffer.from(result.stdout, "utf8");
|
|
17
|
-
}
|
|
18
|
-
async function queryBatchObject(batch, expression) {
|
|
19
|
-
const record = await batch.request(expression, {
|
|
20
|
-
maxContentBytes: MAX_LEDGER_COMMIT_BYTES,
|
|
21
|
-
});
|
|
22
|
-
if (record.kind === "missing" || record.kind === "ambiguous") {
|
|
23
|
-
throw new Error(`[git cat-file ${expression}] object is ${record.kind === "missing" ? "missing" : "ambiguous"}`);
|
|
24
|
-
}
|
|
25
|
-
return {
|
|
26
|
-
oid: record.record.oid,
|
|
27
|
-
type: record.record.type,
|
|
28
|
-
content: record.record.content,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
function parseCommitObject(object) {
|
|
32
|
-
if (object.type !== "commit") {
|
|
33
|
-
throw new Error(`git cat-file expected commit ${object.oid}, received ${object.type}`);
|
|
34
|
-
}
|
|
35
|
-
const content = object.content.toString("utf8");
|
|
36
|
-
const messageStart = content.indexOf("\n\n");
|
|
37
|
-
if (messageStart < 0)
|
|
38
|
-
throw new Error(`git commit ${object.oid} has no message separator`);
|
|
39
|
-
const parents = [];
|
|
40
|
-
for (const line of content.slice(0, messageStart).split("\n")) {
|
|
41
|
-
if (!line.startsWith("parent "))
|
|
42
|
-
continue;
|
|
43
|
-
const parent = line.slice("parent ".length);
|
|
44
|
-
if (!/^[0-9a-f]{40,64}$/.test(parent)) {
|
|
45
|
-
throw new Error(`git commit ${object.oid} has an invalid parent header`);
|
|
46
|
-
}
|
|
47
|
-
parents.push(parent);
|
|
48
|
-
}
|
|
49
|
-
return { oid: object.oid, parents, message: content.slice(messageStart + 2) };
|
|
50
|
-
}
|
|
51
|
-
function parseLedgerRefs(output) {
|
|
52
|
-
return output
|
|
53
|
-
.toString("utf8")
|
|
54
|
-
.split(/\r?\n/)
|
|
55
|
-
.filter((ref) => ref.startsWith(ACTIVE_LEDGER_REF_PREFIX))
|
|
56
|
-
.map((ref) => ({ contractId: ref.slice(ACTIVE_LEDGER_REF_PREFIX.length), ref }));
|
|
57
|
-
}
|
|
58
|
-
async function readLedgerFromBatch(batch, ref, commits) {
|
|
59
|
-
const headObject = await queryBatchObject(batch, `${ref.ref}^{commit}`);
|
|
60
|
-
const head = parseCommitObject(headObject);
|
|
61
|
-
commits.set(head.oid, Promise.resolve(head));
|
|
62
|
-
const ordered = [];
|
|
63
|
-
const visited = new Set();
|
|
64
|
-
const visiting = new Set();
|
|
65
|
-
const visit = async (oid) => {
|
|
66
|
-
if (visited.has(oid))
|
|
67
|
-
return;
|
|
68
|
-
if (visiting.has(oid))
|
|
69
|
-
throw new Error(`git ledger ancestry contains a cycle at ${oid}`);
|
|
70
|
-
visiting.add(oid);
|
|
71
|
-
let pending = commits.get(oid);
|
|
72
|
-
if (!pending) {
|
|
73
|
-
pending = queryBatchObject(batch, oid).then(parseCommitObject);
|
|
74
|
-
commits.set(oid, pending);
|
|
75
|
-
}
|
|
76
|
-
const commit = await pending;
|
|
77
|
-
for (const parent of commit.parents)
|
|
78
|
-
await visit(parent);
|
|
79
|
-
visiting.delete(oid);
|
|
80
|
-
visited.add(oid);
|
|
81
|
-
ordered.push(commit);
|
|
82
|
-
};
|
|
83
|
-
await visit(head.oid);
|
|
84
|
-
return {
|
|
85
|
-
contractId: ref.contractId,
|
|
86
|
-
ref: ref.ref,
|
|
87
|
-
head: head.oid,
|
|
88
|
-
entries: ordered.map((commit) => parseLedgerEntryCommitMessage(commit.message)),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Read the aggregate status ledger snapshot with at most two Git processes:
|
|
93
|
-
* ref-name discovery and one framed, streaming object session.
|
|
94
|
-
*/
|
|
95
|
-
export async function readStatusLedgers(cwd) {
|
|
96
|
-
const refOutput = await runStatusGit(cwd, [
|
|
97
|
-
"for-each-ref",
|
|
98
|
-
"--sort=refname",
|
|
99
|
-
"--format=%(refname)",
|
|
100
|
-
ACTIVE_LEDGER_REF_PREFIX,
|
|
101
|
-
]);
|
|
102
|
-
const refs = parseLedgerRefs(refOutput);
|
|
103
|
-
if (refs.length === 0)
|
|
104
|
-
return [];
|
|
105
|
-
const batch = openGitStreamingBatch(cwd);
|
|
106
|
-
const commits = new Map();
|
|
107
|
-
const results = [];
|
|
108
|
-
let readError;
|
|
109
|
-
try {
|
|
110
|
-
for (let index = 0; index < refs.length; index += 1) {
|
|
111
|
-
const ref = refs[index];
|
|
112
|
-
try {
|
|
113
|
-
results.push(await readLedgerFromBatch(batch, ref, commits));
|
|
114
|
-
}
|
|
115
|
-
catch (error) {
|
|
116
|
-
const diagnostic = error instanceof Error ? error : new Error(String(error));
|
|
117
|
-
results.push({ id: ref.contractId, error: diagnostic });
|
|
118
|
-
// Framing and mid-frame EOF both destroy the batch channel; isolate remaining refs.
|
|
119
|
-
if (error instanceof GitStreamingBatchFramingError
|
|
120
|
-
|| error instanceof GitStreamingBatchResponseEndedError) {
|
|
121
|
-
const isolationReason = error instanceof GitStreamingBatchResponseEndedError
|
|
122
|
-
? "response ended"
|
|
123
|
-
: "malformed frame";
|
|
124
|
-
for (const pending of refs.slice(index + 1)) {
|
|
125
|
-
results.push({
|
|
126
|
-
id: pending.contractId,
|
|
127
|
-
error: new Error(`git cat-file batch terminated after ${isolationReason}: ${diagnostic.message}`),
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
readError = error;
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
const close = await batch.close();
|
|
140
|
-
if (!close.aborted && (close.timedOut || close.signal || close.code !== 0)) {
|
|
141
|
-
throw wrapGitError("cat-file --batch", {
|
|
142
|
-
message: close.timedOut
|
|
143
|
-
? "git timed out"
|
|
144
|
-
: close.signal
|
|
145
|
-
? `git exited from signal ${close.signal}`
|
|
146
|
-
: `git exited with status ${close.code ?? "unknown"}`,
|
|
147
|
-
stderr: close.stderr.toString("utf8"),
|
|
148
|
-
}, cwd);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
if (!readError)
|
|
153
|
-
readError = error;
|
|
154
|
-
}
|
|
155
|
-
if (readError)
|
|
156
|
-
throw readError;
|
|
157
|
-
return results;
|
|
158
|
-
}
|
|
1
|
+
export { readAllLedgerSnapshots as readStatusLedgers, } from "../ledger-batch.js";
|
package/build/core/task/index.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Only symbols required by current external consumers are re-exported.
|
|
4
4
|
* Package-internal modules must import each other directly, not through this file.
|
|
5
5
|
*/
|
|
6
|
-
export { reconcileTaskForfeit, settleTaskBoardForBind, settleTaskBoardForLanding, } from "./task-contract.js";
|
|
6
|
+
export { previewTaskBoardForBind, reconcileTaskForfeit, settleTaskBoardForBind, settleTaskBoardForLanding, } from "./task-contract.js";
|
|
7
7
|
export { resolveTaskCreator } from "./task-git-actor.js";
|
|
8
8
|
export { openTaskCommandRepository, readTaskDoctor, taskCommandContractId, } from "./task-git-runtime.js";
|
|
9
|
-
export { openTaskStore } from "./task-git-store.js";
|
|
9
|
+
export { openTaskStore, readTaskHistory } from "./task-git-store.js";
|
|
10
10
|
export { taskRepositoryFromStore } from "./task-store-repository.js";
|
|
11
11
|
export { isTaskId } from "./validation.js";
|
|
12
12
|
export { buildTaskTree, createTask, doneTask, dropTask, parseTaskDocument, readTaskBoard, resolveTaskReadId, serializeTaskDocument, startTask, stopTask, updateTask, deriveTaskDisplay, selectTaskList, taskUpdatedAt, advancedTaskUpdatedAt, } from "./task.js";
|
|
@@ -7,6 +7,24 @@ import { buildTaskSourceBoard } from "./source-board.js";
|
|
|
7
7
|
import { composeLandingTree, containsCommit, createLandingCommit, createTaskBoardCommit, findAcceptedLandingCommit, publishTaskMutationCommit, inspectCurrentBranch, publishForwardTaskBoardCommit, reconcileTaskPathsetIndex, refHead, taskContractAtCommit, taskSettlementOwnershipAtCommit, taskSourcesAtCommit, taskWritesFromSources, trackedTaskPaths, updateBranch, updateTargetRef, } from "./settlement-git.js";
|
|
8
8
|
import { contractPointerDocument, doneTaskReplacements, reopenedTaskDocument, requireHealthySettlementBoard, requireSettledTask, selectReadyTasksForBind, tasksForContract, } from "./settlement-policy.js";
|
|
9
9
|
import { stableRepoRoot } from "../worktree-path.js";
|
|
10
|
+
/**
|
|
11
|
+
* Read the same healthy task board and run the same ready-task selector as bind
|
|
12
|
+
* under the task admission lock, without replacing sources or publishing.
|
|
13
|
+
*/
|
|
14
|
+
export async function previewTaskBoardForBind(input) {
|
|
15
|
+
await requireGitRepository(input.cwd, "task bind");
|
|
16
|
+
return await withLockHeldTaskBoard(input.cwd, async (transaction) => {
|
|
17
|
+
if (transaction.location.mode !== "git") {
|
|
18
|
+
if (transaction.location.capability === "unavailable") {
|
|
19
|
+
assertGitObservation({ capability: "unavailable" }, "task bind");
|
|
20
|
+
}
|
|
21
|
+
throw new FlowError("NOT_GIT_REPO", "task bind requires Git task storage");
|
|
22
|
+
}
|
|
23
|
+
const repository = taskRepositoryFromLockedBoard(transaction);
|
|
24
|
+
const board = requireHealthySettlementBoard(await repository.read());
|
|
25
|
+
return { tasks: input.taskIds === undefined ? [] : selectReadyTasksForBind(board, input.taskIds) };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
10
28
|
function sourcePath(root, source) {
|
|
11
29
|
return source.path.slice(root.length + 1).split("\\").join("/");
|
|
12
30
|
}
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import { observeGitRepository } from "../../git/branches.js";
|
|
2
1
|
import { listContractIds } from "../ledger.js";
|
|
3
2
|
import { contractBoundByEngineWorktree, stableRepoRoot } from "../worktree-path.js";
|
|
4
|
-
import { inspectTaskLock, openTaskStore } from "./task-git-store.js";
|
|
3
|
+
import { inspectTaskLock, locateTaskBoard, openTaskStore, openTaskStoreAt, } from "./task-git-store.js";
|
|
5
4
|
import { taskRepositoryFromStore } from "./task-store-repository.js";
|
|
6
5
|
import { withTaskWorktreePolicy } from "./task-worktree.js";
|
|
7
6
|
import { inspectTaskDoctor } from "./task-doctor.js";
|
|
8
7
|
export async function taskCommandContractId(cwd) {
|
|
9
|
-
const
|
|
10
|
-
if (
|
|
11
|
-
throw observation.error;
|
|
12
|
-
if (observation.capability !== "available" || observation.coordinate !== "repository")
|
|
8
|
+
const board = await locateTaskBoard(cwd);
|
|
9
|
+
if (board.mode !== "git")
|
|
13
10
|
return undefined;
|
|
14
|
-
return contractBoundByEngineWorktree(cwd);
|
|
11
|
+
return contractBoundByEngineWorktree(cwd, board.root);
|
|
15
12
|
}
|
|
16
13
|
export async function openTaskCommandRepository(cwd) {
|
|
17
|
-
const
|
|
14
|
+
const board = await locateTaskBoard(cwd);
|
|
15
|
+
const store = openTaskStoreAt(board);
|
|
18
16
|
const repository = taskRepositoryFromStore(store);
|
|
19
|
-
if (
|
|
17
|
+
if (board.mode !== "git")
|
|
20
18
|
return repository;
|
|
21
|
-
const contractId = await contractBoundByEngineWorktree(cwd);
|
|
19
|
+
const contractId = await contractBoundByEngineWorktree(cwd, board.root);
|
|
22
20
|
return contractId ? withTaskWorktreePolicy(repository, contractId) : repository;
|
|
23
21
|
}
|
|
24
22
|
export async function taskContractIds(cwd) {
|