@astrosheep/keiyaku 4.5.8 → 4.5.9

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 (79) hide show
  1. package/README.md +1 -1
  2. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +2 -2
  3. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +15 -15
  4. package/build/src/akuma/akuma.d.ts +24 -4
  5. package/build/src/akuma/archetype.js +1 -1
  6. package/build/src/akuma/body.d.ts +2 -1
  7. package/build/src/akuma/body.js +7 -2
  8. package/build/src/akuma/call-request.js +1 -1
  9. package/build/src/akuma/heart/facts.d.ts +10 -0
  10. package/build/src/akuma/heart/facts.js +11 -0
  11. package/build/src/akuma/heart/index.d.ts +3 -2
  12. package/build/src/akuma/heart/index.js +4 -1
  13. package/build/src/akuma/projection.d.ts +63 -12
  14. package/build/src/akuma/projection.js +5 -11
  15. package/build/src/akuma/provider.js +8 -1
  16. package/build/src/akuma/providers/claude/events.d.ts +2 -2
  17. package/build/src/akuma/providers/claude/events.js +16 -3
  18. package/build/src/akuma/providers/codex-app-server/events.d.ts +2 -2
  19. package/build/src/akuma/providers/codex-app-server/events.js +25 -4
  20. package/build/src/akuma/providers/pi/index.js +55 -69
  21. package/build/src/akuma/request-lifecycle.d.ts +1 -1
  22. package/build/src/akuma/request-lifecycle.js +3 -1
  23. package/build/src/akuma/request-serve.d.ts +1 -1
  24. package/build/src/akuma/request-serve.js +4 -4
  25. package/build/src/akuma/turn-drive.js +8 -2
  26. package/build/src/body/amend.js +18 -19
  27. package/build/src/cli/commands/akuma.js +2 -2
  28. package/build/src/cli/commands/contract-invoke.js +27 -22
  29. package/build/src/cli/commands/contract.d.ts +6 -10
  30. package/build/src/cli/commands/contract.js +29 -11
  31. package/build/src/cli/commands/install.d.ts +1 -0
  32. package/build/src/cli/commands/install.js +2 -1
  33. package/build/src/cli/parse.js +23 -9
  34. package/build/src/cli/render/akuma-activity.d.ts +1 -1
  35. package/build/src/cli/render/akuma-activity.js +4 -3
  36. package/build/src/cli/render/akuma.js +1 -1
  37. package/build/src/cli/render/catalog.js +1 -1
  38. package/build/src/cli/render/contract.js +13 -0
  39. package/build/src/cli/render/kanshi-akuma.js +26 -5
  40. package/build/src/cli/render/kanshi.js +26 -3
  41. package/build/src/cli/render/terminal.d.ts +4 -1
  42. package/build/src/cli/render/terminal.js +12 -1
  43. package/build/src/cli/runtime.js +1 -1
  44. package/build/src/git/nuke.js +3 -3
  45. package/build/src/git/process.js +7 -4
  46. package/build/src/git/read-observation.js +1 -1
  47. package/build/src/git/reconcile.d.ts +2 -1
  48. package/build/src/git/reconcile.js +6 -0
  49. package/build/src/git/ref-migration.d.ts +22 -0
  50. package/build/src/git/ref-migration.js +55 -0
  51. package/build/src/git/repository.d.ts +4 -4
  52. package/build/src/git/repository.js +8 -8
  53. package/build/src/library/audit.d.ts +13 -5
  54. package/build/src/library/audit.js +9 -21
  55. package/build/src/library/composition.d.ts +41 -27
  56. package/build/src/library/composition.js +3 -3
  57. package/build/src/library/contract-forwarding-reconciliation-result.d.ts +17 -11
  58. package/build/src/library/contract-forwarding-reconciliation-result.js +11 -2
  59. package/build/src/library/contract-forwarding-result.d.ts +1628 -3887
  60. package/build/src/library/contract-forwarding-result.js +1 -1
  61. package/build/src/library/contract-forwarding.d.ts +12 -13
  62. package/build/src/library/contract-forwarding.js +17 -28
  63. package/build/src/library/contract-handle.d.ts +3 -1
  64. package/build/src/library/contract-handle.js +23 -22
  65. package/build/src/library/contract-operations.d.ts +61 -893
  66. package/build/src/library/contract-operations.js +2 -3
  67. package/build/src/library/contract-types.d.ts +7 -3
  68. package/build/src/library/contract.d.ts +12 -4
  69. package/build/src/library/contract.js +21 -6
  70. package/build/src/library/fleet-result.d.ts +138 -12
  71. package/build/src/library/fleet-result.js +2 -0
  72. package/build/src/library/input.d.ts +1 -1
  73. package/build/src/library/input.js +7 -1
  74. package/build/src/library/keiyaku.d.ts +44 -27
  75. package/build/src/library/keiyaku.js +6 -1
  76. package/build/src/runtime/proc/run.d.ts +1 -0
  77. package/build/src/runtime/proc/run.js +25 -5
  78. package/build/src/runtime/proc/windows-launch.exe +0 -0
  79. package/package.json +1 -1
