@astrosheep/keiyaku 4.6.11 → 4.6.13

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 (138) hide show
  1. package/build/plugins/claude/skills/keiyaku/SKILL.md +3 -1
  2. package/build/plugins/claude/skills/keiyaku-babysit/SKILL.md +1 -1
  3. package/build/plugins/claude/skills/keiyaku-bind/SKILL.md +3 -2
  4. package/build/plugins/claude/skills/keiyaku-workflow/SKILL.md +9 -8
  5. package/build/plugins/codex/skills/keiyaku/SKILL.md +3 -1
  6. package/build/plugins/codex/skills/keiyaku-babysit/SKILL.md +1 -1
  7. package/build/plugins/codex/skills/keiyaku-bind/SKILL.md +3 -2
  8. package/build/plugins/codex/skills/keiyaku-workflow/SKILL.md +9 -8
  9. package/build/plugins/opencode/skills/keiyaku/SKILL.md +3 -1
  10. package/build/plugins/opencode/skills/keiyaku-babysit/SKILL.md +1 -1
  11. package/build/plugins/opencode/skills/keiyaku-bind/SKILL.md +3 -2
  12. package/build/plugins/opencode/skills/keiyaku-workflow/SKILL.md +9 -8
  13. package/build/skills/keiyaku/SKILL.md +3 -1
  14. package/build/skills/keiyaku-babysit/SKILL.md +1 -1
  15. package/build/skills/keiyaku-bind/SKILL.md +3 -2
  16. package/build/skills/keiyaku-workflow/SKILL.md +9 -8
  17. package/build/src/akuma/akuma-errors.d.ts +5 -0
  18. package/build/src/akuma/akuma-errors.js +9 -0
  19. package/build/src/akuma/akuma-owner.d.ts +14 -19
  20. package/build/src/akuma/akuma-owner.js +56 -54
  21. package/build/src/akuma/akuma-selection-execution.d.ts +5 -4
  22. package/build/src/akuma/akuma-selection-execution.js +59 -17
  23. package/build/src/akuma/akuma.d.ts +1 -2
  24. package/build/src/akuma/allowed.d.ts +2 -3
  25. package/build/src/akuma/allowed.js +1 -3
  26. package/build/src/akuma/body.js +17 -7
  27. package/build/src/akuma/call-initial-tell.d.ts +18 -0
  28. package/build/src/akuma/call-initial-tell.js +18 -0
  29. package/build/src/akuma/call-request.d.ts +20 -4
  30. package/build/src/akuma/heart/facts.d.ts +13 -0
  31. package/build/src/akuma/heart/facts.js +6 -0
  32. package/build/src/akuma/heart/index.d.ts +29 -14
  33. package/build/src/akuma/heart/index.js +75 -29
  34. package/build/src/akuma/heart/rows.d.ts +9 -2
  35. package/build/src/akuma/heart/rows.js +72 -11
  36. package/build/src/akuma/heart/storage.d.ts +2 -8
  37. package/build/src/akuma/heart/storage.js +13 -13
  38. package/build/src/akuma/heart/tells.d.ts +3 -1
  39. package/build/src/akuma/heart/tells.js +18 -2
  40. package/build/src/akuma/provider.d.ts +2 -4
  41. package/build/src/akuma/provider.js +29 -6
  42. package/build/src/akuma/providers/acp/core.js +23 -21
  43. package/build/src/akuma/providers/claude/index.js +0 -8
  44. package/build/src/akuma/providers/codex-app-server/index.js +9 -8
  45. package/build/src/akuma/providers/execution-environment.d.ts +1 -1
  46. package/build/src/akuma/providers/execution-environment.js +1 -0
  47. package/build/src/akuma/providers/opencode-sdk/events.js +111 -81
  48. package/build/src/akuma/providers/opencode-sdk/index.js +179 -150
  49. package/build/src/akuma/providers/opencode-sdk/native-http.js +7 -3
  50. package/build/src/akuma/providers/opencode-sdk/session.d.ts +5 -3
  51. package/build/src/akuma/providers/opencode-sdk/session.js +32 -27
  52. package/build/src/akuma/providers/paseo/index.js +0 -2
  53. package/build/src/akuma/providers/pi/index.js +23 -20
  54. package/build/src/akuma/selection-observation.d.ts +78 -9
  55. package/build/src/akuma/selection-owner-port.d.ts +2 -1
  56. package/build/src/akuma/selection-owner-port.js +6 -1
  57. package/build/src/akuma/selection-request.d.ts +1 -1
  58. package/build/src/akuma/selection-request.js +8 -3
  59. package/build/src/akuma/turn-drive.js +79 -48
  60. package/build/src/akuma-body.js +5 -3
  61. package/build/src/cli/commands/akuma-grammar.js +2 -2
  62. package/build/src/cli/commands/akuma.js +24 -16
  63. package/build/src/cli/commands/contract-grammar.d.ts +3 -7
  64. package/build/src/cli/commands/contract-grammar.js +12 -10
  65. package/build/src/cli/commands/contract-help.d.ts +3 -13
  66. package/build/src/cli/commands/contract-help.js +7 -16
  67. package/build/src/cli/commands/contract.js +19 -35
  68. package/build/src/cli/commands/debt.js +30 -29
  69. package/build/src/cli/parse.js +0 -1
  70. package/build/src/cli/render/akuma-activity.d.ts +36 -9
  71. package/build/src/cli/render/akuma-activity.js +142 -65
  72. package/build/src/cli/render/akuma.d.ts +6 -0
  73. package/build/src/cli/render/akuma.js +67 -24
  74. package/build/src/cli/render/catalog.js +0 -1
  75. package/build/src/cli/render/contract-observation.d.ts +16 -3
  76. package/build/src/cli/render/contract-observation.js +3 -0
  77. package/build/src/cli/render/contract.d.ts +2 -2
  78. package/build/src/cli/render/contract.js +4 -4
  79. package/build/src/cli/render/delivery-preview.d.ts +7 -0
  80. package/build/src/cli/render/{audit.js → delivery-preview.js} +29 -27
  81. package/build/src/cli/render/kanshi-akuma.d.ts +3 -1
  82. package/build/src/cli/render/kanshi-akuma.js +23 -8
  83. package/build/src/cli/render/kanshi.js +4 -1
  84. package/build/src/cli/render/receipt.js +3 -3
  85. package/build/src/cli/render/refusal.js +2 -2
  86. package/build/src/cli/render/terminal.d.ts +0 -1
  87. package/build/src/cli/render/terminal.js +1 -1
  88. package/build/src/cli/square-identity.d.ts +15 -0
  89. package/build/src/cli/square-identity.js +180 -0
  90. package/build/src/contract-worktree.js +1 -1
  91. package/build/src/core/verbs/placement.d.ts +2 -1
  92. package/build/src/core/verbs/placement.js +1 -1
  93. package/build/src/debt/context.d.ts +0 -5
  94. package/build/src/debt/context.js +32 -44
  95. package/build/src/git/process.d.ts +1 -0
  96. package/build/src/git/process.js +18 -1
  97. package/build/src/git/read-observation.js +2 -1
  98. package/build/src/git/scratch.d.ts +5 -1
  99. package/build/src/git/scratch.js +41 -12
  100. package/build/src/git/target-placement.d.ts +22 -4
  101. package/build/src/git/target-placement.js +44 -20
  102. package/build/src/index.d.ts +2 -2
  103. package/build/src/kanshi/read.js +187 -105
  104. package/build/src/kanshi/report.d.ts +5 -0
  105. package/build/src/library/address.d.ts +18 -4
  106. package/build/src/library/address.js +27 -6
  107. package/build/src/library/contract-address.d.ts +6 -0
  108. package/build/src/library/contract-address.js +12 -1
  109. package/build/src/library/contract-execution.d.ts +2 -2
  110. package/build/src/library/contract-execution.js +13 -17
  111. package/build/src/library/contract-operations.d.ts +59 -94
  112. package/build/src/library/contract-operations.js +23 -19
  113. package/build/src/library/contract-outcomes.d.ts +3 -3
  114. package/build/src/library/contract-settings.d.ts +1 -0
  115. package/build/src/library/contract-settings.js +1 -1
  116. package/build/src/library/invocation.d.ts +2 -2
  117. package/build/src/library/invocation.js +1 -1
  118. package/build/src/library/keiyaku.d.ts +2 -2
  119. package/build/src/library/keiyaku.js +2 -2
  120. package/build/src/library/outcome.d.ts +148 -268
  121. package/build/src/library/outcome.js +4 -8
  122. package/build/src/library/refusal.d.ts +2 -2
  123. package/build/src/library/refusal.js +2 -2
  124. package/build/src/plugin/runtime.js +15 -19
  125. package/build/src/protocol/{audit.d.ts → delivery-preview.d.ts} +62 -97
  126. package/build/src/protocol/delivery-preview.js +167 -0
  127. package/build/src/protocol/progress.d.ts +1 -1
  128. package/build/src/runtime/proc/run.js +22 -4
  129. package/build/src/runtime/proc/windows-launch.exe +0 -0
  130. package/build/src/verification/execution.d.ts +1 -1
  131. package/build/src/verification/execution.js +5 -3
  132. package/package.json +5 -2
  133. package/skills/keiyaku/SKILL.md +3 -1
  134. package/skills/keiyaku-babysit/SKILL.md +1 -1
  135. package/skills/keiyaku-bind/SKILL.md +3 -2
  136. package/skills/keiyaku-workflow/SKILL.md +9 -8
  137. package/build/src/cli/render/audit.d.ts +0 -7
  138. package/build/src/protocol/audit.js +0 -203
