@astrosheep/keiyaku 2.9.0 → 2.9.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 (47) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/cli/commands/akuma/list/handler.js +1 -1
  3. package/build/cli/commands/akuma/view/handler.js +1 -1
  4. package/build/cli/commands/contract/amend/handler.js +1 -1
  5. package/build/cli/commands/contract/arc/handler.js +1 -1
  6. package/build/cli/commands/contract/bind/handler.js +1 -1
  7. package/build/cli/commands/contract/forfeit/handler.js +1 -1
  8. package/build/cli/commands/contract/log/handler.js +1 -1
  9. package/build/cli/commands/contract/petition/handler.js +1 -1
  10. package/build/cli/commands/contract/renew/handler.js +1 -1
  11. package/build/cli/commands/projection/call/handler.js +5 -2
  12. package/build/cli/commands/projection/kill/handler.js +3 -1
  13. package/build/cli/commands/projection/revive/handler.js +6 -3
  14. package/build/cli/commands/projection/status/handler.js +4 -2
  15. package/build/cli/commands/projection/tell/handler.js +3 -1
  16. package/build/cli/commands/projection/wait/handler.js +3 -1
  17. package/build/cli/commands/shared.js +5 -5
  18. package/build/cli/commands/system/completion/handler.js +1 -1
  19. package/build/cli/parse-flags.js +197 -0
  20. package/build/cli/parse-metadata.js +129 -0
  21. package/build/cli/parse-selectors.js +65 -0
  22. package/build/cli/parse.js +19 -373
  23. package/build/cli/skills-install.js +1 -1
  24. package/build/config/akuma-loader.js +5 -2
  25. package/build/core/amend.js +2 -5
  26. package/build/core/arc.js +2 -4
  27. package/build/core/call/call.js +7 -2
  28. package/build/core/call/context.js +27 -14
  29. package/build/core/call-persist.js +9 -2
  30. package/build/core/path-coordinate.js +27 -0
  31. package/build/core/petition-run.js +2 -5
  32. package/build/core/projection-coordinate.js +14 -2
  33. package/build/core/renew.js +2 -4
  34. package/build/core/status/board.js +6 -3
  35. package/build/core/task-contract.js +13 -0
  36. package/build/core/task-git-runtime.js +5 -2
  37. package/build/core/task-git-store.js +31 -21
  38. package/build/core/worktree-path.js +9 -2
  39. package/build/flow-error.js +2 -0
  40. package/build/generated/version.js +1 -1
  41. package/build/git/branches.js +33 -9
  42. package/build/git/core.js +9 -0
  43. package/package.json +1 -1
  44. package/skills/keiyaku/SKILL.md +30 -13
  45. package/skills/keiyaku-akuma/SKILL.md +30 -62
  46. package/skills/keiyaku-task/SKILL.md +51 -0
  47. package/skills/keiyaku-workflow/SKILL.md +71 -0
@@ -1,342 +1,11 @@
1
1
  import { FlowError } from "../flow-error.js";
2
2
  import { agentNameSchema } from "../config/settings/schema.js";
3
3
  import { isResponseArtifactId } from "../core/response-artifact-id.js";
