@astrosheep/keiyaku 4.0.6 → 4.1.0

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.
Files changed (65) hide show
  1. package/build/src/akuma/archetype.js +8 -1
  2. package/build/src/akuma/provider-recipe.d.ts +2 -0
  3. package/build/src/akuma/provider-recipe.js +10 -0
  4. package/build/src/akuma/providers/acp/config.d.ts +2 -0
  5. package/build/src/akuma/providers/acp/config.js +9 -1
  6. package/build/src/akuma/providers/acp/core.d.ts +5 -2
  7. package/build/src/akuma/providers/acp/core.js +29 -16
  8. package/build/src/akuma/providers/acp/index.js +8 -2
  9. package/build/src/akuma/providers/claude/index.d.ts +1 -1
  10. package/build/src/akuma/providers/claude/index.js +9 -5
  11. package/build/src/akuma/providers/codex-app-server/index.js +4 -1
  12. package/build/src/akuma/providers/grok-build/index.js +15 -1
  13. package/build/src/akuma/providers/index.js +1 -1
  14. package/build/src/akuma/providers/opencode-sdk/index.js +3 -0
  15. package/build/src/akuma/providers/pi/index.js +3 -1
  16. package/build/src/body/amend.js +0 -9
  17. package/build/src/body/region.js +2 -2
  18. package/build/src/cli/accepted.js +2 -0
  19. package/build/src/cli/commands/contract.d.ts +5 -3
  20. package/build/src/cli/commands/contract.js +23 -13
  21. package/build/src/cli/commands/task-invoke.d.ts +4 -1
  22. package/build/src/cli/commands/task-invoke.js +6 -3
  23. package/build/src/cli/commands/task.js +10 -2
  24. package/build/src/cli/parse.js +2 -2
  25. package/build/src/cli/render/akuma.js +42 -11
  26. package/build/src/cli/render/catalog.js +5 -1
  27. package/build/src/cli/render/contract.js +25 -3
  28. package/build/src/cli/render/kanshi.js +142 -94
  29. package/build/src/cli/render/receipt.d.ts +1 -0
  30. package/build/src/cli/render/receipt.js +18 -14
  31. package/build/src/cli/render/refusal.js +6 -0
  32. package/build/src/cli/render/task.js +63 -11
  33. package/build/src/cli/result.d.ts +8 -1
  34. package/build/src/contract-worktree.d.ts +0 -2
  35. package/build/src/contract-worktree.js +13 -24
  36. package/build/src/git/nuke.js +1 -6
  37. package/build/src/git/reconcile.d.ts +0 -2
  38. package/build/src/git/reconcile.js +2 -33
  39. package/build/src/git/tender.d.ts +13 -2
  40. package/build/src/git/tender.js +43 -4
  41. package/build/src/kanshi/read.js +32 -12
  42. package/build/src/kanshi/report.d.ts +2 -1
  43. package/build/src/library/continuation.d.ts +26 -0
  44. package/build/src/library/continuation.js +78 -0
  45. package/build/src/library/contract.d.ts +9 -5
  46. package/build/src/library/contract.js +27 -2
  47. package/build/src/library/delivery.d.ts +18 -13
  48. package/build/src/library/keiyaku.d.ts +1 -1
  49. package/build/src/library/mutation.js +11 -4
  50. package/build/src/protocol/deliver.d.ts +12 -2
  51. package/build/src/protocol/deliver.js +52 -19
  52. package/build/src/protocol/operations.d.ts +7 -2
  53. package/build/src/settlement/fence.d.ts +0 -2
  54. package/build/src/settlement/fence.js +0 -24
  55. package/build/src/task/board.d.ts +21 -2
  56. package/build/src/task/board.js +25 -12
  57. package/build/src/task/identity.js +14 -4
  58. package/build/src/task/index.d.ts +1 -0
  59. package/build/src/task/index.js +7 -1
  60. package/build/src/task/operations.d.ts +2 -1
  61. package/build/src/task/operations.js +15 -4
  62. package/build/src/task/query.js +2 -2
  63. package/build/src/task/store.d.ts +2 -0
  64. package/build/src/task/store.js +12 -83
  65. package/package.json +1 -1
@@ -25,6 +25,7 @@ import { reconcileInput, scopeForRepo } from "./repo.js";
25
25
  import { injectedBodyRequests, requestBodyDeliver, requestBodyReview, } from "../akuma/requests.js";
26
26
  import { auditContract } from "./audit.js";
27
27
  import { Delivery, deliveryHandle } from "./delivery.js";
28
+ import { continueDeliveredDependents } from "./continuation.js";
28
29
  import { completionInput, completeHolderMutation, completeMutation, } from "./mutation.js";
29
30
  import { completeReconcile } from "./reconcile.js";
30
31
  import { admitBindWithAppointment } from "./bind.js";
@@ -75,9 +76,22 @@ async function executeLocalDelivery(input) {
75
76
  });
76
77
  if (outcome.kind === "integration-conflict-materialized")
77
78
  return outcome;
79
+ const accepted = requireAccepted(outcome);
80
+ const continued = accepted.value.completion === undefined
81
+ ? accepted
82
+ : await continueDeliveredDependents({
83
+ scope: input.scope,
84
+ channel,
85
+ contractId: input.contractId,
86
+ accepted,
87
+ deriveDocument: derivedDocument,
88
+ resolveHereWorkspace: async (id) => await resolveHereWorkspace(input.scope, id),
89
+ ...(input.actor === undefined ? {} : { actor: input.actor }),
90
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
91
+ });
78
92
  return completeMutation({
79
93
  ...completionInput(input.scope, channel, input.contractId, (delivery) => delivery, input.hooks),
80
- accepted: requireAccepted(outcome),
94
+ accepted: continued,
81
95
  });
