@astrosheep/keiyaku 2.9.6 → 2.9.8

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 (87) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/harness/event-persistence.js +7 -5
  3. package/build/agents/harness/events.js +3 -2
  4. package/build/agents/harness/outcome.js +10 -0
  5. package/build/agents/providers/codex-app-server/adapter.js +6 -1
  6. package/build/agents/providers/codex-app-server/session.js +8 -7
  7. package/build/agents/selector.js +12 -1
  8. package/build/cli/commands/akuma/view/handler.js +3 -11
  9. package/build/cli/commands/contract/amend/handler.js +1 -1
  10. package/build/cli/commands/contract/amend/meta.js +4 -4
  11. package/build/cli/commands/contract/bind/handler.js +10 -4
  12. package/build/cli/commands/contract/bind/meta.js +6 -6
  13. package/build/cli/commands/contract/petition/handler.js +16 -3
  14. package/build/cli/commands/contract/petition/meta.js +4 -4
  15. package/build/cli/commands/metadata.js +3 -2
  16. package/build/cli/commands/projection/status/handler.js +5 -4
  17. package/build/cli/commands/projection/status/meta.js +2 -2
  18. package/build/cli/commands/projection/tell/handler.js +10 -2
  19. package/build/cli/commands/projection/tell/meta.js +3 -3
  20. package/build/cli/commands/task/add/meta.js +9 -1
  21. package/build/cli/commands/task/catalog.js +2 -0
  22. package/build/cli/commands/task/log/handler.js +12 -0
  23. package/build/cli/commands/task/log/meta.js +8 -0
  24. package/build/cli/commands/task/shared.js +2 -1
  25. package/build/cli/completion.js +8 -0
  26. package/build/cli/flags.js +8 -0
  27. package/build/cli/index.js +10 -6
  28. package/build/cli/parse-flags.js +6 -0
  29. package/build/cli/parse-metadata.js +1 -1
  30. package/build/cli/render/kanshi.js +2 -2
  31. package/build/cli/render/line-width.js +33 -0
  32. package/build/cli/render/path-prefix-compaction.js +131 -0
  33. package/build/cli/render/petition.js +4 -0
  34. package/build/cli/render/projection-activity.js +103 -21
  35. package/build/cli/render/shared.js +38 -15
  36. package/build/cli/render/status.js +8 -5
  37. package/build/cli/render/success-response.js +2 -0
  38. package/build/cli/render/tool-presentation.js +3 -3
  39. package/build/cli/render/wait.js +68 -48
  40. package/build/cli/subagent-guard.js +3 -0
  41. package/build/cli/types.js +1 -1
  42. package/build/config/settings/disease.js +4 -4
  43. package/build/config/settings/loader.js +44 -21
  44. package/build/core/addressing.js +40 -9
  45. package/build/core/amend.js +21 -5
  46. package/build/core/bind.js +111 -6
  47. package/build/core/call/context.js +19 -3
  48. package/build/core/call/execution.js +43 -20
  49. package/build/core/draft.js +1 -1
  50. package/build/core/ledger-batch.js +194 -0
  51. package/build/core/projection/generation/database.js +43 -2
  52. package/build/core/projection/generation/model.js +24 -3
  53. package/build/core/projection/generation/projection-generation-continuation.js +75 -10
  54. package/build/core/projection/generation/projection-generation-execution.js +46 -37
  55. package/build/core/projection/generation/projection-generation-launcher.js +148 -20
  56. package/build/core/projection/generation/projection-generation-process.js +3 -1
  57. package/build/core/projection/generation/projection-generation-runner.js +145 -66
  58. package/build/core/projection/generation/projection-generation-runtime.js +93 -11
  59. package/build/core/projection/generation/store.js +26 -1
  60. package/build/core/projection/generation/transitions.js +193 -14
  61. package/build/core/projection/index.js +5 -5
  62. package/build/core/projection/projection-core.js +1 -1
  63. package/build/core/projection/projection-kill.js +53 -10
  64. package/build/core/projection/projection-life-protocol.js +10 -0
  65. package/build/core/projection/projection-runner-lock.js +177 -37
  66. package/build/core/projection/projection-status.js +143 -55
  67. package/build/core/projection/projection-wait.js +53 -15
  68. package/build/core/projection/projection-wake.js +177 -46
  69. package/build/core/projection/tell/database.js +18 -0
  70. package/build/core/projection/tell/model.js +1 -0
  71. package/build/core/projection/tell/store.js +102 -55
  72. package/build/core/registry.js +82 -69
  73. package/build/core/scope.js +9 -9
  74. package/build/core/status/board.js +42 -4
  75. package/build/core/status/drift.js +21 -5
  76. package/build/core/status/ledger-batch.js +1 -158
  77. package/build/core/task/index.js +2 -2
  78. package/build/core/task/task-contract.js +18 -0
  79. package/build/core/task/task-git-runtime.js +8 -10
  80. package/build/core/task/task-git-store.js +55 -3
  81. package/build/core/worktree-path.js +39 -25
  82. package/build/flow-error.js +1 -1
  83. package/build/generated/version.js +2 -2
  84. package/build/git/refs.js +47 -1
  85. package/package.json +1 -1
  86. package/skills/keiyaku-akuma/SKILL.md +28 -0
  87. package/skills/keiyaku-workflow/SKILL.md +142 -18