4
- import { BOOLEAN_FLAGS, CONTROL_FLAGS, HELP_FLAGS, SHORT_FLAG_ALIASES, VALUE_FLAGS, } from "./flags.js";
5
- import { commandParseOrder, getCommandMetadata } from "./commands/registry.js";
6
- import { TASK_SUBCOMMANDS } from "./types.js";
7
- /** Commands that may carry an explicit commission selector (@ or --contract). */
8
- const COMMISSION_SELECTOR_COMMANDS = new Set([
9
- "arc",
10
- "amend",
11
- "call",
12
- "wait",
13
- "tell",
14
- "kill",
15
- "log",
16
- "renew",
17
- "petition",
18
- "forfeit",
19
- ]);
20
- /** Parse-time allowed flags; prefers meta.parseFlags when help flags omit a mouth-only flag. */
21
- function allowedFlagsForParse(command) {
22
- const meta = getCommandMetadata(command);
23
- return meta.parseFlags ?? meta.flags;
24
- }
25
- export function normalizeContractAddress(raw) {
26
- const value = raw.startsWith("@") ? raw.slice(1) : raw;
27
- if (!value.trim()) {
28
- throw new FlowError("EMPTY_PARAM", "contract address cannot be empty");
29
- }
30
- return value.trim();
31
- }
32
- export function isCallCommand(command) {
33
- return command === "call";
34
- }
35
- /** Identify petition --dry-run before parsing without mistaking flag values or literal payload. */
36
- export function isPetitionDryRunInvocation(tokens) {
37
- const boundary = tokens.indexOf("--");
38
- const head = boundary === -1 ? tokens : tokens.slice(0, boundary);
39
- if (!head.some((token) => token === "--dry-run" || token.startsWith("--dry-run=")))
40
- return false;
41
- for (let index = 0; index < head.length; index += 1) {
42
- const token = head[index];
43
- if (HELP_FLAGS.has(token) || token.startsWith("@"))
44
- continue;
45
- if (/^-[A-Za-z]$/.test(token)) {
46
- const name = SHORT_FLAG_ALIASES[token.slice(1)];
47
- if (name && VALUE_FLAGS.has(name))
48
- index += 1;
49
- continue;
50
- }
51
- if (token.startsWith("--")) {
52
- const eq = token.indexOf("=");
53
- const name = parseFlagName(eq === -1 ? token : token.slice(0, eq));
54
- if (eq === -1 && VALUE_FLAGS.has(name))
55
- index += 1;
56
- continue;
57
- }
58
- return token === "petition";
59
- }
60
- return false;
61
- }
62
- export function commandUsesPromptInput(command) {
63
- return isCallCommand(command) || command === "tell" || command === "revive";
64
- }
65
- export function parseWaitTimeout(value) {
66
- const matched = /^(?<amount>[1-9][0-9]*)(?<unit>[smh])$/.exec(value);
67
- if (!matched?.groups) {
68
- throw new FlowError("EMPTY_PARAM", "wait timeout must use <positive integer><s|m|h>, for example 45s, 5m, or 2h (maximum 24h)");
69
- }
70
- const amount = Number(matched.groups.amount);
71
- const multiplier = matched.groups.unit === "s" ? 1_000 : matched.groups.unit === "m" ? 60_000 : 3_600_000;
72
- const timeoutMs = amount * multiplier;
73
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs > 24 * 3_600_000) {
74
- throw new FlowError("EMPTY_PARAM", "wait timeout must use <positive integer><s|m|h>, for example 45s, 5m, or 2h (maximum 24h)");
75
- }
76
- return timeoutMs;
77
- }
78
- export function commandPositionalRange(command) {
79
- if (command === "akuma view")
80
- return { min: 1, max: 1, label: "<name>" };
81
- if (command === "log")
82
- return { min: 0, max: 0, label: "" };
83
- if (isCallCommand(command))
84
- return { min: 1, max: 1, label: "<BODY|->" };
85
- if (command === "tell")
86
- return { min: 2, max: 2, label: "<PROJECTION> <BODY|->" };
87
- if (command === "revive")
88
- return { min: 1, max: 2, label: "<ARTIFACT> [BODY|-]" };
89
- if (command === "wait")
90
- return { min: 1, max: 1, label: "<projection-address>" };
91
- if (command === "kill")
92
- return { min: 1, max: 1, label: "<projection-address>" };
93
- if (command === "status")
94
- return { min: 0, max: 1, label: "[projection-id]" };
95
- if (command === "task add")
96
- return { min: 1, max: 1, label: "<BODY|->" };
97
- if (command === "task view" || command === "task start" || command === "task drop")
98
- return { min: 1, max: 1, label: "<ID>" };
99
- if (command === "task update")
100
- return { min: 1, max: 1, label: "<ID>" };
101
- if (command === "task stop" || command === "task done")
102
- return { min: 0, max: 1, label: "[ID]" };
103
- return { min: 0, max: 0 };
104
- }
105
- function commandRequiresStdin(command) {
106
- return getCommandMetadata(command).stdin === "required";
107
- }
108
- function commandTokens(command) {
109
- return command.split(" ");
110
- }
111
- function parseFlagName(raw) {
112
- return raw.slice(2);
113
- }
114
- function assignFlag(flags, name, value) {
115
- switch (name) {
116
- case "cwd":
117
- flags.cwd = String(value);
118
- return;
119
- case "repo":
120
- flags.repo = String(value);
121
- return;
122
- case "contract":
123
- flags.contractId = normalizeContractAddress(String(value));
124
- flags.contractAddressSource = "explicit-flag";
125
- return;
126
- case "akuma":
127
- flags.akuma = String(value);
128
- return;
129
- case "projection":
130
- flags.projection = String(value);
131
- return;
132
- case "alias":
133
- flags.alias = String(value);
134
- return;
135
- case "timeout":
136
- case "wait":
137
- flags.timeoutMs = parseWaitTimeout(String(value));
138
- return;
139
- case "model":
140
- flags.model = String(value);
141
- return;
142
- case "effort": {
143
- const effort = String(value);
144
- flags.effort = effort;
145
- return;
146
- }
147
- case "incognito":
148
- flags.incognito = Boolean(value);
149
- return;
150
- case "bare":
151
- flags.bare = Boolean(value);
152
- return;
153
- case "detach":
154
- flags.detach = Boolean(value);
155
- return;
156
- case "draft-only":
157
- flags.draftOnly = Boolean(value);
158
- return;
159
- case "exclusive":
160
- flags.exclusive = Boolean(value);
161
- return;
162
- case "place":
163
- if (flags.bindPlace !== undefined) {
164
- throw new FlowError("EMPTY_PARAM", "option --place may only be specified once");
165
- }
166
- flags.bindPlace = String(value);
167
- return;
168
- case "objective":
169
- if (flags.bindObjective !== undefined) {
170
- throw new FlowError("EMPTY_PARAM", "option --objective may only be specified once");
171
- }
172
- flags.bindObjective = String(value);
173
- return;
174
- case "scope":
175
- if (flags.bindScope !== undefined) {
176
- throw new FlowError("EMPTY_PARAM", "option --scope may only be specified once");
177
- }
178
- flags.bindScope = String(value);
179
- return;
180
- case "checks":
181
- if (flags.bindChecks !== undefined) {
182
- throw new FlowError("EMPTY_PARAM", "option --checks may only be specified once");
183
- }
184
- flags.bindChecks = String(value);
185
- return;
186
- case "task":
187
- if (flags.bindTaskId !== undefined)
188
- throw new FlowError("EMPTY_PARAM", "option --task may only be specified once");
189
- flags.bindTaskId = String(value);
190
- return;
191
- case "after":
192
- flags.after = [...(flags.after ?? []), String(value)];
193
- return;
194
- case "force":
195
- flags.force = Boolean(value);
196
- return;
197
- case "reason":
198
- flags.reason = String(value);
199
- return;
200
- case "dry-run":
201
- flags.dryRun = Boolean(value);
202
- return;
203
- case "waive":
204
- flags.waivePaths = [...(flags.waivePaths ?? []), String(value)];
205
- return;
206
- case "shell":
207
- flags.shell = String(value);
208
- return;
209
- case "word":
210
- flags.word = String(value);
211
- return;
212
- case "previous":
213
- flags.previous = String(value);
214
- return;
215
- case "complete":
216
- flags.complete = Boolean(value);
217
- return;
218
- case "all":
219
- flags.all = Boolean(value);
220
- return;
221
- case "target":
222
- if (flags.target !== undefined) {
223
- throw new FlowError("EMPTY_PARAM", "option --target may only be specified once");
224
- }
225
- flags.target = String(value);
226
- return;
227
- case "json":
228
- flags.json = Boolean(value);
229
- return;
230
- case "done":
231
- flags.done = Boolean(value);
232
- return;
233
- case "pri": {
234
- const raw = String(value);
235
- if (!/^[0-3]$/.test(raw))
236
- throw new FlowError("EMPTY_PARAM", "task priority must be an integer from 0 through 3");
237
- if (flags.taskPri !== undefined)
238
- throw new FlowError("EMPTY_PARAM", "option --pri may only be specified once");
239
- flags.taskPri = Number(raw);
240
- return;
241
- }
242
- case "needs":
243
- flags.taskNeeds = [...(flags.taskNeeds ?? []), String(value)];
244
- return;
245
- case "drop-needs":
246
- flags.taskDropNeeds = [...(flags.taskDropNeeds ?? []), String(value)];
247
- return;
248
- case "from":
249
- flags.taskFrom = [...(flags.taskFrom ?? []), String(value)];
250
- return;
251
- case "parent":
252
- if (flags.taskParent !== undefined)
253
- throw new FlowError("EMPTY_PARAM", "option --parent may only be specified once");
254
- flags.taskParent = String(value);
255
- return;
256
- case "no-parent":
257
- flags.taskNoParent = Boolean(value);
258
- return;
259
- case "title":
260
- if (flags.taskTitle !== undefined)
261
- throw new FlowError("EMPTY_PARAM", "option --title may only be specified once");
262
- flags.taskTitle = String(value);
263
- return;
264
- case "body":
265
- if (flags.taskBody !== undefined)
266
- throw new FlowError("EMPTY_PARAM", "option --body may only be specified once");
267
- flags.taskBody = String(value);
268
- return;
269
- default:
270
- throw new FlowError("EMPTY_PARAM", `unknown option: --${name}`);
271
- }
272
- }
273
- function assertAllowedFlag(command, name) {
274
- if (!allowedFlagsForParse(command).includes(name)) {
275
- throw new FlowError("EMPTY_PARAM", `option --${name} is not valid for ${command}`);
276
- }
277
- }
278
- function resolveCommand(tokens) {
279
- if (tokens[0] === "petition" && (tokens[1] === "claim" || tokens[1] === "forfeit")) {
280
- throw new FlowError("UNKNOWN_COMMAND", `unknown command: petition ${tokens[1]}`);
281
- }
282
- for (const command of commandParseOrder()) {
283
- const parts = commandTokens(command);
284
- if (parts.every((part, index) => tokens[index] === part)) {
285
- return { command, rest: tokens.slice(parts.length) };
286
- }
287
- }
288
- const [head] = tokens;
289
- if (head === "skills") {
290
- return { command: "skills", rest: tokens.slice(1) };
291
- }
292
- if (!head)
293
- throw new FlowError("EMPTY_PARAM", "command cannot be empty");
294
- throw new FlowError("UNKNOWN_COMMAND", `unknown command: ${head ?? ""}`.trim());
295
- }
296
- function splitAtLiteralBoundary(tokens) {
297
- const boundary = tokens.indexOf("--");
298
- if (boundary === -1)
299
- return { head: tokens, payload: [] };
300
- return { head: tokens.slice(0, boundary), payload: tokens.slice(boundary + 1) };
301
- }
302
- function readValueFlag(name, token, eq, tokens, index) {
303
- // --target and task --body own emptiness in core; empty remains supplied.
304
- // Other value flags keep the historical requires-a-value rejection.
305
- if (eq !== -1) {
306
- const value = token.slice(eq + 1);
307
- if (value === "" && name !== "target" && name !== "body") {
308
- throw new FlowError("EMPTY_PARAM", `option --${name} requires a value`);
309
- }
310
- return { value, nextIndex: index };
311
- }
312
- const value = tokens[index + 1];
313
- if (value === undefined || value === "--" || value.startsWith("--")) {
314
- throw new FlowError("EMPTY_PARAM", `option --${name} requires a value`);
315
- }
316
- if (value === "" && name !== "target" && name !== "body") {
317
- throw new FlowError("EMPTY_PARAM", `option --${name} requires a value`);
318
- }
319
- // Short-flag values may begin with @; they are flag values, not commission tokens.
320
- return { value, nextIndex: index + 1 };
321
- }
322
- function commissionConflictMessage(atForm, contractForm) {
323
- return `commission address supplied twice: ${atForm} and --contract ${contractForm}; use exactly one form`;
324
- }
325
- function formatSelectorList(items) {
326
- if (items.length <= 1)
327
- return items[0] ?? "";
328
- if (items.length === 2)
329
- return `${items[0]} and ${items[1]}`;
330
- return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
331
- }
332
- function multiCommissionSelectorMessage(command, spellings) {
333
- return `${command} accepts one commission address; got ${formatSelectorList(spellings)}`;
334
- }
335
- function commissionSelectorSpelling(source, value, rawToken) {
336
- if (source === "at-address")
337
- return rawToken ?? `@${value}`;
338
- return `--contract ${rawToken ?? value}`;
339
- }
4
+ import { HELP_FLAGS } from "./flags.js";
5
+ import { assertAllowedFlag, assignFlag, BOOLEAN_FLAGS, CONTROL_FLAGS, parseFlagName, readValueFlag, SHORT_FLAG_ALIASES, VALUE_FLAGS, } from "./parse-flags.js";
6
+ import { commandPositionalRange, commandRequiresStdin, isCallCommand, resolveCommand, splitAtLiteralBoundary, TASK_SUBCOMMANDS, } from "./parse-metadata.js";
7
+ import { applyCommissionSelector, assertCommissionSelectorAllowed, assertProjectionSelectorPresent, } from "./parse-selectors.js";
8
+ export { commandPositionalRange, commandUsesPromptInput, isCallCommand, isPetitionDryRunInvocation, normalizeContractAddress, parseWaitTimeout, } from "./parse-metadata.js";
340
9
  export function parseCliArgs(tokens) {
341
10
  const { head, payload } = splitAtLiteralBoundary(tokens);
342
11
  const flags = {};
@@ -411,29 +80,21 @@ export function parseCliArgs(tokens) {
411
80
  }
412
81
  }