82
96
  });
83
97
  }
@@ -93,9 +107,20 @@ async function executeLocalReview(input) {
93
107
  ...(input.actor === undefined ? {} : { actor: input.actor }),
94
108
  resolveHereWorkspace: async (id) => await resolveHereWorkspace(input.scope, id),
95
109
  }));
110
+ const continued = accepted.value.completion === undefined
111
+ ? accepted
112
+ : await continueDeliveredDependents({
113
+ scope: input.scope,
114
+ channel,
115
+ contractId: input.contractId,
116
+ accepted,
117
+ deriveDocument: derivedDocument,
118
+ resolveHereWorkspace: async (id) => await resolveHereWorkspace(input.scope, id),
119
+ ...(input.actor === undefined ? {} : { actor: input.actor }),
120
+ });
96
121
  return completeMutation({
97
122
  ...completionInput(input.scope, channel, input.contractId, (review) => review, input.hooks),
98
- accepted,
123
+ accepted: continued,
99
124
  });
100
125
  });
101
126
  }
@@ -1,19 +1,24 @@
1
1
  import type { SnapshotId } from "../core/facts/types.js";
2
- import type { DeliverValue } from "../protocol/deliver.js";
2
+ import type { DeliverValue as ProtocolDeliverValue } from "../protocol/deliver.js";
3
+ import type { ContinuationReport } from "./continuation.js";
4
+ export type DeliveryValue = ProtocolDeliverValue & Readonly<{
5
+ continuation?: ContinuationReport;
6
+ }>;
3
7
  declare class DeliveryHandle {
4
8
  private readonly readDiff;
5
- readonly completion?: DeliverValue["completion"];
6
- readonly verification?: DeliverValue["verification"];
7
- readonly verificationReuse?: DeliverValue["verificationReuse"];
8
- readonly verificationSummary?: DeliverValue["verificationSummary"];
9
- readonly placement?: DeliverValue["placement"];
10
- readonly cleanup?: DeliverValue["cleanup"];
11
- readonly leak?: DeliverValue["leak"];
9
+ readonly completion?: DeliveryValue["completion"];
10
+ readonly verification?: DeliveryValue["verification"];
11
+ readonly verificationReuse?: DeliveryValue["verificationReuse"];
12
+ readonly verificationSummary?: DeliveryValue["verificationSummary"];
13
+ readonly placement?: DeliveryValue["placement"];
14
+ readonly cleanup?: DeliveryValue["cleanup"];
15
+ readonly leak?: DeliveryValue["leak"];
16
+ readonly continuation?: DeliveryValue["continuation"];
12
17
  readonly tenderSnapshot: SnapshotId;
13
- readonly integration: DeliverValue["integration"];
14
- readonly method: DeliverValue["method"];
15
- readonly policy: DeliverValue["policy"];
16
- constructor(identity: Pick<DeliverValue, "tenderSnapshot" | "integration" | "method" | "policy">, readDiff: () => Promise<string | null>, outcomes?: Partial<Pick<DeliverValue, "completion" | "verification" | "verificationReuse" | "verificationSummary" | "placement" | "cleanup" | "leak">>);
18
+ readonly integration: DeliveryValue["integration"];
19
+ readonly method: DeliveryValue["method"];
20
+ readonly policy: DeliveryValue["policy"];
21
+ constructor(identity: Pick<DeliveryValue, "tenderSnapshot" | "integration" | "method" | "policy">, readDiff: () => Promise<string | null>, outcomes?: Partial<Pick<DeliveryValue, "completion" | "verification" | "verificationReuse" | "verificationSummary" | "placement" | "cleanup" | "leak" | "continuation">>);
17
22
  diff(): Promise<string | null>;
18
23
  }
19
24
  export type Delivery = DeliveryHandle;
@@ -22,5 +27,5 @@ type HandleType<T extends object> = Readonly<{
22
27
  [Symbol.hasInstance](value: unknown): boolean;
23
28
  }>;
24
29
  export declare const Delivery: HandleType<DeliveryHandle>;
25
- export declare function deliveryHandle(delivery: DeliverValue, readDiff: () => Promise<string | null>): Delivery;
30
+ export declare function deliveryHandle(delivery: DeliveryValue, readDiff: () => Promise<string | null>): Delivery;
26
31
  export {};
@@ -8,7 +8,7 @@ export { Delivery, KeiyakuRefused, KeiyakuRetry, } from "./contract.js";
8
8
  export { NoGitWorldError, Repo } from "./repo.js";
9
9
  export { gatesFrom, requireBranchesToBeUpToDateFrom, SettingsError, worktreeHooksFrom, } from "./configuration.js";
10
10
  export { AkumaWorldScopeError } from "./address.js";
