@astrosheep/keiyaku 2.8.5 → 2.9.1
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 +35 -15
- package/build/.tsbuildinfo +1 -1
- package/build/agents/{call-terms_v2.js → call-terms.js} +12 -8
- package/build/agents/harness/control-types.js +1 -0
- package/build/agents/harness/execution-handle.js +23 -3
- package/build/agents/harness/index.js +6 -607
- package/build/agents/harness/outcome.js +141 -0
- package/build/agents/harness/projection.js +310 -0
- package/build/agents/harness/router.js +201 -0
- package/build/agents/harness/runtime.js +2 -1
- package/build/agents/{launch-snapshot_v2.js → launch-snapshot/model.js} +69 -74
- package/build/agents/launch-snapshot/resolve.js +72 -0
- package/build/agents/opencode-sdk.js +24 -3
- package/build/agents/providers/{claude-agent-sdk.js → claude-agent-sdk/adapter.js} +46 -368
- package/build/agents/providers/claude-agent-sdk/events.js +218 -0
- package/build/agents/providers/claude-agent-sdk/session.js +158 -0
- package/build/agents/providers/codex-app-server/adapter.js +347 -0
- package/build/agents/providers/codex-app-server/events.js +352 -0
- package/build/agents/providers/codex-app-server/session.js +266 -0
- package/build/agents/providers/codex-sdk.js +23 -5
- package/build/agents/providers/opencode-sdk/adapter.js +344 -0
- package/build/agents/providers/opencode-sdk/events.js +354 -0
- package/build/agents/providers/opencode-sdk/session.js +180 -0
- package/build/agents/providers/pi/adapter.js +269 -0
- package/build/agents/providers/pi/checkpoint.js +41 -0
- package/build/agents/providers/pi/events.js +282 -0
- package/build/agents/{selector_v2.js → selector.js} +8 -6
- package/build/cli/commands/akuma/akuma/handler.js +4 -0
- package/build/cli/commands/akuma/akuma/meta.js +16 -0
- package/build/cli/commands/akuma/catalog.js +8 -0
- package/build/cli/commands/akuma/list/handler.js +15 -0
- package/build/cli/commands/akuma/list/meta-ls.js +9 -0
- package/build/cli/commands/akuma/view/handler.js +34 -0
- package/build/cli/commands/akuma/view/meta.js +8 -0
- package/build/cli/commands/akuma.js +1 -1
- package/build/cli/commands/contract/amend/handler.js +10 -0
- package/build/cli/commands/contract/amend/meta.js +12 -0
- package/build/cli/commands/contract/arc/handler.js +9 -0
- package/build/cli/commands/contract/arc/meta.js +26 -0
- package/build/cli/commands/contract/bind/handler.js +23 -0
- package/build/cli/commands/contract/bind/meta.js +30 -0
- package/build/cli/commands/contract/catalog.js +16 -0
- package/build/cli/commands/contract/forfeit/handler.js +13 -0
- package/build/cli/commands/contract/forfeit/meta.js +12 -0
- package/build/cli/commands/contract/log/handler.js +12 -0
- package/build/cli/commands/contract/log/meta.js +12 -0
- package/build/cli/commands/contract/petition/handler.js +26 -0
- package/build/cli/commands/contract/petition/meta.js +23 -0
- package/build/cli/commands/contract/renew/handler.js +10 -0
- package/build/cli/commands/contract/renew/meta.js +17 -0
- package/build/cli/commands/projection/call/handler.js +42 -0
- package/build/cli/commands/projection/call/meta.js +9 -0
- package/build/cli/commands/projection/catalog.js +14 -0
- package/build/cli/commands/projection/kill/handler.js +37 -0
- package/build/cli/commands/projection/kill/meta.js +11 -0
- package/build/cli/commands/projection/revive/handler.js +56 -0
- package/build/cli/commands/projection/revive/meta.js +9 -0
- package/build/cli/commands/projection/status/handler.js +25 -0
- package/build/cli/commands/projection/status/meta.js +9 -0
- package/build/cli/commands/projection/tell/handler.js +36 -0
- package/build/cli/commands/projection/tell/meta.js +9 -0
- package/build/cli/commands/projection/wait/handler.js +23 -0
- package/build/cli/commands/projection/wait/meta.js +11 -0
- package/build/cli/commands/registry.js +100 -0
- package/build/cli/commands/shared.js +71 -0
- package/build/cli/commands/skills/catalog.js +6 -0
- package/build/cli/commands/skills/install/handler.js +10 -0
- package/build/cli/commands/skills/install/meta.js +9 -0
- package/build/cli/commands/skills/skills/handler.js +4 -0
- package/build/cli/commands/skills/skills/meta.js +10 -0
- package/build/cli/commands/system/catalog.js +8 -0
- package/build/cli/commands/system/completion/handler.js +10 -0
- package/build/cli/commands/system/completion/meta.js +9 -0
- package/build/cli/commands/system/dump-env/handler.js +3 -0
- package/build/cli/commands/system/dump-env/meta.js +10 -0
- package/build/cli/commands/system/guide/handler.js +4 -0
- package/build/cli/commands/system/guide/meta.js +9 -0
- package/build/cli/commands/task/add/handler.js +21 -0
- package/build/cli/commands/task/add/meta.js +8 -0
- package/build/cli/commands/task/catalog.js +22 -0
- package/build/cli/commands/task/doctor/handler.js +4 -0
- package/build/cli/commands/task/doctor/meta.js +8 -0
- package/build/cli/commands/task/done/handler.js +17 -0
- package/build/cli/commands/task/done/meta.js +8 -0
- package/build/cli/commands/task/drop/handler.js +6 -0
- package/build/cli/commands/task/drop/meta.js +8 -0
- package/build/cli/commands/task/ls/handler.js +13 -0
- package/build/cli/commands/task/ls/meta.js +8 -0
- package/build/cli/commands/task/shared.js +131 -0
- package/build/cli/commands/task/start/handler.js +6 -0
- package/build/cli/commands/task/start/meta.js +8 -0
- package/build/cli/commands/task/stop/handler.js +17 -0
- package/build/cli/commands/task/stop/meta.js +8 -0
- package/build/cli/commands/task/task/handler.js +5 -0
- package/build/cli/commands/task/task/meta.js +11 -0
- package/build/cli/commands/task/update/handler.js +18 -0
- package/build/cli/commands/task/update/meta.js +8 -0
- package/build/cli/commands/task/view/handler.js +14 -0
- package/build/cli/commands/task/view/meta.js +8 -0
- package/build/cli/completion.js +26 -27
- package/build/cli/flags.js +79 -2
- package/build/cli/help.js +27 -49
- package/build/cli/index.js +121 -374
- package/build/cli/output-stream-error-policy.js +17 -0
- package/build/cli/parse-flags.js +197 -0
- package/build/cli/parse-metadata.js +129 -0
- package/build/cli/parse-selectors.js +65 -0
- package/build/cli/parse.js +46 -313
- package/build/cli/projection-address.js +5 -0
- package/build/cli/render/arc.js +1 -1
- package/build/cli/render/errors.js +4 -3
- package/build/cli/render/format.js +14 -0
- package/build/cli/render/kanshi.js +106 -0
- package/build/cli/render/petition.js +24 -1
- package/build/cli/render/projection-activity.js +5 -1
- package/build/cli/render/shared.js +2 -2
- package/build/cli/render/status.js +79 -13
- package/build/cli/render/task.js +113 -0
- package/build/cli/render/wait.js +23 -3
- package/build/cli/types.js +1 -1
- package/build/config/{akuma-loader_v2.js → akuma-loader.js} +7 -4
- package/build/config/env.js +9 -4
- package/build/config/settings/disease.js +26 -0
- package/build/config/settings/knobs.js +8 -0
- package/build/config/settings/loader.js +244 -0
- package/build/config/settings/schema.js +276 -0
- package/build/core/addressing.js +1 -1
- package/build/core/amend.js +5 -8
- package/build/core/arc.js +7 -8
- package/build/core/bind-workspace.js +123 -0
- package/build/core/bind.js +75 -155
- package/build/core/call/call.js +229 -0
- package/build/core/call/context.js +251 -0
- package/build/core/call/execution.js +217 -0
- package/build/core/call/prompt.js +42 -0
- package/build/core/call/reference.js +24 -0
- package/build/core/call-persist.js +10 -3
- package/build/core/claim-delivery.js +247 -0
- package/build/core/claim.js +39 -277
- package/build/core/context.js +2 -1
- package/build/core/{render.js → contract-view.js} +12 -140
- package/build/core/draft.js +107 -9
- package/build/core/entry.js +9 -1
- package/build/core/execution-coordinate.js +4 -20
- package/build/core/execution-pact.js +16 -31
- package/build/core/forfeit.js +35 -22
- package/build/core/hints.js +2 -2
- package/build/core/log.js +2 -2
- package/build/core/path-coordinate.js +27 -0
- package/build/core/petition-claim-gates.js +8 -6
- package/build/core/petition-preview.js +97 -0
- package/build/core/petition-run.js +5 -7
- package/build/core/petition.js +39 -26
- package/build/core/projection/generation/database.js +306 -0
- package/build/core/projection/generation/identity.js +84 -0
- package/build/core/projection/generation/model.js +201 -0
- package/build/core/projection/generation/store.js +73 -0
- package/build/core/projection/generation/transitions.js +258 -0
- package/build/core/projection/heart.js +0 -307
- package/build/core/projection/tell/database.js +109 -0
- package/build/core/projection/tell/model.js +97 -0
- package/build/core/{projection-tells.js → projection/tell/store.js} +3 -158
- package/build/core/projection-activity.js +23 -2
- package/build/core/projection-coordinate.js +14 -2
- package/build/core/projection-core.js +2 -1
- package/build/core/projection-execution-observer.js +259 -0
- package/build/core/projection-generation-continuation.js +1 -1
- package/build/core/projection-generation-execution.js +90 -20
- package/build/core/projection-generation-launcher.js +1 -1
- package/build/core/projection-generation-runner.js +198 -4
- package/build/core/projection-identity.js +1 -1
- package/build/core/projection-kill.js +50 -17
- package/build/core/projection-life-observer.js +1 -1
- package/build/core/projection-life-protocol.js +10 -0
- package/build/core/projection-mint.js +1 -1
- package/build/core/projection-status.js +45 -32
- package/build/core/projection-wait.js +12 -2
- package/build/core/projection-wake.js +5 -2
- package/build/core/{queue_v2.js → queue.js} +2 -1
- package/build/core/registry.js +1 -1
- package/build/core/renew.js +9 -11
- package/build/core/response-artifact-catalog-sqlite.js +112 -0
- package/build/core/response-artifact-catalog.js +7 -0
- package/build/core/seal.js +44 -16
- package/build/core/status/board.js +289 -0
- package/build/core/status/drift.js +99 -0
- package/build/core/status/lifecycle.js +84 -0
- package/build/core/status/reconciliation.js +131 -0
- package/build/core/stored-agent-event.js +28 -4
- package/build/core/target-ref.js +10 -0
- package/build/core/task/board.js +175 -0
- package/build/core/task/commands.js +211 -0
- package/build/core/task/document.js +189 -0
- package/build/core/task/model.js +1 -0
- package/build/core/task/settlement-git.js +207 -0
- package/build/core/task/settlement-policy.js +82 -0
- package/build/core/task/source-board.js +84 -0
- package/build/core/task/source-model.js +1 -0
- package/build/core/task/validation.js +44 -0
- package/build/core/task-contract.js +237 -0
- package/build/core/task-doctor.js +14 -0
- package/build/core/task-git-actor.js +38 -0
- package/build/core/task-git-runtime.js +37 -0
- package/build/core/task-git-store.js +385 -0
- package/build/core/task-repository.js +1 -0
- package/build/core/task-store-repository.js +86 -0
- package/build/core/task-store.js +1 -0
- package/build/core/task-worktree.js +51 -0
- package/build/core/task.js +4 -0
- package/build/core/transcripts.js +138 -56
- package/build/core/validation.js +17 -0
- package/build/core/verdict.js +2 -2
- package/build/core/worktree-path.js +33 -8
- package/build/flow-error.js +5 -2
- package/build/generated/version.js +1 -1
- package/build/git/branches.js +33 -9
- package/build/git/commits.js +3 -0
- package/build/git/core.js +9 -0
- package/build/git/diff/parsers.js +0 -94
- package/build/index.js +2 -0
- package/build/keiyaku-constants.js +2 -0
- package/build/keiyaku.js +4 -3
- package/package.json +3 -2
- package/skills/keiyaku/SKILL.md +30 -13
- package/skills/keiyaku-akuma/SKILL.md +30 -57
- package/skills/keiyaku-task/SKILL.md +51 -0
- package/skills/keiyaku-workflow/SKILL.md +71 -0
- package/build/agents/harness/pump.js +0 -158
- package/build/agents/providers/codex-app-server.js +0 -864
- package/build/agents/providers/opencode-sdk.js +0 -363
- package/build/agents/providers/pi.js +0 -557
- package/build/config/settings.js +0 -533
- package/build/core/call.js +0 -686
- package/build/core/projection/generation.js +0 -412
- package/build/core/projection/identity.js +0 -81
- package/build/core/projection/mint.js +0 -174
- package/build/core/projection-generation-store.js +0 -707
- package/build/core/status.js +0 -539
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
function transitionFromEntry(current, entry) {
|
|
2
|
+
if (current === "claimed" || current === "forfeited" || current === "corrupt") {
|
|
3
|
+
return "corrupt";
|
|
4
|
+
}
|
|
5
|
+
switch (entry.kind) {
|
|
6
|
+
case "bind":
|
|
7
|
+
return "corrupt";
|
|
8
|
+
case "bootstrap":
|
|
9
|
+
// Placement legality is owned exclusively by deriveContractState.
|
|
10
|
+
return "corrupt";
|
|
11
|
+
case "arc":
|
|
12
|
+
return current === "bound" || current === "active" ? "active" : "corrupt";
|
|
13
|
+
case "seal":
|
|
14
|
+
if (current === "active")
|
|
15
|
+
return "active";
|
|
16
|
+
// Derived default arc a0 may seal from bound (bind/bootstrap → seal0 → petition).
|
|
17
|
+
if (current === "bound" && entry.data.arc === 0)
|
|
18
|
+
return "active";
|
|
19
|
+
return "corrupt";
|
|
20
|
+
case "amend":
|
|
21
|
+
return current === "petitioned" ? "active" : current;
|
|
22
|
+
case "renew":
|
|
23
|
+
return current;
|
|
24
|
+
case "verdict":
|
|
25
|
+
if (current !== "petitioned")
|
|
26
|
+
return "corrupt";
|
|
27
|
+
return entry.data.result === "rejected" ? "active" : "petitioned";
|
|
28
|
+
case "petition":
|
|
29
|
+
return current === "bound" || current === "active" ? "petitioned" : "corrupt";
|
|
30
|
+
case "claim":
|
|
31
|
+
return current === "petitioned" ? "claimed" : "corrupt";
|
|
32
|
+
case "forfeit":
|
|
33
|
+
return "forfeited";
|
|
34
|
+
}
|
|
35
|
+
return "corrupt";
|
|
36
|
+
}
|
|
37
|
+
export function deriveContractState(entries) {
|
|
38
|
+
if (entries.length === 0 || entries[0]?.kind !== "bind") {
|
|
39
|
+
return { state: "corrupt", at: 0, reason: "ledger must start with bind" };
|
|
40
|
+
}
|
|
41
|
+
const bind = entries[0];
|
|
42
|
+
let state = "bound";
|
|
43
|
+
for (let index = 1; index < entries.length; index += 1) {
|
|
44
|
+
const entry = entries[index];
|
|
45
|
+
if (entry.kind === "bootstrap") {
|
|
46
|
+
if (index !== 1 || state !== "bound") {
|
|
47
|
+
return { state: "corrupt", at: index + 1, reason: "bootstrap must immediately follow bind" };
|
|
48
|
+
}
|
|
49
|
+
if (!bind.data.bootstrap) {
|
|
50
|
+
return { state: "corrupt", at: index + 1, reason: "bootstrap without bind plan" };
|
|
51
|
+
}
|
|
52
|
+
// Legal bootstrap is a durable fact and leaves the derived state bound.
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const next = transitionFromEntry(state, entry);
|
|
56
|
+
if (next === "corrupt") {
|
|
57
|
+
return { state: "corrupt", at: index + 1, reason: `invalid ${entry.kind} after ${state}` };
|
|
58
|
+
}
|
|
59
|
+
state = next;
|
|
60
|
+
}
|
|
61
|
+
return { state };
|
|
62
|
+
}
|
|
63
|
+
export function deriveBootstrapDiagnostics(entries) {
|
|
64
|
+
const bind = entries.find((entry) => entry.kind === "bind");
|
|
65
|
+
if (!bind?.data.bootstrap)
|
|
66
|
+
return [];
|
|
67
|
+
if (entries.some((entry) => entry.kind === "bootstrap"))
|
|
68
|
+
return [];
|
|
69
|
+
return [{ kind: "plan-without-result" }];
|
|
70
|
+
}
|
|
71
|
+
export function isTerminalState(state) {
|
|
72
|
+
return state.state === "claimed" || state.state === "forfeited";
|
|
73
|
+
}
|
|
74
|
+
/** Current-petition verdict predicate used by board queue-head stall logic. */
|
|
75
|
+
export function hasCurrentPetitionVerdict(entries) {
|
|
76
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
77
|
+
const entry = entries[index];
|
|
78
|
+
if (entry.kind === "verdict")
|
|
79
|
+
return true;
|
|
80
|
+
if (entry.kind === "petition")
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { createGit } from "../../git/core.js";
|
|
4
|
+
import { stableRepoRoot } from "../worktree-path.js";
|
|
5
|
+
function isClaimReceipt(value) {
|
|
6
|
+
if (!value || typeof value !== "object")
|
|
7
|
+
return false;
|
|
8
|
+
const receipt = value;
|
|
9
|
+
return receipt.kind === "receipt" && [receipt.oldHead, receipt.newHead, receipt.contract].every((field) => typeof field === "string" && field.length > 0);
|
|
10
|
+
}
|
|
11
|
+
function isClaimReceiptBaseline(value) {
|
|
12
|
+
if (!value || typeof value !== "object")
|
|
13
|
+
return false;
|
|
14
|
+
const baseline = value;
|
|
15
|
+
if (baseline.kind !== "baseline" || typeof baseline.head !== "string" || typeof baseline.by !== "string" || baseline.by.length === 0) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (typeof baseline.establishedAt !== "string" || !baseline.establishedAt.endsWith("Z"))
|
|
19
|
+
return false;
|
|
20
|
+
const at = Date.parse(baseline.establishedAt);
|
|
21
|
+
return Number.isFinite(at) && new Date(at).toISOString() === baseline.establishedAt;
|
|
22
|
+
}
|
|
23
|
+
async function readClaimReceipts(cwd) {
|
|
24
|
+
const receiptsPath = path.join(await stableRepoRoot(cwd), ".keiyaku", "claim-receipts.jsonl");
|
|
25
|
+
try {
|
|
26
|
+
const content = fs.readFileSync(receiptsPath, "utf8");
|
|
27
|
+
return content
|
|
28
|
+
.split("\n")
|
|
29
|
+
.map((line, index) => ({ line, index: index + 1 }))
|
|
30
|
+
.filter(({ line }) => line.trim().length > 0)
|
|
31
|
+
.map(({ line, index }) => {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(line);
|
|
34
|
+
return isClaimReceipt(parsed) || isClaimReceiptBaseline(parsed)
|
|
35
|
+
? { line: index, record: parsed }
|
|
36
|
+
: { line: index, issue: { line: index, reason: "malformed" } };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { line: index, issue: { line: index, reason: "malformed" } };
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function computeClaimReconciliation(cwd, defaultBranch) {
|
|
48
|
+
if (!defaultBranch)
|
|
49
|
+
return null;
|
|
50
|
+
const receiptLines = await readClaimReceipts(cwd);
|
|
51
|
+
if (receiptLines.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
const git = createGit(cwd);
|
|
54
|
+
const firstParentHistory = (await git.raw(["rev-list", "--first-parent", defaultBranch.head])).trim().split("\n").filter(Boolean);
|
|
55
|
+
const firstParentSet = new Set(firstParentHistory);
|
|
56
|
+
let baselineIndex = -1;
|
|
57
|
+
for (let index = receiptLines.length - 1; index >= 0; index -= 1) {
|
|
58
|
+
const record = receiptLines[index].record;
|
|
59
|
+
if (record?.kind === "baseline" && firstParentSet.has(record.head)) {
|
|
60
|
+
baselineIndex = index;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (baselineIndex < 0)
|
|
65
|
+
return null;
|
|
66
|
+
const baseline = receiptLines[baselineIndex].record;
|
|
67
|
+
const issues = receiptLines.slice(baselineIndex + 1).flatMap((line) => {
|
|
68
|
+
if (line.issue)
|
|
69
|
+
return [line.issue];
|
|
70
|
+
return line.record?.kind === "baseline" ? [{ line: line.line, reason: "non-chain" }] : [];
|
|
71
|
+
});
|
|
72
|
+
// Only records after the latest valid baseline belong to this fence window.
|
|
73
|
+
let revList;
|
|
74
|
+
try {
|
|
75
|
+
const output = await git.raw(["rev-list", "--first-parent", "--reverse", `${baseline.head}..${defaultBranch.head}`]);
|
|
76
|
+
revList = output.trim().split("\n").filter((h) => h.length > 0);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return {
|
|
80
|
+
defaultBranch: defaultBranch.branch,
|
|
81
|
+
head: defaultBranch.head,
|
|
82
|
+
unclaimedSegments: [],
|
|
83
|
+
receiptIssues: [...issues, ...receiptLines.slice(baselineIndex + 1).filter((line) => line.record?.kind === "receipt").map((line) => ({ line: line.line, reason: "non-chain" }))],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const firstParent = new Map();
|
|
87
|
+
let parent = baseline.head;
|
|
88
|
+
for (const commit of revList) {
|
|
89
|
+
firstParent.set(commit, parent);
|
|
90
|
+
parent = commit;
|
|
91
|
+
}
|
|
92
|
+
const indexByCommit = new Map(revList.map((commit, index) => [commit, index]));
|
|
93
|
+
const claimedHeads = new Set();
|
|
94
|
+
let previousReceiptIndex = -1;
|
|
95
|
+
for (const line of receiptLines.slice(baselineIndex + 1)) {
|
|
96
|
+
if (line.record?.kind !== "receipt")
|
|
97
|
+
continue;
|
|
98
|
+
const index = indexByCommit.get(line.record.newHead);
|
|
99
|
+
if (index === undefined || index <= previousReceiptIndex || firstParent.get(line.record.newHead) !== line.record.oldHead) {
|
|
100
|
+
issues.push({ line: line.line, reason: "non-chain" });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
previousReceiptIndex = index;
|
|
104
|
+
claimedHeads.add(line.record.newHead);
|
|
105
|
+
}
|
|
106
|
+
// Walk default-branch first-parent edges and group only movement not covered
|
|
107
|
+
// by a valid receipt. These are facts, not a judgment about their source.
|
|
108
|
+
const segments = [];
|
|
109
|
+
let segmentStart = null;
|
|
110
|
+
let segmentCount = 0;
|
|
111
|
+
for (const commit of revList) {
|
|
112
|
+
if (claimedHeads.has(commit)) {
|
|
113
|
+
if (segmentStart !== null) {
|
|
114
|
+
segments.push({ oldHead: segmentStart, newHead: firstParent.get(commit), commitCount: segmentCount });
|
|
115
|
+
segmentStart = null;
|
|
116
|
+
segmentCount = 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
if (segmentStart === null) {
|
|
121
|
+
segmentStart = firstParent.get(commit);
|
|
122
|
+
}
|
|
123
|
+
segmentCount++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Trailing unclaimed segment up to HEAD
|
|
127
|
+
if (segmentStart !== null) {
|
|
128
|
+
segments.push({ oldHead: segmentStart, newHead: defaultBranch.head, commitCount: segmentCount });
|
|
129
|
+
}
|
|
130
|
+
return { defaultBranch: defaultBranch.branch, head: defaultBranch.head, unclaimedSegments: segments, receiptIssues: issues };
|
|
131
|
+
}
|
|
@@ -1,11 +1,28 @@
|
|
|
1
|
-
import { activityEventBodySchema, AGENT_EVENT_TEXT_MAX_CHARS, AGENT_UNKNOWN_PAYLOAD_MAX_CHARS, } from "../agents/harness/index.js";
|
|
2
|
-
import { EXEC_EVENT_BUFFER_HARD_CAP } from "../agents/harness/event-channel.js";
|
|
1
|
+
import { activityEventBodySchema, AGENT_EVENT_TEXT_MAX_CHARS, AGENT_UNKNOWN_PAYLOAD_MAX_CHARS, EXEC_EVENT_BUFFER_HARD_CAP, } from "../agents/harness/index.js";
|
|
3
2
|
import { isSubagentProvider } from "../keiyaku.js";
|
|
4
3
|
const FAIL_CODES = new Set(["SUBAGENT_TIMEOUT", "SUBAGENT_NON_ZERO_EXIT", "SUBAGENT_SPAWN_ERROR", "SUBAGENT_EXEC_ERROR", "SUBAGENT_PROVIDER_UNAVAILABLE", "PROJECTION_STATE_ERROR"]);
|
|
5
4
|
const KNOWN = new Set(["message", "unknown", "activity", "thought", "plan", "turn", "usage", "warning", "diagnostic", "truncation", "done", "fail"]);
|
|
6
5
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
6
|
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
8
7
|
const optionalString = (value, key) => value[key] === undefined || typeof value[key] === "string";
|
|
8
|
+
const nonNegativeSafeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
9
|
+
const safeIntegerOrNull = (value) => value === null || (typeof value === "number" && Number.isSafeInteger(value));
|
|
10
|
+
function isValidStoredWarning(warning) {
|
|
11
|
+
if (warning.reason === "retry") {
|
|
12
|
+
return nonNegativeSafeInteger(warning.attempt)
|
|
13
|
+
&& nonNegativeSafeInteger(warning.maxRetries)
|
|
14
|
+
&& nonNegativeSafeInteger(warning.retryDelayMs)
|
|
15
|
+
&& safeIntegerOrNull(warning.errorStatus);
|
|
16
|
+
}
|
|
17
|
+
if (warning.reason === "refusal" || warning.reason === "overload") {
|
|
18
|
+
return optionalString(warning, "originalModel")
|
|
19
|
+
&& optionalString(warning, "fallbackModel")
|
|
20
|
+
&& optionalString(warning, "direction")
|
|
21
|
+
&& (warning.requestId === undefined || warning.requestId === null || typeof warning.requestId === "string")
|
|
22
|
+
&& optionalString(warning, "signerModel");
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
9
26
|
export function classifyStoredAgentEvent(value) {
|
|
10
27
|
if (!record(value) || typeof value.type !== "string")
|
|
11
28
|
return "malformed";
|
|
@@ -20,7 +37,14 @@ export function classifyStoredAgentEvent(value) {
|
|
|
20
37
|
|| value.turn < 0)
|
|
21
38
|
return "malformed";
|
|
22
39
|
switch (value.type) {
|
|
23
|
-
case "message":
|
|
40
|
+
case "message":
|
|
41
|
+
return typeof value.id === "string"
|
|
42
|
+
&& value.id.length > 0
|
|
43
|
+
&& value.id.length <= AGENT_EVENT_TEXT_MAX_CHARS
|
|
44
|
+
&& typeof value.text === "string"
|
|
45
|
+
&& value.text.length <= AGENT_EVENT_TEXT_MAX_CHARS
|
|
46
|
+
? "valid"
|
|
47
|
+
: "malformed";
|
|
24
48
|
case "unknown": {
|
|
25
49
|
if (typeof value.provider !== "string" || value.provider.length > AGENT_EVENT_TEXT_MAX_CHARS ||
|
|
26
50
|
typeof value.nativeType !== "string" || value.nativeType.length > AGENT_EVENT_TEXT_MAX_CHARS ||
|
|
@@ -51,7 +75,7 @@ export function classifyStoredAgentEvent(value) {
|
|
|
51
75
|
case "plan": return typeof value.id === "string" && Array.isArray(value.items) && value.items.every((item) => record(item) && typeof item.text === "string" && typeof item.done === "boolean" && (item.active === undefined || item.active === true)) ? "valid" : "malformed";
|
|
52
76
|
case "turn": return value.phase === "started" || value.phase === "completed" ? "valid" : "malformed";
|
|
53
77
|
case "usage": return finite(value.inputTokens) && finite(value.outputTokens) && (value.cachedInputTokens === undefined || finite(value.cachedInputTokens)) && (value.costUsd === undefined || finite(value.costUsd)) && optionalString(value, "model") ? "valid" : "malformed";
|
|
54
|
-
case "warning": return record(value.warning) && (value.warning
|
|
78
|
+
case "warning": return record(value.warning) && isValidStoredWarning(value.warning) ? "valid" : "malformed";
|
|
55
79
|
case "diagnostic": return typeof value.text === "string" ? "valid" : "malformed";
|
|
56
80
|
case "truncation": {
|
|
57
81
|
return Number.isInteger(value.limit)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FlowError } from "../flow-error.js";
|
|
2
|
+
import { fullBranchRefSchema } from "./entry.js";
|
|
3
|
+
/** Prefix bare branch names and validate the full ref. Sole status/bind normalizer. */
|
|
4
|
+
export function normalizeTargetRef(raw) {
|
|
5
|
+
const ref = raw.startsWith("refs/heads/") ? raw : `refs/heads/${raw}`;
|
|
6
|
+
if (!fullBranchRefSchema.safeParse(ref).success) {
|
|
7
|
+
throw new FlowError("INVALID_TARGET", `invalid target ref '${raw}' — pass a branch name or refs/heads/<name>`);
|
|
8
|
+
}
|
|
9
|
+
return ref;
|
|
10
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { FlowError } from "../../flow-error.js";
|
|
2
|
+
import { TASK_ID } from "./validation.js";
|
|
3
|
+
const TASK_READ_ID = /^k-[0-9a-f]{7,12}$/;
|
|
4
|
+
function compareText(left, right) {
|
|
5
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
6
|
+
}
|
|
7
|
+
function diseaseSort(left, right) {
|
|
8
|
+
return compareText(left.code, right.code)
|
|
9
|
+
|| compareText(left.id ?? "", right.id ?? "")
|
|
10
|
+
|| compareText(left.message, right.message);
|
|
11
|
+
}
|
|
12
|
+
function taskSort(left, right) {
|
|
13
|
+
const leftTerminal = left.state === "done" || left.state === "drop" ? 1 : 0;
|
|
14
|
+
const rightTerminal = right.state === "done" || right.state === "drop" ? 1 : 0;
|
|
15
|
+
return leftTerminal - rightTerminal
|
|
16
|
+
|| left.pri - right.pri
|
|
17
|
+
|| compareText(left.createdAt, right.createdAt)
|
|
18
|
+
|| compareText(left.id, right.id);
|
|
19
|
+
}
|
|
20
|
+
function cycleComponents(tasks) {
|
|
21
|
+
let nextIndex = 0;
|
|
22
|
+
const indexes = new Map();
|
|
23
|
+
const low = new Map();
|
|
24
|
+
const stack = [];
|
|
25
|
+
const onStack = new Set();
|
|
26
|
+
const cycles = [];
|
|
27
|
+
const visit = (id) => {
|
|
28
|
+
indexes.set(id, nextIndex);
|
|
29
|
+
low.set(id, nextIndex);
|
|
30
|
+
nextIndex += 1;
|
|
31
|
+
stack.push(id);
|
|
32
|
+
onStack.add(id);
|
|
33
|
+
for (const need of tasks.get(id).needs) {
|
|
34
|
+
if (!tasks.has(need))
|
|
35
|
+
continue;
|
|
36
|
+
if (!indexes.has(need)) {
|
|
37
|
+
visit(need);
|
|
38
|
+
low.set(id, Math.min(low.get(id), low.get(need)));
|
|
39
|
+
}
|
|
40
|
+
else if (onStack.has(need)) {
|
|
41
|
+
low.set(id, Math.min(low.get(id), indexes.get(need)));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (low.get(id) !== indexes.get(id))
|
|
45
|
+
return;
|
|
46
|
+
const component = [];
|
|
47
|
+
while (stack.length > 0) {
|
|
48
|
+
const member = stack.pop();
|
|
49
|
+
onStack.delete(member);
|
|
50
|
+
component.push(member);
|
|
51
|
+
if (member === id)
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (component.length > 1 || tasks.get(id).needs.includes(id)) {
|
|
55
|
+
cycles.push(new Set(component));
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
for (const id of [...tasks.keys()].sort())
|
|
59
|
+
if (!indexes.has(id))
|
|
60
|
+
visit(id);
|
|
61
|
+
return cycles;
|
|
62
|
+
}
|
|
63
|
+
/** Derive the complete storage-neutral board from domain records. */
|
|
64
|
+
export function buildTaskBoardFromDocuments(documents) {
|
|
65
|
+
const diseases = [];
|
|
66
|
+
const counts = new Map();
|
|
67
|
+
for (const task of documents)
|
|
68
|
+
counts.set(task.id, (counts.get(task.id) ?? 0) + 1);
|
|
69
|
+
const candidates = new Map();
|
|
70
|
+
for (const task of documents) {
|
|
71
|
+
if (counts.get(task.id) > 1) {
|
|
72
|
+
diseases.push({
|
|
73
|
+
code: "duplicate_id",
|
|
74
|
+
message: `duplicate authoritative task id ${task.id}`,
|
|
75
|
+
id: task.id,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
candidates.set(task.id, task);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const component of cycleComponents(candidates)) {
|
|
83
|
+
const relatedIds = [...component].sort();
|
|
84
|
+
for (const id of relatedIds) {
|
|
85
|
+
diseases.push({
|
|
86
|
+
code: "needs_cycle",
|
|
87
|
+
message: `task ${id} participates in a needs cycle`,
|
|
88
|
+
id,
|
|
89
|
+
relatedIds,
|
|
90
|
+
});
|
|
91
|
+
candidates.delete(id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
let changed = true;
|
|
95
|
+
while (changed) {
|
|
96
|
+
changed = false;
|
|
97
|
+
for (const [id, task] of [...candidates].sort(([a], [b]) => compareText(a, b))) {
|
|
98
|
+
const missing = [...task.needs, ...(task.parent ? [task.parent] : []), ...task.from]
|
|
99
|
+
.filter((related) => !candidates.has(related));
|
|
100
|
+
if (missing.length === 0)
|
|
101
|
+
continue;
|
|
102
|
+
const relatedIds = [...new Set(missing)].sort();
|
|
103
|
+
diseases.push({
|
|
104
|
+
code: "missing_reference",
|
|
105
|
+
message: `task ${id} references missing task(s): ${relatedIds.join(", ")}`,
|
|
106
|
+
id,
|
|
107
|
+
relatedIds,
|
|
108
|
+
});
|
|
109
|
+
candidates.delete(id);
|
|
110
|
+
changed = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const blocks = new Map();
|
|
114
|
+
for (const task of candidates.values()) {
|
|
115
|
+
for (const need of task.needs)
|
|
116
|
+
blocks.set(need, [...(blocks.get(need) ?? []), task.id]);
|
|
117
|
+
}
|
|
118
|
+
const tasks = [...candidates.values()].sort(taskSort).map((task) => {
|
|
119
|
+
const blockingNeeds = task.needs.filter((id) => {
|
|
120
|
+
const state = candidates.get(id).state;
|
|
121
|
+
return state === "open" || state === "in_progress";
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
id: task.id,
|
|
125
|
+
title: task.title,
|
|
126
|
+
state: task.state,
|
|
127
|
+
pri: task.pri,
|
|
128
|
+
needs: [...task.needs],
|
|
129
|
+
parent: task.parent,
|
|
130
|
+
from: [...task.from],
|
|
131
|
+
createdAt: task.createdAt,
|
|
132
|
+
creator: task.creator,
|
|
133
|
+
...(task.startedAt === undefined ? {} : { startedAt: task.startedAt }),
|
|
134
|
+
...(task.contractId === undefined ? {} : { contractId: task.contractId }),
|
|
135
|
+
body: task.body,
|
|
136
|
+
blocks: [...(blocks.get(task.id) ?? [])].sort(),
|
|
137
|
+
blocked: task.state === "open" && blockingNeeds.length > 0,
|
|
138
|
+
releasedNeeds: task.needs.filter((id) => candidates.get(id).state === "drop").sort(),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
tasks,
|
|
143
|
+
diseases: diseases.sort(diseaseSort),
|
|
144
|
+
byId: new Map(tasks.map((task) => [task.id, task])),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function resolveTaskReadId(board, input) {
|
|
148
|
+
return resolveTaskId(board, input);
|
|
149
|
+
}
|
|
150
|
+
export function resolveTaskId(board, input) {
|
|
151
|
+
if (!TASK_READ_ID.test(input)) {
|
|
152
|
+
throw new FlowError("INVALID_TARGET", `invalid task id '${input}'`);
|
|
153
|
+
}
|
|
154
|
+
const matches = board.tasks
|
|
155
|
+
.map((task) => task.id)
|
|
156
|
+
.filter((id) => id.startsWith(input))
|
|
157
|
+
.sort();
|
|
158
|
+
if (matches.length === 0)
|
|
159
|
+
throw new FlowError("INVALID_TARGET", `task '${input}' not found`);
|
|
160
|
+
if (matches.length > 1) {
|
|
161
|
+
throw Object.assign(new FlowError("CONTRACT_AMBIGUOUS", `task prefix '${input}' is ambiguous: ${matches.join(", ")}`), { candidates: matches });
|
|
162
|
+
}
|
|
163
|
+
return matches[0];
|
|
164
|
+
}
|
|
165
|
+
export function requireCanonicalTaskId(input) {
|
|
166
|
+
if (!TASK_ID.test(input)) {
|
|
167
|
+
throw new FlowError("INVALID_TARGET", "task mutations require a canonical full task id");
|
|
168
|
+
}
|
|
169
|
+
return input;
|
|
170
|
+
}
|
|
171
|
+
export function requireHealthy(board) {
|
|
172
|
+
if (board.diseases.length > 0) {
|
|
173
|
+
throw new FlowError("INTERNAL_STATE", `authoritative task board is diseased: ${board.diseases.map((item) => item.message).join("; ")}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { FlowError } from "../../flow-error.js";
|
|
2
|
+
import { requireCanonicalTaskId, requireHealthy, resolveTaskId, } from "./board.js";
|
|
3
|
+
import { dateTimestamp, invalid, nonblank, } from "./validation.js";
|
|
4
|
+
export async function readTaskBoard(repository) {
|
|
5
|
+
return repository.read();
|
|
6
|
+
}
|
|
7
|
+
function documentFromView(view) {
|
|
8
|
+
return {
|
|
9
|
+
id: view.id,
|
|
10
|
+
title: view.title,
|
|
11
|
+
state: view.state,
|
|
12
|
+
pri: view.pri,
|
|
13
|
+
needs: [...view.needs],
|
|
14
|
+
parent: view.parent,
|
|
15
|
+
from: [...view.from],
|
|
16
|
+
createdAt: view.createdAt,
|
|
17
|
+
creator: view.creator,
|
|
18
|
+
...(view.startedAt === undefined ? {} : { startedAt: view.startedAt }),
|
|
19
|
+
...(view.contractId === undefined ? {} : { contractId: view.contractId }),
|
|
20
|
+
body: view.body,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function replaceDocument(document) {
|
|
24
|
+
return {
|
|
25
|
+
replacements: [document],
|
|
26
|
+
select(resulting) {
|
|
27
|
+
const value = resulting.byId.get(document.id);
|
|
28
|
+
if (!value)
|
|
29
|
+
throw new FlowError("INTERNAL_STATE", `mutated task ${document.id} disappeared from the board`);
|
|
30
|
+
return value;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function requireExisting(board, id) {
|
|
35
|
+
const task = board.byId.get(requireCanonicalTaskId(id));
|
|
36
|
+
if (!task)
|
|
37
|
+
throw new FlowError("INVALID_TARGET", `task '${id}' not found`);
|
|
38
|
+
return task;
|
|
39
|
+
}
|
|
40
|
+
function resolveMutationIds(board, ids) {
|
|
41
|
+
return ids.map((id) => resolveTaskId(board, id));
|
|
42
|
+
}
|
|
43
|
+
export async function createTask(repository, input, deps) {
|
|
44
|
+
nonblank("title", input.title);
|
|
45
|
+
nonblank("creator", input.creator);
|
|
46
|
+
const pri = input.pri ?? 2;
|
|
47
|
+
if (!Number.isInteger(pri) || pri < 0 || pri > 3)
|
|
48
|
+
invalid("task priority must be an integer from 0 through 3");
|
|
49
|
+
return repository.mutate((board) => {
|
|
50
|
+
requireHealthy(board);
|
|
51
|
+
const needs = resolveMutationIds(board, input.needs ?? []);
|
|
52
|
+
if (new Set(needs).size !== needs.length)
|
|
53
|
+
invalid("task needs must be unique");
|
|
54
|
+
const parent = input.parent == null ? null : resolveTaskId(board, input.parent);
|
|
55
|
+
const from = resolveMutationIds(board, input.from ?? []);
|
|
56
|
+
if (new Set(from).size !== from.length)
|
|
57
|
+
invalid("task from edges must be unique");
|
|
58
|
+
let id;
|
|
59
|
+
for (let attempt = 0; attempt < 32; attempt += 1) {
|
|
60
|
+
const bytes = deps.randomBytes(6);
|
|
61
|
+
if (bytes.byteLength !== 6)
|
|
62
|
+
throw new FlowError("INTERNAL_STATE", "randomBytes(6) must return exactly 6 bytes");
|
|
63
|
+
const candidate = `k-${Buffer.from(bytes).toString("hex")}`;
|
|
64
|
+
if (!board.byId.has(candidate)) {
|
|
65
|
+
id = candidate;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!id)
|
|
70
|
+
throw new FlowError("CONCURRENT_MODIFICATION", "could not allocate a unique task id after 32 attempts");
|
|
71
|
+
const document = {
|
|
72
|
+
id,
|
|
73
|
+
title: input.title,
|
|
74
|
+
state: "open",
|
|
75
|
+
pri,
|
|
76
|
+
needs,
|
|
77
|
+
parent,
|
|
78
|
+
from,
|
|
79
|
+
createdAt: dateTimestamp("createdAt", deps.now()),
|
|
80
|
+
creator: input.creator,
|
|
81
|
+
body: input.body ?? "",
|
|
82
|
+
};
|
|
83
|
+
return replaceDocument(document);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export async function updateTask(repository, fullIdValue, patch) {
|
|
87
|
+
const hasChange = Object.values(patch).some((value) => value !== undefined);
|
|
88
|
+
if (!hasChange)
|
|
89
|
+
throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
|
|
90
|
+
return repository.mutate((board) => {
|
|
91
|
+
requireHealthy(board);
|
|
92
|
+
const id = resolveTaskId(board, fullIdValue);
|
|
93
|
+
const current = requireExisting(board, id);
|
|
94
|
+
const document = documentFromView(current);
|
|
95
|
+
if (patch.title !== undefined)
|
|
96
|
+
document.title = nonblank("title", patch.title);
|
|
97
|
+
if (patch.body !== undefined)
|
|
98
|
+
document.body = patch.body;
|
|
99
|
+
if (patch.pri !== undefined) {
|
|
100
|
+
if (!Number.isInteger(patch.pri) || patch.pri < 0 || patch.pri > 3)
|
|
101
|
+
invalid("task priority must be an integer from 0 through 3");
|
|
102
|
+
document.pri = patch.pri;
|
|
103
|
+
}
|
|
104
|
+
const additions = resolveMutationIds(board, patch.addNeeds ?? []);
|
|
105
|
+
const removals = new Set(resolveMutationIds(board, patch.dropNeeds ?? []));
|
|
106
|
+
document.needs = [...new Set([...document.needs.filter((need) => !removals.has(need)), ...additions])];
|
|
107
|
+
if (patch.parent !== undefined)
|
|
108
|
+
document.parent = patch.parent === null ? null : resolveTaskId(board, patch.parent);
|
|
109
|
+
if (patch.from !== undefined) {
|
|
110
|
+
document.from = [...new Set([...document.from, ...resolveMutationIds(board, patch.from)])];
|
|
111
|
+
}
|
|
112
|
+
if (JSON.stringify(document) === JSON.stringify(documentFromView(current))) {
|
|
113
|
+
throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
|
|
114
|
+
}
|
|
115
|
+
return replaceDocument(document);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async function transition(repository, fullIdValue, apply) {
|
|
119
|
+
return repository.mutate((board) => {
|
|
120
|
+
requireHealthy(board);
|
|
121
|
+
const id = fullIdValue === undefined
|
|
122
|
+
? selectSoleInProgress(board).id
|
|
123
|
+
: resolveTaskId(board, fullIdValue);
|
|
124
|
+
const document = documentFromView(requireExisting(board, id));
|
|
125
|
+
apply(document);
|
|
126
|
+
return replaceDocument(document);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function rejectBound(document, action = "state") {
|
|
130
|
+
if (document.contractId !== undefined) {
|
|
131
|
+
if (action === "drop") {
|
|
132
|
+
throw new FlowError("UNSUPPORTED_POLICY", `task ${document.id} is bound to contract ${document.contractId}; forfeit the contract before dropping this task`);
|
|
133
|
+
}
|
|
134
|
+
throw new FlowError("UNSUPPORTED_POLICY", `task ${document.id} is bound to contract ${document.contractId}; its state follows the contract`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function startTask(repository, fullId, now) {
|
|
138
|
+
return transition(repository, fullId, (document) => {
|
|
139
|
+
rejectBound(document);
|
|
140
|
+
if (document.state !== "open")
|
|
141
|
+
invalid(`task ${document.id} is ${document.state}, not open`);
|
|
142
|
+
document.state = "in_progress";
|
|
143
|
+
document.startedAt = dateTimestamp("startedAt", now);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
export async function stopTask(repository, fullId) {
|
|
147
|
+
return transition(repository, fullId, (document) => {
|
|
148
|
+
rejectBound(document);
|
|
149
|
+
if (document.state !== "in_progress")
|
|
150
|
+
invalid(`task ${document.id} is ${document.state}, not in_progress`);
|
|
151
|
+
document.state = "open";
|
|
152
|
+
delete document.startedAt;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
export async function doneTask(repository, fullId) {
|
|
156
|
+
return transition(repository, fullId, (document) => {
|
|
157
|
+
rejectBound(document);
|
|
158
|
+
if (document.state !== "open" && document.state !== "in_progress") {
|
|
159
|
+
invalid(`task ${document.id} is ${document.state}, not open or in_progress`);
|
|
160
|
+
}
|
|
161
|
+
document.state = "done";
|
|
162
|
+
delete document.startedAt;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export async function dropTask(repository, fullId) {
|
|
166
|
+
return transition(repository, fullId, (document) => {
|
|
167
|
+
rejectBound(document, "drop");
|
|
168
|
+
if (document.state !== "open" && document.state !== "in_progress") {
|
|
169
|
+
invalid(`task ${document.id} is ${document.state}, not open or in_progress`);
|
|
170
|
+
}
|
|
171
|
+
document.state = "drop";
|
|
172
|
+
delete document.startedAt;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
export function selectSoleInProgress(board) {
|
|
176
|
+
const candidates = board.tasks.filter((task) => task.state === "in_progress");
|
|
177
|
+
if (candidates.length === 1)
|
|
178
|
+
return candidates[0];
|
|
179
|
+
const ids = candidates.map((task) => task.id).sort();
|
|
180
|
+
const code = candidates.length === 0 ? "INVALID_TARGET" : "CONTRACT_AMBIGUOUS";
|
|
181
|
+
throw Object.assign(new FlowError(code, candidates.length === 0
|
|
182
|
+
? "no task in progress"
|
|
183
|
+
: "more than one task in progress; pass an ID"), { candidates: ids, candidateTasks: candidates });
|
|
184
|
+
}
|
|
185
|
+
/** Reopen task pointers whose contract authority has disappeared. The caller
|
|
186
|
+
* supplies the ledger fact; this operation itself remains storage-neutral. */
|
|
187
|
+
export async function rollbackOrphanTaskPointers(repository, contractIds) {
|
|
188
|
+
const before = await repository.read();
|
|
189
|
+
const orphans = before.tasks
|
|
190
|
+
.filter((task) => task.contractId !== undefined && !contractIds.has(task.contractId))
|
|
191
|
+
.map((task) => ({ taskId: task.id, contractId: task.contractId }))
|
|
192
|
+
.sort((a, b) => a.taskId.localeCompare(b.taskId));
|
|
193
|
+
if (orphans.length === 0)
|
|
194
|
+
return [];
|
|
195
|
+
await repository.mutate((board) => {
|
|
196
|
+
const replacements = orphans.flatMap(({ taskId: id }) => {
|
|
197
|
+
const current = board.byId.get(id);
|
|
198
|
+
if (!current || current.contractId === undefined || contractIds.has(current.contractId))
|
|
199
|
+
return [];
|
|
200
|
+
const document = documentFromView(current);
|
|
201
|
+
document.state = "open";
|
|
202
|
+
delete document.startedAt;
|
|
203
|
+
delete document.contractId;
|
|
204
|
+
return [document];
|
|
205
|
+
});
|
|
206
|
+
if (replacements.length === 0)
|
|
207
|
+
throw new FlowError("CONCURRENT_MODIFICATION", "task pointers changed while doctor was reconciling them");
|
|
208
|
+
return { replacements, select: () => undefined };
|
|
209
|
+
});
|
|
210
|
+
return orphans;
|
|
211
|
+
}
|