@@ -1,7 +1,7 @@
1
1
  import { effectCleanup, effectExecutionStops } from "./effects.js";
2
- import { executionCleanupLines, executionStopLines, receiptPayload, receiptRow, stopLines, titleLines, } from "./receipt.js";
2
+ import { executionCleanupLines, executionStopLines, receiptPayload, receiptRow, titleLines } from "./receipt.js";
3
3
  import { renderRefusalFacts } from "./refusal.js";
4
- import { abbreviateGitIds, displayGitId } from "./contract-observation.js";
4
+ import { abbreviateGitIds, displayGitId, gateFact } from "./contract-observation.js";
5
5
  import { DEFAULT_CLI_COLUMNS, gitShortStat, renderTextBlock, safeText } from "./terminal.js";
6
6
  const CHILD = " ";
7
7
  function workspaceEvidence(workspace) {
@@ -16,11 +16,16 @@ function candidateLines(report, columns, addressed, abbreviations) {
16
16
  return lines;
17
17
  }
18
18
  const identity = candidate.identity;
19
- receiptRow(lines, " ", "candidate", [{ text: "ready" }, { text: displayGitId(identity.tenderSnapshot, abbreviations), opaque: true }], columns);
19
+ receiptRow(lines, " ", "candidate", [{ text: "prepared" }, { text: displayGitId(identity.tenderSnapshot, abbreviations), opaque: true }], columns);
20
20
  lines.push(`${CHILD}integration result ${displayGitId(identity.integration.snapshot, abbreviations)}`);
21
21
  if (!/^0{40}$/u.test(identity.integration.changeId))
22
22
  lines.push(`${CHILD}content identity ${displayGitId(identity.integration.changeId, abbreviations)}`);
23
23
  lines.push(...workspaceEvidence(candidate.workspace));
24
+ for (const report of candidate.requirements.gates)
25
+ lines.push(`${CHILD}${gateFact(report)}`);
26
+ for (const prerequisite of candidate.requirements.unmetPrerequisites) {
27
+ receiptRow(lines, "!", "prerequisite", [{ text: prerequisite.contractId }, { text: prerequisite.state }], columns);
28
+ }
24
29
  lines.push(...renderTextBlock(gitShortStat(candidate.scope), CHILD, columns));
25
30
  if (candidate.scope.paths !== undefined) {
26
31
  for (const path of candidate.scope.paths) {
@@ -31,32 +36,23 @@ function candidateLines(report, columns, addressed, abbreviations) {
31
36
  receiptPayload(lines, "diff", candidate.diff);
32
37
  return lines;
33
38
  }
34
- function verificationLines(verification, columns, addressed) {
39
+ function verificationLines(verification, columns) {
35
40
  const lines = [];
36
- if (verification.kind === "undeclared") {
37
- receiptRow(lines, " ", "verification", [{ text: "none declared" }], columns);
38
- return lines;
39
- }
40
- if (verification.kind === "not-run") {
41
- receiptRow(lines, " ", "verification", [{ text: "not run" }], columns);
42
- return lines;
43
- }
44
- if (verification.kind === "stopped") {
45
- return stopLines(verification.stop, columns, addressed);
46
- }
47
- if (verification.kind === "reused") {
48
- receiptRow(lines, " ", "verification", [{ text: "reused" }, { text: verification.verdict }], columns);
49
- if (verification.summary !== undefined)
50
- receiptPayload(lines, "summary", auditSummary(verification.summary));
41
+ if (verification.kind !== "recorded") {
42
+ const text = verification.kind === "undeclared"
43
+ ? "none declared"
44
+ : verification.kind === "required"
45
+ ? "required · not run"
46
+ : "not run";
47
+ receiptRow(lines, " ", "verification", [{ text }], columns);
51
48
  return lines;
52
49
  }
53
- receiptRow(lines, verification.kind === "satisfied" ? "✓" : "!", "verification", [{ text: verification.kind }, { text: `${verification.passed} of ${verification.total}` }], columns);
54
- if (verification.summary !== undefined) {
55
- receiptPayload(lines, "summary", auditSummary(verification.summary));
56
- }
50
+ receiptRow(lines, verification.verdict === "unsatisfied" ? "!" : " ", "verification", [{ text: "recorded" }, { text: verification.verdict }], columns);
51
+ if (verification.summary !== undefined)
52
+ receiptPayload(lines, "summary", previewSummary(verification.summary));
57
53
  return lines;
58
54
  }
59
- function auditSummary(summary) {
55
+ function previewSummary(summary) {
60
56
  return summary.replace(/\[(\d+) (bash|zsh|pwsh) exit (-?\d+)( output-truncated)?\]/gu, (_, number, executor, exit, truncated) => `declaration ${number} · ${executor} exit ${exit}${truncated ?? ""}`);
61
57
  }
62
58
  function admittedCandidateLines(report, columns) {
@@ -86,6 +82,12 @@ function targetLines(report, columns, addressed, abbreviations) {
86
82
  opaque: true,
87
83
  },
88
84
  ], columns);
85
+ for (const checkout of target.checkouts) {
86
+ if (checkout.kind === "followable")
87
+ continue;
88
+ receiptRow(lines, "!", "checkout cannot follow", [{ text: checkout.path, opaque: true }], columns);
89
+ receiptPayload(lines, "reason", checkout.diagnostic);
90
+ }
89
91
  return lines;
90
92
  }
91
93
  if (target.kind === "moved") {
@@ -114,7 +116,7 @@ function obligationLines(result, columns) {
114
116
  ...executionStopLines(effectExecutionStops(result.effects), columns),
115
117
  ];
116
118
  }
117
- export function renderAcceptedAudit(result, context) {
119
+ export function renderDeliveryPreview(result, context) {
118
120
  const report = result.value;
119
121
  const columns = context?.columns ?? DEFAULT_CLI_COLUMNS;
120
122
  const ids = report.candidate.kind === "ready"
@@ -130,10 +132,10 @@ export function renderAcceptedAudit(result, context) {
130
132
  ids.push(report.target.expected, report.target.observed ?? "");
131
133
  const abbreviations = abbreviateGitIds(ids);
132
134
  return [
133
- ...titleLines("✓", "audit", result.contract, columns),
135
+ ...titleLines("✓", "deliver --dry-run", result.contract, columns),
134
136
  ...candidateLines(report, columns, result.contract, abbreviations),
135
137
  ...admittedCandidateLines(report, columns),
136
- ...verificationLines(report.verification, columns, result.contract),
138
+ ...verificationLines(report.verification, columns),
137
139
  ...targetLines(report, columns, result.contract, abbreviations),
138
140
  ...obligationLines(result, columns),
139
141
  ].join("\n");
@@ -5,7 +5,9 @@ declare function endpointFact(id: string, observed: string | undefined): string;
5
5
  /**
6
6
  * One Akuma board section. The World board omits an empty section because its other
7
7
  * sections already say what the World holds; a category board states that emptiness
8
- * itself, since it is the whole answer.
8
+ * itself, since it is the whole answer. A failed Alias observation belongs to the
9
+ * section, so an empty roster still states its emptiness rather than dropping that
10
+ * evidence.
9
11
  */
10
12
  declare function renderAkumaBoard(section: Section<AkumaKanshiWorld>, observedAt: string, context: TextRenderContext, emptyRows: readonly string[]): readonly string[];
11
13
  declare function renderAkuma(report: KanshiReport, context: TextRenderContext): readonly string[];
@@ -1,6 +1,6 @@
1
1
  import { snapshotActivityLines } from "./akuma-activity.js";
2
2
  import { akumaMark } from "./marks.js";
3
- import { ageText, elapsedMilliseconds, entityLines, identityLine, RECENT_TONE_MS, renderSectionBlock, safeText, tone, } from "./terminal.js";
3
+ import { ageText, elapsedMilliseconds, emptyCatalogue, entityLines, identityLine, RECENT_TONE_MS, renderSectionBlock, safeText, tone, } from "./terminal.js";
4
4
  const NARROW_COLUMNS = 72;
5
5
  const ACTIVITY_INDENT = " ";
6
6
  function mostRecentTimestamp(...values) {
@@ -29,9 +29,11 @@ function latestActivityLines(snapshot, context) {
29
29
  const bounded = { ...context, columns: Math.max(1, context.columns - ACTIVITY_INDENT.length) };
30
30
  return snapshotActivityLines(snapshot, bounded, { latest: true, tailAnswer: true }).map((line) => `${ACTIVITY_INDENT}${line}`);
31
31
  }
32
- function akumaLabel(row) {
32
+ function akumaLabel(row, aliasesUnavailable) {
33
33
  const aliases = row.aliases ?? [];
34
- return aliases.length === 0 ? "" : `(${aliases.join(" ")})`;
34
+ if (aliases.length > 0)
35
+ return `(${aliases.join(" ")})`;
36
+ return aliasesUnavailable ? "(aliases unavailable)" : "";
35
37
  }
36
38
  function endpointFact(id, observed) {
37
39
  // A bounded or failed Contract read leaves the disposition unknown; a parenthetical that
@@ -41,7 +43,9 @@ function endpointFact(id, observed) {
41
43
  /**
42
44
  * One Akuma board section. The World board omits an empty section because its other
43
45
  * sections already say what the World holds; a category board states that emptiness
44
- * itself, since it is the whole answer.
46
+ * itself, since it is the whole answer. A failed Alias observation belongs to the
47
+ * section, so an empty roster still states its emptiness rather than dropping that
48
+ * evidence.
45
49
  */
46
50
  function renderAkumaBoard(section, observedAt, context, emptyRows) {
47
51
  if (section.kind === "absent")
@@ -49,8 +53,15 @@ function renderAkumaBoard(section, observedAt, context, emptyRows) {
49
53
  if (section.kind === "failed")
50
54
  return ["AKUMA // unavailable", "", tone(`! ${safeText(section.failure.message)}`, "alert", context.color)];
51
55
  const rows = section.value.rows;
52
- if (rows.length === 0)
53
- return emptyRows;
56
+ const aliasesUnavailable = section.value.aliasesUnavailable === true;
57
+ if (rows.length === 0) {
58
+ if (!aliasesUnavailable)
59
+ return emptyRows;
60
+ return [
61
+ ...(emptyRows.length === 0 ? [emptyCatalogue("akuma")] : emptyRows),
62
+ tone("! aliases unavailable", "alert", context.color),
63
+ ];
64
+ }
54
65
  const rowLines = rows.map((row) => {
55
66
  const statusTone = akumaStatusTone(row, observedAt);
56
67
  const mark = statusTone === null ? akumaMark(row.life) : tone(akumaMark(row.life), statusTone, context.color);
@@ -62,10 +73,14 @@ function renderAkumaBoard(section, observedAt, context, emptyRows) {
62
73
  const key = row.life === "stranded" && "strandedReason" in row && row.strandedReason === "resume-unsupported"
63
74
  ? [life, ...activity, "resume unsupported"]
64
75
  : [life, ...activity];
65
- const relation = row.contract === undefined ? [] : [endpointFact(row.contract.id, row.contract.observed)];
76
+ const relation = row.contract === undefined
77
+ ? row.contractAssociationUnavailable === true
78
+ ? ["contract association unavailable"]
79
+ : []
80
+ : [endpointFact(row.contract.id, row.contract.observed)];
66
81
  const snapshot = row.snapshot;
67
82
  const snapshotLines = snapshot === undefined ? [] : latestActivityLines(snapshot, context);
68
- const aliases = akumaLabel(row);
83
+ const aliases = akumaLabel(row, aliasesUnavailable);
69
84
  if (context.columns > NARROW_COLUMNS) {
70
85
  const lines = [identityLine(mark, row.id, `${aliases} · ${[...key, ...relation].join(" · ")}`.trim())];
71
86
  return [...lines, ...snapshotLines];
@@ -64,6 +64,8 @@ function isTerminalAkuma(life) {
64
64
  return life === "killed" || life === "stillborn";
65
65
  }
66
66
  function linkedAkumaSummary(row, akuma) {
67
+ if (row.rosterUnavailable === true)
68
+ return "akuma unavailable";
67
69
  if (row.roster.length === 0)
68
70
  return undefined;
69
71
  if (akuma === undefined || akuma.kind !== "present")
@@ -98,7 +100,6 @@ function payload(label, text, context) {
98
100
  columns: context.columns,
99
101
  maxLines: 3,
100
102
  quote: "“",
101
- openQuote: false,
102
103
  truncated: false,
103
104
  });
104
105
  }
@@ -248,6 +249,8 @@ function renderDebts(report, context) {
248
249
  return [];
249
250
  const rowLines = rows.map((row) => {
250
251
  const relation = row.contract === undefined ? [] : [endpointFact(row.contract.id, row.contract.observed)];
252
+ if (row.contractAssociationUnavailable === true)
253
+ relation.push("contract association unavailable");
251
254
  const childFacts = row.children === undefined ? [] : [`children ${row.children.live} live · ${row.children.total} total`];
252
255
  const blockerFacts = (row.blockers ?? []).map((blocker) => `blocked ${blocker.id}`);
253
256
  const association = relation.length === 0 ? "" : ` · ${relation.join(" · ")}`;
@@ -1,4 +1,4 @@
1
- import { gateDisplayName } from "./contract-observation.js";
1
+ import { gateDisplayName, contractOperationName } from "./contract-observation.js";
2
2
  import { renderRefusalFacts } from "./refusal.js";
3
3
  import { DEFAULT_CLI_COLUMNS, displayColumns, orderRefusalFacts, renderOpaqueBlock, safeText, quotedText, } from "./terminal.js";
4
4
  export function receiptRow(lines, mark, label, segments, columns) {
@@ -25,7 +25,7 @@ export function receiptPayload(lines, label, payload) {
25
25
  lines.push(` ${label.trim()}`, ...renderOpaqueBlock(payload, " ", DEFAULT_CLI_COLUMNS), "");
26
26
  }
27
27
  export function outcomeLines(mark, verb, word, contract, columns = DEFAULT_CLI_COLUMNS) {
28
- const base = `${mark} ${verb} ${word}`;
28
+ const base = `${mark} ${contractOperationName(verb)} ${word}`;
29
29
  if (contract === undefined)
30
30
  return [base];
31
31
  const inline = `${base} ${contract}`;
@@ -296,7 +296,7 @@ export function executionFailureLines(receipt, category, diagnostic, columns) {
296
296
  cleanup.push(effect.issue);
297
297
  }
298
298
  const lines = [];
299
- receiptRow(lines, "!", "execution failed after admission", [{ text: category }, ...(contract === undefined ? [] : [{ text: contract, opaque: true }])], columns);
299
+ receiptRow(lines, "!", receipt.operation === "deliver-preview" ? "deliver --dry-run failed" : "execution failed after admission", [{ text: category }, ...(contract === undefined ? [] : [{ text: contract, opaque: true }])], columns);
300
300
  receiptPayload(lines, "reason", diagnostic);
301
301
  lines.push(...executionStopLines(stops, columns), ...executionCleanupLines(cleanup, columns, contract));
302
302
  return lines;
@@ -1,5 +1,5 @@
1
1
  import { usageAcceptanceLines } from "../usage.js";
2
- import { shortGitId } from "./contract-observation.js";
2
+ import { shortGitId, contractOperationName } from "./contract-observation.js";
3
3
  import { DEFAULT_CLI_COLUMNS, checkoutNotFollowableLines, gitShortStat, orderRefusalFacts, renderOpaqueBlock, safeText, } from "./terminal.js";
4
4
  function wrap(lines, text, indent, columns) {
5
5
  lines.push(...renderOpaqueBlock(text, indent, columns));
@@ -112,7 +112,7 @@ export function renderRefusalFacts(refusal, indent, columns, addressed) {
112
112
  return orderRefusalFacts(refusalFacts(refusal, indent, columns, addressed).map((line) => line.slice(indent.length))).map((line) => `${indent}${line}`);
113
113
  }
114
114
  function refusalTitle(verb) {
115
- return `× ${verb} refused`;
115
+ return `× ${contractOperationName(verb)} refused`;
116
116
  }
117
117
  /**
118
118
  * The one refusal receipt assembly: a denied-verdict title, a labeled reason, labeled facts, and the
@@ -38,7 +38,6 @@ export declare function renderBoundedPayload(input: Readonly<{
38
38
  columns: number;
39
39
  maxLines: number;
40
40
  quote: string;
41
- openQuote: boolean;
42
41
  truncated: boolean;
43
42
  }>): readonly string[];
44
43
  export declare function quotedText(value: string): string;
@@ -186,7 +186,7 @@ export function renderBoundedPayload(input) {
186
186
  if (rest.length === 0 && !input.quote && index === 0)
187
187
  return [prefix.trimEnd()];
188
188
  const opening = input.quote && index === 0 ? input.quote : "";
189
- const closing = input.quote && !input.openQuote ? (input.quote === "“" ? "”" : input.quote) : "";
189
+ const closing = input.quote === "“" ? "”" : input.quote;
190
190
  const budget = Math.max(0, input.columns - displayColumns(prefix + opening + closing));
191
191
  if (budget === 0)
192
192
  return lines.length === 0 ? [prefix.trimEnd()] : lines;
@@ -0,0 +1,15 @@
1
+ import type { WorldRoot } from "../world.js";
2
+ export type SquareSeatInput = Readonly<{
3
+ path: WorldRoot;
4
+ environment: NodeJS.ProcessEnv;
5
+ }>;
6
+ export type SquareSeatControl = Readonly<{
7
+ signal?: AbortSignal;
8
+ budgetMs?: number;
9
+ }>;
10
+ /**
11
+ * The Square participant name this submitting session should speak as, or
12
+ * undefined when no honest choice exists. Identity preparation is optional, so it
13
+ * never refuses the enclosing operation and always settles within one budget.
14
+ */
15
+ export declare function squareSeat(input: SquareSeatInput, control?: SquareSeatControl): Promise<string | undefined>;
@@ -0,0 +1,180 @@
1
+ import { createHash } from "node:crypto";
2
+ import { realpath } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { inbox, Square, SquareError, squareAssignedParticipantName } from "@astrosheep/square";
5
+ /**
6
+ * The Square integration's submitting-edge identity policy. Call, fork, tell, and
7
+ * ask share this one adapter, so no command and no generic caller decoder has to
8
+ * know how Square names a participant. Every Square read below is rooted in the
9
+ * submitted environment's own coordinates, never in this process's ambient ones.
10
+ */
11
+ /** Square's own harness-session coordinates, in its resolution order. */
12
+ const HARNESS_SESSIONS = [
13
+ { provider: "claude", variable: "CLAUDE_CODE_SESSION_ID" },
14
+ { provider: "codex", variable: "CODEX_THREAD_ID" },
15
+ { provider: "opencode", variable: "OPENCODE_SESSION_ID" },
16
+ { provider: "pi", variable: "PI_SESSION_ID" },
17
+ { provider: undefined, variable: "PASEO_AGENT_ID" },
18
+ ];
19
+ const SEAT_PREFIX = "keiyaku-assigned-";
20
+ const SEAT_DIGEST_DOMAIN = "keiyaku/square-seat/v1";
21
+ const SEAT_DIGEST_LENGTH = 12;
22
+ /** One bounded identity observation; a non-cooperative Square never holds a submission. */
23
+ const IDENTITY_BUDGET_MS = 5_000;
24
+ function declared(environment, variable) {
25
+ const value = environment[variable]?.trim();
26
+ return value === undefined || value === "" ? undefined : value;
27
+ }
28
+ /** A stable, collision-resistant seat for one provider session coordinate. */
29
+ function squareSeatName(provider, session) {
30
+ const digest = createHash("sha256")
31
+ .update(`${SEAT_DIGEST_DOMAIN}\u0000${provider}\u0000${session}`, "utf8")
32
+ .digest("hex");
33
+ return `${SEAT_PREFIX}${digest.slice(0, SEAT_DIGEST_LENGTH)}`;
34
+ }
35
+ /** Every harness session coordinate this process declares, able to name a seat or not. */
36
+ function sessionIds(environment) {
37
+ return HARNESS_SESSIONS.flatMap(({ variable }) => {
38
+ const session = declared(environment, variable);
39
+ return session === undefined ? [] : [session];
40
+ });
41
+ }
42
+ /**
43
+ * The deterministic candidate for this process. Only one provider-backed session
44
+ * may exist: a second coordinate — another provider, or the same raw token under
45
+ * a different one — is ambiguous and selects nothing.
46
+ */
47
+ function seatCandidate(environment) {
48
+ const names = HARNESS_SESSIONS.flatMap(({ provider, variable }) => {
49
+ const session = declared(environment, variable);
50
+ return provider === undefined || session === undefined ? [] : [squareSeatName(provider, session)];
51
+ });
52
+ return names.length === 1 ? names[0] : undefined;
53
+ }
54
+ /**
55
+ * Square owns the name grammar, so the edge borrows one of Square's own handles
56
+ * instead of restating it. An in-memory square validates without touching any
57
+ * artifact.
58
+ */
59
+ async function usableName(name) {
60
+ try {
61
+ await Square.inMemory({ markdown: "" }).participant(name);
62
+ return true;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ /** Square's own canonical coordinate for a location it already published. */
69
+ async function canonical(path) {
70
+ try {
71
+ return await realpath(path);
72
+ }
73
+ catch {
74
+ return resolve(path);
75
+ }
76
+ }
77
+ /**
78
+ * Bindings this session already holds in this Square. Square's session inbox
79
+ * reads the same host registry as native hooks, and it is the only public read
80
+ * that takes the caller's own environment coordinate. Its page is bounded, so a
81
+ * view that hides rows proves nothing about absence here.
82
+ */
83
+ async function joinedBindings(environment, ids, location, signal) {
84
+ const pages = await Promise.all(ids.map(async (session) => await inbox({ env: environment }, session, {}, { signal })));
85
+ const here = await canonical(location);
86
+ return {
87
+ here: pages.flatMap(({ rows }) => rows).filter(({ squarePath }) => squarePath === here).length,
88
+ complete: pages.every(({ rows, total }) => total === rows.length),
89
+ };
90
+ }
91
+ /**
92
+ * A session whose former participant already finished here has no replacement
93
+ * identity: releasing the binding leaves Square's roster as the only evidence of
94
+ * that standing, and a fresh seat would rename a finished caller into a new one.
95
+ */
96
+ async function finishedHere(square, environment, signal) {
97
+ const former = squareAssignedParticipantName(environment);
98
+ const previous = seatCandidate(environment)?.replace(SEAT_PREFIX, "keiyaku-seat-");
99
+ if (former === undefined && previous === undefined)
100
+ return false;
101
+ const { participants } = await square.participants({}, { signal });
102
+ return participants.some(({ name, state }) => (name === former || name === previous) && state === "done");
103
+ }
104
+ /**
105
+ * A binding this edge cannot stand on is never replaced by a fresh name: Square
106
+ * adjudicates standing, ownership, and lifecycle. The edge only distinguishes a
107
+ * Square that is genuinely not there yet from evidence it cannot trust.
108
+ */
109
+ async function standingAt(location, environment, ids, signal) {
110
+ let square;
111
+ try {
112
+ square = await Square.at({ path: location, env: environment }, { signal });
113
+ }
114
+ catch (error) {
115
+ // Only a Square that is genuinely not there yet may receive a new seat.
116
+ return error instanceof SquareError && error.code === "unavailable"
117
+ ? { kind: "absent" }
118
+ : { kind: "indeterminate" };
119
+ }
120
+ try {
121
+ const recognized = await square.recognize(environment);
122
+ if (recognized !== null)
123
+ return { kind: "standing", name: recognized.name };
124
+ const bindings = await joinedBindings(environment, ids, square.location, signal);
125
+ if (bindings.here > 0 || !bindings.complete)
126
+ return { kind: "indeterminate" };
127
+ return (await finishedHere(square, environment, signal)) ? { kind: "indeterminate" } : { kind: "absent" };
128
+ }
129
+ catch {
130
+ return { kind: "indeterminate" };
131
+ }
132
+ finally {
133
+ await square.close().catch(() => undefined);
134
+ }
135
+ }
136
+ /**
137
+ * The one bounded attempt. A late settlement keeps no authority: the adapter only
138
+ * reads Square, so nothing arrives after the caller has settled, and no refusal
139
+ * escapes as an unhandled rejection.
140
+ */
141
+ async function bounded(attempt, control) {
142
+ const budget = new AbortController();
143
+ const timer = setTimeout(() => budget.abort(new Error("Square identity observation expired")), control.budgetMs ?? IDENTITY_BUDGET_MS);
144
+ const signal = control.signal === undefined ? budget.signal : AbortSignal.any([control.signal, budget.signal]);
145
+ const pending = attempt(signal);
146
+ pending.catch(() => undefined);
147
+ try {
148
+ return await Promise.race([pending, whenAborted(signal)]);
149
+ }
150
+ finally {
151
+ clearTimeout(timer);
152
+ }
153
+ }
154
+ function whenAborted(signal) {
155
+ return new Promise((settle) => {
156
+ if (signal.aborted)
157
+ settle(undefined);
158
+ else
159
+ signal.addEventListener("abort", () => settle(undefined), { once: true });
160
+ });
161
+ }
162
+ /**
163
+ * The Square participant name this submitting session should speak as, or
164
+ * undefined when no honest choice exists. Identity preparation is optional, so it
165
+ * never refuses the enclosing operation and always settles within one budget.
166
+ */
167
+ export async function squareSeat(input, control = {}) {
168
+ const name = declared(input.environment, "SQUARE_PARTICIPANT_NAME");
169
+ if (name !== undefined)
170
+ return (await usableName(name)) ? name : undefined;
171
+ const ids = sessionIds(input.environment);
172
+ // No session coordinate: nothing to reuse, nothing to derive, nothing to read.
173
+ if (ids.length === 0)
174
+ return undefined;
175
+ const location = join(input.path, ".square", "KEIYAKU.square");
176
+ const standing = await bounded(async (signal) => await standingAt(location, input.environment, ids, signal), control);
177
+ if (standing?.kind === "standing")
178
+ return standing.name;
179
+ return standing?.kind === "absent" ? seatCandidate(input.environment) : undefined;
180
+ }
@@ -9,7 +9,7 @@ import { appointmentFor, placeRegisterPath } from "./workspace-place.js";
9
9
  import { repairNamespaceContext } from "./debt/context.js";
10
10
  import { contractNamespace } from "./debt/identity.js";
11
11
  const IGNORE_BYTES = ".gitignore\nKEIYAKU.md\n";
12
- const PRIMARY_IGNORE_BYTES = "*\n!settings.json\n!debts/\n!debts/**\n";
12
+ const PRIMARY_IGNORE_BYTES = "*\n!settings.json\n";
13
13
  const SEAT_IGNORE_BYTES = ".gitignore\nSKILL.md\n";
14
14
  const SEAT_SKILLS = [
15
15
  ["keiyaku-onboarding", CONTRACT_ONBOARDING_SKILL],
@@ -1,5 +1,5 @@
1
1
  import { type GateReport } from "../facts/gate.js";
2
- import { type ActorId, type ContractId } from "../facts/types.js";
2
+ import { type ActorId, type ContractId, type ContractState } from "../facts/types.js";
3
3
  import type { DecideInput, OfferDecision } from "../decide.js";
4
4
  type PlacementInput = Readonly<{
5
5
  contractId: ContractId;
@@ -24,5 +24,6 @@ export type PlacementRefusal = Readonly<{
24
24
  contractId: ContractId;
25
25
  unmet: readonly UnmetPrerequisite[];
26
26
  }>;
27
+ export declare function unmetPrerequisites(prerequisites: readonly ContractId[], observation: ReadonlyMap<ContractId, ContractState | null>): readonly UnmetPrerequisite[];
27
28
  export declare function decidePlacement({ input, attempt, observation, }: DecideInput<PlacementInput>): OfferDecision<PlacementRefusal>;
28
29
  export {};
@@ -1,7 +1,7 @@
1
1
  import { gateReports } from "../facts/gate.js";
2
2
  import { activeContract, contractState } from "../facts/observation.js";
3
3
  import { journalEntry } from "../facts/types.js";
4
- function unmetPrerequisites(prerequisites, observation) {
4
+ export function unmetPrerequisites(prerequisites, observation) {
5
5
  const unmet = [];
6
6
  for (const contractId of prerequisites) {
7
7
  const state = contractState(observation, contractId);
@@ -1,7 +1,3 @@
1
- export type NamespaceContextRead = readonly string[] | "absent" | Readonly<{
2
- kind: "malformed";
3
- path: string;
4
- }>;
5
1
  export type NamespaceContextInput = Readonly<{
6
2
  directory: string;
7
3
  boundary: string;
@@ -12,7 +8,6 @@ export type ResolvedNamespaceContext = Readonly<{
12
8
  namespace: readonly string[];
13
9
  source: NamespaceContextSource;
14
10
  }>;
15
- export declare function readNamespaceContext(coordinates: NamespaceContextInput): Promise<NamespaceContextRead>;
16
11
  export declare function installNamespaceContext(root: string, segments: readonly string[]): Promise<void>;
17
12
  export declare function repairNamespaceContext(root: string, segments: readonly string[]): Promise<"kept" | "installed">;
18
13
  export declare function resolveDebtNamespaceContext(input: NamespaceContextInput): Promise<ResolvedNamespaceContext | Readonly<{
@@ -1,5 +1,5 @@
1
1
  import { lstat, mkdir, readFile } from "node:fs/promises";
2
- import { dirname, join, resolve } from "node:path";
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
3
  import { repairDerivedFile, replaceFileDurably } from "../coordination/durable-file.js";
4
4
  import { normalizeIdentityStem } from "../identity/normalize.js";
5
5
  function directory(root) {
@@ -21,9 +21,10 @@ function validNamespaceSegments(value) {
21
21
  segment !== ".."));
22
22
  }
23
23
  async function readAt(root) {
24
+ const path = currentPath(root);
24
25
  let bytes;
25
26
  try {
26
- const path = currentPath(root), stat = await lstat(path);
27
+ const stat = await lstat(path);
27
28
  if (!stat.isFile() || stat.isSymbolicLink())
28
29
  return { kind: "malformed", path };
29
30
  bytes = await readFile(path, "utf8");
@@ -33,33 +34,31 @@ async function readAt(root) {
33
34
  return "absent";
34
35
  throw error;
35
36
  }
36
- if (!bytes.endsWith("\n") || bytes.slice(0, -1).includes("\n")) {
37
- return { kind: "malformed", path: currentPath(root) };
38
- }
37
+ if (!bytes.endsWith("\n") || bytes.slice(0, -1).includes("\n"))
38
+ return { kind: "malformed", path };
39
39
  const content = bytes.slice(0, -1);
40
40
  const segments = content === "" ? [] : content.split("/");
41
- return validNamespaceSegments(segments) ? segments : { kind: "malformed", path: currentPath(root) };
42
- }
43
- async function readAtWithPath(root) {
44
- const selected = await readAt(root);
45
- if (selected === "absent" || (typeof selected === "object" && "kind" in selected))
46
- return selected;
47
- return { value: selected, path: currentPath(root) };
41
+ return validNamespaceSegments(segments) ? segments : { kind: "malformed", path };
48
42
  }
49
- export async function readNamespaceContext(coordinates) {
50
- let current = coordinates.directory;
43
+ /** One ancestor walk: the nearest context at or below the boundary, and the directory that answers it. */
44
+ async function walkNamespaceContext(coordinates) {
45
+ const boundary = resolve(coordinates.boundary);
46
+ let current = resolve(coordinates.directory);
51
47
  for (;;) {
52
- const selected = await readAt(current);
53
- if (selected !== "absent")
54
- return selected;
55
- if (current === coordinates.boundary)
56
- return "absent";
57
- const resolvedParent = dirname(resolve(current));
58
- if (resolvedParent === current || !resolvedParent.startsWith(`${coordinates.boundary}/`))
59
- return "absent";
60
- current = resolvedParent;
48
+ const read = await readAt(current);
49
+ if (read !== "absent" || current === boundary)
50
+ return { read, root: current };
51
+ const parent = dirname(current);
52
+ if (parent === current || !descendsFrom(parent, boundary))
53
+ return { read, root: current };
54
+ current = parent;
61
55
  }
62
56
  }
57
+ /** Whether `candidate` is the boundary or lies beneath it; a sibling or ancestor never does. */
58
+ function descendsFrom(candidate, boundary) {
59
+ const step = relative(boundary, candidate);
60
+ return step === "" || (!isAbsolute(step) && step !== ".." && !step.startsWith(`..${sep}`));
61
+ }
63
62
  export async function installNamespaceContext(root, segments) {
64
63
  if (!validNamespaceSegments(segments))
65
64
  throw new TypeError("namespace must contain normalized segments");
@@ -72,8 +71,7 @@ async function installIgnore(root) {
72
71
  await repairDerivedFile(ignorePath(root), "*\n");
73
72
  }
74
73
  export async function repairNamespaceContext(root, segments) {
75
- const current = await readNamespaceContext({ directory: root, boundary: root });
76
- if (Array.isArray(current)) {
74
+ if (Array.isArray(await readAt(root))) {
77
75
  await installIgnore(root);
78
76
  return "kept";
79
77
  }
@@ -81,23 +79,13 @@ export async function repairNamespaceContext(root, segments) {
81
79
  return "installed";
82
80
  }
83
81
  export async function resolveDebtNamespaceContext(input) {
84
- let current = input.directory;
85
- for (;;) {
86
- const selected = await readAtWithPath(current);
87
- if (selected !== "absent") {
88
- if (typeof selected === "object" && "kind" in selected)
89
- return { kind: "invalid-namespace-context", path: selected.path };
90
- return {
91
- namespace: "value" in selected ? selected.value : selected,
92
- source: current === input.boundary && input.managed === true ? "contract-installed" : "local-override",
93
- };
94
- }
95
- if (current === input.boundary)
96
- return { namespace: [], source: "default-root" };
97
- const resolvedParent = dirname(resolve(current));
98
- if (resolvedParent === current || !resolvedParent.startsWith(`${input.boundary}/`)) {
99
- return { namespace: [], source: "default-root" };
100
- }
101
- current = resolvedParent;
102
- }
82
+ const { read, root } = await walkNamespaceContext(input);
83
+ if (read === "absent")
84
+ return { namespace: [], source: "default-root" };
85
+ if (typeof read === "object" && "kind" in read)
86
+ return { kind: "invalid-namespace-context", path: read.path };
87
+ return {
88
+ namespace: read,
89
+ source: root === resolve(input.boundary) && input.managed === true ? "contract-installed" : "local-override",
90
+ };
103
91
  }
@@ -29,6 +29,7 @@ export declare class GitPlumbingError extends Error {
29
29
  pid?: number | null;
30
30
  }>);
31
31
  }
32
+ export declare function gitChildEnvironment(environment?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
32
33
  export declare function executeGit(repository: GitRepository, args: readonly string[], input?: string | Uint8Array, environment?: NodeJS.ProcessEnv): Promise<Buffer>;
33
34
  /** Connect two owned Git commands without materializing the intermediate output. */
34
35
  export declare function runGitPipe(repository: GitRepository, sourceArgs: readonly string[], sinkArgs: readonly string[]): Promise<Buffer>;