@@ -34,11 +34,11 @@ export const CONTRACT_COMMAND_SPECS = {
34
34
  usage: "amend [<contract>|@<contract>] [--after <kei/...> ... | --clear-after] [--gates <name,...>] [--actor <actor>] [--json] [-]",
35
35
  purpose: "Amend one Contract's document operations or structured terms.",
36
36
  details: [
37
+ " ## Context|Objective|Design|Region|Criteria|Verification|<extension> (replace, or add new extension)",
37
38
  " ## Replace: Context|Objective|Design|Region|Criteria|Verification|<extension>",
38
39
  " ## Append: Context|Objective|Design|Criteria|<extension>",
39
40
  " ## Add: Criteria|<new-extension-title>",
40
41
  " ## Update: <existing-extension-title>",
41
- " ## Remove: Criterion <existing-title>",
42
42
  " ## Remove: <existing-extension-title>",
43
43
  ].join("\n"),
44
44
  },
@@ -46,13 +46,12 @@ export const CONTRACT_COMMAND_SPECS = {
46
46
  positional: "optional",
47
47
  stdin: "none",
48
48
  flags: {
49
- actor: "value",
50
49
  message: "value",
51
50
  "include-dirty": "boolean",
52
51
  "materialize-conflict": "boolean",
53
52
  json: "boolean",
54
53
  },
55
- usage: "deliver [<contract>|@<contract>] [--message <text>] [--include-dirty] [--materialize-conflict] [--actor <actor>] [--json]",
54
+ usage: "deliver [<contract>|@<contract>] [--message <text>] [--include-dirty] [--materialize-conflict] [--json]",
56
55
  purpose: "Deliver one Contract candidate from the appointed worktree.",
57
56
  details: [
58
57
  " --include-dirty Capture the complete non-ignored worktree tree as the",
@@ -66,8 +65,8 @@ export const CONTRACT_COMMAND_SPECS = {
66
65
  review: {
67
66
  positional: "optional",
68
67
  stdin: "optional",
69
- flags: { actor: "value", satisfied: "boolean", unsatisfied: "boolean", summary: "value", json: "boolean" },
70
- usage: "review [<contract>|@<contract>] (--satisfied | --unsatisfied) (--summary <text> | -) [--actor <actor>] [--json]",
68
+ flags: { satisfied: "boolean", unsatisfied: "boolean", summary: "value", json: "boolean" },
69
+ usage: "review [<contract>|@<contract>] (--satisfied | --unsatisfied) (--summary <text> | -) [--json]",
71
70
  purpose: "Record one review verdict.",
72
71
  },
73
72
  arc: {
@@ -108,8 +107,8 @@ export const CONTRACT_COMMAND_SPECS = {
108
107
  audit: {
109
108
  positional: "optional",
110
109
  stdin: "none",
111
- flags: { "include-dirty": "boolean", diff: "boolean", actor: "value", json: "boolean" },
112
- usage: "audit [<contract>|@<contract>] [--include-dirty] [--diff] [--actor <actor>] [--json]",
110
+ flags: { "include-dirty": "boolean", diff: "boolean", json: "boolean" },
111
+ usage: "audit [<contract>|@<contract>] [--include-dirty] [--diff] [--json]",
113
112
  purpose: "Ask what candidate preparation, Verification, and target placement would do.",
114
113
  },
115
114
  reconcile: {
@@ -131,7 +130,7 @@ export const CONTRACT_COMMAND_SPECS = {
131
130
  stdin: "none",
132
131
  flags: { json: "boolean" },
133
132
  usage: "settings [--json]",
134
- purpose: "Read user and project Settings resources.",
133
+ purpose: "Show effective Settings (user + project, read-only)",
135
134
  },
136
135
  region: {
137
136
  positional: "optional",
@@ -218,7 +217,6 @@ function parseDeliver(parts) {
218
217
  return {
219
218
  command: "deliver",
220
219
  ...(contract === undefined ? {} : { contract }),
221
- ...(parts.actor === undefined ? {} : { actor: parts.actor }),
222
220
  ...(message === undefined ? {} : { message }),
223
221
  includeDirty: parts.flags["include-dirty"] === true,
224
222
  materializeConflict: parts.flags["materialize-conflict"] === true,
@@ -240,7 +238,6 @@ function parseReview(parts) {
240
238
  verdict: parts.flags.satisfied === true ? "satisfied" : "unsatisfied",
241
239
  ...(summary === undefined ? {} : { summary }),
242
240
  ...(parts.stdin ? { summaryFromStdin: true } : {}),
243
- ...(parts.actor === undefined ? {} : { actor: parts.actor }),
244
241
  output: parts.output,
245
242
  };
246
243
  }
@@ -301,6 +298,28 @@ function parseLs(parts) {
301
298
  }
302
299
  export function renderContractHelp(command) {
303
300
  const spec = CONTRACT_COMMAND_SPECS[command];
301
+ if (command === "settings") {
302
+ return [
303
+ "Show effective Settings — the merged read-only view of:",
304
+ " user ~/.keiyaku/settings.json",
305
+ " project <WorldRoot>/.keiyaku/settings.json",
306
+ "A project record wholly shadows the same-name user record.",
307
+ "",
308
+ "Shape: namespace -> entry -> JSON value. There is no write",
309
+ "command; edit the files directly.",
310
+ "",
311
+ "Recognized settings:",
312
+ " gates gate bundles selected by bind and amend",
313
+ " worktree commands run when worktrees are created or destroyed",
314
+ " git.requireBranchesToBeUpToDate whether deliver and audit require up-to-date target branches",
315
+ " providers the available Akuma and how each is run",
316
+ "",
317
+ "Each entry is validated by the feature that reads it. A rejected",
318
+ "value's diagnostic states the expected shape.",
319
+ "",
320
+ usageLine(spec.usage),
321
+ ].join("\n");
322
+ }
304
323
  const help = `${spec.purpose}\n\n${usageLine(spec.usage)}`;
305
324
  return spec.details === undefined ? help : `${help}\n\n${spec.details}`;
306
325
  }
@@ -352,7 +371,6 @@ export function parseContractCommand(parts) {
352
371
  ...(contract === undefined ? {} : { contract }),
353
372
  includeDirty: parts.flags["include-dirty"] === true,
354
373
  showDiff: parts.flags.diff === true,
355
- ...(parts.actor === undefined ? {} : { actor: parts.actor }),
356
374
  output: parts.output,
357
375
  };
358
376
  }
@@ -17,6 +17,7 @@ export type InstallInvocationResult = Readonly<{
17
17
  }>;
18
18
  export type InstallRunner = (input: ProcessInput) => Promise<ProcessOutcome>;
19
19
  export declare const INSTALL_USAGE = "install <codex|claude|opencode|pi> [--json]\ninstall --all [--json]";
20
+ export declare const INSTALL_ROOT_PURPOSE = "Install Keiyaku into coding harnesses";
20
21
  export declare function parseInstallCommand(argv: readonly string[]): ParsedInstallCommand;
21
22
  export declare function renderInstallHelp(): string;
22
23
  export declare function installAssetsRoot(): string;
@@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url";
3
3
  import { CliUsageError, usageLine } from "../usage.js";
4
4
  export const HARNESS_NAMES = ["codex", "claude", "opencode", "pi"];
5
5
  export const INSTALL_USAGE = "install <codex|claude|opencode|pi> [--json]\ninstall --all [--json]";
6
+ export const INSTALL_ROOT_PURPOSE = "Install Keiyaku into coding harnesses";
6
7
  function isHarness(value) {
7
8
  return value !== undefined && HARNESS_NAMES.includes(value);
8
9
  }
@@ -42,7 +43,7 @@ export function parseInstallCommand(argv) {
42
43
  };
43
44
  }
44
45
  export function renderInstallHelp() {
45
- return `Install the Keiyaku skills into one or more agent harnesses.\n\n${usageLine(INSTALL_USAGE)}`;
46
+ return `Install Keiyaku into your coding harnesses via each harness's native plugin/package installer. --all continues past failures; any failure exits 1.\n\n${usageLine(INSTALL_USAGE)}`;
46
47
  }
47
48
  const INSTALL_TIMEOUT_MS = 5 * 60 * 1000;
48
49
  const ASSET_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../../integrations/marketplace");
@@ -1,25 +1,39 @@
1
1
  import { isTaskAction, parseTaskCommand, renderTaskHelp, renderTaskUsage, } from "./commands/task.js";
2
2
  import { isAkumaAction, parseAkumaCommand, renderAkumaHelp, renderAkumaRootRows, renderAkumaUsage, } from "./commands/akuma.js";
3
- import { parseInstallCommand, renderInstallHelp } from "./commands/install.js";
3
+ import { INSTALL_ROOT_PURPOSE, parseInstallCommand, renderInstallHelp, } from "./commands/install.js";
4
4
  import { CONTRACT_COMMAND_SPECS, parseContractCommand, renderContractHelp as renderContractHelpForOwner, renderContractUsage, } from "./commands/contract.js";
5
5
  import { renderTextBlock } from "./render/terminal.js";
6
6
  import { CliUsageError, isBlankInput } from "./usage.js";
7
7
  export { CliUsageError } from "./usage.js";
8
8
  export { renderContractHelp } from "./commands/contract.js";
9
- const ROOT_USAGE = "usage: keiyaku [-C <path>] [--repo <path>] <command> [<contract>|@<contract>] [--flag ...] [-]";
9
+ const ROOT_USAGE = [
10
+ "usage: keiyaku <command> [options]",
11
+ " keiyaku <command> --help shows that command's complete usage",
12
+ ].join("\n");
10
13
  export function renderRootHelp(columns) {
11
14
  return renderHelpText([
15
+ "keiyaku — Contract, Task, and Akuma control for one repository",
16
+ "",
12
17
  ROOT_USAGE,
13
18
  "",
14
- "global options:",
15
- " -C, --cwd <path> Set the invocation working directory.",
16
- " --repo <path> Select the Git repository coordinate.",
19
+ "Contract — standing acceptance",
20
+ ...Object.entries(CONTRACT_COMMAND_SPECS)
21
+ .filter(([command]) => command !== "settings")
22
+ .map(([command, spec]) => ` ${command.padEnd(10)} ${spec.purpose}`),
23
+ "",
24
+ "Task — plan memory",
25
+ " task Task coordination; see `keiyaku task --help`.",
17
26
  "",
18
- "commands:",
19
- ...Object.entries(CONTRACT_COMMAND_SPECS).map(([command, spec]) => ` keiyaku ${command} ${spec.purpose}`),
20
- " keiyaku install Install the Keiyaku skills into an agent harness.",
21
- " keiyaku task Task coordination; see `keiyaku task --help`.",
27
+ "Akuma — invoke capability",
22
28
  ...renderAkumaRootRows(),
29
+ "",
30
+ "Workspace",
31
+ ` install ${INSTALL_ROOT_PURPOSE}`,
32
+ ` settings ${CONTRACT_COMMAND_SPECS.settings.purpose}`,
33
+ "",
34
+ "Global options:",
35
+ " -C, --cwd <path> Set the invocation working directory.",
36
+ " --repo <path> Select the Git repository coordinate.",
23
37
  ].join("\n"), columns);
24
38
  }
25
39
  function renderHelpText(help, columns) {
@@ -37,5 +37,5 @@ export declare function historyText(command: Extract<ParsedCommand, {
37
37
  export declare function tellText(result: Extract<AkumaInvocationResult, {
38
38
  action: "tell";
39
39
  mode: "ordinary";
40
- }>): string;
40
+ }>, context: TextRenderContext): string;
41
41
  export {};
@@ -377,12 +377,13 @@ export function historyText(command, result, context) {
377
377
  ...groupedRows(result.history.rows, context, true),
378
378
  ].join("\n");
379
379
  }
380
- export function tellText(result) {
380
+ export function tellText(result, context) {
381
381
  const wake = result.result.tell.wake;
382
382
  const target = identity(result.result.akuma, result.alias);
383
+ const row = groupedRows([result.result.tell.row], context).join("\n");
383
384
  if (wake.kind === "failed") {
384
385
  const child = "child" in wake ? wake.child : undefined;
385
- return `${target}\n${SNAPSHOT_RULER}\n! tell delivery failed · ${safeText(wake.diagnostic)}${child === undefined ? "" : ` · log ${child.log.path} ${child.log.from}..${child.log.to}`}`;
386
+ return `${target}\n${SNAPSHOT_RULER}\n${row}\n! tell delivery failed · ${safeText(wake.diagnostic)}${child === undefined ? "" : ` · log ${child.log.path} ${child.log.from}..${child.log.to}`}`;
386
387
  }
387
- return `${target}\n${SNAPSHOT_RULER}\n✓ tell ${wake.kind}${wake.kind === "pursuing" ? ` body=${wake.bodySequence}` : ""}`;
388
+ return `${target}\n${SNAPSHOT_RULER}\n${row}`;
388
389
  }
@@ -69,7 +69,7 @@ export function renderAkumaText(command, result, context = DEFAULT_CONTEXT) {
69
69
  return waitText(result, context);
70
70
  case "tell":
71
71
  return result.mode === "ordinary"
72
- ? tellText(result)
72
+ ? tellText(result, context)
73
73
  : mutationObservationStageText(result.result.id, result.result.observation, context, {
74
74
  ...(result.alias === undefined ? {} : { alias: result.alias }),
75
75
  showLife: false,
@@ -140,7 +140,7 @@ export function renderCatalogText(catalog) {
140
140
  return renderContractCatalog(catalog);
141
141
  if (catalog.kind === "archetypes") {
142
142
  return [
143
- `archetypes ${catalog.rows.length}`,
143
+ `available Akuma ${catalog.rows.length}`,
144
144
  "",
145
145
  ...catalog.rows.flatMap((row) => [
146
146
  `${safeText(row.name)}${row.model === undefined ? "" : ` - ${safeText(row.model)}`}`,
@@ -11,6 +11,16 @@ function retryLines(detail, indent, columns) {
11
11
  }
12
12
  return renderOpaqueBlock(detail.kind, indent, columns);
13
13
  }
14
+ function refMigrationConflictRows(lag, columns) {
15
+ const lines = [];
16
+ receiptRow(lines, "!", "lag", [
17
+ {
18
+ text: `ref-migration-conflict legacy=${lag.legacyRef}@${lag.legacyOid} current=${lag.currentRef}@${lag.currentOid}`,
19
+ opaque: true,
20
+ },
21
+ ], columns);
22
+ return lines;
23
+ }
14
24
  function lagRows(lag, columns) {
15
25
  const lines = [];
16
26
  if (lag.kind === "worktree-retained") {
@@ -49,6 +59,9 @@ function lagRows(lag, columns) {
49
59
  receiptRow(lines, "!", "lag", [{ text: `contract-file-failed ${lag.worktree} ${lag.path}`, opaque: true }], columns);
50
60
  receiptPayload(lines, "diagnostic", lag.diagnostic);
51
61
  }
62
+ else if (lag.kind === "ref-migration-conflict") {
63
+ return refMigrationConflictRows(lag, columns);
64
+ }
52
65
  else {
53
66
  receiptRow(lines, "!", "lag", [{ text: `reconcile-failed ${lag.stage}`, opaque: true }], columns);
54
67
  receiptPayload(lines, "diagnostic", lag.diagnostic);
@@ -1,7 +1,28 @@
1
1
  import { visibleFleetRows } from "../../kanshi/fleet.js";
2
- import { boundedActivity, entityLines, identityLine, plumbFacts, renderSectionBlock, safeText, tone, } from "./terminal.js";
2
+ import { boundedActivity, elapsedMilliseconds, entityLines, identityLine, plumbFacts, RECENT_TONE_MS, renderSectionBlock, safeText, tone, } from "./terminal.js";
3
3
  const PLUMB = " │ ";
4
4
  const NARROW_COLUMNS = 72;
5
+ function mostRecentTimestamp(...values) {
6
+ return values.reduce((latest, value) => {
7
+ if (value === null || value === undefined)
8
+ return latest;
9
+ return latest === null || value > latest ? value : latest;
10
+ }, null);
11
+ }
12
+ function akumaStatusTone(row, observedAt) {
13
+ if (row.life === "stillborn" || row.life === "hung" || row.life === "stranded")
14
+ return "alert";
15
+ if (row.life === "killed")
16
+ return "dim";
17
+ const lifeAt = "lifeAt" in row ? row.lifeAt : null;
18
+ const lastActivityAt = "lastActivityAt" in row ? row.lastActivityAt : null;
19
+ const latestAge = elapsedMilliseconds(mostRecentTimestamp(lifeAt, lastActivityAt), observedAt);
20
+ if (row.life === "asleep")
21
+ return latestAge !== null && latestAge <= RECENT_TONE_MS ? "recent" : "dim";
22
+ if (row.life === "running" && latestAge !== null && latestAge <= RECENT_TONE_MS)
23
+ return "recent";
24
+ return null;
25
+ }
5
26
  function akumaMark(life) {
6
27
  return life === "running"
7
28
  ? "●"
@@ -48,6 +69,8 @@ function renderAkuma(report, context) {
48
69
  return ["AKUMA // unavailable", "", tone(`! ${safeText(section.failure.message)}`, "alert", context.color)];
49
70
  const rows = visibleFleetRows(section.value.rows);
50
71
  const rowLines = rows.map((row) => {
72
+ const statusTone = akumaStatusTone(row, report.observedAt);
73
+ const mark = statusTone === null ? akumaMark(row.life) : tone(akumaMark(row.life), statusTone, context.color);
51
74
  const lifeAt = "lifeAt" in row ? row.lifeAt : null;
52
75
  const life = `${row.life} · ${formatAge(lifeAt, report.observedAt)}`;
53
76
  const activity = "lastActivityAt" in row && row.lastActivityAt !== null
@@ -60,16 +83,14 @@ function renderAkuma(report, context) {
60
83
  const snapshot = activitySnapshotLine(row);
61
84
  const aliases = akumaLabel(row);
62
85
  if (context.columns > NARROW_COLUMNS) {
63
- const lines = [
64
- identityLine(akumaMark(row.life), row.id, `${aliases} · ${[...key, ...relation].join(" · ")}`.trim()),
65
- ];
86
+ const lines = [identityLine(mark, row.id, `${aliases} · ${[...key, ...relation].join(" · ")}`.trim())];
66
87
  return snapshot === undefined
67
88
  ? lines
68
89
  : [...lines, ...plumbFacts([boundedActivity(snapshot, context.columns, PLUMB)], context.columns)];
69
90
  }
70
91
  const identity = aliases.length === 0 ? row.id : `${row.id} ${aliases}`;
71
92
  const lines = entityLines({
72
- mark: akumaMark(row.life),
93
+ mark,
73
94
  identity,
74
95
  state: key[0],
75
96
  title: "",
@@ -1,8 +1,29 @@
1
1
  import { abbreviateGitIds, afterWording, candidateFact, dependentWording, displayGitId, gateFact, gitIdsInRow, mergeSummary, targetMovementFacts, } from "./contract-observation.js";
2
- import { entityLines, identityLine, plumbFacts, renderTextBlock, renderSectionBlock, safeText, tone, } from "./terminal.js";
2
+ import { entityLines, elapsedMilliseconds, identityLine, plumbFacts, RECENT_TONE_MS, renderTextBlock, renderSectionBlock, safeText, tone, } from "./terminal.js";
3
3
  import { akumaMark, endpointFact, formatAge, NARROW_COLUMNS, renderAkuma } from "./kanshi-akuma.js";
4
4
  const PLUMB = " │ ";
5
5
  const MAX_VISIBLE_ROWS = 10;
6
+ const REVIEW_ATTENTION_MS = 15 * 60 * 1_000;
7
+ const PENDING_ATTENTION_MS = 60 * 60 * 1_000;
8
+ function contractHasError(row) {
9
+ return (row.title === null ||
10
+ row.gates.reports.some((report) => report.current.kind === "attested" && report.current.verdict === "unsatisfied") ||
11
+ row.targetLag.kind === "unknown" ||
12
+ row.workspaceObservation.kind === "failed" ||
13
+ row.workspaceObservation.kind === "unavailable" ||
14
+ row.issue !== undefined);
15
+ }
16
+ function contractStatusTone(row, observedAt) {
17
+ if (contractHasError(row))
18
+ return "alert";
19
+ const phaseAge = elapsedMilliseconds(row.phaseAt, observedAt);
20
+ if (row.phase === "tendered" && phaseAge !== null && phaseAge >= REVIEW_ATTENTION_MS)
21
+ return "attention";
22
+ if ((row.phase === "waiting" || row.phase === "bound") && phaseAge !== null && phaseAge >= PENDING_ATTENTION_MS)
23
+ return "attention";
24
+ const journalAge = elapsedMilliseconds(row.lastJournalAt, observedAt);
25
+ return journalAge !== null && journalAge <= RECENT_TONE_MS ? "recent" : null;
26
+ }
6
27
  function contractMark(row) {
7
28
  if (row.phase === "claimed")
8
29
  return "✓";
@@ -189,9 +210,10 @@ function namespaceTaskFacts(row) {
189
210
  }
190
211
  function renderSelectedContractRow(row, report, context, abbreviations) {
191
212
  const title = row.title ?? "title unavailable";
213
+ const statusTone = contractStatusTone(row, report.observedAt);
192
214
  const lines = [
193
215
  ...entityLines({
194
- mark: contractMark(row),
216
+ mark: statusTone === null ? contractMark(row) : tone(contractMark(row), statusTone, context.color),
195
217
  identity: row.id,
196
218
  state: `${row.phase} · ${formatAge(row.phaseAt, report.observedAt)}`,
197
219
  title,
@@ -255,8 +277,9 @@ function renderWorldContractRow(row, report, context) {
255
277
  ...(row.holder.kind === "unavailable" ? ["! task · unavailable"] : []),
256
278
  ...(linkedAkumaSummary(row, report) === undefined ? [] : [linkedAkumaSummary(row, report)]),
257
279
  ];
280
+ const statusTone = contractStatusTone(row, report.observedAt);
258
281
  return entityLines({
259
- mark: contractMark(row),
282
+ mark: statusTone === null ? contractMark(row) : tone(contractMark(row), statusTone, context.color),
260
283
  identity: row.id,
261
284
  state: `${row.phase} · ${formatAge(row.lastJournalAt, report.observedAt)}`,
262
285
  title,
@@ -32,7 +32,10 @@ type CheckoutNotFollowable = Readonly<{
32
32
  export declare function checkoutNotFollowableLines(refusal: CheckoutNotFollowable): readonly string[];
33
33
  export declare function renderTextBlock(value: string, indent: string, columns: number): readonly string[];
34
34
  export declare function renderOpaqueBlock(value: string, indent: string, columns: number): readonly string[];
35
- export declare function tone(value: string, kind: "dim" | "alert", color: boolean): string;
35
+ export type SemanticTone = "dim" | "recent" | "attention" | "alert";
36
+ export declare const RECENT_TONE_MS: number;
37
+ export declare function elapsedMilliseconds(source: string | null | undefined, observedAt: string): number | null;
38
+ export declare function tone(value: string, kind: SemanticTone, color: boolean): string;
36
39
  export declare function boundedActivity(value: string, columns: number, prefix: string): string;
37
40
  export declare function plumbFacts(facts: readonly string[], columns: number): readonly string[];
38
41
  export declare function identityLine(mark: string, identity: string, extra?: string): string;
@@ -188,10 +188,21 @@ export function renderOpaqueBlock(value, indent, columns) {
188
188
  }
189
189
  return lines;
190
190
  }
191
+ export const RECENT_TONE_MS = 5 * 60 * 1_000;
192
+ export function elapsedMilliseconds(source, observedAt) {
193
+ if (source === null || source === undefined)
194
+ return null;
195
+ const sourceMs = Date.parse(source);
196
+ const observedMs = Date.parse(observedAt);
197
+ if (!Number.isFinite(sourceMs) || !Number.isFinite(observedMs))
198
+ return null;
199
+ return Math.max(0, observedMs - sourceMs);
200
+ }
191
201
  export function tone(value, kind, color) {
192
202
  if (!color)
193
203
  return value;
194
- return kind === "dim" ? `\u001b[2m${value}\u001b[0m` : `\u001b[31m${value}\u001b[0m`;
204
+ const code = kind === "dim" ? 2 : kind === "recent" ? 32 : kind === "attention" ? 33 : 31;
205
+ return `\u001b[${code}m${value}\u001b[0m`;
195
206
  }
196
207
  const KANSHI_NARROW_COLUMNS = 72;
197
208
  export function boundedActivity(value, columns, prefix) {
@@ -11,7 +11,7 @@ export function invocationStart(command) {
11
11
  return "⧖ auditing";
12
12
  if (command.command === "reconcile")
13
13
  return "⧖ reconciling";
14
- return command.command === "install" ? "⧖ installing skills" : undefined;
14
+ return command.command === "install" ? "⧖ installing harness integrations" : undefined;
15
15
  }
16
16
  function writeCliStream(stream, body) {
17
17
  stream.write(body.endsWith("\n") ? body : `${body}\n`);
@@ -3,7 +3,7 @@ import { isAbsolute, relative, sep } from "node:path";
3
3
  import { nukeEmptyPlaceAuthority, readPlaceRegister, releaseManagedWorktrees, withPlaceAuthorityFence, } from "../workspace-place.js";
4
4
  import { GitPlumbingError, runGit } from "./process.js";
5
5
  import { confirmPrivateStatePublication, withPrivateStatePublicationSeat } from "./private-state-seat.js";
6
- import { CANDIDATE_PIN_REF_NAMESPACE, DELIVERY_REF_NAMESPACE, GIT_REF, MIGRATION_CANDIDATE_PIN_REF_NAMESPACE, MIGRATION_DELIVERY_REF_NAMESPACE, NoGitWorldError, readRef, registeredWorktrees, repositoryAt, worktreeGitDirectory, } from "./repository.js";
6
+ import { CANDIDATE_PIN_REF_NAMESPACE, DELIVERY_REF_NAMESPACE, GIT_REF, LEGACY_CANDIDATE_PIN_REF_NAMESPACE, LEGACY_DELIVERY_REF_NAMESPACE, NoGitWorldError, readRef, registeredWorktrees, repositoryAt, worktreeGitDirectory, } from "./repository.js";
7
7
  import { worktreePath } from "./workspace.js";
8
8
  async function managedCustody(repository) {
9
9
  const register = await readPlaceRegister(repository);
@@ -120,8 +120,8 @@ async function removeOwnedRefs(repository) {
120
120
  const roots = [
121
121
  DELIVERY_REF_NAMESPACE,
122
122
  CANDIDATE_PIN_REF_NAMESPACE,
123
- MIGRATION_DELIVERY_REF_NAMESPACE,
124
- MIGRATION_CANDIDATE_PIN_REF_NAMESPACE,
123
+ LEGACY_DELIVERY_REF_NAMESPACE,
124
+ LEGACY_CANDIDATE_PIN_REF_NAMESPACE,
125
125
  ];
126
126
  for (const root of roots) {
127
127
  const refs = (await runGit(repository, ["for-each-ref", "--format=%(refname)", root]))
@@ -58,10 +58,13 @@ async function executeGit(repository, args, input, environment) {
58
58
  inputError = error;
59
59
  });
60
60
  child.stdin.end(input);
61
- const terminal = await new Promise((resolveTerminal) => {
62
- child.once("error", (error) => resolveTerminal({ code: null, error }));
63
- child.once("close", (code) => resolveTerminal({ code }));
64
- });
61
+ const terminal = await Promise.race([
62
+ new Promise((resolveTerminal) => {
63
+ child.once("error", (error) => resolveTerminal({ code: null, error }));
64
+ child.once("close", (code) => resolveTerminal({ code }));
65
+ }),
66
+ owned.terminationFailure,
67
+ ]);
65
68
  await owned.waitTermination();
66
69
  const output = Buffer.concat(stdout);
67
70
  if (!owned.cancelled() && terminal.code === 0 && inputError === undefined)
@@ -61,7 +61,7 @@ function batchError(child, stderr, message, status) {
61
61
  async function closeBatchProcess(process, child, closed, spawnError, stderr) {
62
62
  if (!child.stdin.destroyed)
63
63
  child.stdin.end();
64
- const outcome = await closed;
64
+ const outcome = await Promise.race([closed, process.terminationFailure]);
65
65
  await process.waitTermination();
66
66
  if (spawnError !== null || outcome.code !== 0) {
67
67
  throw batchError(child, stderr, process.cancelled() ? "git process cancelled" : "git cat-file --batch did not close cleanly", outcome.code);
@@ -4,6 +4,7 @@ import type { ContractId, ContractState, SnapshotId } from "../core/facts/types.
4
4
  import type { GitDecodeChannel } from "./read-observation.js";
5
5
  import { type TargetCheckoutEffect, type TargetCheckoutLag } from "./target-placement.js";
6
6
  import { type WorktreeHookLag, type WorktreeHooks } from "./hooks.js";
7
+ import { type RefMigrationConflict } from "./ref-migration.js";
7
8
  import type { UnsealedBytes } from "./terminal-seal.js";
8
9
  export type Effect = Readonly<{
9
10
  kind: "worktree";
@@ -53,7 +54,7 @@ export type ReconcileFailure = Readonly<{
53
54
  stage: "observation" | "effect";
54
55
  diagnostic: string;
55
56
  }>;
56
- export type ReconcileLag = WorktreeRetained | WorktreeFollowRetained | UnsealedBytes | TargetCheckoutLag | WorktreeHookLag | ReconcileFailure;
57
+ export type ReconcileLag = WorktreeRetained | WorktreeFollowRetained | UnsealedBytes | TargetCheckoutLag | WorktreeHookLag | RefMigrationConflict | ReconcileFailure;
57
58
  export type ReconcileResult = Readonly<{
58
59
  effects: readonly Effect[];
59
60
  lag: readonly ReconcileLag[];
@@ -10,6 +10,7 @@ import { acquireTargetPlacementFence, recoverTargetPlacement, } from "./target-p
10
10
  import { runCreateHooks } from "./hooks.js";
11
11
  import { followDependentManagedWorktree, followManagedWorktree, worktreePath } from "./workspace.js";
12
12
  import { removeCollectableScratchWorktrees } from "./scratch.js";
13
+ import { migrateContractCustodyRefs } from "./ref-migration.js";
13
14
  import { reconcileTerminalManagedWorktree, removeRef, updateRef } from "./terminal-reconcile.js";
14
15
  const pathExists = (path) => access(path).then(() => true, () => false);
15
16
  function deliveryRefFor(contract) {
@@ -151,6 +152,11 @@ async function reconcileWithTopology(input, state, topology) {
151
152
  await removeCollectableScratch(repository, topology, effects, lag);
152
153
  if (!state)
153
154
  return complete(effects, lag);
155
+ const migration = await migrateContractCustodyRefs(repository, state.id);
156
+ effects.push(...migration.effects);
157
+ lag.push(...migration.lag);
158
+ if (migration.lag.length > 0)
159
+ return complete(effects, lag);
154
160
  const targetCheckouts = await reconcileTargetCheckouts(repository, state);
155
161
  effects.push(...targetCheckouts.effects);
156
162
  lag.push(...targetCheckouts.lag);
@@ -0,0 +1,22 @@
1
+ import type { ContractId } from "../core/facts/types.js";
2
+ import { type GitRepository } from "./process.js";
3
+ import { type GitOid } from "./repository.js";
4
+ export type RefMigrationEffect = Readonly<{
5
+ kind: "ref";
6
+ name: string;
7
+ before: GitOid | null;
8
+ after: GitOid | null;
9
+ action: "created" | "removed";
10
+ }>;
11
+ export type RefMigrationConflict = Readonly<{
12
+ kind: "ref-migration-conflict";
13
+ legacyRef: string;
14
+ legacyOid: GitOid;
15
+ currentRef: string;
16
+ currentOid: GitOid;
17
+ }>;
18
+ export type RefMigrationResult = Readonly<{
19
+ effects: readonly RefMigrationEffect[];
20
+ lag: readonly RefMigrationConflict[];
21
+ }>;
22
+ export declare function migrateContractCustodyRefs(repository: GitRepository, contract: ContractId): Promise<RefMigrationResult>;
@@ -0,0 +1,55 @@
1
+ import { contractPhysicalName } from "./identity.js";
2
+ import { runGit } from "./process.js";
3
+ import { CANDIDATE_PIN_REF_NAMESPACE, DELIVERY_REF_NAMESPACE, LEGACY_CANDIDATE_PIN_REF_NAMESPACE, LEGACY_DELIVERY_REF_NAMESPACE, readRef, } from "./repository.js";
4
+ export async function migrateContractCustodyRefs(repository, contract) {
5
+ const leaf = contractPhysicalName(contract);
6
+ const pairs = [
7
+ {
8
+ legacy: `${LEGACY_DELIVERY_REF_NAMESPACE}/${leaf}`,
9
+ current: `${DELIVERY_REF_NAMESPACE}/${leaf}`,
10
+ },
11
+ {
12
+ legacy: `${LEGACY_CANDIDATE_PIN_REF_NAMESPACE}/${leaf}`,
13
+ current: `${CANDIDATE_PIN_REF_NAMESPACE}/${leaf}`,
14
+ },
15
+ ];
16
+ const observed = await Promise.all(pairs.map(async (pair) => {
17
+ const [legacyOid, currentOid] = await Promise.all([
18
+ readRef(repository, pair.legacy),
19
+ readRef(repository, pair.current),
20
+ ]);
21
+ return { ...pair, legacyOid, currentOid };
22
+ }));
23
+ const lag = observed.flatMap((pair) => pair.legacyOid !== null && pair.currentOid !== null && pair.currentOid !== pair.legacyOid
24
+ ? [
25
+ {
26
+ kind: "ref-migration-conflict",
27
+ legacyRef: pair.legacy,
28
+ legacyOid: pair.legacyOid,
29
+ currentRef: pair.current,
30
+ currentOid: pair.currentOid,
31
+ },
32
+ ]
33
+ : []);
34
+ if (lag.length > 0)
35
+ return { effects: [], lag };
36
+ const migrating = observed.filter((pair) => pair.legacyOid !== null);
37
+ if (migrating.length === 0)
38
+ return { effects: [], lag: [] };
39
+ const lines = ["start"];
40
+ const effects = [];
41
+ for (const pair of migrating) {
42
+ if (pair.currentOid === null) {
43
+ lines.push(`create ${pair.current} ${pair.legacyOid}`);
44
+ effects.push({ kind: "ref", name: pair.current, before: null, after: pair.legacyOid, action: "created" });
45
+ }
46
+ else {
47
+ lines.push(`verify ${pair.current} ${pair.currentOid}`);
48
+ }
49
+ lines.push(`delete ${pair.legacy} ${pair.legacyOid}`);
50
+ effects.push({ kind: "ref", name: pair.legacy, before: pair.legacyOid, after: null, action: "removed" });
51
+ }
52
+ lines.push("prepare", "commit", "");
53
+ await runGit(repository, ["update-ref", "--stdin", "--no-deref"], lines.join("\n"));
54
+ return { effects, lag: [] };
55
+ }
@@ -2,10 +2,10 @@ import { type GitRepository } from "./process.js";
2
2
  import { type TreeChange, type TreeEntry } from "./tree.js";
3
3
  export type { TreeChange } from "./tree.js";
4
4
  export declare const GIT_REF = "refs/heads/keiyaku-state";
5
- export declare const DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
6
- export declare const CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
7
- export declare const MIGRATION_DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
8
- export declare const MIGRATION_CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
5
+ export declare const DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
6
+ export declare const CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
7
+ export declare const LEGACY_DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
8
+ export declare const LEGACY_CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
9
9
  export declare const GIT_FORMAT_PATH = "meta/format.json";
10
10
  export declare const GIT_FORMAT_BYTES = "{\"version\":4}\n";
11
11
  export declare const STATE_COMMIT_SUBJECT = "keiyaku authority - do not delete or rewrite";
@@ -5,10 +5,10 @@ import { gitObjectId } from "./identity.js";
5
5
  import { GitPlumbingError, runGit, runGitWithEnvironment } from "./process.js";
6
6
  import { parseTreeObject, prepareTreeUpdate, treeUpdate, validPath, } from "./tree.js";
7
7
  export const GIT_REF = "refs/heads/keiyaku-state";
8
- export const DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
9
- export const CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
10
- export const MIGRATION_DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
11
- export const MIGRATION_CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
8
+ export const DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
9
+ export const CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
10
+ export const LEGACY_DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
11
+ export const LEGACY_CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
12
12
  export const GIT_FORMAT_PATH = "meta/format.json";
13
13
  const CURRENT_FORMAT_VERSION = 4;
14
14
  export const GIT_FORMAT_BYTES = `{"version":${CURRENT_FORMAT_VERSION}}\n`;
@@ -19,10 +19,10 @@ export function isKeiyakuOwnedRef(ref) {
19
19
  ref.startsWith(`${DELIVERY_REF_NAMESPACE}/`) ||
20
20
  ref === CANDIDATE_PIN_REF_NAMESPACE ||
21
21
  ref.startsWith(`${CANDIDATE_PIN_REF_NAMESPACE}/`) ||
22
- ref === MIGRATION_DELIVERY_REF_NAMESPACE ||
23
- ref.startsWith(`${MIGRATION_DELIVERY_REF_NAMESPACE}/`) ||
24
- ref === MIGRATION_CANDIDATE_PIN_REF_NAMESPACE ||
25
- ref.startsWith(`${MIGRATION_CANDIDATE_PIN_REF_NAMESPACE}/`));
22
+ ref === LEGACY_DELIVERY_REF_NAMESPACE ||
23
+ ref.startsWith(`${LEGACY_DELIVERY_REF_NAMESPACE}/`) ||
24
+ ref === LEGACY_CANDIDATE_PIN_REF_NAMESPACE ||
25
+ ref.startsWith(`${LEGACY_CANDIDATE_PIN_REF_NAMESPACE}/`));
26
26
  }
27
27
  export class NoGitWorldError extends Error {
28
28
  path;