@astrosheep/keiyaku 2.8.0 → 2.8.2

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 (36) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/call-terms_v2.js +6 -2
  3. package/build/agents/harness/index.js +5 -1
  4. package/build/agents/harness/pump.js +22 -5
  5. package/build/agents/launch-snapshot_v2.js +14 -9
  6. package/build/agents/selector_v2.js +15 -3
  7. package/build/cli/commands/akuma.js +21 -3
  8. package/build/cli/completion.js +4 -1
  9. package/build/cli/flags.js +10 -1
  10. package/build/cli/help.js +5 -3
  11. package/build/cli/index.js +67 -14
  12. package/build/cli/parse.js +65 -24
  13. package/build/cli/projection-address.js +97 -10
  14. package/build/cli/render/address.js +12 -3
  15. package/build/cli/render/call.js +10 -5
  16. package/build/cli/render/shared.js +2 -2
  17. package/build/cli/render/status.js +19 -4
  18. package/build/cli/render/wait.js +3 -3
  19. package/build/config/provider-policy-ownership.js +39 -0
  20. package/build/config/settings.js +1 -11
  21. package/build/core/addressing.js +82 -38
  22. package/build/core/call.js +33 -5
  23. package/build/core/projection/mint.js +23 -3
  24. package/build/core/projection-alias.js +123 -0
  25. package/build/core/projection-coordinate.js +23 -7
  26. package/build/core/projection-core.js +8 -47
  27. package/build/core/projection-kill.js +26 -0
  28. package/build/core/projection-mint.js +24 -3
  29. package/build/core/projection-runner-lock.js +1 -1
  30. package/build/core/projection-status.js +111 -12
  31. package/build/core/projection-wake.js +3 -0
  32. package/build/core/response-artifact-id.js +5 -0
  33. package/build/core/status.js +1 -1
  34. package/build/core/transcripts.js +42 -15
  35. package/build/generated/version.js +1 -1
  36. package/package.json +4 -4
