intentdna 1.9.1 → 1.9.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.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "intentdna",
11
11
  "description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
12
- "version": "1.9.1",
12
+ "version": "1.9.4",
13
13
  "author": {
14
14
  "name": "Samuel"
15
15
  },
@@ -25,5 +25,5 @@
25
25
  ]
26
26
  }
27
27
  ],
28
- "version": "1.9.1"
28
+ "version": "1.9.4"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.9.1",
3
+ "version": "1.9.4",
4
4
  "description": "Declarative policy layer for AI agent behavior",
5
5
  "author": {
6
6
  "name": "Samuel"
@@ -1,5 +1,7 @@
1
+ import { detectDNAConfigs } from "../project-config.js";
1
2
  import type { DNASourceProvenance, IntentDNA } from "../schema/types.js";
2
3
  import type { Diagnostic } from "./diagnostics.js";
4
+ export { detectDNAConfigs };
3
5
  export interface ResolveDNAInputOptions {
4
6
  cwd?: string;
5
7
  includeDiagnostics?: boolean;
@@ -11,7 +13,6 @@ export interface ResolvedDNAInputs {
11
13
  }
12
14
  export declare function parseDNAFile(filePath: string): Promise<IntentDNA>;
13
15
  export declare function resolveSpeciesReference(ref: string): string | null;
14
- export declare function detectDNAConfigs(projectDir?: string): Promise<string[]>;
15
16
  export interface ExpandDNAInputOptions {
16
17
  cwd?: string;
17
18
  enterprisePolicyDirs?: string[];
@@ -1,20 +1,14 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
- import { readdir, readFile, stat } from "node:fs/promises";
3
+ import { readFile, stat } from "node:fs/promises";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { TextDecoder } from "node:util";
7
+ import { detectDNAConfigs } from "../project-config.js";
7
8
  import { validateDNA } from "../schema/validate.js";
8
9
  import { parseYAML } from "../schema/yaml-parser.js";
9
10
  import { DNADiagnosticsError, hasDiagnosticErrors } from "./diagnostics.js";
10
- const DNA_CONFIG_CANDIDATES = [
11
- ".dna/config.yaml",
12
- ".dna/config.yml",
13
- ".dna/config.json",
14
- ".dna.yaml",
15
- ".dna.yml",
16
- ".dna.json",
17
- ];
11
+ export { detectDNAConfigs };
18
12
  async function fileExists(path) {
19
13
  try {
20
14
  await stat(path);
@@ -39,27 +33,6 @@ export function resolveSpeciesReference(ref) {
39
33
  const thisDir = dirname(fileURLToPath(import.meta.url));
40
34
  return resolve(thisDir, "..", "species", `${name}.dna.json`);
41
35
  }
42
- export async function detectDNAConfigs(projectDir = process.cwd()) {
43
- const configsDir = resolve(projectDir, ".dna", "configs");
44
- try {
45
- const files = await readdir(configsDir);
46
- const configs = files
47
- .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"))
48
- .sort()
49
- .map((file) => resolve(configsDir, file));
50
- if (configs.length > 0)
51
- return configs;
52
- }
53
- catch {
54
- // Directory does not exist.
55
- }
56
- for (const candidate of DNA_CONFIG_CANDIDATES) {
57
- const path = resolve(projectDir, candidate);
58
- if (await fileExists(path))
59
- return [path];
60
- }
61
- return [];
62
- }
63
36
  function fingerprint(snapshotBytes) {
64
37
  return `sha256:${createHash("sha256").update(snapshotBytes).digest("hex")}`;
65
38
  }
package/dist/hooks/cli.js CHANGED
@@ -20,6 +20,7 @@ import { spawn } from "node:child_process";
20
20
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
21
21
  import { createHash, randomUUID } from "node:crypto";
22
22
  import { fileURLToPath } from "node:url";
23
+ import { hasDNAProjectConfigBoundary } from "../project-config.js";
23
24
  import { readStdinResult, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
24
25
  import { isCurrentClaudeSubagentStopInput, validateHookInput } from "./schema.js";
25
26
  import { hookFailureOutput } from "./enforcement-boundary.js";
@@ -91,20 +92,19 @@ function isTerminalWorkflowDeactivation(event, rawInput, state) {
91
92
  return args.active === false && args.workflow === state.workflow;
92
93
  }
93
94
  async function hookProjectBoundaryExists(projectDir) {
94
- for (const marker of [".dna", ".git"]) {
95
- try {
96
- await stat(resolve(projectDir, marker));
95
+ try {
96
+ await stat(resolve(projectDir, ".git"));
97
+ return true;
98
+ }
99
+ catch (error) {
100
+ const code = error instanceof Error && "code" in error
101
+ ? error.code
102
+ : undefined;
103
+ if (code !== "ENOENT" && code !== "ENOTDIR")
97
104
  return true;
98
- }
99
- catch (error) {
100
- const code = error instanceof Error && "code" in error
101
- ? error.code
102
- : undefined;
103
- if (code !== "ENOENT" && code !== "ENOTDIR")
104
- return true;
105
- }
106
105
  }
107
- return false;
106
+ // Artifact-only directories such as .dna/reviews do not establish policy ownership.
107
+ return hasDNAProjectConfigBoundary(projectDir);
108
108
  }
109
109
  async function resolveHookRuntimePaths(cwd, irPath = DEFAULT_IR_PATH, explicitIRPath = false) {
110
110
  const initialDir = resolve(cwd);
@@ -0,0 +1,2 @@
1
+ export declare function detectDNAConfigs(projectDir?: string): Promise<string[]>;
2
+ export declare function hasDNAProjectConfigBoundary(projectDir: string): Promise<boolean>;
@@ -0,0 +1,71 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ const LEGACY_DNA_CONFIG_CANDIDATES = [
4
+ ".dna/config.yaml",
5
+ ".dna/config.yml",
6
+ ".dna/config.json",
7
+ ".dna.yaml",
8
+ ".dna.yml",
9
+ ".dna.json",
10
+ ];
11
+ const DNA_PROJECT_CONFIG_BOUNDARY_MARKERS = [
12
+ ".dna/config.yaml",
13
+ ".dna/config.yml",
14
+ ".dna/config.json",
15
+ ".dna/configs",
16
+ ".dna.yaml",
17
+ ".dna.yml",
18
+ ".dna.json",
19
+ ];
20
+ function filesystemErrorCode(error) {
21
+ return error instanceof Error && "code" in error
22
+ ? error.code
23
+ : undefined;
24
+ }
25
+ function pathIsAbsent(error) {
26
+ const code = filesystemErrorCode(error);
27
+ return code === "ENOENT" || code === "ENOTDIR";
28
+ }
29
+ async function fileExists(path) {
30
+ try {
31
+ await stat(path);
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ export async function detectDNAConfigs(projectDir = process.cwd()) {
39
+ const configsDir = resolve(projectDir, ".dna", "configs");
40
+ try {
41
+ const files = await readdir(configsDir);
42
+ const configs = files
43
+ .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"))
44
+ .sort()
45
+ .map((file) => resolve(configsDir, file));
46
+ if (configs.length > 0)
47
+ return configs;
48
+ }
49
+ catch {
50
+ // Directory does not exist or cannot be read.
51
+ }
52
+ for (const candidate of LEGACY_DNA_CONFIG_CANDIDATES) {
53
+ const path = resolve(projectDir, candidate);
54
+ if (await fileExists(path))
55
+ return [path];
56
+ }
57
+ return [];
58
+ }
59
+ export async function hasDNAProjectConfigBoundary(projectDir) {
60
+ for (const marker of DNA_PROJECT_CONFIG_BOUNDARY_MARKERS) {
61
+ try {
62
+ await stat(resolve(projectDir, marker));
63
+ return true;
64
+ }
65
+ catch (error) {
66
+ if (!pathIsAbsent(error))
67
+ return true;
68
+ }
69
+ }
70
+ return false;
71
+ }
@@ -255,6 +255,26 @@ function syncDirectory(path) {
255
255
  closeSync(descriptor);
256
256
  }
257
257
  }
258
+ function replaceFileSync(temporary, path) {
259
+ for (let attempt = 0;; attempt += 1) {
260
+ try {
261
+ renameSync(temporary, path);
262
+ return;
263
+ }
264
+ catch (error) {
265
+ const retryable = process.platform === "win32"
266
+ && ["EACCES", "EBUSY", "EPERM"].some((code) => isErrno(error, code));
267
+ if (!retryable || attempt >= 9) {
268
+ try {
269
+ unlinkSync(temporary);
270
+ }
271
+ catch { }
272
+ throw error;
273
+ }
274
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25 * (attempt + 1));
275
+ }
276
+ }
277
+ }
258
278
  function atomicWriteJson(path, value) {
259
279
  const directory = dirname(path);
260
280
  const temporary = join(directory, `.${basename(path)}.tmp.${process.pid}.${randomUUID()}`);
@@ -266,7 +286,7 @@ function atomicWriteJson(path, value) {
266
286
  finally {
267
287
  closeSync(descriptor);
268
288
  }
269
- renameSync(temporary, path);
289
+ replaceFileSync(temporary, path);
270
290
  syncDirectory(directory);
271
291
  }
272
292
  function readJson(path) {
@@ -1683,7 +1703,7 @@ export class LocalAttemptExecutionAuthority {
1683
1703
  finally {
1684
1704
  closeSync(descriptor);
1685
1705
  }
1686
- renameSync(temporary, path);
1706
+ replaceFileSync(temporary, path);
1687
1707
  syncDirectory(this.rootDirectory);
1688
1708
  return next;
1689
1709
  });
@@ -19,12 +19,29 @@ if (!recordPath) process.exit(64);
19
19
 
20
20
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
21
21
  const isErrno = (error, code) => error && error.code === code;
22
+ const renameWait = new Int32Array(new SharedArrayBuffer(4));
22
23
  const canonical = (value) => {
23
24
  if (value === null || typeof value !== "object") return JSON.stringify(value);
24
25
  if (Array.isArray(value)) return "[" + value.map(canonical).join(",") + "]";
25
26
  return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonical(value[key])).join(",") + "}";
26
27
  };
27
28
 
29
+ function replaceFile(temporary, target) {
30
+ for (let attempt = 0; ; attempt += 1) {
31
+ try {
32
+ fs.renameSync(temporary, target);
33
+ return;
34
+ } catch (error) {
35
+ const retryable = ["EACCES", "EBUSY", "EPERM"].some((code) => isErrno(error, code));
36
+ if (!retryable || attempt >= 9) {
37
+ try { fs.unlinkSync(temporary); } catch {}
38
+ throw error;
39
+ }
40
+ Atomics.wait(renameWait, 0, 0, 25 * (attempt + 1));
41
+ }
42
+ }
43
+ }
44
+
28
45
  function atomicWrite(target, value) {
29
46
  const directory = path.dirname(target);
30
47
  const temporary = path.join(directory, "." + path.basename(target) + ".tmp." + process.pid + "." + randomUUID());
@@ -35,7 +52,7 @@ function atomicWrite(target, value) {
35
52
  } finally {
36
53
  fs.closeSync(fd);
37
54
  }
38
- fs.renameSync(temporary, target);
55
+ replaceFile(temporary, target);
39
56
  try {
40
57
  const directoryFd = fs.openSync(directory, "r");
41
58
  try { fs.fsyncSync(directoryFd); } finally { fs.closeSync(directoryFd); }
@@ -18,9 +18,9 @@ export function createCodexExecutionProvider(options = {}) {
18
18
  "--sandbox",
19
19
  options.sandbox ?? "workspace-write",
20
20
  ...(options.extraArgs ?? []),
21
- packet.standalone_prompt,
21
+ "-",
22
22
  ],
23
- stdin: null,
23
+ stdin: packet.standalone_prompt,
24
24
  cwd: packet.workspace.working_directory,
25
25
  ...(options.env ? { env: options.env } : {}),
26
26
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.9.1",
3
+ "version": "1.9.4",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,7 +11,7 @@
11
11
  "dna-mcp": "dist/mcp/index.js"
12
12
  },
13
13
  "scripts": {
14
- "build": "tsc && mkdir -p dist/species dist/templates && cp src/species/*.json dist/species/ && cp src/templates/*.yaml dist/templates/ && node scripts/sync-plugin-version.cjs",
14
+ "build": "tsc && node scripts/copy-runtime-assets.mjs && node scripts/sync-plugin-version.cjs",
15
15
  "dev": "tsc --watch",
16
16
  "release:sync-plugin": "node scripts/sync-release-plugin.cjs",
17
17
  "verify:windows-job-keeper": "node scripts/verify-windows-job-keeper.mjs",