11
- export type { AbandonInput, ActorId, AmendInput, AmendResult, ArcInput, AttestationVerdict, AuditInput, AuditReport, DeliveryPreparationRefusal, BindInput, BindResult, ContractBoard, ContractDisposition, ContractGateCurrent, ContractGateReport, ContractHistory, ContractHistoryEvent, ContractListInput, ContractObservation, ContractObservationInput, ContractPhase, ContractRow, DeliverInput, Fact, IntegrationConflictMaterialized, FactKind, KeiyakuOfInput, KeiyakuRefusal, KeiyakuRetryReason, Lag, MutationResult, PlacementStop, ReconcileReport, Review, ReviewInput, TaskId, TopologyEffect, VerificationReuse, VerificationStop, } from "./contract.js";
11
+ export type { AbandonInput, ActorId, AmendInput, AmendResult, ArcInput, AttestationVerdict, AuditInput, AuditReport, DeliveryPreparationRefusal, BindInput, BindResult, ContractBoard, ContractDisposition, ContractGateCurrent, ContractGateReport, ContractHistory, ContractHistoryEvent, ContractListInput, ContractObservation, ContractObservationInput, ContractPhase, ContractRow, ContinuationReport, DeliverInput, Fact, IntegrationConflictMaterialized, FactKind, KeiyakuOfInput, KeiyakuRefusal, KeiyakuRetryReason, Lag, MutationResult, PlacementStop, ReconcileReport, Review, ReviewInput, TaskId, TopologyEffect, VerificationReuse, VerificationStop, } from "./contract.js";
12
12
  export type { ContractId, ContractState, ChangeId, SnapshotId, } from "../core/facts/types.js";
13
13
  export type { Gate, GatesFromInput, HookCommand, RequireBranchesToBeUpToDateFromInput, WorktreeHooks, } from "./configuration.js";
14
14
  export type { RegionOverlap } from "./region.js";
@@ -5,7 +5,11 @@ export function completionInput(scope, channel, contractId, value, hooks) {
5
5
  }
6
6
  export async function completeMutation(input) {
7
7
  const { scope, channel, contractId, accepted, value, hooks } = input;
8
- const report = await completeReconcile({ scope, channel, contractId, hooks, retryHooks: false });
8
+ const contracts = [...new Set([contractId, ...accepted.facts.map((fact) => fact.contract)])];
9
+ const reports = [];
10
+ for (const affected of contracts) {
11
+ reports.push(await completeReconcile({ scope, channel, contractId: affected, hooks, retryHooks: false }));
12
+ }
9
13
  const obligations = {
10
14
  ...(accepted.cleanup === undefined ? {} : { cleanup: accepted.cleanup }),
11
15
  ...(accepted.leak === undefined ? {} : { leak: accepted.leak }),
@@ -14,9 +18,12 @@ export async function completeMutation(input) {
14
18
  facts: accepted.facts,
15
19
  head: accepted.head,
16
20
  value: value(accepted.value),
17
- effects: [...(accepted.physical?.effects ?? []), ...report.effects],
18
- lags: [...(accepted.physical?.lag ?? []), ...report.lag],
19
- settlement: report.settlement,
21
+ effects: [...(accepted.physical?.effects ?? []), ...reports.flatMap((report) => report.effects)],
22
+ lags: [...(accepted.physical?.lag ?? []), ...reports.flatMap((report) => report.lag)],
23
+ settlement: {
24
+ actions: reports.flatMap((report) => report.settlement.actions),
25
+ lags: reports.flatMap((report) => report.settlement.lags),
26
+ },
20
27
  ...obligations,
21
28
  };
22
29
  }
@@ -1,6 +1,6 @@
1
- import type { ActorId, ContractId, ContractState, DeliverData, SnapshotId } from "../core/facts/types.js";
1
+ import type { ActorId, ContractId, ContractState, DeliverData, JournalEntry, SnapshotId } from "../core/facts/types.js";
2
2
  import type { CurrentVerifiedAttestation } from "./intent.js";
3
- import { type CompletionEvidence } from "./completion.js";
3
+ import { type CompletionEvidence, type CompletionResult } from "./completion.js";
4
4
  import { type ManagedWorktreeAppointment } from "../workspace-place.js";
5
5
  import type { DeliveryPreparationRefusal, DocumentDerivation, IntentOutcome, MutationOperationInput } from "./operations.js";
6
6
  type DeliveryIdentity = DeliverData;
@@ -45,5 +45,15 @@ export declare function prepareDelivery(repository: import("../git/process.js").
45
45
  kind: "refused";
46
46
  refusal: DeliveryPreparationRefusal;
47
47
  }>;
48
+ export declare function continueDeliveryOperation(input: Readonly<{
49
+ scope: MutationOperationInput["scope"];
50
+ channel: MutationOperationInput["channel"];
51
+ state: ContractState;
52
+ journal: readonly JournalEntry[];
53
+ deriveDocument: (state: ContractState) => DocumentDerivation;
54
+ resolveHereWorkspace?: MutationOperationInput["resolveHereWorkspace"];
55
+ actor?: ActorId;
56
+ signal?: AbortSignal;
57
+ }>): Promise<CompletionResult>;
48
58
  export declare function deliverOperation(input: DeliverOperationInput): Promise<IntentOutcome<DeliverValue> | IntegrationConflictMaterialized>;
49
59
  export {};
@@ -34,10 +34,12 @@ export async function prepareDelivery(repository, stage, input) {
34
34
  coordinates,
35
35
  ...(appointed === undefined ? {} : { place: appointed.place }),
36
36
  ...(stage.workspacePath === undefined ? {} : { workspacePath: stage.workspacePath }),
37
+ captureMergeState: true,
38
+ rejectUnmerged: true,
37
39
  });
38
40
  if (tender.kind === "refused")
39
41
  return tender;
