@astrosheep/keiyaku 2.9.8 → 2.9.10
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 +50 -182
- package/build/.tsbuildinfo +1 -1
- package/build/agents/harness/control-types.js +18 -1
- package/build/agents/harness/index.js +2 -0
- package/build/agents/harness/outcome-error.js +81 -0
- package/build/agents/harness/projection.js +10 -7
- package/build/agents/harness/router.js +5 -1
- package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
- package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
- package/build/agents/providers/claude-agent-sdk/session.js +19 -5
- package/build/agents/providers/codex-app-server/adapter.js +11 -6
- package/build/agents/providers/codex-app-server/events.js +67 -6
- package/build/agents/providers/codex-app-server/registration.js +11 -1
- package/build/agents/providers/opencode-sdk/adapter.js +8 -1
- package/build/agents/providers/opencode-sdk/events.js +24 -2
- package/build/agents/providers/opencode-sdk/registration.js +11 -1
- package/build/agents/providers/pi/checkpoint.js +9 -1
- package/build/agents/providers/pi/events.js +2 -0
- package/build/agents/providers/pi/registration.js +14 -1
- package/build/cli/commands/akuma/akuma/handler.js +1 -1
- package/build/cli/commands/akuma/akuma/meta.js +3 -3
- package/build/cli/commands/akuma/catalog.js +2 -2
- package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
- package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
- package/build/cli/commands/akuma.js +1 -1
- package/build/cli/commands/contract/amend/handler.js +5 -4
- package/build/cli/commands/contract/arc/handler.js +5 -4
- package/build/cli/commands/contract/audit/handler.js +2 -2
- package/build/cli/commands/contract/bind/handler.js +12 -3
- package/build/cli/commands/contract/bind/meta.js +45 -15
- package/build/cli/commands/contract/forfeit/handler.js +9 -5
- package/build/cli/commands/contract/log/handler.js +2 -2
- package/build/cli/commands/contract/petition/handler.js +34 -7
- package/build/cli/commands/contract/renew/handler.js +7 -7
- package/build/cli/commands/contract/renew/meta.js +3 -3
- package/build/cli/commands/metadata.js +10 -6
- package/build/cli/commands/projection/call/meta.js +1 -1
- package/build/cli/commands/projection/catalog.js +2 -0
- package/build/cli/commands/projection/history/handler.js +21 -0
- package/build/cli/commands/projection/history/meta.js +9 -0
- package/build/cli/commands/projection/shared.js +3 -4
- package/build/cli/commands/projection/status/handler.js +3 -3
- package/build/cli/commands/projection/wait/handler.js +20 -11
- package/build/cli/commands/task/add/handler.js +14 -5
- package/build/cli/commands/task/add/meta.js +2 -2
- package/build/cli/commands/task/catalog.js +4 -0
- package/build/cli/commands/task/done/handler.js +1 -4
- package/build/cli/commands/task/done/meta.js +1 -1
- package/build/cli/commands/task/drop/meta.js +1 -1
- package/build/cli/commands/task/hold/handler.js +3 -0
- package/build/cli/commands/task/hold/meta.js +8 -0
- package/build/cli/commands/task/log/handler.js +1 -1
- package/build/cli/commands/task/log/meta.js +2 -2
- package/build/cli/commands/task/resume/handler.js +3 -0
- package/build/cli/commands/task/resume/meta.js +8 -0
- package/build/cli/commands/task/shared.js +39 -14
- package/build/cli/commands/task/show/handler.js +1 -1
- package/build/cli/commands/task/show/meta.js +1 -1
- package/build/cli/commands/task/start/meta.js +1 -1
- package/build/cli/commands/task/stop/handler.js +1 -4
- package/build/cli/commands/task/stop/meta.js +1 -1
- package/build/cli/commands/task/update/handler.js +11 -3
- package/build/cli/commands/task/update/meta.js +3 -3
- package/build/cli/completion.js +3 -2
- package/build/cli/flags.js +51 -14
- package/build/cli/help.js +4 -1
- package/build/cli/index.js +40 -25
- package/build/cli/parse-flags.js +40 -7
- package/build/cli/parse-metadata.js +5 -2
- package/build/cli/parse-selectors.js +2 -3
- package/build/cli/parse.js +40 -6
- package/build/cli/projection-address.js +12 -10
- package/build/cli/render/amendment-warning.js +8 -0
- package/build/cli/render/arc.js +6 -2
- package/build/cli/render/audit.js +400 -81
- package/build/cli/render/kanshi.js +242 -79
- package/build/cli/render/lifecycle.js +56 -0
- package/build/cli/render/misc.js +24 -28
- package/build/cli/render/petition.js +5 -0
- package/build/cli/render/projection-activity.js +65 -29
- package/build/cli/render/projection-history.js +64 -0
- package/build/cli/render/shared.js +23 -9
- package/build/cli/render/status.js +13 -35
- package/build/cli/render/task.js +51 -26
- package/build/cli/render/terminal-failure.js +3 -1
- package/build/cli/render/wait.js +45 -69
- package/build/cli/subagent-guard.js +1 -1
- package/build/cli/types.js +1 -1
- package/build/config/env-keys.js +3 -1
- package/build/config/env.js +24 -1
- package/build/config/settings/schema.js +3 -1
- package/build/core/address-carrier.js +89 -0
- package/build/core/addressing.js +255 -196
- package/build/core/amend.js +115 -38
- package/build/core/amendment-criteria.js +17 -0
- package/build/core/amendment-warning.js +10 -0
- package/build/core/arc.js +184 -83
- package/build/core/audit/candidate.js +110 -9
- package/build/core/audit/coordinates.js +120 -65
- package/build/core/audit/evidence.js +19 -23
- package/build/core/audit/facade.js +34 -11
- package/build/core/audit/report.js +5 -5
- package/build/core/bind-reconciliation.js +390 -0
- package/build/core/bind-workspace.js +12 -20
- package/build/core/bind.js +242 -273
- package/build/core/call/call.js +1 -0
- package/build/core/call/context.js +46 -71
- package/build/core/call/prompt.js +6 -3
- package/build/core/context.js +49 -35
- package/build/core/contract-carrier-runtime.js +408 -0
- package/build/core/contract-carrier.js +270 -0
- package/build/core/contract-view.js +33 -27
- package/build/core/contract.js +5 -1
- package/build/core/derived-replay.js +207 -0
- package/build/core/draft.js +125 -57
- package/build/core/forfeit.js +113 -56
- package/build/core/identifier-slug.js +63 -13
- package/build/core/ids.js +4 -4
- package/build/core/lifecycle-history.js +35 -0
- package/build/core/lifecycle-recovery.js +117 -0
- package/build/core/lifecycle-runner.js +377 -0
- package/build/core/log.js +37 -11
- package/build/core/outcome-base.js +181 -0
- package/build/core/process-group.js +139 -0
- package/build/core/projection/generation/database.js +82 -7
- package/build/core/projection/generation/model.js +9 -36
- package/build/core/projection/generation/projection-generation-continuation.js +10 -22
- package/build/core/projection/generation/projection-generation-execution.js +13 -6
- package/build/core/projection/index.js +5 -4
- package/build/core/projection/projection-activity.js +29 -8
- package/build/core/projection/projection-alias.js +23 -10
- package/build/core/projection/projection-core.js +21 -7
- package/build/core/projection/projection-execution-observer.js +91 -39
- package/build/core/projection/projection-history.js +474 -0
- package/build/core/projection/projection-runner-lock.js +12 -0
- package/build/core/projection/projection-status-observation.js +365 -0
- package/build/core/projection/projection-status.js +35 -337
- package/build/core/projection/projection-terminal-failure.js +13 -8
- package/build/core/projection/projection-wait.js +7 -2
- package/build/core/projection/tell/launch-store.js +244 -0
- package/build/core/projection/tell/model.js +3 -3
- package/build/core/projection/tell/store.js +70 -118
- package/build/core/registry.js +22 -87
- package/build/core/renew-build.js +424 -0
- package/build/core/renew-plan.js +239 -0
- package/build/core/renew-rewrite.js +77 -0
- package/build/core/renew.js +295 -520
- package/build/core/repository-ledger/accepted-fold-read.js +19 -0
- package/build/core/repository-ledger/accepted-tail.js +116 -0
- package/build/core/repository-ledger/admission.js +38 -0
- package/build/core/repository-ledger/authoritative-transaction.js +1 -0
- package/build/core/repository-ledger/claim-evidence.js +147 -0
- package/build/core/repository-ledger/codec.js +297 -0
- package/build/core/repository-ledger/current-state-store.js +853 -0
- package/build/core/repository-ledger/fold-repository.js +375 -0
- package/build/core/repository-ledger/fold.js +248 -0
- package/build/core/repository-ledger/git-object-store.js +243 -0
- package/build/core/repository-ledger/history-read.js +15 -0
- package/build/core/repository-ledger/incremental-read.js +153 -0
- package/build/core/repository-ledger/index.js +15 -0
- package/build/core/repository-ledger/inventory-plan.js +75 -0
- package/build/core/repository-ledger/inventory.js +310 -0
- package/build/core/repository-ledger/publication-admission.js +125 -0
- package/build/core/repository-ledger/publication.js +353 -0
- package/build/core/repository-ledger/read-model.js +469 -0
- package/build/core/repository-ledger/transaction-membership.js +78 -0
- package/build/core/repository-ledger/traversal.js +404 -0
- package/build/core/repository-ledger/write-transaction.js +339 -0
- package/build/core/seal.js +168 -161
- package/build/core/settlement/claim-delivery.js +42 -215
- package/build/core/settlement/claim.js +235 -153
- package/build/core/settlement/index.js +4 -4
- package/build/core/settlement/petition-claim-gates.js +52 -33
- package/build/core/settlement/petition-forfeit.js +12 -5
- package/build/core/settlement/petition-head-guard.js +8 -5
- package/build/core/settlement/petition-preview.js +68 -56
- package/build/core/settlement/petition-window.js +50 -0
- package/build/core/settlement/petition.js +252 -131
- package/build/core/settlement/queue-read-model.js +72 -95
- package/build/core/settlement/queue-seat-allocation.js +35 -126
- package/build/core/settlement/queue.js +1 -1
- package/build/core/settlement/settlement.js +172 -80
- package/build/core/settlement/verdict.js +112 -66
- package/build/core/settlement/verification-coordination.js +305 -0
- package/build/core/settlement/verification-supervisor.js +275 -0
- package/build/core/settlement/verification.js +398 -31
- package/build/core/status/board.js +125 -127
- package/build/core/status/lifecycle.js +7 -80
- package/build/core/structured-query.js +23 -0
- package/build/core/target-ref.js +1 -1
- package/build/core/task/board.js +59 -24
- package/build/core/task/commands.js +181 -33
- package/build/core/task/coordinate.js +56 -0
- package/build/core/task/document.js +25 -9
- package/build/core/task/index.js +7 -4
- package/build/core/task/query.js +17 -27
- package/build/core/task/settlement-git.js +164 -179
- package/build/core/task/settlement-policy.js +15 -11
- package/build/core/task/source-board.js +31 -13
- package/build/core/task/task-bind-preparation.js +193 -0
- package/build/core/task/task-contract.js +102 -193
- package/build/core/task/task-file-transaction.js +171 -0
- package/build/core/task/task-git-runtime.js +76 -14
- package/build/core/task/task-git-store.js +68 -169
- package/build/core/task/task-store-repository.js +13 -6
- package/build/core/task/task-worktree.js +6 -33
- package/build/core/task/task.js +1 -1
- package/build/core/task/tree.js +3 -3
- package/build/core/task/validation.js +4 -3
- package/build/core/transcripts.js +93 -1
- package/build/core/worktree-bootstrap.js +44 -36
- package/build/core/worktree-path.js +210 -77
- package/build/flow-error.js +7 -7
- package/build/generated/version.js +2 -2
- package/build/git/core.js +76 -13
- package/build/git/refs.js +168 -46
- package/build/git/streaming-batch.js +73 -13
- package/package.json +1 -1
- package/skills/keiyaku/SKILL.md +1 -1
- package/skills/keiyaku-akuma/SKILL.md +3 -3
- package/skills/keiyaku-task/SKILL.md +4 -2
- package/skills/keiyaku-workflow/SKILL.md +88 -203
- package/build/core/entry.js +0 -305
- package/build/core/ledger-batch.js +0 -194
- package/build/core/ledger.js +0 -84
- package/build/core/projection/leash.js +0 -87
- package/build/core/ref-log.js +0 -119
- package/build/core/renew-session.js +0 -128
- package/build/core/status/ledger-batch.js +0 -1
- package/build/core/status/reconciliation.js +0 -137
|
@@ -1,30 +1,92 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { KEIYAKU_FILE } from "../../keiyaku.js";
|
|
4
|
+
import { readCurrentContractLifecycle, readRepositoryLifecycle } from "../repository-ledger/read-model.js";
|
|
5
|
+
import { repositoryDeliveryRef } from "../repository-ledger/codec.js";
|
|
6
|
+
import { contractIdentityFromWorktreeWitness, stableRepoRoot } from "../worktree-path.js";
|
|
3
7
|
import { inspectTaskLock, locateTaskBoard, openTaskStore, openTaskStoreAt, } from "./task-git-store.js";
|
|
4
8
|
import { taskRepositoryFromStore } from "./task-store-repository.js";
|
|
5
9
|
import { withTaskWorktreePolicy } from "./task-worktree.js";
|
|
6
10
|
import { inspectTaskDoctor } from "./task-doctor.js";
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
import { taskNamespaceFromScope } from "./coordinate.js";
|
|
12
|
+
import { FlowError } from "../../flow-error.js";
|
|
13
|
+
import { runGitProcess } from "../../git/core.js";
|
|
14
|
+
function taskNamespaceFromBind(bind) {
|
|
15
|
+
const namespaces = bind.data.scope.flatMap((term) => taskNamespaceFromScope(term) ?? []);
|
|
16
|
+
if (namespaces.length !== 1)
|
|
17
|
+
throw new FlowError("INTERNAL_STATE", `contract ${bind.contract} must have exactly one bind task namespace`);
|
|
18
|
+
return namespaces[0];
|
|
19
|
+
}
|
|
20
|
+
async function taskCommandContext(cwd, signal) {
|
|
21
|
+
const board = await locateTaskBoard(cwd, (repository, args, input) => runGitProcess(repository, args, { input, signal }));
|
|
9
22
|
if (board.mode !== "git")
|
|
10
|
-
return
|
|
11
|
-
|
|
23
|
+
return { board };
|
|
24
|
+
if (board.surfaceRoot === board.root)
|
|
25
|
+
return { board };
|
|
26
|
+
const identity = await contractIdentityFromWorktreeWitness(cwd, board.root, signal);
|
|
27
|
+
if (!identity) {
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(path.join(board.surfaceRoot, KEIYAKU_FILE));
|
|
30
|
+
throw new FlowError("INVALID_TARGET", "contract task namespace is unavailable");
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof FlowError)
|
|
34
|
+
throw error;
|
|
35
|
+
if (error.code !== "ENOENT")
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return { board };
|
|
39
|
+
}
|
|
40
|
+
const outcome = await readCurrentContractLifecycle(board.root, identity.contractId, { signal });
|
|
41
|
+
if (outcome.status !== "healthy" || outcome.model.resources.worktree === null) {
|
|
42
|
+
throw new FlowError("INVALID_TARGET", `contract ${identity.contractId} task namespace is unavailable`);
|
|
43
|
+
}
|
|
44
|
+
const expectedPath = await fs.realpath(path.join(board.root, ".keiyaku", "wt", outcome.model.resources.worktree.place));
|
|
45
|
+
if (identity.branch !== repositoryDeliveryRef(identity.contractId)
|
|
46
|
+
|| identity.branch !== outcome.model.resources.deliveryRef
|
|
47
|
+
|| identity.path !== expectedPath) {
|
|
48
|
+
throw new FlowError("INVALID_TARGET", `contract ${identity.contractId} task namespace is unavailable`);
|
|
49
|
+
}
|
|
50
|
+
const bind = outcome.model.history.find((row) => row.entry.kind === "bind")?.entry;
|
|
51
|
+
if (!bind || bind.kind !== "bind")
|
|
52
|
+
throw new FlowError("INTERNAL_STATE", `contract ${identity.contractId} has no accepted bind`);
|
|
53
|
+
const namespace = taskNamespaceFromBind(bind);
|
|
54
|
+
return { board, contractId: identity.contractId, namespace };
|
|
12
55
|
}
|
|
13
|
-
export async function
|
|
14
|
-
|
|
15
|
-
|
|
56
|
+
export async function taskCommandContractId(cwd) {
|
|
57
|
+
return (await taskCommandContext(cwd)).contractId;
|
|
58
|
+
}
|
|
59
|
+
export async function openTaskCommandRepository(cwd, signal) {
|
|
60
|
+
const context = await taskCommandContext(cwd, signal);
|
|
61
|
+
const board = context.board;
|
|
62
|
+
const store = openTaskStoreAt(board, { signal });
|
|
16
63
|
const repository = taskRepositoryFromStore(store);
|
|
17
|
-
if (
|
|
64
|
+
if (!context.contractId || !context.namespace)
|
|
18
65
|
return repository;
|
|
19
|
-
|
|
20
|
-
|
|
66
|
+
return withTaskWorktreePolicy(repository, context.contractId, context.namespace);
|
|
67
|
+
}
|
|
68
|
+
export async function openTaskMutationContext(cwd, signal) {
|
|
69
|
+
const context = await taskCommandContext(cwd, signal);
|
|
70
|
+
const store = openTaskStoreAt(context.board, { signal });
|
|
71
|
+
const repository = taskRepositoryFromStore(store);
|
|
72
|
+
return {
|
|
73
|
+
repository: context.contractId && context.namespace
|
|
74
|
+
? withTaskWorktreePolicy(repository, context.contractId, context.namespace)
|
|
75
|
+
: repository,
|
|
76
|
+
namespace: context.namespace ?? null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export async function taskCommandNamespace(cwd) {
|
|
80
|
+
const context = await taskCommandContext(cwd);
|
|
81
|
+
return context.namespace ?? null;
|
|
21
82
|
}
|
|
22
83
|
export async function taskContractIds(cwd) {
|
|
23
|
-
|
|
84
|
+
const read = await readRepositoryLifecycle(await stableRepoRoot(cwd));
|
|
85
|
+
return new Set(read.contracts.map((model) => model.contract));
|
|
24
86
|
}
|
|
25
87
|
export async function readTaskDoctor(cwd) {
|
|
26
88
|
const store = await openTaskStore(cwd);
|
|
27
|
-
const repository =
|
|
89
|
+
const repository = await openTaskCommandRepository(cwd);
|
|
28
90
|
const gitMode = store.mode === "git";
|
|
29
91
|
const lockHeld = (await inspectTaskLock(cwd)).state === "held";
|
|
30
92
|
return inspectTaskDoctor(store, repository, {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
1
|
import * as fs from "node:fs";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import { DatabaseSync } from "node:sqlite";
|
|
5
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
6
5
|
import { FlowError } from "../../flow-error.js";
|
|
7
|
-
import { syncDirectorySync } from "../../fs/durability.js";
|
|
8
6
|
import { normalizeFilesystemCoordinate } from "../../fs/path-coordinate.js";
|
|
9
7
|
import { isGitExecutableUnavailable, NOT_GIT_REPOSITORY_PATTERNS, runGitProcess } from "../../git/core.js";
|
|
10
8
|
import { gitCommonRootPath, normalizeGitPath } from "../../git/path-coordinate.js";
|
|
11
9
|
import { inspectCurrentBranch, publishTaskMutationCommit } from "./settlement-git.js";
|
|
12
|
-
import {
|
|
10
|
+
import { createTaskFileTransaction, generationFor, sameSources, taskBoardGeneration, validateTaskMutationWrites, } from "./task-file-transaction.js";
|
|
11
|
+
export { assertTaskBoardGeneration, atomicReplaceTaskFile } from "./task-file-transaction.js";
|
|
12
|
+
import { isTaskCoordinate, parseTaskCoordinate, taskCoordinateFromSourcePath } from "./coordinate.js";
|
|
13
13
|
const SQLITE_BUSY = 5;
|
|
14
14
|
const LOCK_FILE = "task-lock.sqlite";
|
|
15
15
|
const GIT_LOCK_FILE = "keiyaku-task-lock.sqlite";
|
|
@@ -99,15 +99,16 @@ export async function locateTaskBoard(cwd, runGitProbe = defaultRunGit) {
|
|
|
99
99
|
return {
|
|
100
100
|
mode: "git",
|
|
101
101
|
root: taskBoardRootFromGitDir(gitDir),
|
|
102
|
+
surfaceRoot: normalizedCheckoutRoot,
|
|
102
103
|
gitDir,
|
|
103
104
|
};
|
|
104
105
|
}
|
|
105
106
|
function taskDirectory(board) {
|
|
106
|
-
return path.join(board.root, ".keiyaku", "tasks");
|
|
107
|
+
return path.join(board.mode === "git" ? board.surfaceRoot : board.root, ".keiyaku", "tasks");
|
|
107
108
|
}
|
|
108
109
|
const TASK_HISTORY_FORMAT = "%aI%x00%an%x00%s";
|
|
109
|
-
function taskHistoryPath(
|
|
110
|
-
return `.keiyaku/tasks/${
|
|
110
|
+
function taskHistoryPath(coordinate) {
|
|
111
|
+
return `.keiyaku/tasks/${coordinate}.md`;
|
|
111
112
|
}
|
|
112
113
|
function parseTaskHistory(output) {
|
|
113
114
|
const fields = output.split("\0");
|
|
@@ -129,7 +130,7 @@ function parseTaskHistory(output) {
|
|
|
129
130
|
return entries;
|
|
130
131
|
}
|
|
131
132
|
function assertKeiyakuDirectory(board) {
|
|
132
|
-
const keiyakuDirectory = path.join(board.root, ".keiyaku");
|
|
133
|
+
const keiyakuDirectory = path.join(board.mode === "git" ? board.surfaceRoot : board.root, ".keiyaku");
|
|
133
134
|
const stat = lstatIfPresent(keiyakuDirectory);
|
|
134
135
|
if (stat && (stat.isSymbolicLink() || !stat.isDirectory())) {
|
|
135
136
|
throw new FlowError("INTERNAL_STATE", `.keiyaku is not a regular directory: ${keiyakuDirectory}`);
|
|
@@ -224,23 +225,21 @@ export async function inspectTaskLock(cwd) {
|
|
|
224
225
|
closeQuietly(database);
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
|
-
/** Read the current
|
|
228
|
+
/** Read the current checkout branch's task-specific Git history without acquiring the task lock. */
|
|
228
229
|
export async function readTaskHistory(cwd, taskId, deps = {}) {
|
|
229
|
-
|
|
230
|
-
throw new FlowError("INVALID_TARGET", `task history requires a canonical task id: ${taskId}`);
|
|
231
|
-
}
|
|
230
|
+
const coordinate = parseTaskCoordinate(taskId, "task history coordinate").coordinate;
|
|
232
231
|
const board = await locateTaskBoard(cwd, deps.runGit);
|
|
233
232
|
if (board.mode !== "git") {
|
|
234
233
|
throw new FlowError("NOT_GIT_REPO", "task history is available only in a Git repository");
|
|
235
234
|
}
|
|
236
|
-
const result = await (deps.runGit ?? defaultRunGit)(board.
|
|
235
|
+
const result = await (deps.runGit ?? defaultRunGit)(board.surfaceRoot, [
|
|
237
236
|
"log",
|
|
238
237
|
"-z",
|
|
239
238
|
"--topo-order",
|
|
240
239
|
"--reverse",
|
|
241
240
|
`--format=${TASK_HISTORY_FORMAT}`,
|
|
242
241
|
"--",
|
|
243
|
-
taskHistoryPath(
|
|
242
|
+
taskHistoryPath(coordinate),
|
|
244
243
|
]);
|
|
245
244
|
if (result.error) {
|
|
246
245
|
throw new FlowError("INTERNAL_STATE", `cannot read task history: ${result.error.message}`, { cause: result.error });
|
|
@@ -251,34 +250,6 @@ export async function readTaskHistory(cwd, taskId, deps = {}) {
|
|
|
251
250
|
}
|
|
252
251
|
return parseTaskHistory(result.stdout);
|
|
253
252
|
}
|
|
254
|
-
export function atomicReplaceTaskFile(filePath, text, _deps) {
|
|
255
|
-
const directory = path.dirname(filePath);
|
|
256
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
257
|
-
const temporary = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${randomBytes(12).toString("hex")}.tmp`);
|
|
258
|
-
let descriptor;
|
|
259
|
-
try {
|
|
260
|
-
descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o644);
|
|
261
|
-
fs.writeFileSync(descriptor, text, { encoding: "utf8" });
|
|
262
|
-
fs.fsyncSync(descriptor);
|
|
263
|
-
fs.closeSync(descriptor);
|
|
264
|
-
descriptor = undefined;
|
|
265
|
-
fs.renameSync(temporary, filePath);
|
|
266
|
-
syncDirectorySync(directory);
|
|
267
|
-
}
|
|
268
|
-
catch (error) {
|
|
269
|
-
if (descriptor !== undefined) {
|
|
270
|
-
try {
|
|
271
|
-
fs.closeSync(descriptor);
|
|
272
|
-
}
|
|
273
|
-
catch { /* preserve original failure */ }
|
|
274
|
-
}
|
|
275
|
-
try {
|
|
276
|
-
fs.unlinkSync(temporary);
|
|
277
|
-
}
|
|
278
|
-
catch { /* absent after rename or already cleaned */ }
|
|
279
|
-
throw error;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
253
|
function lstatIfPresent(target) {
|
|
283
254
|
try {
|
|
284
255
|
return fs.lstatSync(target);
|
|
@@ -296,81 +267,29 @@ async function readTaskSources(directory) {
|
|
|
296
267
|
if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
|
|
297
268
|
throw new FlowError("INTERNAL_STATE", `task directory is not a regular directory: ${directory}`);
|
|
298
269
|
}
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
.sort((left, right) => Buffer.from(left).compare(Buffer.from(right)))
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (writes.length === 0) {
|
|
319
|
-
throw new FlowError("INTERNAL_STATE", "task mutation must contain at least one write");
|
|
320
|
-
}
|
|
321
|
-
const targets = new Set();
|
|
322
|
-
for (const write of writes) {
|
|
323
|
-
const target = `${write.id}.md`;
|
|
324
|
-
if (!isTaskId(write.id)) {
|
|
325
|
-
throw new FlowError("INTERNAL_STATE", `invalid task mutation id: ${write.id}`);
|
|
326
|
-
}
|
|
327
|
-
if (targets.has(target))
|
|
328
|
-
throw new FlowError("INTERNAL_STATE", `duplicate task mutation target: ${target}`);
|
|
329
|
-
targets.add(target);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
function mutationTarget(write) {
|
|
333
|
-
return `${write.id}.md`;
|
|
334
|
-
}
|
|
335
|
-
function capturePreviousFiles(directory, writes) {
|
|
336
|
-
return writes.map((write) => {
|
|
337
|
-
const destination = path.join(directory, mutationTarget(write));
|
|
338
|
-
const stat = lstatIfPresent(destination);
|
|
339
|
-
if (!stat)
|
|
340
|
-
return { path: destination };
|
|
341
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
342
|
-
throw new FlowError("INTERNAL_STATE", `task destination is not a regular file: ${destination}`);
|
|
270
|
+
const sources = [];
|
|
271
|
+
const visit = (current, depth) => {
|
|
272
|
+
for (const name of fs.readdirSync(current).sort((left, right) => Buffer.from(left).compare(Buffer.from(right)))) {
|
|
273
|
+
const sourcePath = path.join(current, name);
|
|
274
|
+
const sourceStat = fs.lstatSync(sourcePath);
|
|
275
|
+
if (sourceStat.isSymbolicLink())
|
|
276
|
+
throw new FlowError("INTERNAL_STATE", `task source is a symbolic link: ${sourcePath}`);
|
|
277
|
+
if (sourceStat.isDirectory()) {
|
|
278
|
+
if (depth !== 0 || !isTaskCoordinate(name)) {
|
|
279
|
+
throw new FlowError("INTERNAL_STATE", `task namespace must be one canonical directory level: ${sourcePath}`);
|
|
280
|
+
}
|
|
281
|
+
visit(sourcePath, depth + 1);
|
|
282
|
+
}
|
|
283
|
+
else if (sourceStat.isFile() && name.endsWith(".md")) {
|
|
284
|
+
sources.push({ path: sourcePath, text: fs.readFileSync(sourcePath, "utf8") });
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
throw new FlowError("INTERNAL_STATE", `task source is not a Markdown file: ${sourcePath}`);
|
|
288
|
+
}
|
|
343
289
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
function sameSources(left, right) {
|
|
348
|
-
return left.length === right.length && left.every((source, index) => {
|
|
349
|
-
const candidate = right[index];
|
|
350
|
-
return candidate !== undefined && source.path === candidate.path && source.text === candidate.text;
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
function removeTaskFile(filePath) {
|
|
354
|
-
try {
|
|
355
|
-
fs.unlinkSync(filePath);
|
|
356
|
-
syncDirectorySync(path.dirname(filePath));
|
|
357
|
-
}
|
|
358
|
-
catch (error) {
|
|
359
|
-
if (error.code !== "ENOENT")
|
|
360
|
-
throw error;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
function restoreFiles(previous, deps) {
|
|
364
|
-
for (const entry of previous) {
|
|
365
|
-
if (entry.text === undefined)
|
|
366
|
-
removeTaskFile(entry.path);
|
|
367
|
-
else
|
|
368
|
-
(deps.atomicReplace ?? atomicReplaceTaskFile)(entry.path, entry.text);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
function writeTaskFile(directory, write, deps) {
|
|
372
|
-
const destination = path.join(directory, mutationTarget(write));
|
|
373
|
-
(deps.atomicReplace ?? atomicReplaceTaskFile)(destination, write.text);
|
|
290
|
+
};
|
|
291
|
+
visit(directory, 0);
|
|
292
|
+
return sources;
|
|
374
293
|
}
|
|
375
294
|
function releaseTaskLock(database, deps) {
|
|
376
295
|
try {
|
|
@@ -389,12 +308,6 @@ function releaseTaskLock(database, deps) {
|
|
|
389
308
|
closeQuietly(database);
|
|
390
309
|
}
|
|
391
310
|
}
|
|
392
|
-
function assertTaskDirectorySafety(directory) {
|
|
393
|
-
const stat = lstatIfPresent(directory);
|
|
394
|
-
if (stat && (stat.isSymbolicLink() || !stat.isDirectory())) {
|
|
395
|
-
throw new FlowError("INTERNAL_STATE", `task directory is not a regular directory: ${directory}`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
311
|
export function openTaskStoreAt(board, deps = {}) {
|
|
399
312
|
const directory = taskDirectory(board);
|
|
400
313
|
return {
|
|
@@ -407,23 +320,24 @@ export function openTaskStoreAt(board, deps = {}) {
|
|
|
407
320
|
return withLockHeldTaskBoard(board.root, async (transaction) => {
|
|
408
321
|
const previousSources = transaction.sources;
|
|
409
322
|
const plan = await apply(transaction.sources);
|
|
410
|
-
|
|
323
|
+
validateTaskMutationWrites(plan.writes);
|
|
411
324
|
let subject;
|
|
412
325
|
if (transaction.location.mode === "git") {
|
|
413
326
|
subject = plan.subject?.trim();
|
|
414
327
|
if (!subject) {
|
|
415
328
|
throw new FlowError("INTERNAL_STATE", "git task mutation requires a micro-commit subject");
|
|
416
329
|
}
|
|
417
|
-
await inspectCurrentBranch(transaction.location.
|
|
330
|
+
await inspectCurrentBranch(transaction.location.surfaceRoot, deps.signal);
|
|
418
331
|
}
|
|
419
332
|
await transaction.replace(plan.writes);
|
|
420
333
|
if (transaction.location.mode === "git") {
|
|
421
334
|
try {
|
|
422
335
|
await (deps.publishMutation ?? publishTaskMutationCommit)({
|
|
423
|
-
cwd: transaction.location.
|
|
336
|
+
cwd: transaction.location.surfaceRoot,
|
|
424
337
|
gitDir: transaction.location.gitDir,
|
|
425
338
|
writes: plan.writes,
|
|
426
339
|
subject: subject,
|
|
340
|
+
signal: deps.signal,
|
|
427
341
|
});
|
|
428
342
|
}
|
|
429
343
|
catch (error) {
|
|
@@ -446,55 +360,40 @@ export async function withLockHeldTaskBoard(cwd, apply, deps = {}, locatedBoard)
|
|
|
446
360
|
const database = await acquireTaskLockWithAdmission(taskLockPath(board), deps);
|
|
447
361
|
try {
|
|
448
362
|
let sources = await readTaskSources(directory);
|
|
449
|
-
const transaction = {
|
|
363
|
+
const transaction = createTaskFileTransaction({
|
|
450
364
|
location: board,
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
},
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const currentSources = await readTaskSources(directory);
|
|
459
|
-
if (!sameSources(sources, currentSources)) {
|
|
460
|
-
throw new FlowError("CONCURRENT_MODIFICATION", "task sources changed while applying the mutation");
|
|
461
|
-
}
|
|
462
|
-
assertKeiyakuDirectory(board);
|
|
463
|
-
assertTaskDirectorySafety(directory);
|
|
464
|
-
const previous = capturePreviousFiles(directory, writes);
|
|
465
|
-
let applied = 0;
|
|
466
|
-
try {
|
|
467
|
-
for (const write of writes) {
|
|
468
|
-
writeTaskFile(directory, write, deps);
|
|
469
|
-
applied += 1;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
catch (error) {
|
|
473
|
-
try {
|
|
474
|
-
restoreFiles(previous.slice(0, applied + 1), deps);
|
|
475
|
-
}
|
|
476
|
-
catch { /* the original replacement failure remains authoritative */ }
|
|
477
|
-
throw error;
|
|
478
|
-
}
|
|
479
|
-
sources = await readTaskSources(directory);
|
|
480
|
-
},
|
|
481
|
-
async restore(writes, previousSources) {
|
|
482
|
-
validateWrites(writes);
|
|
483
|
-
const currentSources = await readTaskSources(directory);
|
|
484
|
-
if (!sameSources(sources, currentSources)) {
|
|
485
|
-
throw new FlowError("CONCURRENT_MODIFICATION", "task sources changed while rolling back a failed publication");
|
|
486
|
-
}
|
|
487
|
-
const previousById = new Map(previousSources.map((source) => [path.basename(source.path, ".md"), source.text]));
|
|
488
|
-
restoreFiles(writes.map((write) => ({
|
|
489
|
-
path: path.join(directory, mutationTarget(write)),
|
|
490
|
-
text: previousById.get(write.id),
|
|
491
|
-
})), deps);
|
|
492
|
-
sources = await readTaskSources(directory);
|
|
493
|
-
},
|
|
494
|
-
};
|
|
365
|
+
directory,
|
|
366
|
+
getSources: () => sources,
|
|
367
|
+
setSources: (next) => { sources = next; },
|
|
368
|
+
readSources: () => readTaskSources(directory),
|
|
369
|
+
assertBoardSafety: () => assertKeiyakuDirectory(board),
|
|
370
|
+
deps,
|
|
371
|
+
});
|
|
495
372
|
return await apply(transaction);
|
|
496
373
|
}
|
|
497
374
|
finally {
|
|
498
375
|
releaseTaskLock(database, deps);
|
|
499
376
|
}
|
|
500
377
|
}
|
|
378
|
+
export async function readTaskBoardGeneration(cwd, signal) {
|
|
379
|
+
return withLockHeldTaskBoard(cwd, taskBoardGeneration, { signal });
|
|
380
|
+
}
|
|
381
|
+
/** Read-only stable observation for preflight paths that must not create the task lock database. */
|
|
382
|
+
export async function observeTaskBoardSnapshot(cwd, deps = {}) {
|
|
383
|
+
const location = await locateTaskBoard(cwd, deps.runGit);
|
|
384
|
+
const directory = taskDirectory(location);
|
|
385
|
+
const first = await readTaskSources(directory);
|
|
386
|
+
const generation = generationFor(location, first);
|
|
387
|
+
const second = await readTaskSources(directory);
|
|
388
|
+
if (!sameSources(first, second)) {
|
|
389
|
+
throw new FlowError("CONCURRENT_MODIFICATION", "task sources changed during read-only observation");
|
|
390
|
+
}
|
|
391
|
+
return { generation, sources: second };
|
|
392
|
+
}
|
|
393
|
+
export async function readTaskNamespaces(cwd) {
|
|
394
|
+
const snapshot = await observeTaskBoardSnapshot(cwd);
|
|
395
|
+
return new Set(snapshot.sources.flatMap((source) => {
|
|
396
|
+
const namespace = taskCoordinateFromSourcePath(source.path).namespace;
|
|
397
|
+
return namespace === null ? [] : [namespace];
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { requireHealthy } from "./board.js";
|
|
2
|
-
import {
|
|
2
|
+
import { serializeTaskDocument } from "./document.js";
|
|
3
3
|
import { buildTaskSourceBoard } from "./source-board.js";
|
|
4
|
+
import { formatTaskCoordinate, taskCoordinateFromSourcePath } from "./coordinate.js";
|
|
4
5
|
export function taskBoardFromSourceBoard(sourceBoard) {
|
|
5
6
|
const tasks = sourceBoard.tasks.map((task) => ({
|
|
6
7
|
id: task.id,
|
|
8
|
+
namespace: task.namespace,
|
|
9
|
+
coordinate: task.coordinate,
|
|
7
10
|
title: task.title,
|
|
8
11
|
state: task.state,
|
|
9
12
|
pri: task.pri,
|
|
10
13
|
needs: [...task.needs],
|
|
11
14
|
parent: task.parent,
|
|
12
15
|
from: [...task.from],
|
|
16
|
+
supersedes: [...task.supersedes],
|
|
17
|
+
relatesTo: [...task.relatesTo],
|
|
13
18
|
createdAt: task.createdAt,
|
|
14
19
|
...(task.updatedAt === undefined ? {} : { updatedAt: task.updatedAt }),
|
|
15
20
|
creator: task.creator,
|
|
@@ -19,6 +24,8 @@ export function taskBoardFromSourceBoard(sourceBoard) {
|
|
|
19
24
|
blocks: [...task.blocks],
|
|
20
25
|
blocked: task.blocked,
|
|
21
26
|
releasedNeeds: [...task.releasedNeeds],
|
|
27
|
+
supersededBy: [...task.supersededBy],
|
|
28
|
+
relatedTasks: [...task.relatedTasks],
|
|
22
29
|
}));
|
|
23
30
|
return {
|
|
24
31
|
tasks,
|
|
@@ -28,21 +35,21 @@ export function taskBoardFromSourceBoard(sourceBoard) {
|
|
|
28
35
|
...(disease.id === undefined ? {} : { id: disease.id }),
|
|
29
36
|
...(disease.relatedIds === undefined ? {} : { relatedIds: [...disease.relatedIds] }),
|
|
30
37
|
})),
|
|
31
|
-
byId: new Map(tasks.map((task) => [task.
|
|
38
|
+
byId: new Map(tasks.map((task) => [task.coordinate, task])),
|
|
32
39
|
};
|
|
33
40
|
}
|
|
34
41
|
export function prepareTaskDocumentReplacements(sources, replacements) {
|
|
35
42
|
const serialized = replacements.map((document) => ({
|
|
36
|
-
id: document.id,
|
|
43
|
+
id: document.coordinate ?? formatTaskCoordinate(document.namespace ?? null, document.id),
|
|
37
44
|
text: serializeTaskDocument(document),
|
|
38
45
|
}));
|
|
39
46
|
const replacementIds = new Set(serialized.map((replacement) => replacement.id));
|
|
40
47
|
const paths = new Map();
|
|
41
48
|
const retained = sources.filter((source) => {
|
|
42
49
|
try {
|
|
43
|
-
const
|
|
44
|
-
paths.set(
|
|
45
|
-
return !replacementIds.has(
|
|
50
|
+
const sourceCoordinate = taskCoordinateFromSourcePath(source.path).coordinate;
|
|
51
|
+
paths.set(sourceCoordinate, source.path);
|
|
52
|
+
return !replacementIds.has(sourceCoordinate);
|
|
46
53
|
}
|
|
47
54
|
catch {
|
|
48
55
|
// Malformed sources remain quarantined and visible to the resulting board.
|
|
@@ -1,40 +1,13 @@
|
|
|
1
1
|
import { FlowError } from "../../flow-error.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
id: task.id,
|
|
5
|
-
title: task.title,
|
|
6
|
-
state: task.state,
|
|
7
|
-
pri: task.pri,
|
|
8
|
-
needs: [...task.needs],
|
|
9
|
-
parent: task.parent,
|
|
10
|
-
from: [...task.from],
|
|
11
|
-
createdAt: task.createdAt,
|
|
12
|
-
creator: task.creator,
|
|
13
|
-
...(task.startedAt === undefined ? {} : { startedAt: task.startedAt }),
|
|
14
|
-
...(task.contractId === undefined ? {} : { contractId: task.contractId }),
|
|
15
|
-
body: task.body,
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
function isNeedsAppend(prior, next) {
|
|
19
|
-
return next.needs.length > prior.needs.length
|
|
20
|
-
&& prior.needs.every((need, index) => next.needs[index] === need)
|
|
21
|
-
&& new Set(next.needs).size === next.needs.length
|
|
22
|
-
&& documentFacts(prior) === documentFacts({ ...next, needs: prior.needs });
|
|
23
|
-
}
|
|
24
|
-
export function validateTaskWorktreeMutation(before, replacements, after, contractId) {
|
|
25
|
-
const existing = new Map(before.tasks.map((task) => [task.id, task]));
|
|
26
|
-
const bound = before.tasks.filter((task) => task.contractId === contractId);
|
|
2
|
+
import { formatTaskCoordinate } from "./coordinate.js";
|
|
3
|
+
export function validateTaskWorktreeMutation(_before, replacements, _after, contractId, namespace) {
|
|
27
4
|
for (const replacement of replacements) {
|
|
28
|
-
|
|
29
|
-
if (!prior)
|
|
30
|
-
continue;
|
|
31
|
-
const next = after.byId.get(replacement.id);
|
|
32
|
-
if (bound.length === 1 && prior.id === bound[0].id && next && isNeedsAppend(prior, next))
|
|
5
|
+
if (replacement.namespace === namespace)
|
|
33
6
|
continue;
|
|
34
|
-
throw new FlowError("UNSUPPORTED_POLICY", `task ${replacement.
|
|
7
|
+
throw new FlowError("UNSUPPORTED_POLICY", `task ${replacement.coordinate ?? formatTaskCoordinate(replacement.namespace ?? null, replacement.id)} is outside contract ${contractId} namespace ${namespace}`);
|
|
35
8
|
}
|
|
36
9
|
}
|
|
37
|
-
export function withTaskWorktreePolicy(repository, contractId) {
|
|
10
|
+
export function withTaskWorktreePolicy(repository, contractId, namespace) {
|
|
38
11
|
return {
|
|
39
12
|
read: repository.read.bind(repository),
|
|
40
13
|
mutate: async (apply) => repository.mutate(async (before) => {
|
|
@@ -43,7 +16,7 @@ export function withTaskWorktreePolicy(repository, contractId) {
|
|
|
43
16
|
replacements: mutation.replacements,
|
|
44
17
|
...(mutation.subject === undefined ? {} : { subject: mutation.subject }),
|
|
45
18
|
select(after) {
|
|
46
|
-
validateTaskWorktreeMutation(before, mutation.replacements, after, contractId);
|
|
19
|
+
validateTaskWorktreeMutation(before, mutation.replacements, after, contractId, namespace);
|
|
47
20
|
return mutation.select(after);
|
|
48
21
|
},
|
|
49
22
|
};
|
package/build/core/task/task.js
CHANGED
|
@@ -4,4 +4,4 @@ export { parseTaskDocument, serializeTaskDocument, } from "./document.js";
|
|
|
4
4
|
export { resolveTaskReadId, } from "./board.js";
|
|
5
5
|
export { buildTaskBoard } from "./source-board.js";
|
|
6
6
|
export { buildTaskTree } from "./tree.js";
|
|
7
|
-
export { readTaskBoard, createTask, updateTask, startTask, stopTask, doneTask, dropTask, selectSoleInProgress, rollbackOrphanTaskPointers, } from "./commands.js";
|
|
7
|
+
export { readTaskBoard, createTask, updateTask, updateTaskWithReceipt, startTask, stopTask, holdTask, resumeTask, doneTask, dropTask, selectSoleInProgress, rollbackOrphanTaskPointers, } from "./commands.js";
|
package/build/core/task/tree.js
CHANGED
|
@@ -8,7 +8,7 @@ function compareBoardOrder(left, right) {
|
|
|
8
8
|
return leftTerminal - rightTerminal
|
|
9
9
|
|| left.pri - right.pri
|
|
10
10
|
|| compareText(left.createdAt, right.createdAt)
|
|
11
|
-
|| compareText(left.
|
|
11
|
+
|| compareText(left.coordinate, right.coordinate);
|
|
12
12
|
}
|
|
13
13
|
function diagnosticOrder(left, right) {
|
|
14
14
|
return compareText(left.code, right.code)
|
|
@@ -25,10 +25,10 @@ export function buildTaskTree(board, rootId) {
|
|
|
25
25
|
for (const children of childrenByParent.values())
|
|
26
26
|
children.sort(compareBoardOrder);
|
|
27
27
|
const visit = (parentId, ancestors) => (childrenByParent.get(parentId) ?? [])
|
|
28
|
-
.filter((child) => !ancestors.has(child.
|
|
28
|
+
.filter((child) => !ancestors.has(child.coordinate))
|
|
29
29
|
.map((child) => ({
|
|
30
30
|
task: child,
|
|
31
|
-
children: visit(child.
|
|
31
|
+
children: visit(child.coordinate, new Set([...ancestors, child.coordinate])),
|
|
32
32
|
}));
|
|
33
33
|
return {
|
|
34
34
|
descendants: visit(rootId, new Set([rootId])),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FlowError } from "../../flow-error.js";
|
|
2
2
|
import { isNormalizedIdentifierSlug } from "../identifier-slug.js";
|
|
3
|
-
|
|
3
|
+
import { isTaskCoordinate } from "./coordinate.js";
|
|
4
|
+
export const TASK_STATES = new Set(["open", "in_progress", "on_hold", "done", "drop"]);
|
|
4
5
|
const CANONICAL_ISO_UTC_MS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
5
6
|
export function invalid(message) {
|
|
6
7
|
throw new FlowError("INVALID_TARGET", message);
|
|
@@ -13,8 +14,8 @@ export function nonblank(field, value) {
|
|
|
13
14
|
}
|
|
14
15
|
export function fullId(field, value) {
|
|
15
16
|
const id = nonblank(field, value);
|
|
16
|
-
if (!
|
|
17
|
-
invalid(`task field '${field}' must be a canonical full task
|
|
17
|
+
if (!isTaskCoordinate(id))
|
|
18
|
+
invalid(`task field '${field}' must be a canonical full task coordinate`);
|
|
18
19
|
return id;
|
|
19
20
|
}
|
|
20
21
|
export function isTaskId(value) {
|