413
82
  const { command, rest } = resolveCommand(free);
414
- if (atTokens.length > 0 && contractFlagRaws.length > 0) {
415
- throw new FlowError("EMPTY_PARAM", commissionConflictMessage(atTokens[0], contractFlagRaws[0]));
416
- }
417
- if (atTokens.length > 1) {
418
- throw new FlowError("EMPTY_PARAM", multiCommissionSelectorMessage(command, atTokens));
419
- }
420
- if (contractFlagRaws.length > 1) {
421
- throw new FlowError("EMPTY_PARAM", multiCommissionSelectorMessage(command, contractFlagRaws.map((raw) => `--contract ${raw}`)));
422
- }
423
- if (atTokens.length === 1) {
424
- flags.contractId = normalizeContractAddress(atTokens[0]);
425
- flags.contractAddressSource = "at-address";
426
- }
427
- else if (contractFlagRaws.length === 1) {
428
- assignFlag(flags, "contract", contractFlagRaws[0]);
429
- }
83
+ applyCommissionSelector(command, flags, atTokens, contractFlagRaws);
430
84
  if (helpRequested) {
431
85
  return {
432
86
  command,
433
- flags: flags.contractId ? { contractId: flags.contractId, contractAddressSource: flags.contractAddressSource, ...(flags.cwd ? { cwd: flags.cwd } : {}), ...(flags.repo ? { repo: flags.repo } : {}) } : {
434
- ...(flags.cwd ? { cwd: flags.cwd } : {}),
435
- ...(flags.repo ? { repo: flags.repo } : {}),
436
- },
87
+ flags: flags.contractId
88
+ ? {
89
+ contractId: flags.contractId,
90
+ contractAddressSource: flags.contractAddressSource,
91
+ ...(flags.cwd ? { cwd: flags.cwd } : {}),
92
+ ...(flags.repo ? { repo: flags.repo } : {}),
93
+ }
94
+ : {
95
+ ...(flags.cwd ? { cwd: flags.cwd } : {}),
96
+ ...(flags.repo ? { repo: flags.repo } : {}),
97
+ },
437
98
  positional: [],
438
99
  helpRequested: true,
439
100
  };
@@ -447,7 +108,6 @@ export function parseCliArgs(tokens) {
447
108
  if (command === "task") {
448
109
  throw new FlowError("EMPTY_PARAM", `task requires subcommand: ${TASK_SUBCOMMANDS.join(", ")}`);
449
110
  }
450
- // --place is bind's H1 domain flag; every other command keeps the retired-selector mouth.
451
111
  if (command !== "bind" && seenFlagNames.has("place")) {
452
112
  throw new FlowError("EMPTY_PARAM", "unknown option --place; use @addr or --contract <addr>");
453
113
  }
@@ -471,22 +131,8 @@ export function parseCliArgs(tokens) {
471
131
  if (command === "petition" && flags.json && !flags.dryRun) {
472
132
  throw new FlowError("EMPTY_PARAM", "petition --json requires --dry-run");
473
133
  }
474
- if (flags.contractId !== undefined && !COMMISSION_SELECTOR_COMMANDS.has(command)) {
475
- throw new FlowError("EMPTY_PARAM", command === "bind"
476
- ? `bind creates a commission and does not accept @${flags.contractId}; use -C/--cwd to select the repository`
477
- : `contract address @${flags.contractId} is not valid for ${command}`);
478
- }
479
- if ((command === "wait" || command === "tell") && flags.contractId !== undefined) {
480
- const selectorSpelling = commissionSelectorSpelling(flags.contractAddressSource === "at-address" ? "at-address" : "explicit-flag", flags.contractId, flags.contractAddressSource === "at-address" ? atTokens[0] : contractFlagRaws[0]);
481
- const scopeOnlyMessage = `${command} requires a projection id or alias; ${selectorSpelling} only limits projection lookup, so add the projection address`;
482
- if (command === "wait" && rest.length + payload.length === 0) {
483
- throw new FlowError("EMPTY_PARAM", scopeOnlyMessage);
484
- }
485
- if (command === "tell" && rest.length === 0) {
486
- // Payload after -- is the message body, never the projection address.
487
- throw new FlowError("EMPTY_PARAM", scopeOnlyMessage);
488
- }
489
- }
134
+ assertCommissionSelectorAllowed(command, flags);
135
+ assertProjectionSelectorPresent(command, flags, atTokens, contractFlagRaws, rest.length, payload.length);
490
136
  const positional = [...rest, ...payload];
491
137
  if (isCallCommand(command)) {
492
138
  if (flags.akuma) {
@@ -7,7 +7,7 @@ import { FlowError, asMessage } from "../flow-error.js";
7
7
  import { VERSION } from "../generated/version.js";
8
8
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
9
9
  const PACKAGE_ROOT = path.resolve(MODULE_DIR, "../..");
10
- const OFFICIAL_SKILLS = ["keiyaku", "keiyaku-akuma"];
10
+ const OFFICIAL_SKILLS = ["keiyaku", "keiyaku-akuma", "keiyaku-workflow", "keiyaku-task"];
11
11
  const SKILL_ROOT = path.join(PACKAGE_ROOT, "skills");
12
12
  const INSTALL_MARKER = ".keiyaku-skill-owned.json";
13
13
  function skillSource(skill) {
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { z } from "zod";
4
4
  import { parseToAST } from "../core/markdown/parser.js";
5
- import { isGitRepo } from "../git/branches.js";
5
+ import { observeGitRepository } from "../git/branches.js";
6
6
  import { BUILTIN_AGENT_PROFILES } from "../keiyaku.js";
7
7
  import { getKeiyakuHome } from "./env.js";
8
8
  import { agentNameSchema, providerInstanceNameSchema } from "./settings/schema.js";
@@ -211,7 +211,10 @@ export async function loadAkumaCatalog(cwd) {
211
211
  selected[name] = candidate;
212
212
  }
213
213
  foldLayer(selected, shadows, candidates, "user", await readLayer(path.join(getKeiyakuHome(), "akuma"), "user"));
214
- if (await isGitRepo(cwd)) {
214
+ const repository = await observeGitRepository(cwd);
215
+ if (repository.capability === "unknown")
216
+ throw repository.error;
217
+ if (repository.capability === "available" && repository.coordinate === "repository") {
215
218
  foldLayer(selected, shadows, candidates, "project", await readLayer(path.join(cwd, ".keiyaku", "akuma"), "project"));
216
219
  }
217
220
  return Object.freeze({
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { FlowError } from "../flow-error.js";
4
- import { getCurrentBranch, isGitRepo } from "../git/branches.js";
4
+ import { getCurrentBranch, requireGitRepository } from "../git/branches.js";
5
5
  import { KEIYAKU_FILE } from "../keiyaku.js";
6
6
  import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
7
7
  import { renderContractView } from "./contract-view.js";
@@ -71,10 +71,7 @@ export async function amendContract(input) {
71
71
  return { contractId: input.contractId, amendmentNumber, ledgerHead: result.head };
72
72
  }
73
73
  export async function amendKeiyaku(input) {
74
- const isRepo = await isGitRepo(input.cwd);
75
- if (!isRepo) {
76
- throw new FlowError("NOT_GIT_REPO", `${input.cwd} is not a git repository`);
77
- }
74
+ await requireGitRepository(input.cwd, "amend");
78
75
  const address = await resolveContractAddress(input.cwd, input.contractId, input.contractAddressSource);
79
76
  return await withResolvedAddress(address, async () => {
80
77
  const contractId = address.binding.commissionId;
package/build/core/arc.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { requireText, FlowError } from "../flow-error.js";
4
- import { getCurrentBranch, isGitRepo } from "../git/branches.js";
4
+ import { getCurrentBranch, requireGitRepository } from "../git/branches.js";
5
5
  import { DIRTY_FILE_CATEGORY, getDirtyFiles, renderDirtyFileStatusLine } from "../git/worktree.js";
6
6
  import { KEIYAKU_FILE, ACTOR_IDENTITY } from "../keiyaku.js";
7
7
  import { buildDirtyWorktreeArtifactHint } from "./hints.js";
@@ -161,9 +161,7 @@ async function readBranchLabels(cwd) {
161
161
  return { currentBranch, baseBranch };
162
162
  }
163
163
  export async function arcKeiyaku(input) {
164
- if (!(await isGitRepo(input.cwd))) {
165
- throw new FlowError("NOT_GIT_REPO", `${input.cwd} is not a git repository`);
166
- }
164
+ await requireGitRepository(input.cwd, "arc");
167
165
  const address = await resolveContractAddress(input.cwd, input.contractId, input.contractAddressSource);
168
166
  return await withResolvedAddress(address, async () => {
169
167
  await assertCleanWorkingTree(input.cwd, []);
@@ -3,7 +3,7 @@ import { selectSubagent } from "../../agents/selector.js";
3
3
  import { applySnapshotEffort } from "../../agents/launch-snapshot/model.js";
4
4
  import { resolveAkumaLaunchSnapshot } from "../../agents/launch-snapshot/resolve.js";
5
5
  import { FlowError, requireText } from "../../flow-error.js";
6
- import { isGitRepo } from "../../git/branches.js";
6
+ import { assertGitObservation, observeGitRepository } from "../../git/branches.js";
7
7
  import { ACTOR_IDENTITY } from "../../keiyaku.js";
8
8
  import { appendDebugBlock } from "../../telemetry/debug-log.js";
9
9
  import { logInfo } from "../../telemetry/logger.js";
@@ -54,12 +54,17 @@ export async function runCall(input) {
54
54
  else if (reviveArtifact?.provenance.authority.kind === "repository") {
55
55
  const artifactHome = await fs.realpath(reviveArtifact.storageRoot);
56
56
  const addressed = input.repo ?? input.cwd;
57
- if (await isGitRepo(addressed)) {
57
+ const observation = await observeGitRepository(addressed);
58
+ const coordinate = assertGitObservation(observation, "revive");
59
+ if (coordinate === "repository") {
58
60
  const addressedRoot = await stableRepoRoot(addressed);
59
61
  if (addressedRoot !== artifactHome) {
60
62
  throw new FlowError("INVALID_RESPONSE_PATH", `Response artifact '${reviveArtifact.artifactId}' belongs to ${artifactHome}, but the addressed repository is ${addressedRoot}.`);
61
63
  }
62
64
  }
65
+ else {
66
+ throw new FlowError("NOT_GIT_REPO", `${addressed} is not a git repository`);
67
+ }
63
68
  rawCwd = artifactHome;
64
69
  rawRepo = artifactHome;
65
70
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs/promises";
2
2
  import { FlowError } from "../../flow-error.js";
3
- import { isGitRepo } from "../../git/branches.js";
3
+ import { assertGitObservation, observeGitRepository } from "../../git/branches.js";
4
4
  import { createGit, wrapGitError } from "../../git/core.js";
5
5
  import { AddressResolutionError, buildResolutionAttempt, buildUncommissionedAddress, buildUncommissionedRepositoryAddress, resolveContractAddress, withResolvedAddress, } from "../addressing.js";
6
6
  import { findOpenArcView } from "../arc.js";
@@ -48,8 +48,16 @@ async function resolveWorkspaceCwd(cwd, contractId, bind) {
48
48
  }
49
49
  export async function readCallKeiyakuContext(executionCwd, input) {
50
50
  const ledgerCandidate = input.repoCwd ?? executionCwd;
51
- const isRepo = await isGitRepo(ledgerCandidate);
52
- if (!isRepo) {
51
+ const ledgerObservation = await observeGitRepository(ledgerCandidate);
52
+ if (ledgerObservation.capability === "unknown")
53
+ throw ledgerObservation.error;
54
+ if (ledgerObservation.capability === "unavailable" && (input.repoCwd || input.contractId)) {
55
+ assertGitObservation(ledgerObservation, input.repoCwd ? `${input.mode ?? "call"} --repo` : "contract addressing");
56
+ }
57
+ if (input.repoCwd && ledgerObservation.capability === "available" && ledgerObservation.coordinate === "non-repository") {
58
+ throw new FlowError("NOT_GIT_REPO", `${ledgerCandidate} is not a git repository`);
59
+ }
60
+ if (ledgerObservation.capability !== "available" || ledgerObservation.coordinate !== "repository") {
53
61
  if (input.bare && input.contractId) {
54
62
  const asTyped = input.contractAddressSource === "at-address"
55
63
  ? `@${input.contractId}`
@@ -146,18 +154,23 @@ export async function readCallKeiyakuContext(executionCwd, input) {
146
154
  if (input.contractId) {
147
155
  address = await resolveContractAddress(ledgerCwd, input.contractId, input.contractAddressSource);
148
156
  }
149
- else if (await isGitRepo(executionCwd)) {
150
- const executionRepo = await stableRepoRoot(executionCwd);
151
- if (input.repoCwd && executionRepo !== ledgerCwd) {
152
- const presented = await contractPresentedByWorktree(executionCwd);
153
- if (presented) {
154
- throw new FlowError("EMPTY_PARAM", `cross-repository contract-worktree addressing is pending: -C presents @${presented} from ${executionRepo}, but --repo selects ${ledgerCwd}`);
157
+ else {
158
+ const executionObservation = await observeGitRepository(executionCwd);
159
+ if (executionObservation.capability === "unknown")
160
+ throw executionObservation.error;
161
+ if (executionObservation.capability === "available" && executionObservation.coordinate === "repository") {
162
+ const executionRepo = await stableRepoRoot(executionCwd);
163
+ if (input.repoCwd && executionRepo !== ledgerCwd) {
164
+ const presented = await contractPresentedByWorktree(executionCwd);
165
+ if (presented) {
166
+ throw new FlowError("EMPTY_PARAM", `cross-repository contract-worktree addressing is pending: -C presents @${presented} from ${executionRepo}, but --repo selects ${ledgerCwd}`);
167
+ }
155
168
  }
156
- }
157
- else {
158
- const presented = await contractPresentedByWorktree(executionCwd);
159
- if (presented) {
160
- address = await resolveContractAddress(ledgerCwd, presented, "worktree-presented");
169
+ else {
170
+ const presented = await contractPresentedByWorktree(executionCwd);
171
+ if (presented) {
172
+ address = await resolveContractAddress(ledgerCwd, presented, "worktree-presented");
173
+ }
161
174
  }
162
175
  }
163
176
  }
@@ -2,7 +2,8 @@ import { appendDebugLog } from "../telemetry/debug-log.js";
2
2
  import { logWarn } from "../telemetry/logger.js";
3
3
  import { runCall } from "./call/call.js";
4
4
  import { persistResponseHistory } from "./transcripts.js";
5
- import { isGitRepo } from "../git/branches.js";
5
+ import { assertGitObservation, observeGitRepository } from "../git/branches.js";
6
+ import { FlowError } from "../flow-error.js";
6
7
  import * as path from "node:path";
7
8
  import * as fs from "node:fs/promises";
8
9
  import { stableRepoRoot } from "./worktree-path.js";
@@ -14,7 +15,13 @@ function responsePathFromPrintingCwd(historyStorageCwd, responsePath) {
14
15
  export async function runCallAndPersist(input) {
15
16
  const historyCandidate = input.repo ?? input.cwd;
16
17
  const canonicalHistoryCandidate = await fs.realpath(historyCandidate);
17
- const historyStorageCwd = (await isGitRepo(historyCandidate))
18
+ const historyObservation = await observeGitRepository(historyCandidate);
19
+ if (input.repo && assertGitObservation(historyObservation, `${input.mode ?? "call"} --repo`) === "non-repository") {
20
+ throw new FlowError("NOT_GIT_REPO", `${historyCandidate} is not a git repository`);
21
+ }
22
+ if (historyObservation.capability === "unknown")
23
+ throw historyObservation.error;
24
+ const historyStorageCwd = (historyObservation.capability === "available" && historyObservation.coordinate === "repository")
18
25
  ? await stableRepoRoot(historyCandidate)
19
26
  : canonicalHistoryCandidate;
20
27
  const historyDisplayCwd = path.resolve(historyCandidate, path.relative(canonicalHistoryCandidate, historyStorageCwd));
@@ -0,0 +1,27 @@
1
+ import * as fs from "node:fs";
2
+ import * as fsPromises from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ /**
5
+ * Git for Windows may emit native, slash-normalized, or MSYS drive paths.
6
+ * Keep one host-native spelling before using a coordinate as a filesystem cwd.
7
+ */
8
+ export function normalizeGitPath(raw, platform = process.platform) {
9
+ const value = raw.trim();
10
+ if (platform !== "win32")
11
+ return path.normalize(value);
12
+ const msysDrive = value.match(/^\/([A-Za-z])(?:\/|$)(.*)$/);
13
+ if (msysDrive)
14
+ return path.win32.normalize(`${msysDrive[1].toUpperCase()}:/${msysDrive[2]}`);
15
+ const normalized = path.win32.normalize(value);
16
+ return normalized.replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
17
+ }
18
+ export function gitCommonRootPath(raw, platform = process.platform) {
19
+ const paths = platform === "win32" ? path.win32 : path.posix;
20
+ return paths.dirname(normalizeGitPath(raw, platform));
21
+ }
22
+ export async function resolveGitCommonRoot(raw) {
23
+ return await fsPromises.realpath(gitCommonRootPath(raw));
24
+ }
25
+ export function resolveGitCommonRootSync(raw) {
26
+ return fs.realpathSync(gitCommonRootPath(raw));
27
+ }
@@ -4,7 +4,7 @@ import { FlowError } from "../flow-error.js";
4
4
  import { readCurrentContractContext } from "./context.js";
5
5
  import { readLedger } from "./ledger.js";
6
6
  import { commissionTarget } from "./status/drift.js";
7
- import { getCurrentBranch, isGitRepo } from "../git/branches.js";
7
+ import { getCurrentBranch, requireGitRepository } from "../git/branches.js";
8
8
  import { claimKeiyaku } from "./petition-claim.js";
9
9
  import { forfeitKeiyaku } from "./petition-forfeit.js";
10
10
  const VERDICT_DENIED_CODE = "VERDICT_DENIED";
@@ -33,10 +33,7 @@ async function resolveClaimGateConfig(cwd) {
33
33
  }
34
34
  }
35
35
  async function loadCloseContext(cwd, requestedContractId) {
36
- const isRepo = await isGitRepo(cwd);
37
- if (!isRepo) {
38
- throw new FlowError("NOT_GIT_REPO", `${cwd} is not a git repository`);
39
- }
36
+ await requireGitRepository(cwd, "petition");
40
37
  if (requestedContractId) {
41
38
  const ledger = await readLedger(cwd, requestedContractId);
42
39
  if (!ledger) {