40
- if ((tender.data.dirty && input.includeDirty !== true) || tender.data.changes.submodules.length > 0) {
42
+ if (((tender.data.dirty || tender.data.mergeHead !== undefined) && input.includeDirty !== true) || tender.data.changes.submodules.length > 0) {
41
43
  return { kind: "refused", refusal: await dirtyTenderRefusal(repository, contractId, tender.data) };
42
44
  }
43
45
  const commit = await prepareDeliveryCommitMetadata(repository, {
@@ -86,18 +88,23 @@ async function deliverAttempt(input, attempt) {
86
88
  workspacePath = state.terminal === null && state.coordinates.workspace === "here"
87
89
  ? await input.resolveHereWorkspace?.(state.id)
88
90
  : undefined;
89
- const prepared = await prepareDelivery(input.scope, {
90
- contractId: state.id,
91
- coordinates: state.coordinates,
92
- ...(workspacePath === undefined ? {} : { workspacePath }),
93
- }, {
94
- title: derivation.title,
95
- document: derivation.bytes,
96
- ...(input.actor === undefined ? {} : { actor: input.actor }),
97
- ...(input.message === undefined ? {} : { message: input.message }),
98
- requireBranchesToBeUpToDate: input.requireBranchesToBeUpToDate,
99
- includeDirty: input.includeDirty,
100
- });
91
+ const materialization = input.materializeConflict === true && state.terminal === null
92
+ ? await materializationMergeStateRefusal(input)
93
+ : undefined;
94
+ const prepared = materialization === undefined
95
+ ? await prepareDelivery(input.scope, {
96
+ contractId: state.id,
97
+ coordinates: state.coordinates,
98
+ ...(workspacePath === undefined ? {} : { workspacePath }),
99
+ }, {
100
+ title: derivation.title,
101
+ document: derivation.bytes,
102
+ ...(input.actor === undefined ? {} : { actor: input.actor }),
103
+ ...(input.message === undefined ? {} : { message: input.message }),
104
+ requireBranchesToBeUpToDate: input.requireBranchesToBeUpToDate,
105
+ includeDirty: input.includeDirty,
106
+ })
107
+ : { kind: "refused", refusal: materialization };
101
108
  preparation = prepared.kind === "refused"
102
109
  ? { kind: "refused", document: derivation.document, refusal: prepared.refusal }
103
110
  : {
@@ -165,6 +172,24 @@ async function completeDelivery(input, first) {
165
172
  ...completed.evidence,
166
173
  });
167
174
  }
175
+ export async function continueDeliveryOperation(input) {
176
+ const contractId = input.state.id;
177
+ const hereWorkspacePath = input.state.coordinates.workspace === "here"
178
+ ? await input.resolveHereWorkspace?.(contractId)
179
+ : undefined;
180
+ return await completeCandidate({
181
+ channel: input.channel,
182
+ repository: input.scope,
183
+ contractId,
184
+ ...(input.actor === undefined ? {} : { actor: input.actor }),
185
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
186
+ ...(input.state.coordinates.target === undefined ? {} : { target: input.state.coordinates.target }),
187
+ verification: input.deriveDocument(input.state).verification,
188
+ initial: { kind: "accepted", state: input.state, journal: input.journal, facts: [] },
189
+ verifyInitial: true,
190
+ ...(hereWorkspacePath === undefined ? {} : { hereWorkspacePath }),
191
+ });
192
+ }
168
193
  function isIntegrationConflict(refusal) {
169
194
  return refusal.kind === "integration-failed" && refusal.reason === "conflict";
170
195
  }
@@ -201,6 +226,17 @@ async function appointedDeliverWorkspace(input) {
201
226
  coordinates: state.coordinates,
202
227
  };
203
228
  }
229
+ async function materializationMergeStateRefusal(input) {
230
+ const appointed = await appointedDeliverWorkspace(input);
231
+ if ("kind" in appointed)
232
+ return appointed.refusal;
233
+ return await mergeStatePresentRefusal(input, appointed.workspace);
234
+ }
235
+ async function mergeStatePresentRefusal(input, workspace) {
236
+ if (!(await workspaceMergeStatePresent(input.scope, workspace.path)))
237
+ return undefined;
238
+ return { kind: "merge-state-present", contractId: input.contractId, workspace };
239
+ }
204
240
  async function materializeDeliverConflict(input, refusal) {
205
241
  if (refusal.conflictPaths === undefined)
206
242
  throw new Error("conflicted integration is missing conflict paths");
@@ -208,12 +244,9 @@ async function materializeDeliverConflict(input, refusal) {
208
244
  if ("kind" in appointed)
209
245
  return appointed;
210
246
  const { workspace, coordinates } = appointed;
211
- if (await workspaceMergeStatePresent(input.scope, workspace.path)) {
212
- return {
213
- kind: "refused",
214
- refusal: { kind: "merge-state-present", contractId: input.contractId, workspace },
215
- };
216
- }
247
+ const mergeState = await mergeStatePresentRefusal(input, workspace);
248
+ if (mergeState !== undefined)
249
+ return { kind: "refused", refusal: mergeState };
217
250
  const tender = await captureTender(input.scope, {
218
251
  contractId: input.contractId,
219
252
  coordinates,
@@ -1,4 +1,4 @@
1
- import { type DirtyWorkspaceRefusal } from "../git/tender.js";
1
+ import type { DirtyWorkspaceRefusal } from "../git/tender.js";
2
2
  import type { GitRepository } from "../git/process.js";
3
3
  import { type GitDecodeChannel } from "../git/read-observation.js";
4
4
  import type { WorktreeLeak } from "../git/scratch.js";
@@ -28,6 +28,11 @@ export type MergeStatePresentRefusal = Readonly<{
28
28
  path: string;
29
29
  }>;
30
30
  }>;
31
+ export type UnmergedPathsRefusal = Readonly<{
32
+ kind: "unmerged-paths";
33
+ contractId: ContractId;
34
+ paths: readonly string[];
35
+ }>;
31
36
  export type DeliverConflictRefusal = Readonly<{
32
37
  kind: "integration-failed";
33
38
  contractId: ContractId;
@@ -42,7 +47,7 @@ export type DeliverConflictRefusal = Readonly<{
42
47
  export type DeliveryPreparationRefusal = Readonly<{
43
48
  kind: "target-missing" | "worktree-missing";
44
49
  contractId: ContractId;
45
- }> | DirtyWorkspaceRefusal | IntegrationPreparationRefusal | MergeStatePresentRefusal | TargetPlacementRefusal;
50
+ }> | DirtyWorkspaceRefusal | UnmergedPathsRefusal | IntegrationPreparationRefusal | MergeStatePresentRefusal | TargetPlacementRefusal;
46
51
  export type IntentRefusal = AbandonRefusal | AmendRefusal | ArcRefusal | BindRefusal | DeliverRefusal | DeliveryPreparationRefusal | DeliverConflictRefusal | PlacementRefusal | AttestationRefusal | TargetInputRefusal | VerificationDeclarationRefusal;
47
52
  export type IntentRetry = ProtocolTerminal;
48
53
  export type IntentOutcome<Value, Refusal = IntentRefusal> = ProtocolIntentOutcome<Value, Refusal>;
@@ -3,5 +3,3 @@ import type { GitRepository } from "../git/process.js";
3
3
  import type { TaskId } from "../task/identity.js";
4
4
  export declare function settlementFencePath(repository: GitRepository, taskId: TaskId): string;
5
5
  export declare function acquireTaskSettlementFence(repository: GitRepository, taskId: TaskId): Promise<HeldSqliteTransactionLock>;
6
- /** Remove only the hash-addressed settlement fences owned by Contract/Git reset. */
7
- export declare function nukeSettlementFences(repository: GitRepository): Promise<void>;
@@ -1,5 +1,4 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readdir } from "node:fs/promises";
3
2
  import { resolve } from "node:path";
4
3
  import { acquireSqliteTransactionLock, } from "../coordination/sqlite-transaction-lock.js";
5
4
  import { commonGitDirectory } from "../git/repository.js";
@@ -13,26 +12,3 @@ export function acquireTaskSettlementFence(repository, taskId) {
13
12
  mode: "immediate",
14
13
  });
15
14
  }
16
- /** Remove only the hash-addressed settlement fences owned by Contract/Git reset. */
17
- export async function nukeSettlementFences(repository) {
18
- const root = resolve(commonGitDirectory(repository), "keiyaku", "locks", "settlement");
19
- let entries;
20
- try {
21
- entries = await readdir(root, { withFileTypes: true });
22
- }
23
- catch (error) {
24
- if (error.code === "ENOENT")
25
- return;
26
- throw error;
27
- }
28
- for (const entry of entries) {
29
- if (!/^[0-9a-f]{64}\.sqlite$/u.test(entry.name) || !entry.isFile() || entry.isSymbolicLink()) {
30
- throw new Error(`Settlement fence custody is invalid: ${resolve(root, entry.name)}`);
31
- }
32
- }
33
- for (const entry of entries) {
34
- const path = resolve(root, entry.name);
35
- const held = await acquireSqliteTransactionLock({ path, mode: "immediate" });
36
- held.close();
37
- }
38
- }
@@ -12,6 +12,12 @@ export type TaskRow = Readonly<{
12
12
  state: TaskState;
13
13
  priority: TaskPriority;
14
14
  disposition: TaskDisposition;
15
+ updatedAt: string;
16
+ bodyPresent: boolean;
17
+ children?: Readonly<{
18
+ live: number;
19
+ total: number;
20
+ }>;
15
21
  }>;
16
22
  export type BlockedTaskRow = TaskRow & Readonly<{
17
23
  blockers: readonly TaskRef[];
@@ -65,14 +71,21 @@ export declare function createTaskRelations(board: TaskBoard): TaskRelationProje
65
71
  export declare function taskDisposition(board: TaskBoard, task: TaskDocument): TaskDisposition;
66
72
  export declare function taskBlocked(board: TaskBoard, task: TaskDocument): boolean;
67
73
  export declare function projectDetailFacts(board: TaskBoard, id: TaskId, relations: TaskRelationProjection): TaskDetailFacts | null;
68
- export declare function projectRows(board: TaskBoard, scope: readonly string[] | null, selection: "active" | "closed" | "all"): readonly TaskRow[];
74
+ export declare function projectTaskRow(board: TaskBoard, relations: TaskRelationProjection, task: TaskDocument): TaskRow;
75
+ export declare function projectRows(board: TaskBoard, relations: TaskRelationProjection, scope: readonly string[] | null, selection: "active" | "closed" | "all"): readonly TaskRow[];
69
76
  export declare function projectBlocked(board: TaskBoard, scope: readonly string[] | null, relations: TaskRelationProjection): readonly BlockedTaskRow[];
70
- export declare function projectStatusRows(board: TaskBoard, scope: readonly string[] | null): (Readonly<{
77
+ export declare function projectStatusRows(board: TaskBoard, relations: TaskRelationProjection, scope: readonly string[] | null): (Readonly<{
71
78
  id: TaskId;
72
79
  title: string;
73
80
  state: TaskState;
74
81
  priority: TaskPriority;
75
82
  disposition: TaskDisposition;
83
+ updatedAt: string;
84
+ bodyPresent: boolean;
85
+ children?: Readonly<{
86
+ live: number;
87
+ total: number;
88
+ }>;
76
89
  }> | {
77
90
  blockers: readonly Readonly<{
78
91
  id: TaskId;
@@ -84,6 +97,12 @@ export declare function projectStatusRows(board: TaskBoard, scope: readonly stri
84
97
  state: TaskState;
85
98
  priority: TaskPriority;
86
99
  disposition: TaskDisposition;
100
+ updatedAt: string;
101
+ bodyPresent: boolean;
102
+ children?: Readonly<{
103
+ live: number;
104
+ total: number;
105
+ }>;
87
106
  })[];
88
107
  export type TaskBoardObservation = Readonly<{
89
108
  statusRows: ReturnType<typeof projectStatusRows>;
@@ -79,33 +79,46 @@ export function projectDetailFacts(board, id, relations) {
79
79
  };
80
80
  }
81
81
  function inScope(task, scope) { return scope === null || sameNamespace(parseTaskId(task.id).namespace, scope); }
82
- export function projectRows(board, scope, selection) {
83
- return [...board.tasks.values()].filter((task) => inScope(task, scope)).filter((task) => selection === "all"
84
- || (selection === "closed") === terminal(task.state)).map((task) => ({
85
- id: task.id, title: task.title, state: task.state, priority: task.priority,
82
+ export function projectTaskRow(board, relations, task) {
83
+ const children = relations.children(task.id);
84
+ const total = children.length;
85
+ const live = children.filter((child) => child.state !== "done" && child.state !== "drop").length;
86
+ return {
87
+ id: task.id,
88
+ title: task.title,
89
+ state: task.state,
90
+ priority: task.priority,
86
91
  disposition: taskDisposition(board, task),
87
- })).sort((a, b) => a.priority - b.priority || Buffer.compare(Buffer.from(a.id), Buffer.from(b.id)));
92
+ updatedAt: task.updatedAt,
93
+ bodyPresent: task.body.length > 0,
94
+ ...(total === 0 ? {} : { children: { live, total } }),
95
+ };
96
+ }
97
+ export function projectRows(board, relations, scope, selection) {
98
+ return [...board.tasks.values()].filter((task) => inScope(task, scope)).filter((task) => selection === "all"
99
+ || (selection === "closed") === terminal(task.state)).map((task) => projectTaskRow(board, relations, task))
100
+ .sort((a, b) => a.priority - b.priority || Buffer.compare(Buffer.from(a.id), Buffer.from(b.id)));
88
101
  }
89
102
  export function projectBlocked(board, scope, relations) {
90
- return projectRows(board, scope, "active").flatMap((row) => {
103
+ return projectRows(board, relations, scope, "active").flatMap((row) => {
91
104
  if (!taskBlocked(board, board.tasks.get(row.id)))
92
105
  return [];
93
106
  return [{ ...row, blockers: projectDetailFacts(board, row.id, relations).blockers }];
94
107
  });
95
108
  }
96
- export function projectStatusRows(board, scope) {
97
- const relations = createTaskRelations(board);
109
+ export function projectStatusRows(board, relations, scope) {
98
110
  const blockers = new Map(projectBlocked(board, scope, relations).map((row) => [row.id, row.blockers]));
99
- return projectRows(board, scope, "all").map((row) => {
111
+ return projectRows(board, relations, scope, "all").map((row) => {
100
112
  const unresolved = blockers.get(row.id);
101
113
  return unresolved === undefined ? row : { ...row, blockers: unresolved };
102
114
  });
103
115
  }
104
116
  export function projectTaskBoardObservation(board) {
105
- const rows = projectRows(board, null, "all");
117
+ const relations = createTaskRelations(board);
118
+ const rows = projectRows(board, relations, null, "all");
106
119
  return {
107
- statusRows: projectStatusRows(board, null),
108
- selectNamespace: (namespace) => projectRows(board, namespace, "all"),
120
+ statusRows: projectStatusRows(board, relations, null),
121
+ selectNamespace: (namespace) => projectRows(board, relations, namespace, "all"),
109
122
  selectCreatedBy: (createdBy) => rows.filter((row) => board.tasks.get(row.id)?.createdBy === createdBy),
110
123
  };
111
124
  }
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { identityCoordinate, identitySegments } from "../identity/coordinates.js";
3
- import { fitIdentityStem, normalizeIdentityStem } from "../identity/normalize.js";
4
- const STEM_BYTES = 48;
3
+ import { normalizeIdentityStem } from "../identity/normalize.js";
4
+ const STEM_CODE_POINTS = 32;
5
5
  export function isTaskSegment(value) {
6
6
  return value.length > 0 && value !== "." && value !== ".." && !value.includes("/")
7
7
  && normalizeIdentityStem({ source: value }) === value;
@@ -30,13 +30,23 @@ export function deriveLocalStem(title) {
30
30
  const stem = normalizeIdentityStem({ source: title });
31
31
  if (stem.length === 0)
32
32
  throw new TypeError("task title must contain a normalized identity segment");
33
- return fitIdentityStem({ stem, maxBytes: STEM_BYTES });
33
+ const words = stem.split("-");
34
+ let fitted = words[0];
35
+ let count = [...fitted].length;
36
+ for (const word of words.slice(1)) {
37
+ const candidate = count + 1 + [...word].length;
38
+ if (candidate > STEM_CODE_POINTS)
39
+ break;
40
+ fitted += `-${word}`;
41
+ count = candidate;
42
+ }
43
+ return fitted;
34
44
  }
35
45
  export function allocateLocalId(stem, occupied) {
36
46
  if (!occupied.has(stem))
37
47
  return stem;
38
48
  for (let suffix = 2; Number.isSafeInteger(suffix); suffix += 1) {
39
- const candidate = fitIdentityStem({ stem, maxBytes: STEM_BYTES, suffix: String(suffix) });
49
+ const candidate = `${stem}-${suffix}`;
40
50
  if (!occupied.has(candidate))
41
51
  return candidate;
42
52
  }
@@ -19,6 +19,7 @@ export type TaskDoctorReport = Readonly<{
19
19
  export type TaskDecompositionTree = TaskOutcome<TaskTreeNode>;
20
20
  export type { AddTaskDocumentInput, AddTaskInput, BlockedTaskRow, TaskBatchResult, TaskCompositionResult, TaskDoctorIssue, TaskId, TaskMutationResult, TaskOutcome, TaskPriority, TaskRef, TaskRefusal, TaskRetry, TaskRow, TaskState, TaskTreeNode, TaskUpdateResult, TaskView, UpdateTaskInput, TaskPage, TaskQueryExpression, TaskQueryPredicate, TaskQueryRow, TaskQuerySort, TaskRelationPredicateField, };
21
21
  export { TaskAuthorityCorruptionError, TASK_RELATION_PREDICATE_FIELDS };
22
+ export declare function observeTaskDetails(world: WorldRoot, ids: readonly TaskId[]): Promise<TaskOutcome<readonly TaskDetail[]>>;
22
23
  declare class TaskHandle {
23
24
  readonly id: TaskId;
24
25
  private readonly world;
@@ -1,11 +1,17 @@
1
1
  import { buildTree, createTaskRelations, diagnoseBoard, projectDetailFacts, } from "./board.js";
2
2
  import { composeTasks } from "./compose.js";
3
3
  import { TaskAuthorityCorruptionError } from "./document.js";
4
- import { addTask, addTaskDocument, batchTasks, blockedTasks, lifecycleTask, listTasks, queryTasks, readyTasks, taskView, updateTask, } from "./operations.js";
4
+ import { addTask, addTaskDocument, batchTasks, blockedTasks, lifecycleTask, listTasks, queryTasks, readTaskDetails, readyTasks, taskView, updateTask, } from "./operations.js";
5
5
  import { readBoard } from "./store.js";
6
6
  import { actor, addInput, closed, limit, namespace, record, signal, sort, taskId as id, taskIds, text, updateInput, } from "./input.js";
7
7
  import { normalizeTaskQuery, TASK_RELATION_PREDICATE_FIELDS, DEFAULT_TASK_LIMIT, } from "./query.js";
8
8
  export { TaskAuthorityCorruptionError, TASK_RELATION_PREDICATE_FIELDS };
9
+ export async function observeTaskDetails(world, ids) {
10
+ const observed = await readTaskDetails(world, ids);
11
+ if (observed.kind !== "accepted")
12
+ return observed;
13
+ return { kind: "accepted", value: observed.value.map((facts) => ({ ...facts, task: taskView(facts.task) })) };
14
+ }
9
15
  class TaskHandle {
10
16
  id;
11
17
  world;
@@ -1,4 +1,4 @@
1
- import { type BlockedTaskRow, type TaskBoardObservation, type TaskRow } from "./board.js";
1
+ import { type BlockedTaskRow, type TaskBoardObservation, type TaskDetailFacts, type TaskRow } from "./board.js";
2
2
  import { type TaskDocument, type TaskPriority, type TaskState } from "./document.js";
3
3
  import { type TaskId } from "./identity.js";
4
4
  import type { WorldRoot } from "../world.js";
@@ -115,6 +115,7 @@ export declare function lifecycleTask(world: WorldRoot, id: TaskId, verb: TaskLi
115
115
  export declare function batchTasks(world: WorldRoot, verb: "done" | "drop" | "hold", ids: readonly TaskId[], signal?: AbortSignal, note?: string): Promise<TaskBatchResult>;
116
116
  export declare function settleTask(world: WorldRoot, id: TaskId): Promise<SettledTaskResult>;
117
117
  export declare function listTasks(world: WorldRoot, namespace: readonly string[] | undefined, selection: "active" | "closed" | "all", scope?: "namespace" | "world", limit?: number): Promise<TaskOutcome<TaskPage<TaskRow>>>;
118
+ export declare function readTaskDetails(world: WorldRoot, ids: readonly TaskId[]): Promise<TaskOutcome<readonly TaskDetailFacts[]>>;
118
119
  export declare function readyTasks(world: WorldRoot, namespace: readonly string[] | undefined, scope?: "namespace" | "world", parent?: TaskId, limit?: number): Promise<TaskOutcome<TaskPage<TaskRow>>>;
119
120
  export declare function blockedTasks(world: WorldRoot, namespace: readonly string[] | undefined, scope?: "namespace" | "world", parent?: TaskId, limit?: number): Promise<TaskOutcome<TaskPage<BlockedTaskRow>>>;
120
121
  export declare function queryTasks(input: Readonly<{
@@ -1,5 +1,5 @@
1
1
  import { documentDiff } from "../markdown/diff.js";
2
- import { createTaskRelations, projectTaskBoardObservation, relationProblem, projectBlocked, projectRows, } from "./board.js";
2
+ import { createTaskRelations, projectDetailFacts, projectTaskBoardObservation, relationProblem, projectBlocked, projectRows, } from "./board.js";
3
3
  import { parseTaskCreationDocument, serializeTaskDocument } from "./document.js";
4
4
  import { allocateLocalId, deriveLocalStem, formatTaskId, parseTaskId, sameNamespace } from "./identity.js";
5
5
  import { authorityPath, nukeTaskAuthority, readBoard, replaceAuthority, withTaskLocks, } from "./store.js";
@@ -185,7 +185,17 @@ function readScope(namespace, scope) {
185
185
  }
186
186
  export async function listTasks(world, namespace, selection, scope, limit = DEFAULT_TASK_LIMIT) {
187
187
  const selected = readScope(namespace, scope);
188
- return { kind: "accepted", value: projectPage(projectRows((await readBoard(world)).board, selected, selection), limit) };
188
+ const board = (await readBoard(world)).board;
189
+ return { kind: "accepted", value: projectPage(projectRows(board, createTaskRelations(board), selected, selection), limit) };
190
+ }
191
+ export async function readTaskDetails(world, ids) {
192
+ const board = (await readBoard(world)).board;
193
+ const relations = createTaskRelations(board);
194
+ const details = ids.map((id) => projectDetailFacts(board, id, relations));
195
+ const missing = details.findIndex((detail) => detail === null);
196
+ return missing < 0
197
+ ? { kind: "accepted", value: details }
198
+ : { kind: "refused", refusal: { kind: "task-missing", taskId: ids[missing] } };
189
199
  }
190
200
  export async function readyTasks(world, namespace, scope, parent, limit = DEFAULT_TASK_LIMIT) {
191
201
  const selected = readScope(namespace, scope);
@@ -200,7 +210,7 @@ export async function readyTasks(world, namespace, scope, parent, limit = DEFAUL
200
210
  expression,
201
211
  limit: Math.max(1, board.tasks.size),
202
212
  }).rows;
203
- const rows = selectedRows.map(({ parent: _parent, needs: _needs, blocks: _blocks, createdAt: _createdAt, updatedAt: _updatedAt, ...row }) => row);
213
+ const rows = selectedRows.map(({ parent: _parent, needs: _needs, blocks: _blocks, createdAt: _createdAt, ...row }) => row);
204
214
  return { kind: "accepted", value: projectPage(rows, limit) };
205
215
  }
206
216
  export async function blockedTasks(world, namespace, scope, parent, limit = DEFAULT_TASK_LIMIT) {
@@ -246,5 +256,6 @@ export async function observeTaskBoard(world) {
246
256
  }
247
257
  /** Internal identity catalog from one complete Task board read. */
248
258
  export async function observeTaskCatalogRows(world) {
249
- return projectRows((await readBoard(world)).board, null, "all");
259
+ const board = (await readBoard(world)).board;
260
+ return projectRows(board, createTaskRelations(board), null, "all");
250
261
  }
@@ -1,5 +1,5 @@
1
1
  import { formatTaskId, parseTaskId, sameNamespace } from "./identity.js";
2
- import { taskBlocked, taskDisposition, taskRef } from "./board.js";
2
+ import { projectTaskRow, taskBlocked, taskDisposition, taskRef } from "./board.js";
3
3
  export const DEFAULT_TASK_LIMIT = 100;
4
4
  export const MAX_TASK_LIMIT = 1_000;
5
5
  export const TASK_RELATION_PREDICATE_FIELDS = ["under", "needs", "blocks"];
@@ -196,7 +196,7 @@ function matches(board, relations, task, expression, under) {
196
196
  }
197
197
  function row(board, relations, task) {
198
198
  return {
199
- id: task.id, title: task.title, state: task.state, priority: task.priority, disposition: taskDisposition(board, task),
199
+ ...projectTaskRow(board, relations, task),
200
200
  parent: task.parent,
201
201
  needs: task.needs.map((need) => taskRef(board, need)),
202
202
  blocks: relations.blocks(task.id),
@@ -5,6 +5,7 @@ export type BoardSnapshot = Readonly<{
5
5
  board: TaskBoard;
6
6
  bytes: ReadonlyMap<TaskId, Uint8Array>;
7
7
  }>;
8
+ export declare const DEFAULT_TASK_LOCK_TIMEOUT_MS = 3000;
8
9
  export declare function readBoard(world: WorldRoot): Promise<BoardSnapshot>;
9
10
  export declare function replaceAuthority(input: Readonly<{
10
11
  path: string;
@@ -16,6 +17,7 @@ export declare function withTaskLocks<T>(input: Readonly<{
16
17
  world: WorldRoot;
17
18
  allocation: boolean;
18
19
  ids: readonly TaskId[];
20
+ timeoutMs?: number;
19
21
  signal?: AbortSignal;
20
22
  }>, action: () => Promise<T>): Promise<T | "busy">;
21
23
  export declare function authorityPath(world: WorldRoot, id: TaskId): string;