@lamentis/naome 1.3.4 → 1.3.6

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.
package/Cargo.lock CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.3.4"
79
+ version = "1.3.6"
80
80
  dependencies = [
81
81
  "naome-core",
82
82
  "serde_json",
@@ -84,7 +84,7 @@ dependencies = [
84
84
 
85
85
  [[package]]
86
86
  name = "naome-core"
87
- version = "1.3.4"
87
+ version = "1.3.6"
88
88
  dependencies = [
89
89
  "serde",
90
90
  "serde_json",
package/README.md CHANGED
@@ -50,8 +50,6 @@ naome route --prompt-file /path/to/prompt.txt --execute --json
50
50
  perfect on day one.
51
51
  - Records verification proof before a task can be treated as complete.
52
52
  - Keeps sync fast by making baseline and deep quality scans explicit.
53
- - Can install optional Codex hooks for earlier agent feedback without making
54
- hooks a human workflow requirement.
55
53
 
56
54
  ## Safety Model
57
55
 
@@ -99,8 +97,6 @@ After sync, NAOME writes the agent-facing workflow into `docs/naome/`:
99
97
  - `docs/naome/testing.md` maps change types to required checks.
100
98
  - `docs/naome/repository-quality.md` explains quality, structure, and cleanup
101
99
  policy.
102
- - `docs/naome/codex-hooks.md` explains the optional Codex hook acceleration
103
- layer.
104
100
 
105
101
  Agents should follow the repository's NAOME docs instead of guessing workflow
106
102
  rules from generic project files.
@@ -116,7 +112,6 @@ The main local policy files are:
116
112
  - `.naome/repository-structure.json` for path role, module, and directory
117
113
  structure policy.
118
114
  - `.naome/task-state.json` for active task state and proof.
119
- - `.codex/hooks.json` only when optional Codex hooks were explicitly enabled.
120
115
 
121
116
  Product defaults stay generic. Repository-specific policy belongs in the local
122
117
  `.naome/` config files.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.3.4"
3
+ version = "1.3.6"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.3.4"
3
+ version = "1.3.6"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -60,16 +60,6 @@ pub const LOCAL_NATIVE_BINARY_PATHS: &[&str] = &[
60
60
  ".naome/task-journal.jsonl",
61
61
  ];
62
62
 
63
- pub const OPTIONAL_CODEX_HOOK_PATHS: &[&str] = &[
64
- ".codex/config.toml",
65
- ".codex/hooks.json",
66
- ".naome/bin/codex-hook.js",
67
- ".naome/bin/codex-hook-io.js",
68
- ".naome/bin/codex-hook-policy.js",
69
- ".naome/bin/codex-hook-runtime.js",
70
- "docs/naome/codex-hooks.md",
71
- ];
72
-
73
63
  #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
74
64
  #[serde(rename_all = "camelCase")]
75
65
  pub struct InstallPlan {
@@ -78,7 +68,6 @@ pub struct InstallPlan {
78
68
  pub machine_owned: Vec<&'static str>,
79
69
  pub project_owned: Vec<&'static str>,
80
70
  pub local_only_machine_owned: Vec<&'static str>,
81
- pub optional_codex_hook_paths: Vec<&'static str>,
82
71
  pub gitignore_entries: Vec<&'static str>,
83
72
  pub git_exclude_entries: Vec<&'static str>,
84
73
  pub git_untrack_paths: Vec<&'static str>,
@@ -104,7 +93,6 @@ pub fn install_plan(harness_version: impl Into<String>) -> InstallPlan {
104
93
  machine_owned: MACHINE_OWNED_PATHS.to_vec(),
105
94
  project_owned: PROJECT_OWNED_PATHS.to_vec(),
106
95
  local_only_machine_owned: LOCAL_ONLY_MACHINE_OWNED_PATHS.to_vec(),
107
- optional_codex_hook_paths: OPTIONAL_CODEX_HOOK_PATHS.to_vec(),
108
96
  gitignore_entries,
109
97
  git_exclude_entries,
110
98
  git_untrack_paths,
@@ -37,8 +37,25 @@ fn git_add_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
37
37
  if paths.is_empty() {
38
38
  return Ok(());
39
39
  }
40
+ let tracked_paths = git_ls_files(root, &[], paths)?;
41
+ if !tracked_paths.is_empty() {
42
+ let mut update_args = vec!["add", "-u", "--"];
43
+ update_args.extend(tracked_paths.iter().map(String::as_str));
44
+ ensure_git_success(
45
+ Command::new("git")
46
+ .args(update_args)
47
+ .current_dir(root)
48
+ .output()?,
49
+ )?;
50
+ }
51
+
52
+ let untracked_paths = git_ls_files(root, &["--others", "--exclude-standard"], paths)?;
53
+ if untracked_paths.is_empty() {
54
+ return Ok(());
55
+ }
56
+
40
57
  let mut args = vec!["add", "--"];
41
- args.extend(paths.iter().map(String::as_str));
58
+ args.extend(untracked_paths.iter().map(String::as_str));
42
59
  let output = Command::new("git").args(args).current_dir(root).output()?;
43
60
  if output.status.success() {
44
61
  Ok(())
@@ -47,6 +64,31 @@ fn git_add_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
47
64
  }
48
65
  }
49
66
 
67
+ fn git_ls_files(
68
+ root: &Path,
69
+ mode_args: &[&str],
70
+ paths: &[String],
71
+ ) -> Result<Vec<String>, NaomeError> {
72
+ let mut args = vec!["ls-files"];
73
+ args.extend(mode_args);
74
+ args.extend(["-z", "--"]);
75
+ args.extend(paths.iter().map(String::as_str));
76
+ let output = Command::new("git").args(args).current_dir(root).output()?;
77
+ if output.status.success() {
78
+ Ok(split_nul_paths(&output.stdout))
79
+ } else {
80
+ Err(NaomeError::new(command_output(&output)))
81
+ }
82
+ }
83
+
84
+ fn split_nul_paths(bytes: &[u8]) -> Vec<String> {
85
+ String::from_utf8_lossy(bytes)
86
+ .split('\0')
87
+ .filter(|path| !path.is_empty())
88
+ .map(ToString::to_string)
89
+ .collect()
90
+ }
91
+
50
92
  pub(super) fn git_commit(root: &Path, message: &str) -> Result<(), NaomeError> {
51
93
  ensure_git_success(git_output(root, &["commit", "-m", message])?)
52
94
  }
@@ -64,9 +106,12 @@ pub(super) fn ensure_git_success(output: Output) -> Result<(), NaomeError> {
64
106
  }
65
107
 
66
108
  pub(super) fn command_output(output: &std::process::Output) -> String {
67
- let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
68
- if !stderr.is_empty() {
69
- return stderr;
109
+ let stderr = String::from_utf8_lossy(&output.stderr);
110
+ let stdout = String::from_utf8_lossy(&output.stdout);
111
+ for message in [stderr.trim(), stdout.trim()] {
112
+ if !message.is_empty() {
113
+ return message.to_string();
114
+ }
70
115
  }
71
- String::from_utf8_lossy(&output.stdout).trim().to_string()
116
+ format!("git exited with status {}", output.status)
72
117
  }
@@ -19,17 +19,6 @@ fn install_plan_marks_machine_docs_and_bins_local_only() {
19
19
  assert!(!plan
20
20
  .local_only_machine_owned
21
21
  .contains(&"docs/naome/architecture.md"));
22
- assert!(plan
23
- .optional_codex_hook_paths
24
- .contains(&".codex/hooks.json"));
25
- assert!(plan
26
- .optional_codex_hook_paths
27
- .contains(&".codex/config.toml"));
28
- assert!(plan
29
- .optional_codex_hook_paths
30
- .contains(&".naome/bin/codex-hook.js"));
31
- assert!(!plan.machine_owned.contains(&".naome/bin/codex-hook.js"));
32
- assert!(!plan.project_owned.contains(&".codex/hooks.json"));
33
22
  }
34
23
 
35
24
  #[test]
@@ -34,16 +34,6 @@ export function createInstallerContext() {
34
34
  installPlan: null,
35
35
  machineOwnedPaths: [],
36
36
  projectOwnedPaths: [],
37
- optionalCodexHookPaths: [
38
- ".codex/config.toml",
39
- ".codex/hooks.json",
40
- ".naome/bin/codex-hook.js",
41
- ".naome/bin/codex-hook-io.js",
42
- ".naome/bin/codex-hook-policy.js",
43
- ".naome/bin/codex-hook-runtime.js",
44
- "docs/naome/codex-hooks.md",
45
- ],
46
- codexHooksEnabled: false,
47
37
  localOnlyMachineOwnedPaths: [],
48
38
  localOnlyGitIgnoreEntries: [],
49
39
  localOnlyGitExcludeEntries: [],
@@ -30,10 +30,6 @@ export function copyTemplateFile(ctx, sourcePath) {
30
30
  const relativePath = relative(ctx.templateRoot, sourcePath);
31
31
  const targetPath = join(ctx.targetRoot, relativePath);
32
32
 
33
- if (ctx.optionalCodexHookPaths.includes(relativePath) && !ctx.codexHooksEnabled) {
34
- return;
35
- }
36
-
37
33
  if (hasSymlinkInTargetPath(ctx, relativePath)) {
38
34
  ctx.skipped.push(relativePath);
39
35
  ctx.unsafeSkipped.push(relativePath);
@@ -1,3 +1,5 @@
1
+ import { posix } from "node:path";
2
+
1
3
  import { ensureArchiveDirectory, copyTemplateFile, walk } from "./filesystem.js";
2
4
  import {
3
5
  ensureBuiltInVerificationChecks,
@@ -14,14 +16,23 @@ import {
14
16
  refreshManifestHealthMetadata,
15
17
  } from "./manifest-state.js";
16
18
  import { installNativeDecisionBinary, patchInstalledMachineOwnedIntegrity } from "./native.js";
19
+ import { removeLegacyHarnessFile } from "./harness-file-ops.js";
17
20
  import { printError } from "./output.js";
18
21
  import { compareVersions } from "./version.js";
19
22
  import { confirmAgentsTakeover, takeoverExistingAgents } from "./agents.js";
20
- import { ensureCodexHooks, resolveCodexHooksPreference } from "./codex-hooks.js";
23
+
24
+ const legacyOptionalHookPaths = [
25
+ ".codex/config.toml",
26
+ ".codex/hooks.json",
27
+ ".naome/bin/codex-hook-io.js",
28
+ ".naome/bin/codex-hook-policy.js",
29
+ ".naome/bin/codex-hook-runtime.js",
30
+ ".naome/bin/codex-hook.js",
31
+ "docs/naome/codex-hooks.md",
32
+ ];
21
33
 
22
34
  export async function runFreshInstall(ctx) {
23
35
  await confirmAgentsTakeover(ctx);
24
- await resolveCodexHooksPreference(ctx);
25
36
 
26
37
  for (const sourcePath of walk(ctx.templateRoot)) {
27
38
  copyTemplateFile(ctx, sourcePath);
@@ -54,10 +65,14 @@ export async function runExistingInstall(ctx, existingInstall) {
54
65
  }
55
66
 
56
67
  ensureArchiveDirectory(ctx);
57
- await runRepair(ctx, existingInstall.version, { fromVersion: existingInstall.version });
68
+ await runRepair(ctx, existingInstall.version, {
69
+ existingManifest: existingInstall.manifest,
70
+ fromVersion: existingInstall.version,
71
+ retireLegacyOptionalHooks: true,
72
+ });
58
73
  } else {
59
74
  ensureArchiveDirectory(ctx);
60
- await runRepair(ctx, existingInstall.version);
75
+ await runRepair(ctx, existingInstall.version, { existingManifest: existingInstall.manifest });
61
76
  }
62
77
  }
63
78
 
@@ -72,17 +87,65 @@ function rejectUnsupportedHistoricalInstall(ctx, version) {
72
87
 
73
88
  async function runRepair(ctx, version, options = {}) {
74
89
  ctx.summaryTitle = "NAOME harness checked";
75
- await resolveCodexHooksPreference(ctx);
76
90
  ensureCoreHarnessFiles(ctx, `repair-${version}`);
77
91
  ensureTaskControlHarnessFiles(ctx, `repair-${version}`);
78
92
  ensureHarnessHealthFiles(ctx, `repair-${version}`);
79
- ensureCodexHooks(ctx, `repair-${version}`);
80
93
  installNativeDecisionBinary(ctx);
81
94
  patchInstalledMachineOwnedIntegrity(ctx);
82
95
  ensureBuiltInVerificationChecks(ctx);
83
96
  ensureTestingProofHarnessSections(ctx);
84
97
  ensureRepositoryStructurePolicyFiles(ctx);
98
+ removeRetiredMachineOwnedFiles(ctx, options.existingManifest, `repair-${version}`, {
99
+ extraPaths: options.retireLegacyOptionalHooks ? legacyOptionalHookPaths : [],
100
+ });
85
101
  refreshManifestHealthMetadata(ctx);
86
102
  ensureCompleteUpgradeState(ctx, options.fromVersion ?? null);
87
103
  ensureLocalOnlySourceControlBoundary(ctx);
88
104
  }
105
+
106
+ function removeRetiredMachineOwnedFiles(ctx, manifest, archiveDirName, options = {}) {
107
+ const currentOwnedPaths = new Set([
108
+ ...ctx.machineOwnedPaths,
109
+ ...ctx.projectOwnedPaths,
110
+ ...ctx.localOnlyMachineOwnedPaths,
111
+ ctx.nativeBinaryRelativePath,
112
+ ].filter(Boolean));
113
+ const retiredPaths = [
114
+ ...(Array.isArray(manifest?.machineOwned) ? manifest.machineOwned : []),
115
+ ...(Array.isArray(options.extraPaths) ? options.extraPaths : []),
116
+ ];
117
+
118
+ for (const relativePath of [...new Set(retiredPaths)]) {
119
+ if (!isSafeManifestPath(relativePath)) {
120
+ const pathLabel = String(relativePath);
121
+ ctx.skipped.push(pathLabel);
122
+ ctx.unsafeSkipped.push(pathLabel);
123
+ continue;
124
+ }
125
+
126
+ if (currentOwnedPaths.has(relativePath)) {
127
+ continue;
128
+ }
129
+
130
+ removeLegacyHarnessFile(ctx, relativePath, archiveDirName);
131
+ }
132
+ }
133
+
134
+ function isSafeManifestPath(relativePath) {
135
+ if (
136
+ typeof relativePath !== "string" ||
137
+ relativePath.length === 0 ||
138
+ relativePath.includes("\0") ||
139
+ relativePath.includes("\\") ||
140
+ relativePath.includes(":")
141
+ ) {
142
+ return false;
143
+ }
144
+
145
+ if (posix.isAbsolute(relativePath)) {
146
+ return false;
147
+ }
148
+
149
+ const normalizedPath = posix.normalize(relativePath);
150
+ return normalizedPath === relativePath && normalizedPath !== "." && !normalizedPath.startsWith("../");
151
+ }
@@ -39,6 +39,10 @@ export function ensureTemplateFile(ctx, relativePath) {
39
39
  }
40
40
 
41
41
  export function removeBranchControlledHookShim(ctx, relativePath, archiveDirName) {
42
+ removeLegacyHarnessFile(ctx, relativePath, archiveDirName);
43
+ }
44
+
45
+ export function removeLegacyHarnessFile(ctx, relativePath, archiveDirName) {
42
46
  const targetPath = join(ctx.targetRoot, relativePath);
43
47
  if (!existsSync(targetPath)) {
44
48
  ctx.skipped.push(relativePath);
@@ -34,9 +34,6 @@ function parseInstallPlan(ctx, output) {
34
34
  assertInstallPlanArray(ctx, installPlan, "machineOwned");
35
35
  assertInstallPlanArray(ctx, installPlan, "projectOwned");
36
36
  assertInstallPlanArray(ctx, installPlan, "localOnlyMachineOwned");
37
- if (installPlan.optionalCodexHookPaths !== undefined) {
38
- assertInstallPlanArray(ctx, installPlan, "optionalCodexHookPaths");
39
- }
40
37
  assertInstallPlanArray(ctx, installPlan, "gitignoreEntries");
41
38
  assertInstallPlanArray(ctx, installPlan, "gitExcludeEntries");
42
39
  assertInstallPlanArray(ctx, installPlan, "gitUntrackPaths");
@@ -63,7 +60,6 @@ function assignInstallPlan(ctx, installPlan) {
63
60
  ctx.machineOwnedPaths = installPlan.machineOwned;
64
61
  ctx.projectOwnedPaths = installPlan.projectOwned;
65
62
  ctx.localOnlyMachineOwnedPaths = installPlan.localOnlyMachineOwned;
66
- ctx.optionalCodexHookPaths = installPlan.optionalCodexHookPaths ?? ctx.optionalCodexHookPaths;
67
63
  ctx.localOnlyGitIgnoreEntries = installPlan.gitignoreEntries;
68
64
  ctx.localOnlyGitExcludeEntries = installPlan.gitExcludeEntries;
69
65
  ctx.localOnlyGitUntrackPaths = installPlan.gitUntrackPaths;
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "Native-first CLI for the NAOME agent harness.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,7 +10,6 @@
10
10
  "ai",
11
11
  "harness",
12
12
  "repository",
13
- "codex",
14
13
  "claude"
15
14
  ],
16
15
  "repository": {
@@ -11,7 +11,7 @@ const nativeBinaryName = process.platform === "win32" ? "naome.exe" : "naome";
11
11
  const expectedMachineOwnedIntegrity = Object.freeze({
12
12
  ".naome/bin/check-harness-health.js": "sha256:dc4de52b79c69600b9ba47b924e2c2b8de61a2cbfab6d1ccc0f1924d963db657",
13
13
  ".naome/bin/check-task-state.js": "sha256:df54489a22b426180266e5e0fb5f9ec381477419f688435248afbf2b9b38ea81",
14
- ".naome/bin/naome.js": "sha256:d343f367da21bf45272331eec2ea34862a0bf056978780c62d6cae02e0f7993a",
14
+ ".naome/bin/naome.js": "sha256:a34c2e50a68d15ff2722a57590ccddc504e025d3c98ea62fd700d7ec1a789b9a",
15
15
  ".naome/package.json": "sha256:8005a3491db7d92f36ac66369861589f9c47123d3a7c71e643fc2c06168cd45a",
16
16
  ".naome/task-contract.schema.json": "sha256:1b3b62350328d0d6d660e36d1d1baaa2b88718530db774f9ab2a9e2fcba369c8",
17
17
  "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
@@ -11,7 +11,7 @@ const nativeBinaryName = process.platform === "win32" ? "naome.exe" : "naome";
11
11
  const expectedMachineOwnedIntegrity = Object.freeze({
12
12
  ".naome/bin/check-harness-health.js": "sha256:dc4de52b79c69600b9ba47b924e2c2b8de61a2cbfab6d1ccc0f1924d963db657",
13
13
  ".naome/bin/check-task-state.js": "sha256:df54489a22b426180266e5e0fb5f9ec381477419f688435248afbf2b9b38ea81",
14
- ".naome/bin/naome.js": "sha256:d343f367da21bf45272331eec2ea34862a0bf056978780c62d6cae02e0f7993a",
14
+ ".naome/bin/naome.js": "sha256:a34c2e50a68d15ff2722a57590ccddc504e025d3c98ea62fd700d7ec1a789b9a",
15
15
  ".naome/package.json": "sha256:8005a3491db7d92f36ac66369861589f9c47123d3a7c71e643fc2c06168cd45a",
16
16
  ".naome/task-contract.schema.json": "sha256:1b3b62350328d0d6d660e36d1d1baaa2b88718530db774f9ab2a9e2fcba369c8",
17
17
  "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
@@ -12,7 +12,7 @@ const expectedNativeBinaryIntegrity = "sha256:generated";
12
12
  function main(argv) {
13
13
  const [command, ...args] = argv;
14
14
 
15
- if (isHelpRequest(argv)) {
15
+ if (["help", "--help", "-h"].includes(argv[0]) || ["help", "--help", "-h"].includes(argv[1])) {
16
16
  printHelp();
17
17
  process.exit(0);
18
18
  }
@@ -36,10 +36,6 @@ function main(argv) {
36
36
  process.exit(command ? 1 : 0);
37
37
  }
38
38
 
39
- function isHelpRequest(args) {
40
- return ["help", "--help", "-h"].includes(args[0]) || ["help", "--help", "-h"].includes(args[1]);
41
- }
42
-
43
39
  function runNativeDecisionCommand(command, args) {
44
40
  const root = findHarnessRoot(process.cwd());
45
41
  if (!root) {
@@ -173,7 +169,22 @@ function commitBaseline(root, message, options = {}) {
173
169
  }
174
170
 
175
171
  const before = gitHead(root);
176
- runGitOrExit(root, ["add", "-A", "--", ...changedPaths]);
172
+ const tracked = runGit(root, ["ls-files", "-z", "--", ...changedPaths]);
173
+ if (tracked.status !== 0) {
174
+ fail(`Cannot determine tracked task paths: ${tracked.stderr.trim() || tracked.stdout.trim()}`);
175
+ }
176
+ const trackedPaths = tracked.stdout.split("\0").filter(Boolean);
177
+ if (trackedPaths.length > 0) {
178
+ runGitOrExit(root, ["add", "-u", "--", ...trackedPaths]);
179
+ }
180
+ const untracked = runGit(root, ["ls-files", "--others", "--exclude-standard", "-z", "--", ...changedPaths]);
181
+ if (untracked.status !== 0) {
182
+ fail(`Cannot determine untracked task paths: ${untracked.stderr.trim() || untracked.stdout.trim()}`);
183
+ }
184
+ const untrackedPaths = untracked.stdout.split("\0").filter(Boolean);
185
+ if (untrackedPaths.length > 0) {
186
+ runGitOrExit(root, ["add", "--", ...untrackedPaths]);
187
+ }
177
188
  runOrExit(root, [process.execPath, [join(root, ".naome", "bin", "check-task-state.js"), "--commit-gate"]]);
178
189
  runGitOrExit(root, ["commit", "-m", message]);
179
190
  return { before, after: gitHead(root) };
@@ -356,47 +367,7 @@ function findAncestorWithAnyMarker(startPath, markers) {
356
367
  }
357
368
 
358
369
  function printHelp() {
359
- const commands = [
360
- "naome status [--json]",
361
- "naome next [--json]",
362
- "naome intent --prompt-file <path> [--json]",
363
- "naome intent --prompt <text> [--json]",
364
- "naome route --prompt-file <path> [--execute] [--json]",
365
- "naome route --prompt <text> [--execute] [--json]",
366
- "naome explain --prompt-file <path> [--json]",
367
- "naome explain --prompt <text> [--json]",
368
- "naome context select --changed [--json]",
369
- "naome context select --prompt-file <path> [--json]",
370
- "naome context select --prompt <text> [--json]",
371
- "naome doctor [--json]",
372
- "naome task render-state [--write] [--json]",
373
- "naome task migrate-ledger [--write] [--json]",
374
- "naome quality init [--baseline|--deep-baseline] [--json]",
375
- "naome quality check --changed [--include-scanned-paths] [--json]",
376
- "naome quality check --path <path> [--path <path>...] [--include-scanned-paths] [--json]",
377
- "naome quality report [--deep] [--include-scanned-paths] [--json]",
378
- "naome quality cache status [--json]",
379
- "naome quality cache clear",
380
- "naome semantic report [--deep] [--json]",
381
- "naome semantic check --changed [--json]",
382
- "naome semantic check --path <path> [--path <path>...] [--json]",
383
- "naome semantic route --finding <id> [--json]",
384
- "naome semantic loop [--json]",
385
- "naome repo model [--write] [--json]",
386
- "naome repo check [--json]",
387
- "naome repo explain --path <path> [--json]",
388
- "naome structure report [--json]",
389
- "naome structure explain --path <path> [--json]",
390
- "naome cleanup plan [--json]",
391
- "naome cleanup route --path <path> [--json]",
392
- "naome refresh-integrity [--json]",
393
- "naome workflow agent-plan|context-delta|proof-plan|capabilities|edit-watchdog|decision-gate|digest [--json]",
394
- "naome workflow search-profile|check-search|phases|processes|mutations [--json]",
395
- "naome install",
396
- "naome sync",
397
- "naome commit -m \"type(scope): message\"",
398
- "node .naome/bin/naome.js commit -m \"type(scope): message\""
399
- ];
370
+ const commands = "naome status [--json]|naome next [--json]|naome intent --prompt-file <path> [--json]|naome intent --prompt <text> [--json]|naome route --prompt-file <path> [--execute] [--json]|naome route --prompt <text> [--execute] [--json]|naome explain --prompt-file <path> [--json]|naome explain --prompt <text> [--json]|naome context select --changed [--json]|naome context select --prompt-file <path> [--json]|naome context select --prompt <text> [--json]|naome doctor [--json]|naome task render-state [--write] [--json]|naome task migrate-ledger [--write] [--json]|naome quality init [--baseline|--deep-baseline] [--json]|naome quality check --changed [--include-scanned-paths] [--json]|naome quality check --path <path> [--path <path>...] [--include-scanned-paths] [--json]|naome quality report [--deep] [--include-scanned-paths] [--json]|naome quality cache status [--json]|naome quality cache clear|naome semantic report [--deep] [--json]|naome semantic check --changed [--json]|naome semantic check --path <path> [--path <path>...] [--json]|naome semantic route --finding <id> [--json]|naome semantic loop [--json]|naome repo model [--write] [--json]|naome repo check [--json]|naome repo explain --path <path> [--json]|naome structure report [--json]|naome structure explain --path <path> [--json]|naome cleanup plan [--json]|naome cleanup route --path <path> [--json]|naome refresh-integrity [--json]|naome workflow agent-plan|context-delta|proof-plan|capabilities|edit-watchdog|decision-gate|digest [--json]|naome workflow search-profile|check-search|phases|processes|mutations [--json]|naome install|naome sync|naome commit -m \"type(scope): message\"|node .naome/bin/naome.js commit -m \"type(scope): message\"".split("|");
400
371
  console.log(["Usage:", ...commands.map((command) => ` ${command}`)].join("\n"));
401
372
  }
402
373
 
@@ -4,7 +4,7 @@
4
4
  "integrity": {
5
5
  ".naome/bin/check-harness-health.js": "sha256:dc4de52b79c69600b9ba47b924e2c2b8de61a2cbfab6d1ccc0f1924d963db657",
6
6
  ".naome/bin/check-task-state.js": "sha256:df54489a22b426180266e5e0fb5f9ec381477419f688435248afbf2b9b38ea81",
7
- ".naome/bin/naome.js": "sha256:d343f367da21bf45272331eec2ea34862a0bf056978780c62d6cae02e0f7993a",
7
+ ".naome/bin/naome.js": "sha256:a34c2e50a68d15ff2722a57590ccddc504e025d3c98ea62fd700d7ec1a789b9a",
8
8
  ".naome/package.json": "sha256:8005a3491db7d92f36ac66369861589f9c47123d3a7c71e643fc2c06168cd45a",
9
9
  ".naome/task-contract.schema.json": "sha256:1b3b62350328d0d6d660e36d1d1baaa2b88718530db774f9ab2a9e2fcba369c8",
10
10
  "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
@@ -17,10 +17,6 @@ touched-file feedback. This catches local size, symbol, duplicate, structure,
17
17
  and stale-policy issues before the task grows. It does not replace the final
18
18
  `repository-quality-check`; always run the changed-file gate before completion.
19
19
 
20
- Optional Codex hooks can provide the same kind of early feedback during an
21
- agent run. They are acceleration only; final proof still comes from the
22
- commands in this document and `.naome/verification.json`.
23
-
24
20
  ## Known Checks
25
21
 
26
22
  | Check id | Command | Cwd | Cost | Last verified |
@@ -1,121 +0,0 @@
1
- import { createInterface } from "node:readline/promises";
2
- import { stdin as input, stdout as output } from "node:process";
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { join } from "node:path";
5
-
6
- import { replaceHarnessFile } from "./harness-file-ops.js";
7
- import { printSection } from "./output.js";
8
-
9
- const HOOK_CONFIG_PATH = ".codex/hooks.json";
10
- const HOOK_FEATURE_CONFIG_PATH = ".codex/config.toml";
11
- const HOOK_DISPATCHER_PATH = ".naome/bin/codex-hook.js";
12
- const HOOK_IO_PATH = ".naome/bin/codex-hook-io.js";
13
- const HOOK_POLICY_PATH = ".naome/bin/codex-hook-policy.js";
14
- const HOOK_RUNTIME_PATH = ".naome/bin/codex-hook-runtime.js";
15
- const HOOK_DOC_PATH = "docs/naome/codex-hooks.md";
16
-
17
- const CODEX_HOOK_MACHINE_PATHS = [
18
- HOOK_FEATURE_CONFIG_PATH,
19
- HOOK_CONFIG_PATH,
20
- HOOK_DISPATCHER_PATH,
21
- HOOK_IO_PATH,
22
- HOOK_POLICY_PATH,
23
- HOOK_RUNTIME_PATH,
24
- HOOK_DOC_PATH,
25
- ];
26
- const CODEX_HOOK_EXECUTABLE_PATHS = [HOOK_DISPATCHER_PATH];
27
-
28
- export async function resolveCodexHooksPreference(ctx) {
29
- const override = parseCodexHooksOverride(process.env.NAOME_CODEX_HOOKS);
30
- if (override !== null) {
31
- setCodexHooksEnabled(ctx, override);
32
- return;
33
- }
34
-
35
- if (hasManagedCodexHooks(ctx)) {
36
- setCodexHooksEnabled(ctx, true);
37
- return;
38
- }
39
-
40
- if (!process.stdin.isTTY || process.env.CI === "true") {
41
- setCodexHooksEnabled(ctx, false);
42
- return;
43
- }
44
-
45
- printSection(ctx, "Optional Codex Hooks");
46
- console.log(`${ctx.color.dim("purpose")} earlier NAOME feedback for Codex agents`);
47
- console.log(`${ctx.color.dim("scope ")} repo-local acceleration only; normal NAOME gates remain authoritative`);
48
- console.log(`${ctx.color.dim("human ")} optional; declining keeps install and sync working`);
49
- console.log("");
50
-
51
- const rl = createInterface({ input, output });
52
- const answer = await rl.question(`${ctx.color.yellow("?")} Install optional Codex Hooks for this repository? (y/N) `);
53
- rl.close();
54
-
55
- setCodexHooksEnabled(ctx, answer.trim().toLowerCase() === "y");
56
- }
57
-
58
- export function ensureCodexHooks(ctx, archiveDirName) {
59
- if (!ctx.codexHooksEnabled) {
60
- return;
61
- }
62
-
63
- ensureCodexHookManifestPaths(ctx);
64
- for (const relativePath of ctx.optionalCodexHookPaths) {
65
- replaceHarnessFile(ctx, relativePath, archiveDirName);
66
- }
67
- }
68
-
69
- export function ensureCodexHookManifestPaths(ctx) {
70
- for (const relativePath of CODEX_HOOK_MACHINE_PATHS) {
71
- appendUnique(ctx.machineOwnedPaths, relativePath);
72
- appendUnique(ctx.localOnlyMachineOwnedPaths, relativePath);
73
- appendUnique(ctx.localOnlyGitExcludeEntries, relativePath);
74
- appendUnique(ctx.localOnlyGitUntrackPaths, relativePath);
75
- }
76
- for (const relativePath of CODEX_HOOK_EXECUTABLE_PATHS) {
77
- ctx.executableMachineOwnedPaths.add(relativePath);
78
- }
79
- }
80
-
81
- function setCodexHooksEnabled(ctx, enabled) {
82
- ctx.codexHooksEnabled = enabled;
83
- if (enabled) {
84
- ensureCodexHookManifestPaths(ctx);
85
- }
86
- }
87
-
88
- function hasManagedCodexHooks(ctx) {
89
- const hookConfigPath = join(ctx.targetRoot, HOOK_CONFIG_PATH);
90
- if (!existsSync(hookConfigPath)) {
91
- return false;
92
- }
93
-
94
- try {
95
- const hooks = JSON.parse(readFileSync(hookConfigPath, "utf8"));
96
- return hooks.schema === "naome.codex-hooks.v1";
97
- } catch {
98
- return false;
99
- }
100
- }
101
-
102
- function parseCodexHooksOverride(value) {
103
- if (value === undefined) {
104
- return null;
105
- }
106
-
107
- const normalized = value.trim().toLowerCase();
108
- if (["1", "true", "yes", "y", "on"].includes(normalized)) {
109
- return true;
110
- }
111
- if (["0", "false", "no", "n", "off"].includes(normalized)) {
112
- return false;
113
- }
114
- return null;
115
- }
116
-
117
- function appendUnique(values, value) {
118
- if (!values.includes(value)) {
119
- values.push(value);
120
- }
121
- }
@@ -1,2 +0,0 @@
1
- [features]
2
- codex_hooks = true
@@ -1,70 +0,0 @@
1
- {
2
- "schema": "naome.codex-hooks.v1",
3
- "version": 1,
4
- "hooks": {
5
- "SessionStart": [
6
- {
7
- "matcher": "startup|resume|clear",
8
- "hooks": [
9
- {
10
- "type": "command",
11
- "command": "node .naome/bin/codex-hook.js --event SessionStart"
12
- }
13
- ]
14
- }
15
- ],
16
- "UserPromptSubmit": [
17
- {
18
- "hooks": [
19
- {
20
- "type": "command",
21
- "command": "node .naome/bin/codex-hook.js --event UserPromptSubmit"
22
- }
23
- ]
24
- }
25
- ],
26
- "PreToolUse": [
27
- {
28
- "matcher": "*",
29
- "hooks": [
30
- {
31
- "type": "command",
32
- "command": "node .naome/bin/codex-hook.js --event PreToolUse"
33
- }
34
- ]
35
- }
36
- ],
37
- "PermissionRequest": [
38
- {
39
- "matcher": "*",
40
- "hooks": [
41
- {
42
- "type": "command",
43
- "command": "node .naome/bin/codex-hook.js --event PermissionRequest"
44
- }
45
- ]
46
- }
47
- ],
48
- "PostToolUse": [
49
- {
50
- "matcher": "*",
51
- "hooks": [
52
- {
53
- "type": "command",
54
- "command": "node .naome/bin/codex-hook.js --event PostToolUse"
55
- }
56
- ]
57
- }
58
- ],
59
- "Stop": [
60
- {
61
- "hooks": [
62
- {
63
- "type": "command",
64
- "command": "node .naome/bin/codex-hook.js --event Stop"
65
- }
66
- ]
67
- }
68
- ]
69
- }
70
- }
@@ -1,122 +0,0 @@
1
- "use strict";
2
-
3
- const { createHash } = require("node:crypto");
4
- const { existsSync, mkdirSync, readFileSync, writeFileSync } = require("node:fs");
5
- const { dirname } = require("node:path");
6
- const { spawnSync } = require("node:child_process");
7
-
8
- const CACHE = ".naome/cache/codex-hook-state.json";
9
-
10
- function readPayload() {
11
- try {
12
- return JSON.parse(readFileSync(0, "utf8") || "{}");
13
- } catch {
14
- return {};
15
- }
16
- }
17
-
18
- function eventName(payload) {
19
- const index = process.argv.indexOf("--event");
20
- return index !== -1 && process.argv[index + 1] ? process.argv[index + 1] : payload?.hook_event_name || payload?.event || "";
21
- }
22
-
23
- function commandOf(payload) {
24
- return [payload?.tool_input?.command, payload?.toolInput?.command, payload?.command].find((value) => typeof value === "string" && value);
25
- }
26
-
27
- function promptOf(payload) {
28
- return [payload?.prompt, payload?.user_prompt, payload?.tool_input?.prompt, payload?.toolInput?.prompt, payload?.reason, payload?.message].find((value) => typeof value === "string" && value) || "";
29
- }
30
-
31
- function readJson(path) {
32
- try {
33
- return existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : null;
34
- } catch {
35
- return null;
36
- }
37
- }
38
-
39
- function parseJson(value) {
40
- try {
41
- return JSON.parse(value);
42
- } catch {
43
- return null;
44
- }
45
- }
46
-
47
- function readCache() {
48
- return readJson(CACHE) || {};
49
- }
50
-
51
- function writeCache(value) {
52
- try {
53
- mkdirSync(dirname(CACHE), { recursive: true });
54
- writeFileSync(CACHE, `${JSON.stringify(value)}\n`);
55
- } catch {
56
- // Best-effort cache only.
57
- }
58
- }
59
-
60
- function run(command, args, env = process.env) {
61
- return spawnSync(command, args, { cwd: process.cwd(), encoding: "utf8", env });
62
- }
63
-
64
- function runGit(args) {
65
- return run("git", args);
66
- }
67
-
68
- function runNaome(args) {
69
- return run(process.execPath, [".naome/bin/naome.js", ...args], { ...process.env, NO_COLOR: "1" });
70
- }
71
-
72
- function taskCheck(args) {
73
- return run(process.execPath, [".naome/bin/check-task-state.js", ...args]);
74
- }
75
-
76
- function diffSignature() {
77
- return createHash("sha256")
78
- .update(runGit(["status", "--porcelain=v1"]).stdout || "")
79
- .update(runGit(["diff", "--no-ext-diff", "HEAD", "--"]).stdout || "")
80
- .digest("hex");
81
- }
82
-
83
- function decide(decision, reason, messages) {
84
- write(decisionObject(decision, reason, messages));
85
- }
86
-
87
- function decisionObject(decision, reason, messages) {
88
- if (decision !== "block") {
89
- return {};
90
- }
91
- const detail = messages.filter(Boolean).join("; ");
92
- return { decision, reason: compact(detail ? `${reason}: ${detail}` : reason) };
93
- }
94
-
95
- function write(value) {
96
- process.stdout.write(`${JSON.stringify(value)}\n`);
97
- }
98
-
99
- function compact(value) {
100
- const text = String(value);
101
- return text.length > 500 ? `${text.slice(0, 497)}...` : text;
102
- }
103
-
104
- module.exports = {
105
- commandOf,
106
- compact,
107
- decide,
108
- decisionObject,
109
- diffSignature,
110
- eventName,
111
- parseJson,
112
- promptOf,
113
- readCache,
114
- readJson,
115
- readPayload,
116
- run,
117
- runGit,
118
- runNaome,
119
- taskCheck,
120
- write,
121
- writeCache
122
- };
@@ -1,180 +0,0 @@
1
- "use strict";
2
-
3
- const { readFileSync } = require("node:fs");
4
-
5
- const CHEAP = new Set(["diff-check", "repository-quality-check", "repository-semantic-check", "task-state-progress-check"]);
6
- const EXPENSIVE = new Set(["installer-tests", "rust-build", "package-dry-run", "harness-health-tests", "task-state-tests", "verification-contract-tests"]);
7
-
8
- function classifyPrompt(prompt) {
9
- const value = prompt.toLowerCase();
10
- if (/status|where are we|progress/.test(value)) return "status_request";
11
- if (/review|comment|pr feedback/.test(value)) return "review";
12
- if (/commit/.test(value)) return "commit";
13
- if (/push/.test(value) && !/ci|check|fail/.test(value)) return "push";
14
- if (/ci|check|failing|failed|workflow|actions/.test(value)) return "ci_fix";
15
- if (/continue|keep going|resume|do so/.test(value)) return "continuation";
16
- return "new_task";
17
- }
18
-
19
- function riskyPromptCodes(prompt) {
20
- const value = prompt.toLowerCase();
21
- return [
22
- ["reset|clean|rm\\s+-rf", "risky-destructive"],
23
- ["force push|--force|--force-with-lease|\\spush\\s+-f", "risky-force-push"],
24
- ["--no-verify|bypass.*hook|skip.*hook", "risky-hook-bypass"],
25
- ["publish|release|npm publish", "risky-release"]
26
- ].filter(([pattern]) => new RegExp(pattern).test(value)).map(([, code]) => code);
27
- }
28
-
29
- function permissionRiskCodes(value) {
30
- const lower = value.toLowerCase();
31
- return [...new Set([
32
- ["https?:|network|fetch|curl|wget|push|pull|gh\\s+|npm\\s+(install|publish)", "network"],
33
- ["\\bgit\\b|push|commit|merge|rebase", "git"],
34
- ["write|stage|commit|push|apply|patch|install|sync", "write"],
35
- ["publish|npm publish", "publish"],
36
- ["release|tag", "release"],
37
- ["reset|clean|--force|-f\\b|rm\\s+-rf", "destructive"]
38
- ].filter(([pattern]) => new RegExp(pattern).test(lower)).map(([, code]) => code))];
39
- }
40
-
41
- function hardBlockedCommand(command) {
42
- const value = command.replace(/\s+/g, " ").trim();
43
- return [
44
- [/(\bgit\s+reset\b.*\B--hard\b|\bgit\s+reset\s+--hard\b)/, "destructive-git-reset-hard: hard reset requires explicit human review."],
45
- [/\bgit\s+clean\b.*(^|\s)-[^\s]*f|\bgit\s+clean\b.*--force\b/, "destructive-git-clean: forced clean requires explicit human review."],
46
- [/\brm\b(?=.*(?:^|\s)-[^\s]*r)(?=.*(?:^|\s)-[^\s]*f)(?=.*(?:^|\s)(?:--\s+)?(?:\.\/)?(?:[^/\s]+\/)*\.git(?:\/|\s|$))/, "destructive-git-dir-removal: removing .git is blocked."],
47
- [/--no-verify\b/, "hook-bypass-command: --no-verify is blocked."],
48
- [/\bgit\s+push\b.*(?:--force(?:-with-lease)?\b|(^|\s)-f(\s|$))/, "force-push-command: force pushes require fresh human review."]
49
- ].find(([pattern]) => pattern.test(value))?.[1] || null;
50
- }
51
-
52
- function searchSegments(command) {
53
- return command
54
- .split(/&&|\|\||[;|]/)
55
- .map((part) => part.trim().replace(/^(?:cd\s+\S+\s+)?/, ""))
56
- .filter((part) => /^(rg|grep|find)\b/.test(part));
57
- }
58
-
59
- function forbiddenPath(payload, command) {
60
- const input = payload?.tool_input || payload?.toolInput || {};
61
- const values = [command, ...["path", "file_path", "filepath", "target_file", "pattern"].map((key) => input[key])].filter(Boolean);
62
- return values.some((value) => tokens(value).some(blockedPath)) ? "read-boundary-violation: command or tool input touches a path blocked by .naomeignore." : null;
63
- }
64
-
65
- function blockedPath(path) {
66
- if (/(^|\/)\.naome\/archive(\/|$)/.test(path)) return true;
67
- return ignoredPatterns().some((pattern) => pathMatches(path, pattern));
68
- }
69
-
70
- function tokens(value) {
71
- return String(value).replace(/\\/g, "/").split(/[\s"'`,]+/).map((token) => token.replace(/^\.\//, "").replace(/[):;]+$/, "")).filter(Boolean);
72
- }
73
-
74
- function ignoredPatterns() {
75
- try {
76
- return readFileSync(".naomeignore", "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
77
- } catch {
78
- return [".naome/archive/"];
79
- }
80
- }
81
-
82
- function pathMatches(path, pattern) {
83
- const actual = String(path).replace(/\\/g, "/");
84
- const glob = String(pattern).replace(/\\/g, "/");
85
- if (glob.endsWith("/")) return actual.startsWith(glob);
86
- if (glob.endsWith("/**")) return actual.startsWith(glob.slice(0, -3));
87
- if (!glob.includes("*")) return actual === glob;
88
- return new RegExp(`^${glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*")}$`).test(actual);
89
- }
90
-
91
- function looksLikeWriteTool(payload, command) {
92
- const name = String(payload?.tool_name || payload?.toolName || payload?.name || "").toLowerCase();
93
- if (/edit|write|patch|notebookedit|multiedit/.test(name)) return true;
94
- if (!command) return false;
95
- if (/^(git status|git diff|rg\b|grep\b|find\b|sed\b|cat\b|nl\b|wc\b|ls\b|pwd\b)/.test(command.trim())) return false;
96
- return />|>>|\b(apply_patch|cp|mv|touch|mkdir|rm|npm install|cargo build|node --test|writeFileSync)\b/.test(command);
97
- }
98
-
99
- function touchedPaths(payload, command) {
100
- const paths = [];
101
- collectInputPaths(payload?.tool_input || payload?.toolInput || {}, paths);
102
- collectCommandPaths(command, paths);
103
- return [...new Set(paths.map(cleanPath).filter(Boolean))];
104
- }
105
-
106
- function collectInputPaths(value, paths) {
107
- if (Array.isArray(value)) return value.forEach((entry) => collectInputPaths(entry, paths));
108
- if (!value || typeof value !== "object") return;
109
- for (const [key, child] of Object.entries(value)) {
110
- if (/^(path|paths|file|files|file_path|filepath|target_file)$/.test(key)) collectPathValue(child, paths);
111
- if (child && typeof child === "object") collectInputPaths(child, paths);
112
- }
113
- }
114
-
115
- function collectPathValue(value, paths) {
116
- if (Array.isArray(value)) return value.forEach((entry) => collectPathValue(entry, paths));
117
- if (typeof value === "string") paths.push(value);
118
- }
119
-
120
- function collectCommandPaths(command, paths) {
121
- if (!command) return;
122
- for (const segment of command.split(/&&|\|\||[;]/).map((part) => part.trim())) {
123
- const parts = tokens(segment);
124
- if (/^(touch|mkdir|rm)$/.test(parts[0])) paths.push(...parts.slice(1).filter((part) => !part.startsWith("-")));
125
- if (/^(cp|mv)$/.test(parts[0])) paths.push(...parts.slice(1).filter((part) => !part.startsWith("-")).slice(-1));
126
- }
127
- }
128
-
129
- function cleanPath(path) {
130
- return String(path).replace(/\\/g, "/").replace(/^\.\//, "").replace(/[):;]+$/, "");
131
- }
132
-
133
- function scopeDrift(paths, allowed) {
134
- if (!Array.isArray(allowed)) return [];
135
- return paths.filter((path) => !isControlStatePath(path) && !allowed.some((pattern) => pathMatches(path, pattern)));
136
- }
137
-
138
- function isControlStatePath(path) {
139
- return path === ".naome/task-state.json" || path.startsWith(".naome/tasks/") || path.startsWith(".naome/cache/");
140
- }
141
-
142
- function owedProof(paths, verification) {
143
- const checks = new Set();
144
- for (const type of verification?.changeTypes || []) {
145
- if (paths.some((path) => (type.paths || []).some((pattern) => pathMatches(path, pattern)))) {
146
- for (const check of type.requiredChecks || []) if (!CHEAP.has(check)) checks.add(check);
147
- }
148
- }
149
- return [...checks].sort();
150
- }
151
-
152
- function expensiveProof(checks) {
153
- return checks.filter((check) => EXPENSIVE.has(check));
154
- }
155
-
156
- function pushUnique(codes, code, messages, message) {
157
- if (!codes.includes(code)) codes.push(code);
158
- if (!messages.includes(message)) messages.push(message);
159
- }
160
-
161
- function primaryReason(codes) {
162
- if (codes.includes("scope-drift")) return "scope-drift";
163
- if (codes.some((code) => code.endsWith("-failed"))) return "cheap-gate-failed";
164
- return codes[0] || "naome-hook-warning";
165
- }
166
-
167
- module.exports = {
168
- classifyPrompt,
169
- expensiveProof,
170
- forbiddenPath,
171
- hardBlockedCommand,
172
- looksLikeWriteTool,
173
- owedProof,
174
- permissionRiskCodes,
175
- primaryReason,
176
- pushUnique,
177
- riskyPromptCodes,
178
- scopeDrift,
179
- searchSegments, touchedPaths
180
- };
@@ -1,174 +0,0 @@
1
- "use strict";
2
-
3
- const io = require("./codex-hook-io.js");
4
- const policy = require("./codex-hook-policy.js");
5
-
6
- function handleCodexHook() {
7
- const payload = io.readPayload();
8
- const event = io.eventName(payload);
9
- try {
10
- if (event === "UserPromptSubmit") return userPrompt(payload); if (event === "PreToolUse") return preTool(payload);
11
- if (event === "PermissionRequest") return permission(payload); if (event === "PostToolUse") return postTool(payload);
12
- if (event === "Stop") return stop(payload);
13
- } catch {
14
- return io.write({});
15
- }
16
- io.write({});
17
- }
18
-
19
- function userPrompt(payload) {
20
- const prompt = io.promptOf(payload);
21
- const classification = policy.classifyPrompt(prompt);
22
- const risky = policy.riskyPromptCodes(prompt);
23
- const status = io.parseJson(io.runNaome(["status", "--json"]).stdout);
24
- const codes = [`prompt-${classification}`, ...risky];
25
- if (status?.state === "dirty_unowned_diff") codes.push("repo-dirty-unowned");
26
- if (["new_task", "ci_fix"].includes(classification)) codes.push("route-required", "context-selection-required");
27
- io.decide("warn", "prompt-guidance", [`prompt:${classification}`, status?.state ? `repo:${status.state}` : null, risky.length ? `risk:${risky.join(",")}` : null, ["new_task", "ci_fix"].includes(classification) ? "run route and context select before feature work" : null], { classification, reasonCodes: codes });
28
- }
29
-
30
- function preTool(payload) {
31
- const command = io.commandOf(payload);
32
- const boundary = policy.forbiddenPath(payload, command);
33
- if (boundary) return io.decide("block", boundary, [boundary], { reasonCodes: [boundary.split(":")[0]] });
34
- if (!command) return io.write({});
35
- const hard = policy.hardBlockedCommand(command);
36
- if (hard) return io.decide("block", hard, [hard], { reasonCodes: [hard.split(":")[0]] });
37
- const preflight = commitPushPreflight(command);
38
- if (preflight) return io.decide("block", preflight, [preflight], { reasonCodes: [preflight.split(":")[0]] });
39
- const search = validateSearch(command);
40
- if (search) return io.decide("block", search, [search], { reasonCodes: [search.split(":")[0]] });
41
- io.write({});
42
- }
43
-
44
- function permission(payload) {
45
- const risks = policy.permissionRiskCodes(`${io.commandOf(payload) || ""} ${io.promptOf(payload)}`);
46
- if (!risks.length) return io.write({});
47
- io.decide("warn", "permission-risk-summary", [`permission risks: ${risks.join(",")}; approve only if this matches the current NAOME task.`], { riskCodes: risks, reasonCodes: ["permission-risk-summary"] });
48
- }
49
-
50
- function postTool(payload) {
51
- const command = io.commandOf(payload);
52
- const changedPaths = changedFiles();
53
- if (!changedPaths.length) return io.write({});
54
- if (!policy.looksLikeWriteTool(payload, command)) {
55
- const cache = io.readCache();
56
- io.writeCache({ ...cache, postToolUse: { signature: io.diffSignature(), decision: {} } });
57
- return io.write({});
58
- }
59
- const checkedPaths = postToolCheckedPaths(payload, command, changedPaths);
60
- if (!checkedPaths.length) return io.write({});
61
- const signature = `${io.diffSignature()}:${checkedPaths.join("\0")}`;
62
- const cache = io.readCache();
63
- if (cache.postToolUse?.signature === signature) return io.write({});
64
- const result = changedState(checkedPaths, { pathScoped: true });
65
- const decision = result.codes.length ? io.decisionObject("warn", policy.primaryReason(result.codes), result.messages, { reasonCodes: result.codes, changedPaths: checkedPaths, owedProof: result.expensiveOwed, cheapGates: result.gates }) : {};
66
- io.writeCache({ ...cache, postToolUse: { signature, decision, changedPaths, checkedPaths, cheapGateCommands: result.gateCommands } });
67
- io.write(decision);
68
- }
69
-
70
- function stop(payload) {
71
- const changedPaths = changedFiles();
72
- const result = changedState(changedPaths);
73
- for (const check of [io.taskCheck([]), io.taskCheck(["--progress"])]) {
74
- const output = `${check.stdout || ""}\n${check.stderr || ""}`;
75
- if (check.status && /outside allowedPaths|scope drift/i.test(output)) policy.pushUnique(result.codes, "scope-drift", result.messages, "scope drift blocks completion");
76
- if (check.status && /missing proof|proofResults missing|failed proof/i.test(output)) policy.pushUnique(result.codes, "missing-proof", result.messages, "missing required proof blocks completion");
77
- }
78
- const prompt = io.promptOf(payload);
79
- if (/push/i.test(prompt) && /\[ahead [1-9]/.test(io.runGit(["status", "--short", "--branch"]).stdout || "")) {
80
- policy.pushUnique(result.codes, "unpushed-branch", result.messages, "branch has unpushed commits");
81
- }
82
- if (/resolve|review comment|github comment/i.test(prompt) && /CHANGES_REQUESTED/.test(io.run("gh", ["pr", "view", "--json", "reviewDecision"]).stdout || "")) {
83
- policy.pushUnique(result.codes, "unresolved-review-comments", result.messages, "GitHub review still appears unresolved");
84
- }
85
- if (result.codes.length) return io.decide("block", policy.primaryReason(result.codes), result.messages, { reasonCodes: result.codes, changedPaths, cheapGates: result.gates });
86
- io.write({});
87
- }
88
-
89
- function changedState(changedPaths, options = {}) {
90
- const codes = [];
91
- const messages = [];
92
- const state = io.readJson(".naome/task-state.json");
93
- const drift = policy.scopeDrift(changedPaths, state?.activeTask?.allowedPaths);
94
- if (drift.length) policy.pushUnique(codes, "scope-drift", messages, `scope drift: ${drift.join(", ")}`);
95
- const expensiveOwed = missingExpensiveProof(changedPaths, policy.expensiveProof(policy.owedProof(changedPaths, io.readJson(".naome/verification.json"))), state?.activeTask);
96
- if (expensiveOwed.length) policy.pushUnique(codes, "owed-proof", messages, `owed proof: ${expensiveOwed.join(", ")}`);
97
- if (repoStale()) policy.pushUnique(codes, "repository-model-stale", messages, "repository model is stale");
98
- const gatePaths = options.pathScoped ? changedPaths : [];
99
- const gates = cheapGates(gatePaths);
100
- for (const gate of gates) policy.pushUnique(codes, gate.code, messages, gate.message);
101
- return { codes, messages, expensiveOwed, gates, gateCommands: cheapGateCommands(gatePaths) };
102
- }
103
-
104
- function commitPushPreflight(command) {
105
- if (!/\bgit\s+(commit|push)\b/.test(command.replace(/\s+/g, " "))) return null;
106
- if (/\bgit\s+commit\b/.test(command)) return "commit-preflight: use `node .naome/bin/naome.js commit -m ...` so NAOME owns staging, proof, and commit gates.";
107
- for (const gate of cheapGates()) return `push-preflight: ${gate.checkId} failed before git push.`;
108
- return repoStale() ? "push-preflight: repository-model-stale failed before git push." : null;
109
- }
110
-
111
- function cheapGates(paths = []) {
112
- return cheapGateChecks(paths).flatMap(([checkId, command, fn]) => fn().status ? [{ code: `${checkId}-failed`, checkId, command, message: `cheap gate failed: ${checkId}` }] : []);
113
- }
114
- function cheapGateCommands(paths = []) {
115
- return cheapGateChecks(paths).map(([, command]) => command);
116
- }
117
- function cheapGateChecks(paths = []) {
118
- const pathArgs = paths.flatMap((path) => ["--path", path]);
119
- const displayPaths = paths.map(shellPath).join(" ");
120
- const checks = [
121
- ["diff-check", paths.length ? `git diff --check -- ${displayPaths}` : "git diff --check", () => io.runGit(paths.length ? ["diff", "--check", "--", ...paths] : ["diff", "--check"])],
122
- ["repository-quality-check", paths.length ? `node .naome/bin/naome.js quality check ${pathArgs.join(" ")}` : "node .naome/bin/naome.js quality check --changed", () => io.runNaome(paths.length ? ["quality", "check", ...pathArgs] : ["quality", "check", "--changed"])],
123
- ["repository-semantic-check", paths.length ? `node .naome/bin/naome.js semantic check ${pathArgs.join(" ")}` : "node .naome/bin/naome.js semantic check --changed", () => io.runNaome(paths.length ? ["semantic", "check", ...pathArgs] : ["semantic", "check", "--changed"])]
124
- ];
125
- if (!paths.length && hasActiveTask()) checks.push(["task-state-progress-check", "node .naome/bin/check-task-state.js --progress", () => io.taskCheck(["--progress"])]);
126
- return checks;
127
- }
128
- function validateSearch(command) {
129
- for (const segment of policy.searchSegments(command)) {
130
- const result = io.runNaome(["workflow", "check-search", "--command", segment, "--json"]);
131
- if (result.status === 0) continue;
132
- const ids = io.parseJson(result.stdout)?.findings?.map((finding) => finding.id).filter(Boolean);
133
- return ids?.length ? `${ids.join(",")}: NAOME rejected this broad search command.` : io.compact(`unsafe-search-command: ${result.stdout || result.stderr}`);
134
- }
135
- return null;
136
- }
137
- function changedFiles() {
138
- const result = io.runGit(["status", "--porcelain=v1"]);
139
- return result.status ? [] : result.stdout.split(/\r?\n/).filter(Boolean).map((line) => line.replace(/^..\s+/, "").split(" -> ").pop()).filter((path, index, paths) => path && paths.indexOf(path) === index);
140
- }
141
- function postToolCheckedPaths(payload, command, changedPaths) {
142
- const touched = policy.touchedPaths(payload, command);
143
- if (!touched.length) return changedPaths;
144
- return changedPaths.filter((path) => touched.some((touch) => path === touch || path.startsWith(`${touch}/`)));
145
- }
146
- function shellPath(path) {
147
- return /[^A-Za-z0-9_./:-]/.test(path) ? JSON.stringify(path) : path;
148
- }
149
- function repoStale() {
150
- const result = io.runNaome(["repo", "check", "--json"]);
151
- return result.status !== 0 && /repository model is stale|repository-model-stale/i.test(`${result.stdout}\n${result.stderr}`);
152
- }
153
-
154
- function hasActiveTask() {
155
- const state = io.readJson(".naome/task-state.json"); return Boolean(state?.activeTask && state.status === "implementing");
156
- }
157
- function missingExpensiveProof(changedPaths, checkIds, activeTask) {
158
- const proofPaths = changedPaths.filter((path) => !isControlStatePath(path));
159
- return proofPaths.length ? checkIds.filter((checkId) => !proofCoversChangedPaths(activeTask, checkId, proofPaths)) : [];
160
- }
161
- function proofCoversChangedPaths(activeTask, checkId, changedPaths) {
162
- const proof = (activeTask?.proofResults || []).find((entry) => entry.checkId === checkId && entry.exitCode === 0);
163
- const evidence = Array.isArray(proof?.evidence) ? proof.evidence : [];
164
- return changedPaths.every((changedPath) => evidence.some((path) => evidenceCoversPath(path, changedPath)));
165
- }
166
- function evidenceCoversPath(evidencePath, changedPath) {
167
- const evidence = String(evidencePath);
168
- return evidence === changedPath || (evidence.endsWith("/") && String(changedPath).startsWith(evidence)) || (evidence.endsWith("/**") && String(changedPath).startsWith(evidence.slice(0, -3)));
169
- }
170
- function isControlStatePath(path) {
171
- return path === ".naome/task-state.json" || path.startsWith(".naome/tasks/") || path.startsWith(".naome/cache/");
172
- }
173
-
174
- module.exports = { handleCodexHook };
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const { handleCodexHook } = require("./codex-hook-runtime.js");
5
-
6
- handleCodexHook();
@@ -1,82 +0,0 @@
1
- # Optional Codex Hooks
2
-
3
- NAOME can install repo-local Codex hooks as an optional acceleration layer for coding agents.
4
-
5
- Hooks are not the source of truth. They provide earlier feedback before an agent spends tokens on work that normal NAOME gates would reject later.
6
-
7
- Authoritative enforcement remains in:
8
-
9
- - `.naome/task-state.json` validation
10
- - `naome route`
11
- - `naome quality` and `naome semantic` gates
12
- - `naome commit`
13
- - repository Git hooks
14
- - verification proof checks
15
-
16
- ## Install Or Skip
17
-
18
- `naome install` and `naome sync` may ask whether to install optional Codex hooks.
19
-
20
- Answering no is valid. The repository remains fully usable for humans, CI, and agents without Codex hook support.
21
-
22
- For non-interactive environments, NAOME skips optional hooks unless explicitly requested:
23
-
24
- ```sh
25
- NAOME_CODEX_HOOKS=1 naome sync
26
- NAOME_CODEX_HOOKS=0 naome sync
27
- ```
28
-
29
- ## Events
30
-
31
- The installed dispatcher listens to these Codex events:
32
-
33
- - `UserPromptSubmit`: performs read-only prompt classification and repo/task inspection. Codex Desktop currently accepts no non-blocking warning decision here, so advisory output is intentionally silent.
34
- - `PreToolUse`: blocks clear unsafe commands before execution, including destructive Git operations, hook bypasses, force pushes, `.naome/archive` reads, `.naomeignore` reads, and broad searches that do not honor NAOME search boundaries. It also runs commit/push preflight checks.
35
- - `PostToolUse`: after edits, records changed-file state, narrows checks to files touched by the current tool call when Codex provides a path, compares those files with active task ownership, checks repository-model freshness, and runs cheap path-scoped gates. Non-blocking edit guidance is cached for later Stop/preflight decisions rather than emitted as an unsupported warning.
36
- - `PermissionRequest`: performs read-only escalation risk classification. Codex Desktop currently accepts no non-blocking warning decision here, so advisory output is intentionally silent.
37
- - `Stop`: prevents misleading completion claims when scope drift, stale repository-model data, missing proof, an unpushed requested branch, or unresolved requested review comments are still visible.
38
-
39
- ## Blocking And Advisory Checks
40
-
41
- Hard blocks are reserved for clear safety violations:
42
-
43
- - destructive commands such as hard resets, forced cleans, and deleting `.git`
44
- - hook bypasses such as `--no-verify`
45
- - force pushes, including `--force`, `--force-with-lease`, and `-f`
46
- - reads or searches inside `.naome/archive` or paths matched by `.naomeignore`
47
- - broad search commands that fail NAOME search-profile validation
48
- - commit/push attempts that would skip required ownership, scope, proof, or repository-model checks
49
- - final responses that would claim completion while required proof or scope checks are still failing
50
-
51
- Hook output is conservative for Codex Desktop compatibility: hooks emit `{}` for allowed/advisory outcomes and emit only `{"decision":"block","reason":"..."}` for true blocks. Stable reason codes are embedded in the block reason text.
52
-
53
- ## Edit-Time Gates
54
-
55
- After file edits, hooks run only fast deterministic gates. When the tool input identifies touched files, PostToolUse scopes these checks to those files instead of the whole diff:
56
-
57
- - `git diff --check -- <touched-paths>`
58
- - `node .naome/bin/naome.js quality check --path <touched-path>`
59
- - `node .naome/bin/naome.js semantic check --path <touched-path>`
60
- - active-task scope checking for the touched paths
61
-
62
- If the touched paths cannot be inferred, PostToolUse falls back to the current changed-file set. These checks are debounced by the current diff signature plus the checked path set. Repeated hook calls over the same content reuse the previous result so normal editing stays responsive.
63
-
64
- Stop, commit, and push preflight stay full-diff checks. They verify the complete changed-file set so earlier unrelated drift or proof failures cannot be hidden by a later narrow edit.
65
-
66
- Full suites such as installer tests, Rust builds, package dry-runs, and CI-like workflows are treated as owed proof based on changed paths. They are not run automatically after every edit because they are too slow and noisy for real-time feedback.
67
-
68
- Every hook advisory calculation maps to a normal NAOME command or gate. If hooks are unavailable, disabled, or not installed, humans can keep working by following the normal route, context-selection, task-state, quality, semantic, verification, and commit gates directly.
69
-
70
- ## Local Files
71
-
72
- Opting in installs:
73
-
74
- - `.codex/config.toml`
75
- - `.codex/hooks.json`
76
- - `.naome/bin/codex-hook.js`
77
- - `.naome/bin/codex-hook-io.js`
78
- - `.naome/bin/codex-hook-policy.js`
79
- - `.naome/bin/codex-hook-runtime.js`
80
- - `docs/naome/codex-hooks.md`
81
-
82
- These files are repo-local. They are managed only after opt-in and are not required by repositories that decline hooks.