@@ -1,7 +1,7 @@
1
1
  import { FlowError } from "../flow-error.js";
2
2
  import { createGit, wrapGitError } from "../git/core.js";
3
3
  import { readLedger } from "./ledger.js";
4
- import { appendRefLogWithRetry } from "./ref-log.js";
4
+ import { appendRefLogWithRetry, readRefLog } from "./ref-log.js";
5
5
  import { patternsOverlap, scopePathEvidence } from "./scope.js";
6
6
  import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
7
7
  export const REGISTRY_REF = "refs/keiyaku/registry";
@@ -39,11 +39,9 @@ async function activeRegistrations(cwd, entries) {
39
39
  }
40
40
  return active;
41
41
  }
42
- export async function registerScope(cwd, input) {
43
- let overlaps = [];
44
- let trackedPaths;
42
+ async function trackedRepositoryPaths(cwd) {
45
43
  try {
46
- trackedPaths = (await createGit(cwd).raw(["ls-files", "-z"]))
44
+ return (await createGit(cwd).raw(["ls-files", "-z"]))
47
45
  .split("\0")
48
46
  .filter(Boolean)
49
47
  .sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
@@ -51,76 +49,91 @@ export async function registerScope(cwd, input) {
51
49
  catch (error) {
52
50
  throw wrapGitError("ls-files -z", error, cwd);
53
51
  }
54
- const result = await appendRefLogWithRetry(cwd, REGISTRY_REF, registrySubject, parseRegistryEntry, async (entries) => {
55
- const nextOverlaps = [];
56
- const active = await activeRegistrations(cwd, entries);
57
- if (input.exclusive && active.length > 0) {
58
- const blocking = active.find((existing) => existing.contract !== input.contractId) ?? active[0];
59
- throw new FlowError("ACTIVE_KEIYAKU_EXISTS", `exclusive bind rejected: active contract ${blocking.contract} already exists`);
52
+ }
53
+ async function scopeRegistrationAdmission(cwd, input, entries, trackedPaths) {
54
+ const nextOverlaps = [];
55
+ const active = await activeRegistrations(cwd, entries);
56
+ if (input.exclusive && active.length > 0) {
57
+ const blocking = active.find((existing) => existing.contract !== input.contractId) ?? active[0];
58
+ throw new FlowError("ACTIVE_KEIYAKU_EXISTS", `exclusive bind rejected: active contract ${blocking.contract} already exists`);
59
+ }
60
+ for (const existing of active) {
61
+ if (existing.contract === input.contractId)
62
+ continue;
63
+ if (existing.tree === input.tree) {
64
+ throw new FlowError("ACTIVE_KEIYAKU_EXISTS", [
65
+ `tree is already occupied by active contract ${existing.contract}`,
66
+ `Run \`keiyaku forfeit @${existing.contract}\` to free this tree.`,
67
+ ].join(" "));
60
68
  }
61
- for (const existing of active) {
62
- if (existing.contract === input.contractId)
69
+ const [nextEvidence, existingEvidence] = await Promise.all([
70
+ scopePathEvidence(cwd, input.scope, trackedPaths),
71
+ scopePathEvidence(cwd, existing.scope, trackedPaths),
72
+ ]);
73
+ const existingByPath = new Map(existingEvidence.map((evidence) => [evidence.path, evidence]));
74
+ const concreteByPatterns = new Map();
75
+ for (const next of nextEvidence) {
76
+ const prior = existingByPath.get(next.path);
77
+ if (!next.matched || !prior?.matched || !next.decisivePattern || !prior.decisivePattern)
63
78
  continue;
64
- if (existing.tree === input.tree) {
65
- throw new FlowError("ACTIVE_KEIYAKU_EXISTS", [
66
- `tree is already occupied by active contract ${existing.contract}`,
67
- `Run \`keiyaku forfeit @${existing.contract}\` to free this tree.`,
68
- ].join(" "));
69
- }
70
- const [nextEvidence, existingEvidence] = await Promise.all([
71
- scopePathEvidence(cwd, input.scope, trackedPaths),
72
- scopePathEvidence(cwd, existing.scope, trackedPaths),
73
- ]);
74
- const existingByPath = new Map(existingEvidence.map((evidence) => [evidence.path, evidence]));
75
- const concreteByPatterns = new Map();
76
- for (const next of nextEvidence) {
77
- const prior = existingByPath.get(next.path);
78
- if (!next.matched || !prior?.matched || !next.decisivePattern || !prior.decisivePattern)
79
- continue;
80
- const key = `${next.decisivePattern.index}:${prior.decisivePattern.index}`;
81
- const group = concreteByPatterns.get(key) ?? {
82
- next: next.decisivePattern.pattern,
83
- existing: prior.decisivePattern.pattern,
84
- paths: [],
85
- };
86
- group.paths.push(next.path);
87
- concreteByPatterns.set(key, group);
88
- }
89
- for (const concrete of concreteByPatterns.values()) {
90
- const preview = concrete.paths.slice(0, SCOPE_OVERLAP_PATH_PREVIEW_LIMIT);
91
- nextOverlaps.push({
92
- contracts: [input.contractId, existing.contract],
93
- patterns: [concrete.next, concrete.existing],
94
- kind: "concrete",
95
- paths: {
96
- preview,
97
- count: concrete.paths.length,
98
- truncated: preview.length < concrete.paths.length,
99
- },
100
- });
101
- }
102
- for (const nextPattern of input.scope) {
103
- for (const existingPattern of existing.scope) {
104
- if (patternsOverlap(nextPattern, existingPattern)) {
105
- const [nextPatternEvidence, existingPatternEvidence] = await Promise.all([
106
- scopePathEvidence(cwd, [nextPattern], trackedPaths),
107
- scopePathEvidence(cwd, [existingPattern], trackedPaths),
108
- ]);
109
- const existingPatternByPath = new Map(existingPatternEvidence.map((evidence) => [evidence.path, evidence]));
110
- const hasCurrentWitness = nextPatternEvidence.some((evidence) => (evidence.matched && existingPatternByPath.get(evidence.path)?.matched));
111
- if (!hasCurrentWitness) {
112
- nextOverlaps.push({
113
- contracts: [input.contractId, existing.contract],
114
- patterns: [nextPattern, existingPattern],
115
- kind: "potential",
116
- paths: { preview: [], count: 0, truncated: false },
117
- });
118
- }
79
+ const key = `${next.decisivePattern.index}:${prior.decisivePattern.index}`;
80
+ const group = concreteByPatterns.get(key) ?? {
81
+ next: next.decisivePattern.pattern,
82
+ existing: prior.decisivePattern.pattern,
83
+ paths: [],
84
+ };
85
+ group.paths.push(next.path);
86
+ concreteByPatterns.set(key, group);
87
+ }
88
+ for (const concrete of concreteByPatterns.values()) {
89
+ const preview = concrete.paths.slice(0, SCOPE_OVERLAP_PATH_PREVIEW_LIMIT);
90
+ nextOverlaps.push({
91
+ contracts: [input.contractId, existing.contract],
92
+ patterns: [concrete.next, concrete.existing],
93
+ kind: "concrete",
94
+ paths: {
95
+ preview,
96
+ count: concrete.paths.length,
97
+ truncated: preview.length < concrete.paths.length,
98
+ },
99
+ });
100
+ }
101
+ for (const nextPattern of input.scope) {
102
+ for (const existingPattern of existing.scope) {
103
+ if (patternsOverlap(nextPattern, existingPattern)) {
104
+ const [nextPatternEvidence, existingPatternEvidence] = await Promise.all([
105
+ scopePathEvidence(cwd, [nextPattern], trackedPaths),
106
+ scopePathEvidence(cwd, [existingPattern], trackedPaths),
107
+ ]);
108
+ const existingPatternByPath = new Map(existingPatternEvidence.map((evidence) => [evidence.path, evidence]));
109
+ const hasCurrentWitness = nextPatternEvidence.some((evidence) => (evidence.matched && existingPatternByPath.get(evidence.path)?.matched));
110
+ if (!hasCurrentWitness) {
111
+ nextOverlaps.push({
112
+ contracts: [input.contractId, existing.contract],
113
+ patterns: [nextPattern, existingPattern],
114
+ kind: "potential",
115
+ paths: { preview: [], count: 0, truncated: false },
116
+ });
119
117
  }
120
118
  }
121
119
  }
122
120
  }
123
- overlaps = nextOverlaps;
121
+ }
122
+ return nextOverlaps;
123
+ }
124
+ /** Read-only bind admission: validate exclusivity and classify scope overlap. */
125
+ export async function previewScopeRegistration(cwd, input) {
126
+ const [entries, trackedPaths] = await Promise.all([
127
+ readRefLog(cwd, REGISTRY_REF, parseRegistryEntry),
128
+ trackedRepositoryPaths(cwd),
129
+ ]);
130
+ return await scopeRegistrationAdmission(cwd, input, entries, trackedPaths);
131
+ }
132
+ export async function registerScope(cwd, input) {
133
+ let overlaps = [];
134
+ const trackedPaths = await trackedRepositoryPaths(cwd);
135
+ const result = await appendRefLogWithRetry(cwd, REGISTRY_REF, registrySubject, parseRegistryEntry, async (entries) => {
136
+ overlaps = await scopeRegistrationAdmission(cwd, input, entries, trackedPaths);
124
137
  return {
125
138
  v: 1,
126
139
  kind: "register",
@@ -38,17 +38,17 @@ export function isBlankScopePatternLine(line) {
38
38
  }
39
39
  /** Parse Keiyaku's source-line Scope opener, not a normalized Markdown node. */
40
40
  export function parseScopeFenceOpening(line) {
41
- const match = line.match(/^(`{3,})$/);
42
- return match ? { length: match[1].length } : null;
41
+ const match = line.match(/^(`{3,}|~{3,})$/);
42
+ return match ? { delimiter: match[1][0], length: match[1].length } : null;
43
43
  }
44
- /** Keiyaku closer: column-zero backticks, at least opener length, then whitespace. */
44
+ /** Keiyaku closer: matching column-zero delimiter, at least opener length, then whitespace. */
45
45
  export function isScopeFenceClosing(line, fence) {
46
- const match = line.match(/^(`+)[ \t]*$/);
47
- return Boolean(match && match[1].length >= fence.length);
46
+ const match = line.match(/^(`+|~+)[ \t]*$/);
47
+ return Boolean(match && match[1][0] === fence.delimiter && match[1].length >= fence.length);
48
48
  }
49
49
  function fencedScopeError(heading, detail) {
50
50
  return new FlowError("INVALID_KEIYAKU_DRAFT", `${heading} must contain exactly one fenced gitignore-subset block; ${detail}. ` +
51
- `Paste-ready canonical example:\n\n${heading}\n\`\`\`\nfoo\nbar\n\`\`\``);
51
+ `Paste-ready canonical example:\n\n${heading}\n~~~\nfoo\nbar\n~~~`);
52
52
  }
53
53
  /** Validate and normalize the one fenced block that constitutes a Scope body. */
54
54
  export function validateScopeFragmentLines(lines, options) {
@@ -152,14 +152,14 @@ export function markdownSourceSections(markdown) {
152
152
  return { ...heading, end: next?.start ?? markdown.length };
153
153
  });
154
154
  }
155
- /** Render canonical Scope bytes with a fence that content cannot close. */
155
+ /** Render canonical Scope bytes with a tilde fence that content cannot close. */
156
156
  export function renderScopeFragment(patterns) {
157
157
  let longestRun = 0;
158
158
  for (const pattern of patterns) {
159
- for (const run of pattern.match(/`+/g) ?? [])
159
+ for (const run of pattern.match(/~+/g) ?? [])
160
160
  longestRun = Math.max(longestRun, run.length);
161
161
  }
162
- const fence = "`".repeat(Math.max(3, longestRun + 1));
162
+ const fence = "~".repeat(Math.max(3, longestRun + 1));
163
163
  return `${fence}\n${patterns.join("\n")}\n${fence}`;
164
164
  }
165
165
  /**
@@ -1,6 +1,7 @@
1
+ import { availableParallelism } from "node:os";
1
2
  import { loadKeiyakuSettings } from "../../config/settings/loader.js";
2
3
  import { observeGitRepository } from "../../git/branches.js";
3
- import { readProjectionStatusBoard } from "../projection/index.js";
4
+ import { readExactProjectionStatus, readOperationalProjectionStatusBoard, } from "../projection/index.js";
4
5
  import { deriveQueueReadModel, queueMembership, } from "../settlement/index.js";
5
6
  import { findOpenArc, getMaxArc, receiptsAfterFence } from "../seal.js";
6
7
  import { openTaskStore, readTaskBoard, taskRepositoryFromStore, taskUpdatedAt, } from "../task/index.js";
@@ -8,6 +9,9 @@ import { deriveBootstrapDiagnostics, deriveContractState, hasCurrentPetitionVerd
8
9
  import { commissionTarget, evaluateBaseDrift, latestBase, resolveDefaultBranchFromSettings, } from "./drift.js";
9
10
  import { computeClaimReconciliation, } from "./reconciliation.js";
10
11
  import { readStatusLedgers } from "./ledger-batch.js";
12
+ // Git object reads benefit from overlap, but a hard cap avoids process and disk contention.
13
+ const STATUS_DRIFT_MAX_CONCURRENCY = 8;
14
+ const STATUS_DRIFT_CONCURRENCY = Math.min(STATUS_DRIFT_MAX_CONCURRENCY, availableParallelism());
11
15
  function compareTaskStatusRows(left, right) {
12
16
  const leftUpdated = taskUpdatedAt(left);
13
17
  const rightUpdated = taskUpdatedAt(right);
@@ -165,6 +169,23 @@ function deriveLastActiveAge(entries, now) {
165
169
  lastActiveMs: Math.max(0, now - atMs),
166
170
  };
167
171
  }
172
+ export async function evaluateDistinctBaseDrifts(cwd, bases, defaultBranch, evaluate = evaluateBaseDrift) {
173
+ const uniqueBases = [...new Set(bases)];
174
+ const results = new Map();
175
+ let nextIndex = 0;
176
+ const worker = async () => {
177
+ while (nextIndex < uniqueBases.length) {
178
+ const index = nextIndex++;
179
+ const base = uniqueBases[index];
180
+ if (base === undefined)
181
+ return;
182
+ results.set(base, await evaluate(cwd, base, defaultBranch));
183
+ }
184
+ };
185
+ const workerCount = Math.min(STATUS_DRIFT_CONCURRENCY, uniqueBases.length);
186
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
187
+ return results;
188
+ }
168
189
  export async function readKanshiBoard(cwd, now, opts) {
169
190
  const targetFilter = opts?.target ?? null;
170
191
  const loadedSettings = await loadKeiyakuSettings(cwd);
@@ -173,9 +194,14 @@ export async function readKanshiBoard(cwd, now, opts) {
173
194
  if (repository.capability === "unknown")
174
195
  throw repository.error;
175
196
  if (repository.capability !== "available" || repository.coordinate !== "repository") {
197
+ const exactProjection = opts?.projectionId === undefined
198
+ ? undefined
199
+ : await readExactProjectionStatus(cwd, now, opts.projectionId);
176
200
  return {
177
201
  contracts: [],
178
- projections: await readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
202
+ projections: opts?.projectionId === undefined
203
+ ? await readOperationalProjectionStatusBoard(cwd, now)
204
+ : exactProjection ? { rows: [exactProjection] } : null,
179
205
  queueLength: 0,
180
206
  diseases: loadedSettings.diseases,
181
207
  claimReconciliation: null,
@@ -203,6 +229,13 @@ export async function readKanshiBoard(cwd, now, opts) {
203
229
  }
204
230
  const queueModel = deriveQueueReadModel(readableLedgers);
205
231
  const boardQueueLength = queueModel.targetQueues.reduce((sum, queue) => sum + queue.ordered.length, 0);
232
+ const baseDrifts = await evaluateDistinctBaseDrifts(cwd, readableLedgers.flatMap((item) => {
233
+ const state = deriveContractState(item.entries);
234
+ return isTerminalState(state) ? [] : (() => {
235
+ const base = latestBase(item.entries);
236
+ return base ? [base] : [];
237
+ })();
238
+ }), defaultBranch);
206
239
  const contracts = [];
207
240
  // Readable-ledger commission targets retained for post-assembly filter only.
208
241
  const derivedTargetById = new Map();
@@ -236,7 +269,7 @@ export async function readKanshiBoard(cwd, now, opts) {
236
269
  const after = (bind?.data.after ?? []).map((id) => ({ id, terminal: terminalById.get(id) ?? false }));
237
270
  const currentArc = deriveCurrentArc(item.entries, bind?.data.objective, state);
238
271
  const baseDrift = base
239
- ? await evaluateBaseDrift(cwd, base, defaultBranch)
272
+ ? baseDrifts.get(base) ?? { state: "unknown", label: "n/a (base drift unavailable)" }
240
273
  : { state: "unknown", label: "n/a (no default branch)" };
241
274
  const nextStep = deriveNextStep({
242
275
  id: item.contractId,
@@ -296,9 +329,14 @@ export async function readKanshiBoard(cwd, now, opts) {
296
329
  ? boardQueueLength
297
330
  : (queueModel.targetQueues.find((queue) => queue.target === targetFilter)?.ordered.length ?? 0);
298
331
  const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
332
+ const exactProjection = opts?.projectionId === undefined
333
+ ? undefined
334
+ : await readExactProjectionStatus(cwd, now, opts.projectionId);
299
335
  return {
300
336
  contracts: filteredContracts,
301
- projections: await readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
337
+ projections: opts?.projectionId === undefined
338
+ ? await readOperationalProjectionStatusBoard(cwd, now)
339
+ : exactProjection ? { rows: [exactProjection] } : null,
302
340
  queueLength,
303
341
  defaultBranch: defaultBranch?.branch,
304
342
  defaultHead: defaultBranch?.head,
@@ -1,7 +1,7 @@
1
1
  import { assertSettingsKnobUsable } from "../../config/settings/disease.js";
2
2
  import { loadKeiyakuSettings } from "../../config/settings/loader.js";
3
3
  import { FlowError } from "../../flow-error.js";
4
- import { SETTINGS_FILE } from "../../keiyaku.js";
4
+ import { resolveLocalBranchHead } from "../../git/refs.js";
5
5
  import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../../git/core.js";
6
6
  function findBindEntry(entries) {
7
7
  return entries.find((entry) => entry.kind === "bind");
@@ -49,10 +49,26 @@ export async function resolveDefaultBranchFromSettings(cwd, loaded, rejectDiseas
49
49
  assertSettingsKnobUsable(loaded, "defaultBranch");
50
50
  const override = loaded.knobs?.defaultBranch;
51
51
  if (override) {
52
- const head = await resolveCommit(cwd, override);
53
- if (!head)
54
- throw new FlowError("INVALID_SETTINGS", `${SETTINGS_FILE} defaultBranch '${override}' does not resolve`);
55
- return { branch: override, head };
52
+ const resolved = await resolveLocalBranchHead(cwd, override);
53
+ if (!resolved) {
54
+ const source = loaded.knobSources.defaultBranch;
55
+ if (source === undefined) {
56
+ throw new Error("admitted defaultBranch has no settings source coordinate");
57
+ }
58
+ throw new FlowError("INVALID_SETTINGS", `${source.coordinate} defaultBranch '${override}' does not resolve`, {
59
+ facts: {
60
+ kind: "invalid_settings",
61
+ diseases: [{
62
+ source: source.source,
63
+ coordinate: source.coordinate,
64
+ knob: "defaultBranch",
65
+ reason: `defaultBranch '${override}' does not resolve as a local branch`,
66
+ }],
67
+ },
68
+ });
69
+ }
70
+ const branch = resolved.ref.slice("refs/heads/".length);
71
+ return { branch, head: resolved.head };
56
72
  }
57
73
  const git = createGit(cwd);
58
74
  for (const branch of ["main", "master"]) {
@@ -1,158 +1 @@
1
- import { wrapGitError, runGitProcess } from "../../git/core.js";
2
- import { GitStreamingBatchFramingError, GitStreamingBatchResponseEndedError, openGitStreamingBatch, } from "../../git/streaming-batch.js";
3
- import { parseLedgerEntryCommitMessage } from "../entry.js";
4
- import { ACTIVE_LEDGER_REF_PREFIX, } from "../ledger.js";
5
- const MAX_LEDGER_COMMIT_BYTES = 1024 * 1024;
6
- async function runStatusGit(cwd, args) {
7
- const result = await runGitProcess(cwd, args);
8
- if (result.error || result.status !== 0) {
9
- throw wrapGitError(args.join(" "), {
10
- message: result.error?.message
11
- ?? `git exited with status ${result.status ?? "unknown"}`,
12
- stderr: result.stderr,
13
- stdout: result.stdout,
14
- }, cwd);
15
- }
16
- return Buffer.from(result.stdout, "utf8");
17
- }
18
- async function queryBatchObject(batch, expression) {
19
- const record = await batch.request(expression, {
20
- maxContentBytes: MAX_LEDGER_COMMIT_BYTES,
21
- });
22
- if (record.kind === "missing" || record.kind === "ambiguous") {
23
- throw new Error(`[git cat-file ${expression}] object is ${record.kind === "missing" ? "missing" : "ambiguous"}`);
24
- }
25
- return {
26
- oid: record.record.oid,
27
- type: record.record.type,
28
- content: record.record.content,
29
- };
30
- }
31
- function parseCommitObject(object) {
32
- if (object.type !== "commit") {
33
- throw new Error(`git cat-file expected commit ${object.oid}, received ${object.type}`);
34
- }
35
- const content = object.content.toString("utf8");
36
- const messageStart = content.indexOf("\n\n");
37
- if (messageStart < 0)
38
- throw new Error(`git commit ${object.oid} has no message separator`);
39
- const parents = [];
40
- for (const line of content.slice(0, messageStart).split("\n")) {
41
- if (!line.startsWith("parent "))
42
- continue;
43
- const parent = line.slice("parent ".length);
44
- if (!/^[0-9a-f]{40,64}$/.test(parent)) {
45
- throw new Error(`git commit ${object.oid} has an invalid parent header`);
46
- }
47
- parents.push(parent);
48
- }
49
- return { oid: object.oid, parents, message: content.slice(messageStart + 2) };
50
- }
51
- function parseLedgerRefs(output) {
52
- return output
53
- .toString("utf8")
54
- .split(/\r?\n/)
55
- .filter((ref) => ref.startsWith(ACTIVE_LEDGER_REF_PREFIX))
56
- .map((ref) => ({ contractId: ref.slice(ACTIVE_LEDGER_REF_PREFIX.length), ref }));
57
- }
58
- async function readLedgerFromBatch(batch, ref, commits) {
59
- const headObject = await queryBatchObject(batch, `${ref.ref}^{commit}`);
60
- const head = parseCommitObject(headObject);
61
- commits.set(head.oid, Promise.resolve(head));
62
- const ordered = [];
63
- const visited = new Set();
64
- const visiting = new Set();
65
- const visit = async (oid) => {
66
- if (visited.has(oid))
67
- return;
68
- if (visiting.has(oid))
69
- throw new Error(`git ledger ancestry contains a cycle at ${oid}`);
70
- visiting.add(oid);
71
- let pending = commits.get(oid);
72
- if (!pending) {
73
- pending = queryBatchObject(batch, oid).then(parseCommitObject);
74
- commits.set(oid, pending);
75
- }
76
- const commit = await pending;
77
- for (const parent of commit.parents)
78
- await visit(parent);
79
- visiting.delete(oid);
80
- visited.add(oid);
81
- ordered.push(commit);
82
- };
83
- await visit(head.oid);
84
- return {
85
- contractId: ref.contractId,
86
- ref: ref.ref,
87
- head: head.oid,
88
- entries: ordered.map((commit) => parseLedgerEntryCommitMessage(commit.message)),
89
- };
90
- }
91
- /**
92
- * Read the aggregate status ledger snapshot with at most two Git processes:
93
- * ref-name discovery and one framed, streaming object session.
94
- */
95
- export async function readStatusLedgers(cwd) {
96
- const refOutput = await runStatusGit(cwd, [
97
- "for-each-ref",
98
- "--sort=refname",
99
- "--format=%(refname)",
100
- ACTIVE_LEDGER_REF_PREFIX,
101
- ]);
102
- const refs = parseLedgerRefs(refOutput);
103
- if (refs.length === 0)
104
- return [];
105
- const batch = openGitStreamingBatch(cwd);
106
- const commits = new Map();
107
- const results = [];
108
- let readError;
109
- try {
110
- for (let index = 0; index < refs.length; index += 1) {
111
- const ref = refs[index];
112
- try {
113
- results.push(await readLedgerFromBatch(batch, ref, commits));
114
- }
115
- catch (error) {
116
- const diagnostic = error instanceof Error ? error : new Error(String(error));
117
- results.push({ id: ref.contractId, error: diagnostic });
118
- // Framing and mid-frame EOF both destroy the batch channel; isolate remaining refs.
119
- if (error instanceof GitStreamingBatchFramingError
120
- || error instanceof GitStreamingBatchResponseEndedError) {
121
- const isolationReason = error instanceof GitStreamingBatchResponseEndedError
122
- ? "response ended"
123
- : "malformed frame";
124
- for (const pending of refs.slice(index + 1)) {
125
- results.push({
126
- id: pending.contractId,
127
- error: new Error(`git cat-file batch terminated after ${isolationReason}: ${diagnostic.message}`),
128
- });
129
- }
130
- break;
131
- }
132
- }
133
- }
134
- }
135
- catch (error) {
136
- readError = error;
137
- }
138
- try {
139
- const close = await batch.close();
140
- if (!close.aborted && (close.timedOut || close.signal || close.code !== 0)) {
141
- throw wrapGitError("cat-file --batch", {
142
- message: close.timedOut
143
- ? "git timed out"
144
- : close.signal
145
- ? `git exited from signal ${close.signal}`
146
- : `git exited with status ${close.code ?? "unknown"}`,
147
- stderr: close.stderr.toString("utf8"),
148
- }, cwd);
149
- }
150
- }
151
- catch (error) {
152
- if (!readError)
153
- readError = error;
154
- }
155
- if (readError)
156
- throw readError;
157
- return results;
158
- }
1
+ export { readAllLedgerSnapshots as readStatusLedgers, } from "../ledger-batch.js";
@@ -3,10 +3,10 @@
3
3
  * Only symbols required by current external consumers are re-exported.
4
4
  * Package-internal modules must import each other directly, not through this file.
5
5
  */
6
- export { reconcileTaskForfeit, settleTaskBoardForBind, settleTaskBoardForLanding, } from "./task-contract.js";
6
+ export { previewTaskBoardForBind, reconcileTaskForfeit, settleTaskBoardForBind, settleTaskBoardForLanding, } from "./task-contract.js";
7
7
  export { resolveTaskCreator } from "./task-git-actor.js";
8
8
  export { openTaskCommandRepository, readTaskDoctor, taskCommandContractId, } from "./task-git-runtime.js";
9
- export { openTaskStore } from "./task-git-store.js";
9
+ export { openTaskStore, readTaskHistory } from "./task-git-store.js";
10
10
  export { taskRepositoryFromStore } from "./task-store-repository.js";
11
11
  export { isTaskId } from "./validation.js";
12
12
  export { buildTaskTree, createTask, doneTask, dropTask, parseTaskDocument, readTaskBoard, resolveTaskReadId, serializeTaskDocument, startTask, stopTask, updateTask, deriveTaskDisplay, selectTaskList, taskUpdatedAt, advancedTaskUpdatedAt, } from "./task.js";
@@ -7,6 +7,24 @@ import { buildTaskSourceBoard } from "./source-board.js";
7
7
  import { composeLandingTree, containsCommit, createLandingCommit, createTaskBoardCommit, findAcceptedLandingCommit, publishTaskMutationCommit, inspectCurrentBranch, publishForwardTaskBoardCommit, reconcileTaskPathsetIndex, refHead, taskContractAtCommit, taskSettlementOwnershipAtCommit, taskSourcesAtCommit, taskWritesFromSources, trackedTaskPaths, updateBranch, updateTargetRef, } from "./settlement-git.js";
8
8
  import { contractPointerDocument, doneTaskReplacements, reopenedTaskDocument, requireHealthySettlementBoard, requireSettledTask, selectReadyTasksForBind, tasksForContract, } from "./settlement-policy.js";
9
9
  import { stableRepoRoot } from "../worktree-path.js";
10
+ /**
11
+ * Read the same healthy task board and run the same ready-task selector as bind
12
+ * under the task admission lock, without replacing sources or publishing.
13
+ */
14
+ export async function previewTaskBoardForBind(input) {
15
+ await requireGitRepository(input.cwd, "task bind");
16
+ return await withLockHeldTaskBoard(input.cwd, async (transaction) => {
17
+ if (transaction.location.mode !== "git") {
18
+ if (transaction.location.capability === "unavailable") {
19
+ assertGitObservation({ capability: "unavailable" }, "task bind");
20
+ }
21
+ throw new FlowError("NOT_GIT_REPO", "task bind requires Git task storage");
22
+ }
23
+ const repository = taskRepositoryFromLockedBoard(transaction);
24
+ const board = requireHealthySettlementBoard(await repository.read());
25
+ return { tasks: input.taskIds === undefined ? [] : selectReadyTasksForBind(board, input.taskIds) };
26
+ });
27
+ }
10
28
  function sourcePath(root, source) {
11
29
  return source.path.slice(root.length + 1).split("\\").join("/");
12
30
  }
@@ -1,24 +1,22 @@
1
- import { observeGitRepository } from "../../git/branches.js";
2
1
  import { listContractIds } from "../ledger.js";
3
2
  import { contractBoundByEngineWorktree, stableRepoRoot } from "../worktree-path.js";
4
- import { inspectTaskLock, openTaskStore } from "./task-git-store.js";
3
+ import { inspectTaskLock, locateTaskBoard, openTaskStore, openTaskStoreAt, } from "./task-git-store.js";
5
4
  import { taskRepositoryFromStore } from "./task-store-repository.js";
6
5
  import { withTaskWorktreePolicy } from "./task-worktree.js";
7
6
  import { inspectTaskDoctor } from "./task-doctor.js";
8
7
  export async function taskCommandContractId(cwd) {
9
- const observation = await observeGitRepository(cwd);
10
- if (observation.capability === "unknown")
11
- throw observation.error;
12
- if (observation.capability !== "available" || observation.coordinate !== "repository")
8
+ const board = await locateTaskBoard(cwd);
9
+ if (board.mode !== "git")
13
10
  return undefined;
14
- return contractBoundByEngineWorktree(cwd);
11
+ return contractBoundByEngineWorktree(cwd, board.root);
15
12
  }
16
13
  export async function openTaskCommandRepository(cwd) {
17
- const store = await openTaskStore(cwd);
14
+ const board = await locateTaskBoard(cwd);
15
+ const store = openTaskStoreAt(board);
18
16
  const repository = taskRepositoryFromStore(store);
19
- if (store.mode !== "git")
17
+ if (board.mode !== "git")
20
18
  return repository;
21
- const contractId = await contractBoundByEngineWorktree(cwd);
19
+ const contractId = await contractBoundByEngineWorktree(cwd, board.root);
22
20
  return contractId ? withTaskWorktreePolicy(repository, contractId) : repository;
23
21
  }
24
22
  export async function taskContractIds(cwd) {