@astrosheep/keiyaku 2.8.2 → 2.8.4

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.
@@ -4,10 +4,12 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { FlowError, asMessage } from "../flow-error.js";
7
+ import { VERSION } from "../generated/version.js";
7
8
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
8
9
  const PACKAGE_ROOT = path.resolve(MODULE_DIR, "../..");
9
10
  const OFFICIAL_SKILLS = ["keiyaku", "keiyaku-akuma"];
10
11
  const SKILL_ROOT = path.join(PACKAGE_ROOT, "skills");
12
+ const INSTALL_MARKER = ".keiyaku-skill-owned.json";
11
13
  function skillSource(skill) {
12
14
  return path.join(SKILL_ROOT, skill);
13
15
  }
@@ -50,6 +52,76 @@ function objectKind(stat) {
50
52
  return "directory";
51
53
  return "other";
52
54
  }
55
+ function updateHashField(hash, value) {
56
+ const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : value;
57
+ const length = Buffer.allocUnsafe(4);
58
+ length.writeUInt32BE(bytes.length);
59
+ hash.update(length);
60
+ hash.update(bytes);
61
+ }
62
+ async function contentHash(root) {
63
+ const hash = crypto.createHash("sha256");
64
+ const walk = async (directory, relativeDirectory) => {
65
+ const entries = await fs.readdir(directory, { withFileTypes: true });
66
+ entries.sort((left, right) => Buffer.compare(Buffer.from(left.name, "utf8"), Buffer.from(right.name, "utf8")));
67
+ for (const entry of entries) {
68
+ const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
69
+ if (relative === INSTALL_MARKER)
70
+ continue;
71
+ const absolute = path.join(directory, entry.name);
72
+ const stat = await fs.lstat(absolute);
73
+ const kind = stat.isDirectory() ? "directory"
74
+ : stat.isFile() ? "file"
75
+ : stat.isSymbolicLink() ? "symlink"
76
+ : "other";
77
+ updateHashField(hash, kind);
78
+ updateHashField(hash, relative);
79
+ if (kind === "file")
80
+ updateHashField(hash, await fs.readFile(absolute));
81
+ if (kind === "symlink")
82
+ updateHashField(hash, await fs.readlink(absolute));
83
+ if (kind === "other")
84
+ updateHashField(hash, `${stat.mode}:${stat.rdev}`);
85
+ if (kind === "directory")
86
+ await walk(absolute, relative);
87
+ }
88
+ };
89
+ await walk(root, "");
90
+ return hash.digest("hex");
91
+ }
92
+ function validMarker(value, skill) {
93
+ if (!value || typeof value !== "object")
94
+ return false;
95
+ const marker = value;
96
+ return marker.kind === "official-skill-copy"
97
+ && marker.skill === skill
98
+ && typeof marker.sourceVersion === "string"
99
+ && typeof marker.contentHash === "string"
100
+ && /^[0-9a-f]{64}$/.test(marker.contentHash);
101
+ }
102
+ async function readInstallMarker(target, skill) {
103
+ try {
104
+ const markerPath = path.join(target, INSTALL_MARKER);
105
+ if (!(await fs.lstat(markerPath)).isFile())
106
+ return undefined;
107
+ const marker = JSON.parse(await fs.readFile(markerPath, "utf8"));
108
+ return validMarker(marker, skill) ? marker : undefined;
109
+ }
110
+ catch (error) {
111
+ if (isMissing(error) || error instanceof SyntaxError)
112
+ return undefined;
113
+ throw error;
114
+ }
115
+ }
116
+ async function writeInstallMarker(target, skill, hash) {
117
+ const marker = {
118
+ kind: "official-skill-copy",
119
+ skill,
120
+ sourceVersion: VERSION,
121
+ contentHash: hash,
122
+ };
123
+ await fs.writeFile(path.join(target, INSTALL_MARKER), `${JSON.stringify(marker, null, 2)}\n`, "utf8");
124
+ }
53
125
  async function classifyTarget(skill, target, canonicalSource) {
54
126
  let stat;
55
127
  try {
@@ -61,21 +133,39 @@ async function classifyTarget(skill, target, canonicalSource) {
61
133
  throw error;
62
134
  }
63
135
  const kind = objectKind(stat);
64
- if (!stat.isSymbolicLink()) {
65
- return { skill, path: target, source: canonicalSource, kind: "foreign", foreignIdentity: { kind, dev: stat.dev, ino: stat.ino } };
66
- }
67
- const originalLink = await fs.readlink(target);
68
- try {
69
- const resolved = await fs.realpath(path.resolve(path.dirname(target), originalLink));
70
- return resolved === canonicalSource
71
- ? { skill, path: target, source: canonicalSource, kind: "owned", originalLink }
72
- : { skill, path: target, source: canonicalSource, kind: "foreign", originalLink, foreignIdentity: { kind, dev: stat.dev, ino: stat.ino, link: originalLink } };
73
- }
74
- catch (error) {
75
- if (isMissing(error))
76
- return { skill, path: target, source: canonicalSource, kind: "foreign", originalLink, foreignIdentity: { kind, dev: stat.dev, ino: stat.ino, link: originalLink } };
77
- throw error;
136
+ if (stat.isDirectory()) {
137
+ const marker = await readInstallMarker(target, skill);
138
+ if (marker) {
139
+ const installedHash = await contentHash(target);
140
+ if (installedHash === marker.contentHash) {
141
+ const sourceHash = await contentHash(canonicalSource);
142
+ return {
143
+ skill,
144
+ path: target,
145
+ source: canonicalSource,
146
+ kind: "owned",
147
+ needsUpgrade: marker.sourceVersion !== VERSION || marker.contentHash !== sourceHash,
148
+ };
149
+ }
150
+ return {
151
+ skill,
152
+ path: target,
153
+ source: canonicalSource,
154
+ kind: "foreign",
155
+ collisionReason: "modified",
156
+ foreignIdentity: { kind, dev: stat.dev, ino: stat.ino },
157
+ };
158
+ }
78
159
  }
160
+ const link = stat.isSymbolicLink() ? await fs.readlink(target) : undefined;
161
+ return {
162
+ skill,
163
+ path: target,
164
+ source: canonicalSource,
165
+ kind: "foreign",
166
+ collisionReason: "foreign",
167
+ foreignIdentity: { kind, dev: stat.dev, ino: stat.ino, link },
168
+ };
79
169
  }
80
170
  async function ensureParent(parent, createdDirectories) {
81
171
  const absentBeforeMutation = [];
@@ -159,9 +249,8 @@ async function rollbackAndVerify(originalError, states, createdDirectories) {
159
249
  }
160
250
  else if (state.kind === "owned") {
161
251
  try {
162
- const link = await fs.readlink(state.path);
163
- const resolved = await fs.realpath(path.resolve(path.dirname(state.path), link));
164
- if (link !== state.originalLink || resolved !== state.source) {
252
+ const marker = await readInstallMarker(state.path, state.skill);
253
+ if (!marker || await contentHash(state.path) !== marker.contentHash) {
165
254
  failures.push(`owned target changed at ${state.path}`);
166
255
  }
167
256
  }
@@ -210,17 +299,21 @@ export async function installOfficialSkills(force = false) {
210
299
  }
211
300
  const collisions = states.filter((state) => state.kind === "foreign");
212
301
  if (collisions.length > 0 && !force) {
213
- throw new FlowError("EMPTY_PARAM", `Foreign skill target already exists at ${collisions.map((state) => state.path).join(", ")}. Re-run with -f or --force to replace it.`);
302
+ const details = collisions.map((state) => state.collisionReason === "modified"
303
+ ? `${state.path} (owned fact exists but copied content was modified)`
304
+ : `${state.path} (foreign target or missing ownership fact)`);
305
+ throw new FlowError("EMPTY_PARAM", `Skill target collision at ${details.join(", ")}. Re-run with -f or --force to replace it.`);
214
306
  }
215
307
  const createdDirectories = [];
216
308
  try {
217
309
  for (const state of states) {
218
- if (state.kind === "owned")
310
+ if (state.kind === "owned" && !state.needsUpgrade)
219
311
  continue;
220
312
  await ensureParent(path.dirname(state.path), createdDirectories);
221
313
  state.temp = siblingArtifact(state.path, "tmp");
222
- await fs.symlink(state.source, state.temp, "dir");
223
- if (state.kind === "foreign") {
314
+ await fs.cp(state.source, state.temp, { recursive: true, errorOnExist: true, force: false });
315
+ await writeInstallMarker(state.temp, state.skill, await contentHash(state.temp));
316
+ if (state.kind === "foreign" || state.kind === "owned") {
224
317
  state.backup = siblingArtifact(state.path, "backup");
225
318
  await fs.rename(state.path, state.backup);
226
319
  }
@@ -248,7 +341,7 @@ export async function installOfficialSkills(force = false) {
248
341
  skill: state.skill,
249
342
  path: state.path,
250
343
  source: state.source,
251
- status: state.kind === "owned" ? "owned" : "installed",
344
+ status: state.installed ? "installed" : "owned",
252
345
  })),
253
346
  warnings,
254
347
  };
@@ -180,10 +180,9 @@ export function buildUncommissionedAddress(input) {
180
180
  inference: [...(input.inference ?? [])],
181
181
  };
182
182
  }
183
- export async function resolveUncommissionedRepositoryAddress(input) {
184
- const repo = await resolvedRepository(input.repoCwd);
183
+ export function buildUncommissionedRepositoryAddress(input) {
185
184
  return buildUncommissionedAddress({
186
- repo,
185
+ repo: { kind: "repository", stableRoot: input.stableRoot },
187
186
  workdir: input.workdir,
188
187
  binding: input.binding,
189
188
  inference: input.inference,
@@ -18,7 +18,7 @@ import { requireResumableArtifact } from "./transcripts.js";
18
18
  import { appendDebugBlock } from "../telemetry/debug-log.js";
19
19
  import { logInfo } from "../telemetry/logger.js";
20
20
  import { SubagentFailure } from "../agents/types.js";
21
- import { AddressResolutionError, buildResolutionAttempt, buildUncommissionedAddress, resolveContractAddress, resolveUncommissionedRepositoryAddress, withResolvedAddress, } from "./addressing.js";
21
+ import { AddressResolutionError, buildResolutionAttempt, buildUncommissionedAddress, buildUncommissionedRepositoryAddress, resolveContractAddress, withResolvedAddress, } from "./addressing.js";
22
22
  import { readLedger } from "./ledger.js";
23
23
  import { findOpenArcView } from "./arc.js";
24
24
  import { deriveContractState, isTerminalState } from "./status.js";
@@ -159,7 +159,7 @@ async function readCallKeiyakuContext(executionCwd, input) {
159
159
  address: buildUncommissionedAddress({
160
160
  repo: { kind: "user" },
161
161
  workdir: executionCwd,
162
- binding: input.bare ? "bare" : "unbound",
162
+ binding: "bare",
163
163
  }),
164
164
  };
165
165
  }
@@ -176,8 +176,8 @@ async function readCallKeiyakuContext(executionCwd, input) {
176
176
  historyCwd: ledgerCwd,
177
177
  profileCwd: ledgerCwd,
178
178
  projectionCwd: ledgerCwd,
179
- address: await resolveUncommissionedRepositoryAddress({
180
- repoCwd: ledgerCwd,
179
+ address: buildUncommissionedRepositoryAddress({
180
+ stableRoot: ledgerCwd,
181
181
  workdir: executionCwd,
182
182
  binding: input.reviveBinding.kind,
183
183
  }),
@@ -185,8 +185,8 @@ async function readCallKeiyakuContext(executionCwd, input) {
185
185
  }
186
186
  }
187
187
  if (input.bare && input.contractId) {
188
- const repositoryAddress = await resolveUncommissionedRepositoryAddress({
189
- repoCwd: ledgerCwd,
188
+ const repositoryAddress = buildUncommissionedRepositoryAddress({
189
+ stableRoot: ledgerCwd,
190
190
  workdir: executionCwd,
191
191
  binding: "bare",
192
192
  });
@@ -209,8 +209,8 @@ async function readCallKeiyakuContext(executionCwd, input) {
209
209
  historyCwd: ledgerCwd,
210
210
  profileCwd: ledgerCwd,
211
211
  projectionCwd: ledgerCwd,
212
- address: await resolveUncommissionedRepositoryAddress({
213
- repoCwd: ledgerCwd,
212
+ address: buildUncommissionedRepositoryAddress({
213
+ stableRoot: ledgerCwd,
214
214
  workdir: executionCwd,
215
215
  binding: "bare",
216
216
  }),
@@ -250,8 +250,8 @@ async function readCallKeiyakuContext(executionCwd, input) {
250
250
  historyCwd: ledgerCwd,
251
251
  profileCwd: ledgerCwd,
252
252
  projectionCwd: ledgerCwd,
253
- address: await resolveUncommissionedRepositoryAddress({
254
- repoCwd: ledgerCwd,
253
+ address: buildUncommissionedRepositoryAddress({
254
+ stableRoot: ledgerCwd,
255
255
  workdir: executionCwd,
256
256
  binding: "unbound",
257
257
  }),
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/generate-version.mjs
2
- export const VERSION = "2.8.2";
2
+ export const VERSION = "2.8.4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "2.8.2",
3
+ "version": "2.8.4",
4
4
  "description": "CLI for running iterative keiyaku workflows with Codex subagents.",
5
5
  "license": "MIT",
6
6
  "type": "module",