@astrosheep/keiyaku 2.9.6 → 2.9.7
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/outcome.js +10 -0
- 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/tell/handler.js +10 -2
- package/build/cli/commands/projection/tell/meta.js +3 -3
- 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/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/line-width.js +33 -0
- package/build/cli/render/path-prefix-compaction.js +88 -0
- package/build/cli/render/petition.js +4 -0
- package/build/cli/render/projection-activity.js +93 -20
- package/build/cli/render/shared.js +34 -12
- 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/core/bind.js +111 -6
- package/build/core/draft.js +1 -1
- package/build/core/projection/generation/database.js +21 -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 +4 -3
- package/build/core/projection/generation/projection-generation-runner.js +96 -53
- package/build/core/projection/generation/projection-generation-runtime.js +19 -0
- package/build/core/projection/generation/store.js +9 -0
- package/build/core/projection/generation/transitions.js +105 -3
- package/build/core/projection/index.js +2 -2
- package/build/core/projection/projection-core.js +1 -1
- package/build/core/projection/projection-kill.js +31 -0
- package/build/core/projection/projection-life-protocol.js +10 -0
- package/build/core/projection/projection-wait.js +53 -15
- package/build/core/projection/projection-wake.js +35 -3
- 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/task/index.js +2 -2
- package/build/core/task/task-contract.js +18 -0
- package/build/core/task/task-git-store.js +50 -0
- package/build/generated/version.js +2 -2
- package/package.json +1 -1
- package/skills/keiyaku-akuma/SKILL.md +10 -0
- package/skills/keiyaku-workflow/SKILL.md +76 -7
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ProjectionStateError, randomHex8 } from "../../atomic-publish.js";
|
|
2
|
-
import { withRead, withWrite } from "./database.js";
|
|
3
|
-
import { assertTellIdRandomHex, originalFromRow, parseFence, requireExecutionId, submittedFromRow, } from "./model.js";
|
|
4
|
-
const PENDING_TELL_LIMIT = 10_000;
|
|
2
|
+
import { withRead, withReadSnapshot, withWrite } from "./database.js";
|
|
3
|
+
import { assertTellIdRandomHex, originalFromRow, PENDING_PROJECTION_TELL_STATES, parseFence, requireExecutionId, submittedFromRow, } from "./model.js";
|
|
5
4
|
function selectTell(database, tellId) {
|
|
6
5
|
return database.prepare(`
|
|
7
6
|
SELECT
|
|
@@ -26,6 +25,77 @@ export function listTellIds(projectionDirectory, state) {
|
|
|
26
25
|
ORDER BY tell_id
|
|
27
26
|
`).all(state).map((row) => row.tell_id));
|
|
28
27
|
}
|
|
28
|
+
/** List continuation fence metadata without loading operator tell bodies. */
|
|
29
|
+
export function listTellMetadataFromDatabase(database, states) {
|
|
30
|
+
const placeholders = states.map(() => "?").join(", ");
|
|
31
|
+
return database.prepare(`
|
|
32
|
+
SELECT tell_id, state, execution_id, effort
|
|
33
|
+
FROM projection_tells
|
|
34
|
+
WHERE state IN (${placeholders})
|
|
35
|
+
ORDER BY tell_id
|
|
36
|
+
`).all(...states).map((row) => ({
|
|
37
|
+
tellId: row.tell_id,
|
|
38
|
+
state: row.state,
|
|
39
|
+
...(row.execution_id ? { executionId: row.execution_id } : {}),
|
|
40
|
+
...(row.effort ? { effort: row.effort } : {}),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
/** List continuation fence metadata without loading operator tell bodies. */
|
|
44
|
+
export function listTellMetadata(projectionDirectory, states) {
|
|
45
|
+
return withRead(projectionDirectory, (database) => listTellMetadataFromDatabase(database, states));
|
|
46
|
+
}
|
|
47
|
+
export function readPendingTellWindowFromDatabase(database, limit) {
|
|
48
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
49
|
+
throw new ProjectionStateError("pending tell window limit must be a non-negative safe integer");
|
|
50
|
+
}
|
|
51
|
+
const pendingPlaceholders = PENDING_PROJECTION_TELL_STATES.map(() => "?").join(", ");
|
|
52
|
+
const total = database.prepare(`
|
|
53
|
+
SELECT COUNT(*) AS count
|
|
54
|
+
FROM projection_tells
|
|
55
|
+
WHERE state IN (${pendingPlaceholders})
|
|
56
|
+
`).get(...PENDING_PROJECTION_TELL_STATES);
|
|
57
|
+
const rows = database.prepare(`
|
|
58
|
+
SELECT tell_id, text, created_at, state
|
|
59
|
+
FROM projection_tells
|
|
60
|
+
WHERE state IN (${pendingPlaceholders})
|
|
61
|
+
ORDER BY tell_id
|
|
62
|
+
LIMIT ?
|
|
63
|
+
`).all(...PENDING_PROJECTION_TELL_STATES, limit);
|
|
64
|
+
return {
|
|
65
|
+
totals: { undelivered: Number(total.count) },
|
|
66
|
+
window: rows.map((row) => ({ tellId: row.tell_id, text: row.text, createdAt: row.created_at, state: row.state })),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function readPendingTellWindow(projectionDirectory, limit) {
|
|
70
|
+
return withReadSnapshot(projectionDirectory, (database) => readPendingTellWindowFromDatabase(database, limit));
|
|
71
|
+
}
|
|
72
|
+
/** Whether a failed successor fence still retains delivered mail for replay. */
|
|
73
|
+
export function hasRetainedDeliveredTellFromDatabase(database, retainedFence) {
|
|
74
|
+
if (retainedFence.length === 0)
|
|
75
|
+
return false;
|
|
76
|
+
const statement = database.prepare(`
|
|
77
|
+
SELECT 1
|
|
78
|
+
FROM projection_tells
|
|
79
|
+
WHERE tell_id = ? AND state = 'delivered'
|
|
80
|
+
LIMIT 1
|
|
81
|
+
`);
|
|
82
|
+
return retainedFence.some((tellId) => statement.get(tellId) !== undefined);
|
|
83
|
+
}
|
|
84
|
+
export function hasRetainedDeliveredTell(projectionDirectory, retainedFence) {
|
|
85
|
+
return withRead(projectionDirectory, (database) => hasRetainedDeliveredTellFromDatabase(database, retainedFence));
|
|
86
|
+
}
|
|
87
|
+
/** Whether interrupted replay has any delivered tell left to return to inbox. */
|
|
88
|
+
export function hasDeliveredTellFromDatabase(database, executionId) {
|
|
89
|
+
return database.prepare(`
|
|
90
|
+
SELECT 1
|
|
91
|
+
FROM projection_tells
|
|
92
|
+
WHERE state = 'delivered' AND execution_id = ?
|
|
93
|
+
LIMIT 1
|
|
94
|
+
`).get(executionId) !== undefined;
|
|
95
|
+
}
|
|
96
|
+
export function hasDeliveredTell(projectionDirectory, executionId) {
|
|
97
|
+
return withRead(projectionDirectory, (database) => hasDeliveredTellFromDatabase(database, executionId));
|
|
98
|
+
}
|
|
29
99
|
export function readTellOriginal(projectionDirectory, state, tellId) {
|
|
30
100
|
return withRead(projectionDirectory, (database) => {
|
|
31
101
|
const row = selectTell(database, tellId);
|
|
@@ -49,14 +119,6 @@ export function writeInboxTell(projectionDirectory, text, nowMs, options) {
|
|
|
49
119
|
}
|
|
50
120
|
const tellId = `${nowMs}-${assertTellIdRandomHex((options?.randomHex ?? randomHex8)())}`;
|
|
51
121
|
return withWrite(projectionDirectory, (database) => {
|
|
52
|
-
const pending = database.prepare(`
|
|
53
|
-
SELECT COUNT(*) AS count
|
|
54
|
-
FROM projection_tells
|
|
55
|
-
WHERE state IN ('inbox', 'inflight', 'submitted', 'delivered')
|
|
56
|
-
`).get();
|
|
57
|
-
if (Number(pending.count) >= PENDING_TELL_LIMIT) {
|
|
58
|
-
throw new ProjectionStateError(`projection has reached the ${PENDING_TELL_LIMIT} pending tell limit`);
|
|
59
|
-
}
|
|
60
122
|
try {
|
|
61
123
|
database.prepare(`
|
|
62
124
|
INSERT INTO projection_tells (
|
|
@@ -123,6 +185,31 @@ export function claimFencedTells(projectionDirectory, fence, executionId) {
|
|
|
123
185
|
claimed.push(tellId);
|
|
124
186
|
continue;
|
|
125
187
|
}
|
|
188
|
+
if (row.state === "submitted" || row.state === "delivered") {
|
|
189
|
+
const submitted = submittedFromRow(row);
|
|
190
|
+
if (submitted.executionId === owner) {
|
|
191
|
+
throw new ProjectionStateError(`submitted tell belongs to current execution: ${tellId}`);
|
|
192
|
+
}
|
|
193
|
+
const replayedFrom = [
|
|
194
|
+
...(submitted.replayedFrom ?? []),
|
|
195
|
+
{ executionId: submitted.executionId, fence: submitted.fence },
|
|
196
|
+
];
|
|
197
|
+
const result = database.prepare(`
|
|
198
|
+
UPDATE projection_tells
|
|
199
|
+
SET
|
|
200
|
+
state = 'inflight',
|
|
201
|
+
execution_id = NULL,
|
|
202
|
+
fence_json = NULL,
|
|
203
|
+
replayed_from_json = ?,
|
|
204
|
+
delivered_at = NULL
|
|
205
|
+
WHERE tell_id = ? AND state = ?
|
|
206
|
+
`).run(JSON.stringify(replayedFrom), tellId, row.state);
|
|
207
|
+
if (Number(result.changes) !== 1) {
|
|
208
|
+
throw new ProjectionStateError(`generation tell fence member changed while claiming: ${tellId}`);
|
|
209
|
+
}
|
|
210
|
+
claimed.push(tellId);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
126
213
|
if (row.state === "consumed" && row.execution_id === owner)
|
|
127
214
|
continue;
|
|
128
215
|
throw new ProjectionStateError(`generation tell fence member is not claimable: ${tellId}`);
|
|
@@ -152,7 +239,7 @@ export function markTellSubmitted(projectionDirectory, tellId, fields) {
|
|
|
152
239
|
state = 'submitted',
|
|
153
240
|
execution_id = ?,
|
|
154
241
|
fence_json = ?,
|
|
155
|
-
replayed_from_json =
|
|
242
|
+
replayed_from_json = COALESCE(?, replayed_from_json)
|
|
156
243
|
WHERE tell_id = ? AND state = 'inflight'
|
|
157
244
|
`).run(executionId, JSON.stringify(fence), replayedFrom.length > 0 ? JSON.stringify(replayedFrom) : null, tellId);
|
|
158
245
|
});
|
|
@@ -190,12 +277,13 @@ export function readProjectionTellCounts(projectionDirectory) {
|
|
|
190
277
|
}
|
|
191
278
|
export function snapshotPendingTells(projectionDirectory) {
|
|
192
279
|
return withRead(projectionDirectory, (database) => {
|
|
280
|
+
const pendingPlaceholders = PENDING_PROJECTION_TELL_STATES.map(() => "?").join(", ");
|
|
193
281
|
const rows = database.prepare(`
|
|
194
282
|
SELECT tell_id, effort
|
|
195
283
|
FROM projection_tells
|
|
196
|
-
WHERE state IN (
|
|
284
|
+
WHERE state IN (${pendingPlaceholders})
|
|
197
285
|
ORDER BY tell_id
|
|
198
|
-
`).all();
|
|
286
|
+
`).all(...PENDING_PROJECTION_TELL_STATES);
|
|
199
287
|
const effort = rows.reduce((selected, row) => row.effort ?? selected, undefined);
|
|
200
288
|
return {
|
|
201
289
|
tellIds: rows.map((row) => row.tell_id),
|
|
@@ -203,44 +291,3 @@ export function snapshotPendingTells(projectionDirectory) {
|
|
|
203
291
|
};
|
|
204
292
|
});
|
|
205
293
|
}
|
|
206
|
-
export function demoteSubmittedTellsForReplay(projectionDirectory, executionId) {
|
|
207
|
-
const currentExecutionId = requireExecutionId(executionId);
|
|
208
|
-
return withWrite(projectionDirectory, (database) => {
|
|
209
|
-
const rows = database.prepare(`
|
|
210
|
-
SELECT
|
|
211
|
-
tell_id,
|
|
212
|
-
text,
|
|
213
|
-
created_at,
|
|
214
|
-
effort,
|
|
215
|
-
state,
|
|
216
|
-
execution_id,
|
|
217
|
-
fence_json,
|
|
218
|
-
replayed_from_json,
|
|
219
|
-
delivered_at
|
|
220
|
-
FROM projection_tells
|
|
221
|
-
WHERE state IN ('submitted', 'delivered')
|
|
222
|
-
ORDER BY tell_id
|
|
223
|
-
`).all();
|
|
224
|
-
for (const row of rows) {
|
|
225
|
-
const submitted = submittedFromRow(row);
|
|
226
|
-
if (submitted.executionId === currentExecutionId) {
|
|
227
|
-
throw new ProjectionStateError(`submitted tell belongs to current execution: ${row.tell_id}`);
|
|
228
|
-
}
|
|
229
|
-
const replayedFrom = [
|
|
230
|
-
...(submitted.replayedFrom ?? []),
|
|
231
|
-
{ executionId: submitted.executionId, fence: submitted.fence },
|
|
232
|
-
];
|
|
233
|
-
database.prepare(`
|
|
234
|
-
UPDATE projection_tells
|
|
235
|
-
SET
|
|
236
|
-
state = 'inflight',
|
|
237
|
-
execution_id = NULL,
|
|
238
|
-
fence_json = NULL,
|
|
239
|
-
replayed_from_json = ?,
|
|
240
|
-
delivered_at = NULL
|
|
241
|
-
WHERE tell_id = ? AND state IN ('submitted', 'delivered')
|
|
242
|
-
`).run(JSON.stringify(replayedFrom), row.tell_id);
|
|
243
|
-
}
|
|
244
|
-
return rows.map((row) => row.tell_id);
|
|
245
|
-
});
|
|
246
|
-
}
|
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
|
/**
|
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
|
}
|
|
@@ -105,6 +105,29 @@ export async function locateTaskBoard(cwd, runGitProbe = defaultRunGit) {
|
|
|
105
105
|
function taskDirectory(board) {
|
|
106
106
|
return path.join(board.root, ".keiyaku", "tasks");
|
|
107
107
|
}
|
|
108
|
+
const TASK_HISTORY_FORMAT = "%aI%x00%an%x00%s";
|
|
109
|
+
function taskHistoryPath(taskId) {
|
|
110
|
+
return `.keiyaku/tasks/${taskId}.md`;
|
|
111
|
+
}
|
|
112
|
+
function parseTaskHistory(output) {
|
|
113
|
+
const fields = output.split("\0");
|
|
114
|
+
if (fields.at(-1) !== "") {
|
|
115
|
+
throw new FlowError("INTERNAL_STATE", "task history returned an incomplete record");
|
|
116
|
+
}
|
|
117
|
+
fields.pop();
|
|
118
|
+
if (fields.length % 3 !== 0) {
|
|
119
|
+
throw new FlowError("INTERNAL_STATE", "task history returned an incomplete record");
|
|
120
|
+
}
|
|
121
|
+
const entries = [];
|
|
122
|
+
for (let index = 0; index < fields.length; index += 3) {
|
|
123
|
+
entries.push({
|
|
124
|
+
timestamp: fields[index],
|
|
125
|
+
actor: fields[index + 1],
|
|
126
|
+
subject: fields[index + 2],
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return entries;
|
|
130
|
+
}
|
|
108
131
|
function assertKeiyakuDirectory(board) {
|
|
109
132
|
const keiyakuDirectory = path.join(board.root, ".keiyaku");
|
|
110
133
|
const stat = lstatIfPresent(keiyakuDirectory);
|
|
@@ -201,6 +224,33 @@ export async function inspectTaskLock(cwd) {
|
|
|
201
224
|
closeQuietly(database);
|
|
202
225
|
}
|
|
203
226
|
}
|
|
227
|
+
/** Read the current hub branch's task-specific Git history without acquiring the task lock. */
|
|
228
|
+
export async function readTaskHistory(cwd, taskId, deps = {}) {
|
|
229
|
+
if (!isTaskId(taskId)) {
|
|
230
|
+
throw new FlowError("INVALID_TARGET", `task history requires a canonical task id: ${taskId}`);
|
|
231
|
+
}
|
|
232
|
+
const board = await locateTaskBoard(cwd, deps.runGit);
|
|
233
|
+
if (board.mode !== "git") {
|
|
234
|
+
throw new FlowError("NOT_GIT_REPO", "task history is available only in a Git repository");
|
|
235
|
+
}
|
|
236
|
+
const result = await (deps.runGit ?? defaultRunGit)(board.root, [
|
|
237
|
+
"log",
|
|
238
|
+
"-z",
|
|
239
|
+
"--topo-order",
|
|
240
|
+
"--reverse",
|
|
241
|
+
`--format=${TASK_HISTORY_FORMAT}`,
|
|
242
|
+
"--",
|
|
243
|
+
taskHistoryPath(taskId),
|
|
244
|
+
]);
|
|
245
|
+
if (result.error) {
|
|
246
|
+
throw new FlowError("INTERNAL_STATE", `cannot read task history: ${result.error.message}`, { cause: result.error });
|
|
247
|
+
}
|
|
248
|
+
if (result.status !== 0) {
|
|
249
|
+
const detail = result.stderr.trim() || `exit ${String(result.status)}`;
|
|
250
|
+
throw new FlowError("INTERNAL_STATE", `cannot read task history: ${detail}`);
|
|
251
|
+
}
|
|
252
|
+
return parseTaskHistory(result.stdout);
|
|
253
|
+
}
|
|
204
254
|
export function atomicReplaceTaskFile(filePath, text, _deps) {
|
|
205
255
|
const directory = path.dirname(filePath);
|
|
206
256
|
fs.mkdirSync(directory, { recursive: true });
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated into build output by scripts/build.mjs
|
|
2
|
-
export const VERSION = "2.9.
|
|
3
|
-
export const GIT_HASH = "
|
|
2
|
+
export const VERSION = "2.9.7";
|
|
3
|
+
export const GIT_HASH = "e0169c3";
|
package/package.json
CHANGED
|
@@ -30,6 +30,8 @@ keiyaku call NAME --detach "do the thing" # fire and forget; note the projec
|
|
|
30
30
|
keiyaku call NAME - < prompt.md # long prompt: literal `-` reads stdin
|
|
31
31
|
keiyaku wait <proj-id> # join a running one; prints its activity
|
|
32
32
|
keiyaku wait <proj-id> --timeout 5m # peek for 5m; timeout = snapshot, not death
|
|
33
|
+
keiyaku wait <proj-a> <proj-b> --any # return when the first member finishes
|
|
34
|
+
keiyaku wait <proj-a> <proj-b> --all # return when every member finishes
|
|
33
35
|
keiyaku tell <proj-id> [--wait DURATION] <BODY|-> # optionally tell, then bounded-join
|
|
34
36
|
keiyaku revive rsp_XXXX "continue..." # new run continuing from a record
|
|
35
37
|
keiyaku kill <proj-id> # stop it; record survives, revive still works
|
|
@@ -50,10 +52,18 @@ Use `keiyaku akuma view NAME` to inspect the resolved source and configuration.
|
|
|
50
52
|
- Omit tell's bracketed `--wait DURATION` option for silent success: exit 0,
|
|
51
53
|
zero output. Supply it to perform the ordinary bounded join after acceptance.
|
|
52
54
|
Silence **is** the receipt — do not re-send. Effects show up in `wait`/`status`.
|
|
55
|
+
- `tell --interrupt` is intentionally destructive. On an active runner, silent
|
|
56
|
+
success means the tell and interrupt intent are durable; provider stop and
|
|
57
|
+
successor wake continue asynchronously under the same projection id. Idle or
|
|
58
|
+
terminal state uses ordinary tell plus wake and reports its typed wake error.
|
|
53
59
|
- Body is required for `call`/`tell`. Literal `-` selects stdin; unselected
|
|
54
60
|
piped bytes are ignored and cannot change an argv body.
|
|
55
61
|
- A `--wait`/`--timeout` expiry prints a live snapshot and exits 0 — the run continues; rejoin with `wait` anytime.
|
|
56
62
|
- `wait` is repeatable: on a finished run it shows the final transcript again.
|
|
63
|
+
- Supervise multiple projections with one plural `wait`. Choose `--any` to
|
|
64
|
+
handle the first terminal member and refill its lane; choose `--all` only for
|
|
65
|
+
final batch convergence. Keep foreground waits blocking; if bounded, use one
|
|
66
|
+
operationally meaningful timeout instead of repeated short-timeout polling.
|
|
57
67
|
|
|
58
68
|
## Occasional flags
|
|
59
69
|
|