@@ -19,11 +19,21 @@ function assertMintSlug(slug) {
19
19
  if (!normalized) {
20
20
  throw new ProjectionStateError("projection slug cannot be empty");
21
21
  }
22
- if (normalized.includes("/") || normalized.includes("\0")) {
22
+ if (!/^[a-zA-Z0-9]+(?:[.-][a-zA-Z0-9]+)*$/.test(normalized)) {
23
23
  throw new ProjectionStateError(`invalid projection slug: ${slug}`);
24
24
  }
25
25
  return normalized;
26
26
  }
27
+ function removeEmptyAkumaRoot(dir) {
28
+ try {
29
+ fs.rmdirSync(dir);
30
+ }
31
+ catch (error) {
32
+ if (isErrnoException(error) && ["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
33
+ return;
34
+ throw error;
35
+ }
36
+ }
27
37
  /**
28
38
  * Validate mint inputs that can fail, then reserve one private staging directory.
29
39
  * Dot-prefixed staging is never a published projection address.
@@ -42,14 +52,19 @@ function reserveProjectionDirectory(input) {
42
52
  }
43
53
  const root = projectionRoot(input.cwd);
44
54
  fs.mkdirSync(root, { recursive: true });
55
+ const akumaRoot = path.join(root, slug);
56
+ const createdAkumaRoot = !fs.existsSync(akumaRoot);
57
+ fs.mkdirSync(akumaRoot, { recursive: true });
45
58
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
46
59
  const hex = (input.randomHex?.(attempt) ?? randomHex8()).toLowerCase();
47
60
  if (!/^[0-9a-f]{8}$/.test(hex)) {
61
+ if (createdAkumaRoot)
62
+ removeEmptyAkumaRoot(akumaRoot);
48
63
  throw new ProjectionStateError(`projection id random segment must be 8 hex chars, got ${hex}`);
49
64
  }
50
- const id = `${slug}-${hex}`;
65
+ const id = `${slug}/${hex}`;
51
66
  const dir = projectionDir(input.cwd, id);
52
- const stagingDir = path.join(root, `.mint-${id}`);
67
+ const stagingDir = path.join(akumaRoot, `.mint-${hex}`);
53
68
  if (fs.existsSync(dir))
54
69
  continue;
55
70
  try {
@@ -59,6 +74,8 @@ function reserveProjectionDirectory(input) {
59
74
  if (isErrnoException(error) && error.code === "EEXIST") {
60
75
  continue;
61
76
  }
77
+ if (createdAkumaRoot)
78
+ removeEmptyAkumaRoot(akumaRoot);
62
79
  throw error;
63
80
  }
64
81
  if (fs.existsSync(dir)) {
@@ -67,6 +84,8 @@ function reserveProjectionDirectory(input) {
67
84
  }
68
85
  return { id, dir, stagingDir };
69
86
  }
87
+ if (createdAkumaRoot)
88
+ removeEmptyAkumaRoot(akumaRoot);
70
89
  throw new ProjectionMintCollisionError(`exhausted ${maxAttempts} projection id attempts under ${PROJECTION_ROOT_SEGMENTS.join("/")} (PROJECTION_MINT_COLLISION)`);
71
90
  }
72
91
  /** Build the pact fields shared by both authority variants. */
@@ -104,6 +123,8 @@ function publishReservedProjection(pact, reserved) {
104
123
  finally {
105
124
  if (!published) {
106
125
  fs.rmSync(reserved.stagingDir, { recursive: true, force: true });
126
+ const parent = path.dirname(reserved.stagingDir);
127
+ removeEmptyAkumaRoot(parent);
107
128
  }
108
129
  }
109
130
  }
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  export const PROJECTION_RUNNER_LOCK_DATABASE = "leash";
5
5
  const SQLITE_BUSY = 5;
6
- const SUPPORTED_PLATFORMS = new Set(["darwin", "linux"]);
6
+ const SUPPORTED_PLATFORMS = new Set(["darwin", "linux", "win32"]);
7
7
  export class ProjectionRunnerLockUnavailableError extends Error {
8
8
  code = "PROJECTION_RUNNER_LOCK_UNAVAILABLE";
9
9
  constructor(message, options) {
@@ -1,7 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { isErrnoException } from "../errno.js";
4
- import { executionEventsPath, projectionRoot, } from "./projection-core.js";
4
+ import { executionEventsPath, isProjectionId, projectionRoot, } from "./projection-core.js";
5
+ import { readProjectionAlias } from "./projection-alias.js";
5
6
  import { ProjectionStateError } from "./atomic-publish.js";
6
7
  import { foldAgentActivity, } from "./projection-activity.js";
7
8
  import { readProjectionIdentity } from "./projection-identity.js";
@@ -12,6 +13,73 @@ import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
12
13
  import { selectCurrentGeneration } from "./projection-life-protocol.js";
13
14
  import { invalidActivityEventDiagnostic, selectValidStoredAgentEvents } from "./stored-agent-event.js";
14
15
  const MAX_STATUS_FILE_BYTES = 256 * 1024;
16
+ export const DEFAULT_PROJECTION_STATUS_ROW_BUDGET = 20;
17
+ const STATUS_GROUP = {
18
+ "startup-timeout": 0,
19
+ lost: 0,
20
+ unknown: 0,
21
+ minting: 1,
22
+ out: 1,
23
+ failed: 2,
24
+ dead: 2,
25
+ done: 3,
26
+ dismissed: 4,
27
+ };
28
+ function isOmissibleProjectionStatusState(state) {
29
+ return state === "failed" || state === "dead" || state === "done" || state === "dismissed";
30
+ }
31
+ function rowSortTime(row) {
32
+ if (row.activityAtMs !== undefined && Number.isFinite(row.activityAtMs)) {
33
+ return row.activityAtMs;
34
+ }
35
+ if (row.mintedAt !== undefined) {
36
+ const mintedAtMs = Date.parse(row.mintedAt);
37
+ if (Number.isFinite(mintedAtMs))
38
+ return mintedAtMs;
39
+ }
40
+ return Number.NEGATIVE_INFINITY;
41
+ }
42
+ function compareProjectionStatusRows(a, b) {
43
+ const groupDifference = STATUS_GROUP[a.state] - STATUS_GROUP[b.state];
44
+ if (groupDifference !== 0)
45
+ return groupDifference;
46
+ const aTime = rowSortTime(a);
47
+ const bTime = rowSortTime(b);
48
+ if (aTime !== bTime) {
49
+ if (aTime === Number.NEGATIVE_INFINITY)
50
+ return 1;
51
+ if (bTime === Number.NEGATIVE_INFINITY)
52
+ return -1;
53
+ return bTime - aTime;
54
+ }
55
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
56
+ }
57
+ export function prioritizeProjectionStatusRows(rows, showAll) {
58
+ const ordered = [...rows].sort(compareProjectionStatusRows);
59
+ if (showAll)
60
+ return { rows: ordered };
61
+ const protectedCount = ordered.filter((row) => STATUS_GROUP[row.state] < 2).length;
62
+ let terminalBudget = Math.max(0, DEFAULT_PROJECTION_STATUS_ROW_BUDGET - protectedCount);
63
+ const visible = [];
64
+ const byState = {
65
+ failed: 0,
66
+ dead: 0,
67
+ done: 0,
68
+ dismissed: 0,
69
+ };
70
+ for (const row of ordered) {
71
+ if (STATUS_GROUP[row.state] < 2 || terminalBudget > 0) {
72
+ visible.push(row);
73
+ if (STATUS_GROUP[row.state] >= 2)
74
+ terminalBudget -= 1;
75
+ continue;
76
+ }
77
+ if (isOmissibleProjectionStatusState(row.state))
78
+ byState[row.state] += 1;
79
+ }
80
+ const total = Object.values(byState).reduce((sum, count) => sum + count, 0);
81
+ return total > 0 ? { rows: visible, omitted: { total, byState } } : { rows: visible };
82
+ }
15
83
  class ProjectionStatusFileError extends Error {
16
84
  diagnostic;
17
85
  constructor(kind, filePath) {
@@ -198,7 +266,7 @@ function readActivityFacts(projectionDir, executionId, nowMs, diagnostics, fallb
198
266
  : {}),
199
267
  };
200
268
  }
201
- function readProjectionRow(projectionDir, id, nowMs) {
269
+ function readProjectionRow(projectionDir, id, alias, nowMs) {
202
270
  const diagnostics = [];
203
271
  const identityFacts = readIdentityFacts(projectionDir, diagnostics);
204
272
  const tellCounts = identityFacts.identity
@@ -232,6 +300,7 @@ function readProjectionRow(projectionDir, id, nowMs) {
232
300
  const state = phase === "active" || phase === "quiescent" ? "out" : phase;
233
301
  return {
234
302
  id,
303
+ ...(alias ? { alias } : {}),
235
304
  state,
236
305
  akuma: identityFacts.akuma,
237
306
  provider: identityFacts.provider,
@@ -243,7 +312,7 @@ function readProjectionRow(projectionDir, id, nowMs) {
243
312
  diagnostics,
244
313
  };
245
314
  }
246
- export function readProjectionStatusBoard(cwd, nowMs) {
315
+ export function readProjectionStatusBoard(cwd, nowMs, options = {}) {
247
316
  const root = projectionRoot(cwd);
248
317
  try {
249
318
  if (!fs.lstatSync(root).isDirectory())
@@ -263,17 +332,47 @@ export function readProjectionStatusBoard(cwd, nowMs) {
263
332
  return null;
264
333
  throw error;
265
334
  }
266
- const rows = entries
267
- .filter((entry) => !entry.startsWith("."))
268
- .filter((entry) => {
335
+ const aliases = new Map();
336
+ const aliasRoot = path.join(path.dirname(path.dirname(root)), ".keiyaku", "projection-alias");
337
+ try {
338
+ for (const alias of fs.readdirSync(aliasRoot).sort()) {
339
+ if (alias.startsWith("."))
340
+ continue;
341
+ const id = readProjectionAlias(cwd, alias);
342
+ if (id)
343
+ aliases.set(id, alias);
344
+ }
345
+ }
346
+ catch (error) {
347
+ if (!(isErrnoException(error) && error.code === "ENOENT"))
348
+ throw error;
349
+ }
350
+ const rows = [];
351
+ for (const akuma of entries.sort()) {
352
+ if (akuma.startsWith("."))
353
+ continue;
354
+ const akumaDir = path.join(root, akuma);
269
355
  try {
270
- return fs.lstatSync(path.join(root, entry)).isDirectory();
356
+ if (!fs.lstatSync(akumaDir).isDirectory())
357
+ continue;
271
358
  }
272
359
  catch {
273
- return false;
360
+ continue;
274
361
  }
275
- })
276
- .sort()
277
- .map((id) => readProjectionRow(path.join(root, id), id, nowMs));
278
- return rows.length > 0 ? { rows } : null;
362
+ for (const hex of fs.readdirSync(akumaDir).sort()) {
363
+ const id = `${akuma}/${hex}`;
364
+ const dir = path.join(akumaDir, hex);
365
+ if (!isProjectionId(id))
366
+ continue;
367
+ try {
368
+ if (!fs.lstatSync(dir).isDirectory())
369
+ continue;
370
+ }
371
+ catch {
372
+ continue;
373
+ }
374
+ rows.push(readProjectionRow(dir, id, aliases.get(id), nowMs));
375
+ }
376
+ }
377
+ return rows.length > 0 ? prioritizeProjectionStatusRows(rows, options.showAll === true) : null;
279
378
  }
@@ -116,6 +116,9 @@ export async function tellProjection(projectionDirectory, effort) {
116
116
  ? await startCommittedSuccessor(projectionDirectory, successorExecutionId)
117
117
  : "pending";
118
118
  }
119
+ if (life.phase !== "done") {
120
+ return "pending";
121
+ }
119
122
  const transition = store.launchIfSettled({
120
123
  executionId: successorExecutionId,
121
124
  facts,
@@ -0,0 +1,5 @@
1
+ /** Neutral response-artifact identity grammar. Sole production regex for rsp_<ULID>. */
2
+ export const RESPONSE_ARTIFACT_ID_RE = /^rsp_[0-9A-HJKMNP-TV-Z]{26}$/;
3
+ export function isResponseArtifactId(value) {
4
+ return RESPONSE_ARTIFACT_ID_RE.test(value);
5
+ }
@@ -528,7 +528,7 @@ export async function readKanshiBoard(cwd, now, opts) {
528
528
  const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
529
529
  return {
530
530
  contracts: filteredContracts,
531
- projections: readProjectionStatusBoard(cwd, now),
531
+ projections: readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
532
532
  queueLength,
533
533
  defaultBranch: defaultBranch?.branch,
534
534
  defaultHead: defaultBranch?.head,
@@ -8,15 +8,17 @@ import { FlowError, requireText } from "../flow-error.js";
8
8
  import { getCurrentBranch } from "../git/branches.js";
9
9
  import { getLatestCommitHash } from "../git/staging.js";
10
10
  import { RESPONSE_ARTIFACT_NOTICE, RESPONSE_HISTORY_DIR } from "../keiyaku.js";
11
+ import { attachAddressCarrier } from "./addressing.js";
11
12
  import { parseExecutionCoordinates } from "./execution-coordinate.js";
12
13
  import { createUlid } from "./ids.js";
13
14
  import { parseToAST } from "./markdown/parser.js";
15
+ import { isResponseArtifactId, RESPONSE_ARTIFACT_ID_RE } from "./response-artifact-id.js";
16
+ export { isResponseArtifactId, RESPONSE_ARTIFACT_ID_RE } from "./response-artifact-id.js";
14
17
  const RESPONSE_HISTORY_SLUG_MAX_LENGTH = 24;
15
18
  const RESPONSE_HISTORY_SESSION_KEY = "session";
16
19
  const RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY = "launch_snapshot";
17
20
  const RESPONSE_HISTORY_PROVENANCE_KEY = "provenance";
18
21
  const REVIEW_HISTORY_FILE_RE = /^\d{8}-\d{9}_REVIEW_(\d+)\.md$/;
19
- export const RESPONSE_ARTIFACT_ID_RE = /^rsp_[0-9A-HJKMNP-TV-Z]{26}$/;
20
22
  export const RESPONSE_ARTIFACT_HEADINGS = {
21
23
  call: "# Call",
22
24
  bind: "# Bind",
@@ -207,24 +209,41 @@ export async function readResponseArtifact(input) {
207
209
  const storageRoot = path.resolve(path.dirname(absolutePath), "..", "..");
208
210
  return { artifactId: provenance.artifactId, responsePath: path.relative(input.cwd, absolutePath), absolutePath, storageRoot, provenance, tool, ...(session ? { session } : {}), ...(launchSnapshot ? { launchSnapshot } : {}), ...(subagentName ? { subagentName } : {}) };
209
211
  }
210
- export async function locateResponseArtifact(cwd, artifactId) {
211
- const identity = requireText("artifactId", artifactId);
212
- if (!RESPONSE_ARTIFACT_ID_RE.test(identity))
213
- throw artifactError(identity, "provenance.artifactId", "must be rsp_ followed by a ULID");
214
- const historyDir = path.join(cwd, RESPONSE_HISTORY_DIR);
212
+ export async function locateResponseArtifact(input) {
213
+ const carry = (error) => {
214
+ throw attachAddressCarrier(error, input.attempt);
215
+ };
216
+ let identity;
217
+ try {
218
+ identity = requireText("artifactId", input.artifactId);
219
+ }
220
+ catch (error) {
221
+ return carry(error);
222
+ }
223
+ if (!isResponseArtifactId(identity)) {
224
+ return carry(new FlowError("INVALID_RESPONSE_PATH", `${identity} is not a response artifact handle; revive expects an artifact id`));
225
+ }
226
+ const historyDir = path.join(input.cwd, RESPONSE_HISTORY_DIR);
215
227
  let names;
216
228
  try {
217
229
  names = await fs.readdir(historyDir);
218
230
  }
219
231
  catch (error) {
220
- if (isErrnoException(error) && error.code === "ENOENT")
221
- throw new FlowError("INVALID_RESPONSE_PATH", `Response artifact '${identity}' was not found in ${historyDir}.`);
222
- throw error;
232
+ if (isErrnoException(error) && error.code === "ENOENT") {
233
+ return carry(new FlowError("INVALID_RESPONSE_PATH", `Response artifact '${identity}' was not found in ${historyDir}.`));
234
+ }
235
+ return carry(error);
223
236
  }
224
237
  let match;
225
238
  for (const name of names.filter((candidate) => candidate.endsWith(".md")).sort()) {
226
239
  const responsePath = path.join(RESPONSE_HISTORY_DIR, name);
227
- const document = await fs.readFile(path.join(cwd, responsePath), "utf8");
240
+ let document;
241
+ try {
242
+ document = await fs.readFile(path.join(input.cwd, responsePath), "utf8");
243
+ }
244
+ catch (error) {
245
+ return carry(error);
246
+ }
228
247
  const encoded = parseToAST(document).frontmatter?.entries?.[RESPONSE_HISTORY_PROVENANCE_KEY];
229
248
  if (encoded === undefined)
230
249
  continue;
@@ -237,15 +256,23 @@ export async function locateResponseArtifact(cwd, artifactId) {
237
256
  }
238
257
  if (!isRecord(indexed) || indexed.artifactId !== identity)
239
258
  continue;
240
- const parsed = await readResponseArtifact({ cwd, responsePath });
259
+ let parsed;
260
+ try {
261
+ parsed = await readResponseArtifact({ cwd: input.cwd, responsePath });
262
+ }
263
+ catch (error) {
264
+ return carry(error);
265
+ }
241
266
  if (!parsed)
242
267
  continue;
243
- if (match)
244
- throw new FlowError("INVALID_RESPONSE_PATH", `Response artifact identity '${identity}' is duplicated in ${historyDir}.`);
268
+ if (match) {
269
+ return carry(new FlowError("INVALID_RESPONSE_PATH", `Response artifact identity '${identity}' is duplicated in ${historyDir}.`));
270
+ }
245
271
  match = parsed;
246
272
  }
247
- if (!match)
248
- throw new FlowError("INVALID_RESPONSE_PATH", `Response artifact '${identity}' was not found in ${historyDir}.`);
273
+ if (!match) {
274
+ return carry(new FlowError("INVALID_RESPONSE_PATH", `Response artifact '${identity}' was not found in ${historyDir}.`));
275
+ }
249
276
  return match;
250
277
  }
251
278
  export function requireResumableArtifact(artifact) {
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/generate-version.mjs
2
- export const VERSION = "2.8.0";
2
+ export const VERSION = "2.8.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
4
4
  "description": "CLI for running iterative keiyaku workflows with Codex subagents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "preversion": "npm test --silent",
34
34
  "version": "npm run sync:version --silent && git add src/generated/version.ts",
35
35
  "dev": "npx tsx src/index.ts",
36
- "build": "rm -rf build && npx tsc && chmod +x build/index.js",
36
+ "build": "node scripts/build.mjs",
37
37
  "test:typecheck": "npx tsc --project tsconfig.tests.json",
38
38
  "prepublishOnly": "npm run build",
39
39
  "start": "node build/index.js",
@@ -48,10 +48,10 @@
48
48
  "dependencies": {
49
49
  "@anthropic-ai/claude-agent-sdk": "^0.3.204",
50
50
  "@anthropic-ai/sdk": "^0.110.0",
51
- "@earendil-works/pi-coding-agent": "0.80.2",
51
+ "@earendil-works/pi-coding-agent": "^0.80.10",
52
52
  "@modelcontextprotocol/sdk": "^1.29.0",
53
- "@opencode-ai/sdk": "1.17.20",
54
53
  "@openai/codex-sdk": "^0.113.0",
54
+ "@opencode-ai/sdk": "1.17.20",
55
55
  "simple-git": "^3.21.0",
56
56
  "zod": "4.3.6"
57
57
  },