@astrosheep/keiyaku 2.9.3 → 2.9.5
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/call-terms.js +14 -94
- package/build/agents/harness/activity-values.js +16 -0
- package/build/agents/harness/event-persistence.js +218 -0
- package/build/agents/harness/execution-handle.js +26 -222
- package/build/agents/harness/index.js +3 -1
- package/build/agents/harness/outcome.js +68 -5
- package/build/agents/harness/projection.js +179 -124
- package/build/agents/harness/provider-adapter.js +58 -0
- package/build/agents/harness/router.js +11 -187
- package/build/agents/harness/runtime.js +17 -4
- package/build/agents/harness/session.js +15 -0
- package/build/agents/launch-snapshot/model.js +15 -236
- package/build/agents/launch-snapshot/parse-helpers.js +114 -0
- package/build/agents/launch-snapshot/resolve.js +17 -43
- package/build/agents/opencode-sdk.js +45 -12
- package/build/agents/providers/claude-agent-sdk/adapter.js +6 -17
- package/build/agents/providers/claude-agent-sdk/registration.js +214 -0
- package/build/agents/providers/claude-agent-sdk/session-schema.js +2 -0
- package/build/agents/providers/codex-app-server/adapter.js +82 -53
- package/build/agents/providers/codex-app-server/events.js +90 -11
- package/build/agents/providers/codex-app-server/identity.js +1 -0
- package/build/agents/providers/codex-app-server/policy.js +29 -0
- package/build/agents/providers/codex-app-server/registration.js +248 -0
- package/build/agents/providers/codex-app-server/session-schema.js +6 -0
- package/build/agents/providers/codex-app-server/session.js +23 -17
- package/build/agents/providers/codex-app-server/terms.js +1 -0
- package/build/agents/providers/opencode-sdk/adapter.js +94 -50
- package/build/agents/providers/opencode-sdk/events.js +8 -4
- package/build/agents/providers/opencode-sdk/registration.js +135 -0
- package/build/agents/providers/opencode-sdk/session-schema.js +2 -0
- package/build/agents/providers/opencode-sdk/session.js +21 -14
- package/build/agents/providers/opencode-sdk/terms.js +1 -0
- package/build/agents/providers/pi/adapter.js +36 -30
- package/build/agents/providers/pi/events.js +4 -3
- package/build/agents/providers/pi/registration.js +145 -0
- package/build/agents/providers/pi/session-schema.js +6 -0
- package/build/agents/providers/pi/terms.js +1 -0
- package/build/agents/providers/registration.js +7 -0
- package/build/agents/providers/registry.js +76 -0
- package/build/agents/selector.js +2 -29
- package/build/agents/types.js +10 -0
- package/build/artifact-directories.js +22 -0
- package/build/cli/commands/akuma/akuma/handler.js +1 -1
- package/build/cli/commands/akuma/akuma/meta.js +1 -1
- package/build/cli/commands/akuma/list/meta-ls.js +1 -1
- package/build/cli/commands/akuma/view/handler.js +5 -4
- package/build/cli/commands/akuma/view/meta.js +1 -1
- package/build/cli/commands/akuma.js +6 -0
- package/build/cli/commands/contract/amend/handler.js +8 -2
- package/build/cli/commands/contract/amend/meta.js +4 -4
- package/build/cli/commands/contract/arc/handler.js +8 -2
- package/build/cli/commands/contract/arc/meta.js +4 -4
- package/build/cli/commands/contract/audit/handler.js +16 -0
- package/build/cli/commands/contract/audit/meta.js +16 -0
- package/build/cli/commands/contract/bind/handler.js +4 -3
- package/build/cli/commands/contract/bind/meta.js +44 -8
- package/build/cli/commands/contract/catalog.js +2 -0
- package/build/cli/commands/contract/forfeit/handler.js +4 -4
- package/build/cli/commands/contract/forfeit/meta.js +1 -1
- package/build/cli/commands/contract/log/handler.js +3 -3
- package/build/cli/commands/contract/log/meta.js +1 -1
- package/build/cli/commands/contract/petition/handler.js +5 -8
- package/build/cli/commands/contract/petition/meta.js +6 -6
- package/build/cli/commands/contract/renew/handler.js +20 -6
- package/build/cli/commands/contract/renew/meta.js +6 -5
- package/build/cli/commands/metadata.js +59 -0
- package/build/cli/commands/projection/call/handler.js +6 -5
- package/build/cli/commands/projection/call/meta.js +4 -1
- package/build/cli/commands/projection/kill/handler.js +16 -32
- package/build/cli/commands/projection/kill/meta.js +1 -1
- package/build/cli/commands/projection/revive/meta.js +4 -1
- package/build/cli/commands/projection/shared.js +28 -0
- package/build/cli/commands/projection/status/handler.js +1 -1
- package/build/cli/commands/projection/status/meta.js +1 -1
- package/build/cli/commands/projection/tell/handler.js +14 -30
- package/build/cli/commands/projection/tell/meta.js +4 -1
- package/build/cli/commands/projection/wait/handler.js +45 -22
- package/build/cli/commands/projection/wait/meta.js +8 -3
- package/build/cli/commands/registry.js +2 -54
- package/build/cli/commands/shared.js +10 -7
- package/build/cli/commands/skills/install/handler.js +2 -5
- package/build/cli/commands/skills/install/meta.js +1 -1
- package/build/cli/commands/skills/skills/handler.js +1 -1
- package/build/cli/commands/skills/skills/meta.js +1 -1
- package/build/cli/commands/system/completion/meta.js +1 -1
- package/build/cli/commands/system/dump-env/meta.js +1 -1
- package/build/cli/commands/system/guide/meta.js +1 -1
- package/build/cli/commands/task/add/handler.js +7 -15
- package/build/cli/commands/task/add/meta.js +4 -1
- package/build/cli/commands/task/catalog.js +2 -2
- package/build/cli/commands/task/doctor/meta.js +1 -1
- package/build/cli/commands/task/done/handler.js +4 -8
- package/build/cli/commands/task/done/meta.js +1 -1
- package/build/cli/commands/task/drop/handler.js +3 -6
- package/build/cli/commands/task/drop/meta.js +1 -1
- package/build/cli/commands/task/ls/handler.js +12 -8
- package/build/cli/commands/task/ls/meta.js +3 -3
- package/build/cli/commands/task/shared.js +14 -4
- package/build/cli/commands/task/{view → show}/handler.js +7 -8
- package/build/cli/commands/task/show/meta.js +8 -0
- package/build/cli/commands/task/start/handler.js +3 -6
- package/build/cli/commands/task/start/meta.js +1 -1
- package/build/cli/commands/task/stop/handler.js +4 -8
- package/build/cli/commands/task/stop/meta.js +1 -1
- package/build/cli/commands/task/task/handler.js +1 -1
- package/build/cli/commands/task/task/meta.js +1 -1
- package/build/cli/commands/task/update/handler.js +6 -11
- package/build/cli/commands/task/update/meta.js +4 -1
- package/build/cli/completion.js +1 -1
- package/build/cli/flags.js +37 -6
- package/build/cli/help.js +7 -14
- package/build/cli/index.js +112 -154
- package/build/cli/missing-subcommand.js +7 -0
- package/build/cli/output-stream-error-policy.js +19 -5
- package/build/cli/parse-flags.js +33 -7
- package/build/cli/parse-metadata.js +3 -9
- package/build/cli/parse-selectors.js +2 -1
- package/build/cli/parse.js +35 -5
- package/build/cli/projection-address.js +12 -12
- package/build/cli/render/address.js +41 -37
- package/build/cli/render/arc.js +24 -12
- package/build/cli/render/audit.js +121 -0
- package/build/cli/render/call.js +11 -5
- package/build/cli/render/errors.js +36 -1
- package/build/cli/render/format.js +4 -0
- package/build/cli/render/kanshi.js +6 -12
- package/build/cli/render/kill.js +9 -0
- package/build/cli/render/misc.js +58 -13
- package/build/cli/render/petition.js +20 -15
- package/build/cli/render/projection-activity.js +15 -6
- package/build/cli/render/shared.js +60 -21
- package/build/cli/render/skills.js +10 -0
- package/build/cli/render/status.js +56 -23
- package/build/cli/render/success-response.js +78 -0
- package/build/cli/render/task.js +66 -57
- package/build/cli/render/tell.js +5 -0
- package/build/cli/render/terminal-failure.js +63 -0
- package/build/cli/render/types.js +1 -0
- package/build/cli/render/wait.js +68 -38
- package/build/cli/types.js +1 -1
- package/build/config/akuma-loader.js +0 -5
- package/build/config/env-keys.js +3 -1
- package/build/config/env.js +3 -0
- package/build/config/provider-policy-ownership.js +3 -32
- package/build/config/settings/disease.js +8 -1
- package/build/config/settings/loader.js +11 -3
- package/build/config/settings/provider-instance-fields.js +51 -0
- package/build/config/settings/schema.js +6 -176
- package/build/core/addressing.js +66 -16
- package/build/core/amend.js +61 -18
- package/build/core/arc.js +33 -73
- package/build/core/audit.js +650 -0
- package/build/core/bind.js +82 -35
- package/build/core/call/call.js +15 -9
- package/build/core/call/context.js +6 -51
- package/build/core/call/execution.js +22 -22
- package/build/core/call/prompt.js +14 -1
- package/build/core/command-markdown.js +45 -0
- package/build/core/context.js +13 -7
- package/build/core/contract-view.js +22 -6
- package/build/core/draft.js +139 -19
- package/build/core/entry.js +3 -4
- package/build/core/forfeit.js +5 -2
- package/build/core/hints.js +1 -75
- package/build/core/identifier-slug.js +66 -0
- package/build/core/ids.js +25 -11
- package/build/core/ledger.js +20 -68
- package/build/core/markdown/lex.js +2 -1
- package/build/core/markdown/parser.js +0 -68
- package/build/core/markdown/render.js +68 -0
- package/build/core/markdown/sections.js +2 -1
- package/build/core/outcome-base.js +4 -0
- package/build/core/projection/generation/database.js +41 -52
- package/build/core/projection/generation/model.js +63 -2
- package/build/core/{projection-generation-continuation.js → projection/generation/projection-generation-continuation.js} +3 -3
- package/build/core/{projection-generation-execution.js → projection/generation/projection-generation-execution.js} +36 -53
- package/build/core/{projection-generation-identity.js → projection/generation/projection-generation-identity.js} +2 -2
- package/build/core/{projection-generation-launcher.js → projection/generation/projection-generation-launcher.js} +9 -7
- package/build/core/{projection-generation-process.js → projection/generation/projection-generation-process.js} +2 -1
- package/build/core/{projection-generation-runner.js → projection/generation/projection-generation-runner.js} +19 -3
- package/build/core/projection/generation/store.js +3 -3
- package/build/core/projection/generation/transitions.js +19 -3
- package/build/core/projection/index.js +27 -0
- package/build/core/{projection-activity.js → projection/projection-activity.js} +12 -0
- package/build/core/{projection-alias.js → projection/projection-alias.js} +13 -13
- package/build/core/projection/projection-coordinate.js +61 -0
- package/build/core/{projection-core.js → projection/projection-core.js} +9 -9
- package/build/core/{projection-execution-observer.js → projection/projection-execution-observer.js} +37 -78
- package/build/core/{projection-identity.js → projection/projection-identity.js} +1 -1
- package/build/core/{projection-kill.js → projection/projection-kill.js} +1 -1
- package/build/core/{projection-life-observer.js → projection/projection-life-observer.js} +16 -2
- package/build/core/{projection-mint.js → projection/projection-mint.js} +10 -10
- package/build/core/{projection-status.js → projection/projection-status.js} +163 -45
- package/build/core/projection/projection-terminal-failure.js +106 -0
- package/build/core/projection/projection-wait.js +275 -0
- package/build/core/projection/projection-wake.js +227 -0
- package/build/core/provider-loader-ports.js +4 -0
- package/build/core/ref-log.js +65 -40
- package/build/core/registry.js +63 -10
- package/build/core/renew-session.js +128 -0
- package/build/core/renew.js +448 -102
- package/build/core/scope.js +272 -68
- package/build/core/seal.js +165 -40
- package/build/core/{claim-delivery.js → settlement/claim-delivery.js} +8 -8
- package/build/core/{claim.js → settlement/claim.js} +32 -8
- package/build/core/settlement/index.js +19 -0
- package/build/core/{petition-claim-gates.js → settlement/petition-claim-gates.js} +24 -49
- package/build/core/{petition-forfeit.js → settlement/petition-forfeit.js} +2 -2
- package/build/core/{petition-head-guard.js → settlement/petition-head-guard.js} +2 -2
- package/build/core/settlement/petition-io.js +1 -0
- package/build/core/{petition-preview.js → settlement/petition-preview.js} +2 -2
- package/build/core/{petition.js → settlement/petition.js} +21 -12
- package/build/core/settlement/queue-read-model.js +143 -0
- package/build/core/settlement/queue-seat-allocation.js +156 -0
- package/build/core/settlement/queue.js +6 -0
- package/build/core/settlement/settlement.js +288 -0
- package/build/core/{verdict.js → settlement/verdict.js} +9 -37
- package/build/core/settlement/verification.js +59 -0
- package/build/core/status/board.js +62 -41
- package/build/core/status/ledger-batch.js +158 -0
- package/build/core/status/reconciliation.js +10 -4
- package/build/core/stored-agent-event.js +6 -2
- package/build/core/task/board.js +41 -4
- package/build/core/task/commands.js +45 -28
- package/build/core/task/document.js +5 -0
- package/build/core/task/index.js +12 -0
- package/build/core/task/model.js +8 -1
- package/build/core/task/query.js +172 -0
- package/build/core/task/settlement-git.js +265 -74
- package/build/core/task/settlement-policy.js +67 -29
- package/build/core/task/source-board.js +1 -0
- package/build/core/task/task-contract.js +444 -0
- package/build/core/{task-doctor.js → task/task-doctor.js} +1 -1
- package/build/core/{task-git-actor.js → task/task-git-actor.js} +5 -10
- package/build/core/{task-git-runtime.js → task/task-git-runtime.js} +3 -3
- package/build/core/{task-git-store.js → task/task-git-store.js} +90 -19
- package/build/core/task/task-repository.js +1 -0
- package/build/core/{task-store-repository.js → task/task-store-repository.js} +6 -4
- package/build/core/task/task-store.js +1 -0
- package/build/core/{task-worktree.js → task/task-worktree.js} +2 -1
- package/build/core/task/task.js +7 -0
- package/build/core/task/tree.js +37 -0
- package/build/core/task/validation.js +5 -2
- package/build/core/transcripts.js +15 -3
- package/build/core/worktree-path.js +43 -28
- package/build/flow-error.js +83 -39
- package/build/fs/path-coordinate.js +16 -0
- package/build/generated/version.js +3 -2
- package/build/git/branches.js +2 -1
- package/build/git/core.js +106 -2
- package/build/git/diff/parsers.js +23 -13
- package/build/git/diff/structured.js +127 -0
- package/build/git/path-coordinate.js +17 -0
- package/build/git/refs.js +57 -0
- package/build/git/streaming-batch.js +301 -0
- package/build/git/worktree.js +65 -0
- package/build/index.js +9 -9
- package/build/keiyaku.js +2 -36
- package/build/telemetry/logger.js +4 -2
- package/package.json +4 -10
- package/skills/keiyaku/SKILL.md +5 -4
- package/skills/keiyaku-akuma/SKILL.md +25 -11
- package/skills/keiyaku-task/SKILL.md +17 -8
- package/skills/keiyaku-workflow/SKILL.md +58 -9
- package/build/agents/providers/codex-sdk.js +0 -329
- package/build/cli/commands/task/view/meta.js +0 -8
- package/build/core/command-io.js +0 -174
- package/build/core/path-coordinate.js +0 -27
- package/build/core/petition-claim.js +0 -117
- package/build/core/petition-run.js +0 -83
- package/build/core/projection-coordinate.js +0 -63
- package/build/core/projection-wait.js +0 -165
- package/build/core/projection-wake.js +0 -136
- package/build/core/queue.js +0 -343
- package/build/core/task-contract.js +0 -237
- package/build/core/task.js +0 -4
- /package/build/{core/task-repository.js → agents/launch-snapshot/types.js} +0 -0
- /package/build/{core/task-store.js → agents/providers/claude-agent-sdk/terms.js} +0 -0
- /package/build/core/{projection-generation-runtime.js → projection/generation/projection-generation-runtime.js} +0 -0
- /package/build/core/{projection-life-protocol.js → projection/projection-life-protocol.js} +0 -0
- /package/build/core/{projection-runner-lock.js → projection/projection-runner-lock.js} +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { FlowError } from "../../flow-error.js";
|
|
2
|
+
import { isCommissionId } from "../ids.js";
|
|
3
|
+
import { taskUpdatedAt } from "./model.js";
|
|
4
|
+
import { isTaskId } from "./validation.js";
|
|
5
|
+
const FILTER_FIELDS = new Set([
|
|
6
|
+
"state", "priority", "parent", "needs", "blocks", "contract", "created", "updated",
|
|
7
|
+
]);
|
|
8
|
+
const ORDERED_FIELDS = new Set(["priority", "created", "updated"]);
|
|
9
|
+
const DISPLAY_VALUES = new Set(["ready", "blocked", "in_progress", "done", "dropped", "bound"]);
|
|
10
|
+
const SORT_KEYS = new Set(["priority", "modified", "created"]);
|
|
11
|
+
const RELATIVE_TIME = /^now-([1-9][0-9]*)([smhdw])$/;
|
|
12
|
+
const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
|
|
13
|
+
export function deriveTaskDisplay(task) {
|
|
14
|
+
if (task.state === "done")
|
|
15
|
+
return "done";
|
|
16
|
+
if (task.state === "drop")
|
|
17
|
+
return "dropped";
|
|
18
|
+
if (task.contractId !== undefined)
|
|
19
|
+
return "bound";
|
|
20
|
+
if (task.state === "in_progress")
|
|
21
|
+
return "in_progress";
|
|
22
|
+
return task.blocked ? "blocked" : "ready";
|
|
23
|
+
}
|
|
24
|
+
function invalidFilter(detail) {
|
|
25
|
+
throw new FlowError("INVALID_TARGET", `invalid task filter: ${detail}`);
|
|
26
|
+
}
|
|
27
|
+
export function parseTaskFilter(expression) {
|
|
28
|
+
const match = /^([a-z][a-z0-9_-]*)(!=|<=|>=|=|<|>)(.*)$/.exec(expression);
|
|
29
|
+
if (!match)
|
|
30
|
+
invalidFilter(`'${expression}' must use FIELD OP VALUE`);
|
|
31
|
+
const field = match[1];
|
|
32
|
+
const operator = match[2];
|
|
33
|
+
if (!FILTER_FIELDS.has(field))
|
|
34
|
+
invalidFilter(`unknown field '${field}'`);
|
|
35
|
+
if (!ORDERED_FIELDS.has(field) && operator !== "=" && operator !== "!=") {
|
|
36
|
+
invalidFilter(`field '${field}' does not support operator '${operator}'`);
|
|
37
|
+
}
|
|
38
|
+
const values = match[3].split(",");
|
|
39
|
+
if (values.some((value) => value.length === 0))
|
|
40
|
+
invalidFilter(`field '${field}' has an empty value`);
|
|
41
|
+
validateFilterValues(field, values);
|
|
42
|
+
return { field, operator, values };
|
|
43
|
+
}
|
|
44
|
+
function parseTimestamp(value, nowMs) {
|
|
45
|
+
const relative = RELATIVE_TIME.exec(value);
|
|
46
|
+
if (relative) {
|
|
47
|
+
const amount = Number(relative[1]);
|
|
48
|
+
const offset = amount * UNIT_MS[relative[2]];
|
|
49
|
+
if (!Number.isSafeInteger(offset))
|
|
50
|
+
invalidFilter(`timestamp '${value}' is outside the supported range`);
|
|
51
|
+
return nowMs - offset;
|
|
52
|
+
}
|
|
53
|
+
const timestamp = Date.parse(value);
|
|
54
|
+
if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== value) {
|
|
55
|
+
invalidFilter(`timestamp '${value}' must be canonical ISO-8601 or now-<duration>`);
|
|
56
|
+
}
|
|
57
|
+
return timestamp;
|
|
58
|
+
}
|
|
59
|
+
function validateFilterValues(field, values) {
|
|
60
|
+
if (field === "state") {
|
|
61
|
+
for (const value of values)
|
|
62
|
+
if (!DISPLAY_VALUES.has(value))
|
|
63
|
+
invalidFilter(`unknown state '${value}'`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (field === "priority") {
|
|
67
|
+
for (const value of values)
|
|
68
|
+
if (!/^[0-3]$/.test(value))
|
|
69
|
+
invalidFilter(`priority '${value}' must be 0 through 3`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (field === "created" || field === "updated") {
|
|
73
|
+
for (const value of values)
|
|
74
|
+
parseTimestamp(value, 0);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (field === "parent" || field === "needs" || field === "blocks") {
|
|
78
|
+
for (const value of values) {
|
|
79
|
+
if (field === "parent" && value === "none")
|
|
80
|
+
continue;
|
|
81
|
+
if (!isTaskId(value))
|
|
82
|
+
invalidFilter(`value '${value}' must be a canonical full task id`);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (field === "contract") {
|
|
87
|
+
for (const value of values) {
|
|
88
|
+
if (value !== "none" && !isCommissionId(value))
|
|
89
|
+
invalidFilter(`value '${value}' must be a full commission id or none`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function parseTaskSort(value) {
|
|
94
|
+
if (value === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
const keys = value.split(",");
|
|
97
|
+
if (keys.some((key) => !SORT_KEYS.has(key))) {
|
|
98
|
+
const invalid = keys.find((key) => !SORT_KEYS.has(key));
|
|
99
|
+
throw new FlowError("INVALID_TARGET", `invalid task sort key '${String(invalid)}'; expected priority, modified, or created`);
|
|
100
|
+
}
|
|
101
|
+
if (new Set(keys).size !== keys.length) {
|
|
102
|
+
throw new FlowError("INVALID_TARGET", "task sort keys must not repeat");
|
|
103
|
+
}
|
|
104
|
+
return keys;
|
|
105
|
+
}
|
|
106
|
+
function compareText(left, right) {
|
|
107
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
108
|
+
}
|
|
109
|
+
function compareValue(left, right, operator) {
|
|
110
|
+
switch (operator) {
|
|
111
|
+
case "=": return left === right;
|
|
112
|
+
case "!=": return left !== right;
|
|
113
|
+
case "<": return left < right;
|
|
114
|
+
case "<=": return left <= right;
|
|
115
|
+
case ">": return left > right;
|
|
116
|
+
case ">=": return left >= right;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function matchesFilter(task, filter, nowMs) {
|
|
120
|
+
return filter.values.some((raw) => {
|
|
121
|
+
if (filter.field === "state")
|
|
122
|
+
return compareValue(deriveTaskDisplay(task), raw, filter.operator);
|
|
123
|
+
if (filter.field === "priority")
|
|
124
|
+
return compareValue(task.pri, Number(raw), filter.operator);
|
|
125
|
+
if (filter.field === "created")
|
|
126
|
+
return compareValue(Date.parse(task.createdAt), parseTimestamp(raw, nowMs), filter.operator);
|
|
127
|
+
if (filter.field === "updated")
|
|
128
|
+
return compareValue(Date.parse(taskUpdatedAt(task)), parseTimestamp(raw, nowMs), filter.operator);
|
|
129
|
+
const expected = raw === "none" ? null : raw;
|
|
130
|
+
if (filter.field === "parent")
|
|
131
|
+
return compareValue(task.parent ?? "\0", expected ?? "\0", filter.operator);
|
|
132
|
+
if (filter.field === "contract")
|
|
133
|
+
return compareValue(task.contractId ?? "\0", expected ?? "\0", filter.operator);
|
|
134
|
+
const relations = filter.field === "needs" ? task.needs : task.blocks;
|
|
135
|
+
return filter.operator === "=" ? relations.includes(raw) : !relations.includes(raw);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function selected(task, selection) {
|
|
139
|
+
const terminal = task.state === "done" || task.state === "drop";
|
|
140
|
+
return selection === "all" || (selection === "done" ? terminal : !terminal);
|
|
141
|
+
}
|
|
142
|
+
function displayOrder(task) {
|
|
143
|
+
const display = deriveTaskDisplay(task);
|
|
144
|
+
return display === "in_progress" ? 0 : display === "ready" ? 1 : display === "blocked" ? 2 : display === "bound" ? 3 : display === "done" ? 4 : 5;
|
|
145
|
+
}
|
|
146
|
+
function defaultCompare(left, right) {
|
|
147
|
+
return displayOrder(left) - displayOrder(right)
|
|
148
|
+
|| left.pri - right.pri
|
|
149
|
+
|| compareText(left.createdAt, right.createdAt)
|
|
150
|
+
|| compareText(left.id, right.id);
|
|
151
|
+
}
|
|
152
|
+
function explicitCompare(keys, left, right) {
|
|
153
|
+
for (const key of keys) {
|
|
154
|
+
const difference = key === "priority"
|
|
155
|
+
? left.pri - right.pri
|
|
156
|
+
: key === "modified"
|
|
157
|
+
? compareText(taskUpdatedAt(right), taskUpdatedAt(left))
|
|
158
|
+
: compareText(right.createdAt, left.createdAt);
|
|
159
|
+
if (difference !== 0)
|
|
160
|
+
return difference;
|
|
161
|
+
}
|
|
162
|
+
return compareText(left.id, right.id);
|
|
163
|
+
}
|
|
164
|
+
export function selectTaskList(tasks, options = {}) {
|
|
165
|
+
const filters = (options.filters ?? []).map(parseTaskFilter);
|
|
166
|
+
const sort = parseTaskSort(options.sort);
|
|
167
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
168
|
+
return tasks
|
|
169
|
+
.filter((task) => selected(task, options.selection ?? "default"))
|
|
170
|
+
.filter((task) => filters.every((filter) => matchesFilter(task, filter, nowMs)))
|
|
171
|
+
.sort(sort === undefined ? defaultCompare : (left, right) => explicitCompare(sort, left, right));
|
|
172
|
+
}
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import * as fs from "node:fs";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import { FlowError } from "../../flow-error.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
input,
|
|
10
|
-
encoding: "utf8",
|
|
11
|
-
env: { ...process.env, ...env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" },
|
|
12
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
-
});
|
|
4
|
+
import { runGitProcess } from "../../git/core.js";
|
|
5
|
+
import { parseTaskDocument } from "./task.js";
|
|
6
|
+
async function gitResult(cwd, args, input, env) {
|
|
7
|
+
return await runGitProcess(cwd, args, { input, env });
|
|
14
8
|
}
|
|
15
|
-
function git(cwd, args, input, env) {
|
|
16
|
-
const result = gitResult(cwd, args, input, env);
|
|
9
|
+
async function git(cwd, args, input, env) {
|
|
10
|
+
const result = await gitResult(cwd, args, input, env);
|
|
17
11
|
if (result.error) {
|
|
18
12
|
throw new FlowError("INTERNAL_STATE", `git ${args[0]} failed: ${result.error.message}`, { cause: result.error });
|
|
19
13
|
}
|
|
@@ -29,36 +23,118 @@ function boardPath(root, source) {
|
|
|
29
23
|
}
|
|
30
24
|
return relative;
|
|
31
25
|
}
|
|
32
|
-
export function inspectCurrentBranch(cwd) {
|
|
33
|
-
const branch = gitResult(cwd, ["symbolic-ref", "--quiet", "HEAD"]);
|
|
26
|
+
export async function inspectCurrentBranch(cwd) {
|
|
27
|
+
const branch = await gitResult(cwd, ["symbolic-ref", "--quiet", "HEAD"]);
|
|
34
28
|
if (branch.error) {
|
|
35
29
|
throw new FlowError("INTERNAL_STATE", `cannot inspect HEAD: ${branch.error.message}`, { cause: branch.error });
|
|
36
30
|
}
|
|
37
31
|
if (branch.status !== 0 || branch.stdout.trim() === "") {
|
|
38
32
|
throw new FlowError("DETACHED_HEAD", "cannot publish task board from detached HEAD");
|
|
39
33
|
}
|
|
40
|
-
return { ref: branch.stdout.trim(), head: git(cwd, ["rev-parse", "HEAD"]).trim() };
|
|
34
|
+
return { ref: branch.stdout.trim(), head: (await git(cwd, ["rev-parse", "HEAD"])).trim() };
|
|
41
35
|
}
|
|
42
|
-
export function createTaskBoardCommit(input) {
|
|
36
|
+
export async function createTaskBoardCommit(input) {
|
|
43
37
|
const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-task-index-"));
|
|
44
38
|
const indexPath = path.join(temporaryDirectory, "index");
|
|
45
39
|
const env = { GIT_INDEX_FILE: indexPath };
|
|
46
40
|
try {
|
|
47
|
-
git(input.cwd, ["read-tree", input.parent], undefined, env);
|
|
48
|
-
git(input.cwd, ["rm", "-r", "--cached", "-f", "--ignore-unmatch", "--", ".keiyaku/tasks"], undefined, env);
|
|
41
|
+
await git(input.cwd, ["read-tree", input.parent], undefined, env);
|
|
49
42
|
for (const source of input.sources) {
|
|
50
|
-
const oid = git(input.cwd, ["hash-object", "-w", "--stdin"], source.text).trim();
|
|
51
|
-
git(input.cwd, ["update-index", "--add", "--cacheinfo", `100644,${oid},${boardPath(input.root, source)}`], undefined, env);
|
|
43
|
+
const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], source.text)).trim();
|
|
44
|
+
await git(input.cwd, ["update-index", "--add", "--cacheinfo", `100644,${oid},${boardPath(input.root, source)}`], undefined, env);
|
|
45
|
+
}
|
|
46
|
+
const tree = (await git(input.cwd, ["write-tree"], undefined, env)).trim();
|
|
47
|
+
return (await git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject])).trim();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function taskMutationPath(write) {
|
|
54
|
+
return `.keiyaku/tasks/${write.id}.md`;
|
|
55
|
+
}
|
|
56
|
+
/** Build a commit that updates only the listed task paths on top of parent. */
|
|
57
|
+
export async function createTaskMutationCommit(input) {
|
|
58
|
+
if (input.writes.length === 0) {
|
|
59
|
+
throw new FlowError("INTERNAL_STATE", "task mutation commit requires at least one write");
|
|
60
|
+
}
|
|
61
|
+
const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-task-index-"));
|
|
62
|
+
const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
|
|
63
|
+
try {
|
|
64
|
+
await git(input.cwd, ["read-tree", input.parent], undefined, env);
|
|
65
|
+
for (const write of input.writes) {
|
|
66
|
+
const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], write.text)).trim();
|
|
67
|
+
await git(input.cwd, [
|
|
68
|
+
"update-index",
|
|
69
|
+
"--add",
|
|
70
|
+
"--cacheinfo",
|
|
71
|
+
`100644,${oid},${taskMutationPath(write)}`,
|
|
72
|
+
], undefined, env);
|
|
52
73
|
}
|
|
53
|
-
const tree = git(input.cwd, ["write-tree"], undefined, env).trim();
|
|
54
|
-
|
|
74
|
+
const tree = (await git(input.cwd, ["write-tree"], undefined, env)).trim();
|
|
75
|
+
const parentTree = (await git(input.cwd, ["rev-parse", `${input.parent}^{tree}`])).trim();
|
|
76
|
+
if (tree === parentTree)
|
|
77
|
+
return input.parent;
|
|
78
|
+
return (await git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject])).trim();
|
|
55
79
|
}
|
|
56
80
|
finally {
|
|
57
81
|
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
58
82
|
}
|
|
59
83
|
}
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
/** Stage only the mutated task paths into the real index; leave unrelated entries alone. */
|
|
85
|
+
export async function reconcileTaskMutationIndex(cwd, writes) {
|
|
86
|
+
for (const write of writes) {
|
|
87
|
+
const oid = (await git(cwd, ["hash-object", "-w", "--stdin"], write.text)).trim();
|
|
88
|
+
await git(cwd, [
|
|
89
|
+
"update-index",
|
|
90
|
+
"--add",
|
|
91
|
+
"--cacheinfo",
|
|
92
|
+
`100644,${oid},${taskMutationPath(write)}`,
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Atomically publish a pathspec-scoped task micro-commit under the hub branch.
|
|
98
|
+
* Unrelated staged and unstaged non-task bytes are never read into the commit.
|
|
99
|
+
*/
|
|
100
|
+
export async function publishTaskMutationCommit(input, deps = {}) {
|
|
101
|
+
let lastError;
|
|
102
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
103
|
+
const branch = await inspectCurrentBranch(input.cwd);
|
|
104
|
+
const commit = await createTaskMutationCommit({
|
|
105
|
+
cwd: input.cwd,
|
|
106
|
+
gitDir: input.gitDir,
|
|
107
|
+
parent: branch.head,
|
|
108
|
+
writes: input.writes,
|
|
109
|
+
subject: input.subject,
|
|
110
|
+
});
|
|
111
|
+
if (commit === branch.head) {
|
|
112
|
+
await reconcileTaskMutationIndex(input.cwd, input.writes);
|
|
113
|
+
return commit;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await updateBranch({
|
|
117
|
+
cwd: input.cwd,
|
|
118
|
+
ref: branch.ref,
|
|
119
|
+
next: commit,
|
|
120
|
+
expected: branch.head,
|
|
121
|
+
}, deps);
|
|
122
|
+
try {
|
|
123
|
+
await reconcileTaskMutationIndex(input.cwd, input.writes);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Branch fact remains authoritative; index may be repaired by a later mutation.
|
|
127
|
+
}
|
|
128
|
+
return commit;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
lastError = error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw lastError;
|
|
135
|
+
}
|
|
136
|
+
async function taskTreeEntries(cwd, commit) {
|
|
137
|
+
return (await git(cwd, ["ls-tree", "-r", "-z", commit, "--", ".keiyaku/tasks"]))
|
|
62
138
|
.split("\0")
|
|
63
139
|
.filter(Boolean)
|
|
64
140
|
.map((record) => {
|
|
@@ -70,47 +146,79 @@ function taskTreeEntries(cwd, commit) {
|
|
|
70
146
|
return { mode, oid, path: record.slice(tab + 1) };
|
|
71
147
|
});
|
|
72
148
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
149
|
+
/** Reconcile only settlement-owned task paths; never rewrite an unrelated index entry. */
|
|
150
|
+
export async function reconcileTaskPathsetIndex(cwd, commit, paths) {
|
|
151
|
+
const entries = new Map((await taskTreeEntries(cwd, commit)).map((entry) => [entry.path, entry]));
|
|
152
|
+
for (const taskPath of paths) {
|
|
153
|
+
const entry = entries.get(taskPath);
|
|
154
|
+
if (entry) {
|
|
155
|
+
await git(cwd, ["update-index", "--add", "--cacheinfo", `${entry.mode},${entry.oid},${entry.path}`]);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await git(cwd, ["update-index", "--force-remove", "--", taskPath]);
|
|
159
|
+
}
|
|
81
160
|
}
|
|
82
161
|
}
|
|
83
|
-
|
|
84
|
-
|
|
162
|
+
/** Task paths tracked by one authoritative commit, independent of the worktree index. */
|
|
163
|
+
export async function trackedTaskPaths(cwd, commit) {
|
|
164
|
+
return (await taskTreeEntries(cwd, commit)).map((entry) => entry.path).sort();
|
|
85
165
|
}
|
|
86
|
-
export function
|
|
87
|
-
|
|
88
|
-
if (result.status !== 0)
|
|
89
|
-
return undefined;
|
|
90
|
-
return result.stdout.trim();
|
|
166
|
+
export async function refHead(cwd, ref) {
|
|
167
|
+
return (await git(cwd, ["rev-parse", ref])).trim();
|
|
91
168
|
}
|
|
92
|
-
export function taskContractAtCommit(cwd, commit, id) {
|
|
169
|
+
export async function taskContractAtCommit(cwd, commit, id) {
|
|
93
170
|
const sourcePath = `.keiyaku/tasks/${id}.md`;
|
|
94
|
-
const result = gitResult(cwd, ["show", `${commit}:${sourcePath}`]);
|
|
171
|
+
const result = await gitResult(cwd, ["show", `${commit}:${sourcePath}`]);
|
|
95
172
|
if (result.status !== 0)
|
|
96
173
|
return undefined;
|
|
97
174
|
return parseTaskDocument({ path: sourcePath, text: result.stdout }).contractId;
|
|
98
175
|
}
|
|
176
|
+
/** Bound task ids and their complete parent ancestry from the immutable bind base. */
|
|
177
|
+
export async function taskSettlementOwnershipAtCommit(cwd, commit, contractId) {
|
|
178
|
+
const documents = new Map();
|
|
179
|
+
for (const entry of await taskTreeEntries(cwd, commit)) {
|
|
180
|
+
const text = await git(cwd, ["cat-file", "blob", entry.oid]);
|
|
181
|
+
const document = parseTaskDocument({ path: entry.path, text });
|
|
182
|
+
documents.set(document.id, document);
|
|
183
|
+
}
|
|
184
|
+
const taskIds = [...documents.values()]
|
|
185
|
+
.filter((document) => document.contractId === contractId)
|
|
186
|
+
.map((document) => document.id)
|
|
187
|
+
.sort();
|
|
188
|
+
const ownedIds = new Set();
|
|
189
|
+
for (const taskId of taskIds) {
|
|
190
|
+
let current = taskId;
|
|
191
|
+
while (current !== null) {
|
|
192
|
+
if (ownedIds.has(current))
|
|
193
|
+
break;
|
|
194
|
+
ownedIds.add(current);
|
|
195
|
+
const document = documents.get(current);
|
|
196
|
+
if (!document) {
|
|
197
|
+
throw new FlowError("INTERNAL_STATE", `bind base ${commit} lost task ${current} from the owned ancestry`);
|
|
198
|
+
}
|
|
199
|
+
current = document.parent;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
taskIds,
|
|
204
|
+
paths: [...ownedIds].map((id) => `.keiyaku/tasks/${id}.md`).sort(),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
99
207
|
export async function updateBranch(input, deps) {
|
|
100
208
|
if (deps.updateBranch)
|
|
101
209
|
await deps.updateBranch(input);
|
|
102
210
|
else
|
|
103
|
-
git(input.cwd, ["update-ref", input.ref, input.next, input.expected]);
|
|
211
|
+
await git(input.cwd, ["update-ref", input.ref, input.next, input.expected]);
|
|
104
212
|
}
|
|
105
213
|
export async function publishForwardTaskBoardCommit(input, deps) {
|
|
106
214
|
let lastError;
|
|
107
215
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
108
|
-
const parent = refHead(input.cwd, input.ref);
|
|
109
|
-
const commit = createTaskBoardCommit({ ...input, parent });
|
|
216
|
+
const parent = await refHead(input.cwd, input.ref);
|
|
217
|
+
const commit = await createTaskBoardCommit({ ...input, parent });
|
|
110
218
|
try {
|
|
111
219
|
await updateBranch({ cwd: input.cwd, ref: input.ref, next: commit, expected: parent }, deps);
|
|
112
220
|
try {
|
|
113
|
-
|
|
221
|
+
await reconcileTaskPathsetIndex(input.cwd, commit, input.paths);
|
|
114
222
|
}
|
|
115
223
|
catch { /* the forward branch fact remains authoritative */ }
|
|
116
224
|
return commit;
|
|
@@ -121,32 +229,32 @@ export async function publishForwardTaskBoardCommit(input, deps) {
|
|
|
121
229
|
}
|
|
122
230
|
throw lastError;
|
|
123
231
|
}
|
|
124
|
-
function treeWithoutTasks(cwd, commit, gitDir) {
|
|
232
|
+
async function treeWithoutTasks(cwd, commit, gitDir) {
|
|
125
233
|
const temporaryDirectory = fs.mkdtempSync(path.join(gitDir, "keiyaku-landing-index-"));
|
|
126
234
|
const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
|
|
127
235
|
try {
|
|
128
|
-
git(cwd, ["read-tree", commit], undefined, env);
|
|
129
|
-
git(cwd, ["rm", "-r", "--cached", "-f", "--ignore-unmatch", "--", ".keiyaku/tasks"], undefined, env);
|
|
130
|
-
return git(cwd, ["write-tree"], undefined, env).trim();
|
|
236
|
+
await git(cwd, ["read-tree", commit], undefined, env);
|
|
237
|
+
await git(cwd, ["rm", "-r", "--cached", "-f", "--ignore-unmatch", "--", ".keiyaku/tasks"], undefined, env);
|
|
238
|
+
return (await git(cwd, ["write-tree"], undefined, env)).trim();
|
|
131
239
|
}
|
|
132
240
|
finally {
|
|
133
241
|
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
134
242
|
}
|
|
135
243
|
}
|
|
136
|
-
function composeLandingTree(input) {
|
|
137
|
-
const baseTree = treeWithoutTasks(input.cwd, input.base, input.gitDir);
|
|
138
|
-
const targetTree = treeWithoutTasks(input.cwd, input.target, input.gitDir);
|
|
139
|
-
const deliveryTree = treeWithoutTasks(input.cwd, input.delivery, input.gitDir);
|
|
244
|
+
export async function composeLandingTree(input) {
|
|
245
|
+
const baseTree = await treeWithoutTasks(input.cwd, input.base, input.gitDir);
|
|
246
|
+
const targetTree = await treeWithoutTasks(input.cwd, input.target, input.gitDir);
|
|
247
|
+
const deliveryTree = await treeWithoutTasks(input.cwd, input.delivery, input.gitDir);
|
|
140
248
|
let mergedTree;
|
|
141
249
|
try {
|
|
142
|
-
mergedTree = git(input.cwd, [
|
|
250
|
+
mergedTree = (await git(input.cwd, [
|
|
143
251
|
"merge-tree",
|
|
144
252
|
"--write-tree",
|
|
145
253
|
"--merge-base",
|
|
146
254
|
baseTree,
|
|
147
255
|
targetTree,
|
|
148
256
|
deliveryTree,
|
|
149
|
-
]).trim();
|
|
257
|
+
])).trim();
|
|
150
258
|
}
|
|
151
259
|
catch (error) {
|
|
152
260
|
throw new FlowError("CLOSE_MERGE_CONFLICT", "claim merge conflict outside task board", { cause: error });
|
|
@@ -154,43 +262,126 @@ function composeLandingTree(input) {
|
|
|
154
262
|
const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-landing-index-"));
|
|
155
263
|
const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
|
|
156
264
|
try {
|
|
157
|
-
git(input.cwd, ["read-tree", mergedTree], undefined, env);
|
|
158
|
-
|
|
265
|
+
await git(input.cwd, ["read-tree", mergedTree], undefined, env);
|
|
266
|
+
// merge-tree deliberately excluded task paths. Restore the target board as
|
|
267
|
+
// the inherited baseline, then overlay only settlement-authorized writes.
|
|
268
|
+
for (const entry of await taskTreeEntries(input.cwd, input.target)) {
|
|
269
|
+
await git(input.cwd, [
|
|
270
|
+
"update-index",
|
|
271
|
+
"--add",
|
|
272
|
+
"--cacheinfo",
|
|
273
|
+
`${entry.mode},${entry.oid},${entry.path}`,
|
|
274
|
+
], undefined, env);
|
|
275
|
+
}
|
|
159
276
|
for (const source of input.sources) {
|
|
160
|
-
const oid = git(input.cwd, ["hash-object", "-w", "--stdin"], source.text).trim();
|
|
161
|
-
git(input.cwd, [
|
|
277
|
+
const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], source.text)).trim();
|
|
278
|
+
await git(input.cwd, [
|
|
162
279
|
"update-index",
|
|
163
280
|
"--add",
|
|
164
281
|
"--cacheinfo",
|
|
165
282
|
`100644,${oid},${boardPath(input.root, source)}`,
|
|
166
283
|
], undefined, env);
|
|
167
284
|
}
|
|
168
|
-
return git(input.cwd, ["write-tree"], undefined, env).trim();
|
|
285
|
+
return (await git(input.cwd, ["write-tree"], undefined, env)).trim();
|
|
169
286
|
}
|
|
170
287
|
finally {
|
|
171
288
|
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
172
289
|
}
|
|
173
290
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
291
|
+
function landingCommitMessage(contractId, targetBranch) {
|
|
292
|
+
return `keiyaku: claim ${contractId}\n\nMerge contract ${contractId} into ${targetBranch}.\n`;
|
|
293
|
+
}
|
|
294
|
+
async function isAncestor(cwd, ancestor, descendant) {
|
|
295
|
+
return (await gitResult(cwd, ["merge-base", "--is-ancestor", ancestor, descendant])).status === 0;
|
|
296
|
+
}
|
|
297
|
+
export async function taskSourcesAtCommit(cwd, root, commit, paths) {
|
|
298
|
+
const sources = [];
|
|
299
|
+
for (const sourcePath of paths) {
|
|
300
|
+
const result = await gitResult(cwd, ["show", `${commit}:${sourcePath}`]);
|
|
301
|
+
if (result.status !== 0)
|
|
302
|
+
return undefined;
|
|
303
|
+
sources.push({ path: path.join(root, ...sourcePath.split("/")), text: result.stdout });
|
|
304
|
+
}
|
|
305
|
+
return sources;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Find the durable landing receipt on the target's first-parent history.
|
|
309
|
+
* Equivalent trees and lookalike messages are insufficient: the commit must
|
|
310
|
+
* have our parent shape, settle every bind-base-owned task, and exactly match
|
|
311
|
+
* a recomposed landing tree over its own first parent.
|
|
312
|
+
*/
|
|
313
|
+
export async function findAcceptedLandingCommit(input) {
|
|
314
|
+
const commits = (await git(input.cwd, ["rev-list", "--first-parent", input.target, `^${input.base}`]))
|
|
315
|
+
.trim()
|
|
316
|
+
.split("\n")
|
|
317
|
+
.filter(Boolean);
|
|
318
|
+
for (const commit of commits) {
|
|
319
|
+
const message = await git(input.cwd, ["show", "-s", "--format=%B", commit]);
|
|
320
|
+
if (message.trimEnd() !== landingCommitMessage(input.contractId, input.targetBranch).trimEnd())
|
|
321
|
+
continue;
|
|
322
|
+
const record = (await git(input.cwd, ["rev-list", "--parents", "-n", "1", commit])).trim().split(/\s+/);
|
|
323
|
+
const parents = record.slice(1);
|
|
324
|
+
const parentShapeAccepted = parents.length === 2
|
|
325
|
+
? parents[1] === input.delivery
|
|
326
|
+
: parents.length === 1 && await isAncestor(input.cwd, input.delivery, parents[0]);
|
|
327
|
+
if (!parentShapeAccepted)
|
|
328
|
+
continue;
|
|
329
|
+
const sources = await taskSourcesAtCommit(input.cwd, input.root, commit, input.ownedPaths);
|
|
330
|
+
if (!sources)
|
|
331
|
+
continue;
|
|
332
|
+
let settled = false;
|
|
333
|
+
try {
|
|
334
|
+
const sourceByPath = new Map(sources.map((source) => [boardPath(input.root, source), source]));
|
|
335
|
+
settled = input.settledPaths.every((settledPath) => {
|
|
336
|
+
const source = sourceByPath.get(settledPath);
|
|
337
|
+
if (!source)
|
|
338
|
+
return false;
|
|
339
|
+
const document = parseTaskDocument(source);
|
|
340
|
+
return document.state === "done" && document.contractId === undefined;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
// A malformed lookalike is not this operation's durable receipt.
|
|
345
|
+
}
|
|
346
|
+
if (!settled)
|
|
347
|
+
continue;
|
|
348
|
+
const expectedTree = await composeLandingTree({
|
|
349
|
+
cwd: input.cwd,
|
|
350
|
+
gitDir: input.gitDir,
|
|
351
|
+
root: input.root,
|
|
352
|
+
base: input.base,
|
|
353
|
+
target: parents[0],
|
|
354
|
+
delivery: input.delivery,
|
|
355
|
+
sources,
|
|
356
|
+
});
|
|
357
|
+
const actualTree = (await git(input.cwd, ["rev-parse", `${commit}^{tree}`])).trim();
|
|
358
|
+
if (actualTree === expectedTree)
|
|
359
|
+
return commit;
|
|
360
|
+
}
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
export async function containsCommit(cwd, ancestor, descendant) {
|
|
364
|
+
return await isAncestor(cwd, ancestor, descendant);
|
|
365
|
+
}
|
|
366
|
+
/** Create the landing commit over a previously composed candidate tree. */
|
|
367
|
+
export async function createLandingCommit(input) {
|
|
368
|
+
// Once delivery is already reachable from target, a single-parent landing
|
|
369
|
+
// records this claim without fabricating a redundant merge parent.
|
|
370
|
+
const parents = input.target === input.delivery || await isAncestor(input.cwd, input.delivery, input.target)
|
|
371
|
+
? ["-p", input.target]
|
|
372
|
+
: ["-p", input.target, "-p", input.delivery];
|
|
373
|
+
return (await git(input.cwd, [
|
|
177
374
|
"commit-tree",
|
|
178
|
-
tree,
|
|
179
|
-
|
|
180
|
-
input.target,
|
|
181
|
-
"-p",
|
|
182
|
-
input.delivery,
|
|
375
|
+
input.tree,
|
|
376
|
+
...parents,
|
|
183
377
|
"-m",
|
|
184
378
|
`keiyaku: claim ${input.contractId}`,
|
|
185
379
|
"-m",
|
|
186
380
|
`Merge contract ${input.contractId} into ${input.targetBranch}.`,
|
|
187
|
-
]).trim();
|
|
188
|
-
}
|
|
189
|
-
export function containsCommit(cwd, ancestor, descendant) {
|
|
190
|
-
return gitResult(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).status === 0;
|
|
381
|
+
])).trim();
|
|
191
382
|
}
|
|
192
|
-
export function updateTargetRef(cwd, ref, next, expected) {
|
|
193
|
-
git(cwd, ["update-ref", ref, next, expected]);
|
|
383
|
+
export async function updateTargetRef(cwd, ref, next, expected) {
|
|
384
|
+
await git(cwd, ["update-ref", ref, next, expected]);
|
|
194
385
|
}
|
|
195
386
|
export function sameTaskSourceBytes(root, left, right) {
|
|
196
387
|
const snapshot = (sources) => new Map(sources.map((source) => [boardPath(root, source), source.text]));
|