navori 0.12.2 → 0.12.3

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 (70) hide show
  1. package/dist/add.js +2 -2
  2. package/dist/adopt.js +2 -2
  3. package/dist/approval-route.js +1 -1
  4. package/dist/args.js +1 -1
  5. package/dist/assets/core/core-assets/agents/orchestrator.md +10 -2
  6. package/dist/assets/core/core-assets/agents/publisher.md +4 -3
  7. package/dist/assets/core/core-assets/agents/reviewer.md +6 -6
  8. package/dist/assets/core/core-assets/hooks/_partials/audit-log.sh +11 -4
  9. package/dist/assets/core/core-assets/hooks/_partials/bash-outcome.sh +93 -7
  10. package/dist/assets/core/core-assets/hooks/_partials/copy-dest.sh +241 -0
  11. package/dist/assets/core/core-assets/hooks/bash-outcome-watch.sh +1 -0
  12. package/dist/assets/core/core-assets/hooks/guard-destructive.sh +25 -3
  13. package/dist/assets/core/core-assets/hooks/implementer-no-markdown.sh +1 -1
  14. package/dist/assets/core/core-assets/hooks/routing-watch.sh +8 -2
  15. package/dist/assets/core/core-assets/lib-skills/citty.md +8 -13
  16. package/dist/assets/core/core-assets/lib-skills/clack.md +5 -5
  17. package/dist/assets/core/core-assets/managed/orquestacion.md +4 -2
  18. package/dist/assets/core/core-assets/managed/sdd.md +2 -0
  19. package/dist/assets/core/core-assets/skills/verify-before-done.md +20 -18
  20. package/dist/audit.js +30 -27
  21. package/dist/backup2.js +2 -2
  22. package/dist/bench.js +2 -2
  23. package/dist/cli-config.js +1 -1
  24. package/dist/cli-event.js +3 -3
  25. package/dist/codex-profile.js +1 -1
  26. package/dist/codex.js +3 -3
  27. package/dist/config.js +1 -1
  28. package/dist/configure.js +3 -3
  29. package/dist/content-identity.js +4 -0
  30. package/dist/delivery-checks.js +2 -2
  31. package/dist/dist.js +68 -0
  32. package/dist/doctor.js +64 -64
  33. package/dist/dominio.js +3 -3
  34. package/dist/evidence.js +4 -4
  35. package/dist/gate.js +2 -7
  36. package/dist/handoff.js +3 -3
  37. package/dist/{health.js → hook-bindings.js} +11 -11
  38. package/dist/index.js +1 -1
  39. package/dist/init.js +4 -4
  40. package/dist/master.js +12 -12
  41. package/dist/migrations.js +2 -2
  42. package/dist/native-integration.js +4 -3
  43. package/dist/paths.js +1 -1
  44. package/dist/plan.js +6 -6
  45. package/dist/preset.js +3 -3
  46. package/dist/receipt.js +1 -1
  47. package/dist/recommended.js +2 -0
  48. package/dist/refresh.js +4 -4
  49. package/dist/registry.js +1 -1
  50. package/dist/registry2.js +4 -4
  51. package/dist/remove.js +1 -1
  52. package/dist/render.js +64 -61
  53. package/dist/requirements.js +1 -1
  54. package/dist/review-evidence.js +19 -12
  55. package/dist/role-tools.js +2 -0
  56. package/dist/scan2.js +3 -3
  57. package/dist/schema.js +1 -1
  58. package/dist/slice.js +12 -9
  59. package/dist/spec.js +1 -1
  60. package/dist/state-root.js +1 -3
  61. package/dist/status.js +2 -2
  62. package/dist/style.js +2 -68
  63. package/dist/sync.js +7 -7
  64. package/dist/ticket.js +5 -5
  65. package/dist/trust.js +2 -2
  66. package/dist/update.js +1 -1
  67. package/dist/workspace.js +3 -3
  68. package/dist/workspace2.js +5 -5
  69. package/package.json +11 -11
  70. package/dist/lockfile.js +0 -2
package/dist/add.js CHANGED
@@ -1,3 +1,3 @@
1
1
 
2
- import{a as e,b as t,d as n,f as r,i,l as a,m as o,n as s,p as c,t as l,u,v as d,y as f}from"./style.js";import{c as p,i as m,n as h}from"./i18n.js";import{c as g,d as _,i as v,n as y,r as b}from"./plugins.js";import{a as x,o as S,u as C}from"./index.js";import{i as w,o as T}from"./config.js";import{r as E,u as D}from"./render.js";import{n as O,r as k,t as A}from"./external-providers.js";import{n as j}from"./detect.js";import{resolve as M}from"node:path";import{existsSync as N,readFileSync as P}from"node:fs";import{spawnSync as F}from"node:child_process";function I(e){if(!N(e))return`es`;try{return h(w(e).language)}catch{return`es`}}const L=3e5;function R(e){if(!e)return null;let t=e.trim();return t===``?null:t.length>4e3?`(start omitted) …\n${t.slice(-4e3)}`:t}function z(e,t,n){let r=F(e,{shell:!0,stdio:n?[`inherit`,`inherit`,`pipe`]:`inherit`,timeout:L,encoding:`utf-8`});if(r.error&&r.error.code===`ETIMEDOUT`)throw new p(t.installTimeout(L/1e3));if(r.signal)throw new p(t.commandKilled(r.signal));if(r.status!==0){let e=n?R(r.stderr):null;throw new p(e?t.commandExitedWithStderr(r.status,e):t.commandExited(r.status))}}function B(e,t){return/\bpnpm\s+add\s+-g\b/.test(e)?t.causePnpmSetup:/\bnpm\s+install\s+-g\b/.test(e)?t.causeNpmGlobalBin:/\bbrew\s+install\b/.test(e)?t.causeHomebrewPath:/\$HOME\/\.local\/bin\b/.test(e)?t.causeLocalBin:t.causeUnknownPath}function V(e,t,r,a,s){if(s.skipIfNoTty&&process.stdin.isTTY!==!0)return n.warn(a.postInstallNoTty(e)),!0;let c=o();try{if(c.start(t),z(e,a,s.captureStderr),s.verifyBinary&&r.checkBinary&&!x(r.checkBinary)){let t=B(e,a);return c.stop(`${i.red(`✗`)} ${a.installedButUnreachable(l(r.name),t)}`,1),!1}return c.stop(`${i.green(`✓`)} ${a.installed(l(r.name))}`),!0}catch(e){return c.stop(`${i.red(`✗`)} ${a.installFailed(e.message)}`,1),!1}}async function H(r,i,a){if(!r.postInstall||i[`skip-install`])return!1;if(process.stdin.isTTY!==!0)return n.warn(a.postInstallNoTty(r.postInstall)),!1;let o=i.yes?!0:await f({message:a.postInstallPrompt(r.name,r.postInstall),initialValue:!1});return t(o)||!o?!1:!V(r.postInstall,a.postInstall(e(r.postInstall)),r,a,{captureStderr:!1})}const U=C({meta:{name:`add`,description:`Register a plugin in navori.config.json and optionally install its external tool`},args:{plugin:{type:`positional`,description:`Plugin id to add (e.g. engram). Omit with --suggest.`,required:!1},suggest:{type:`boolean`,description:`Detect the stack and suggest a preset + plugins (does not install anything).`},cwd:{type:`string`,description:`Directory containing navori.config.json (default: cwd)`},yes:{type:`boolean`,description:`Skip prompts, install external tool if needed`},"skip-install":{type:`boolean`,description:`Do not install external tool (register plugin only)`}},async run({args:r}){let i=M(r.cwd??process.cwd()),a=`${i}/navori.config.json`,o=I(a),p=m(o).add;if(r.suggest?u(s(`add --suggest`)):r.plugin?u(s(`add ${l(r.plugin)}`)):(u(s(`add`)),d(p.pluginRequired),process.exit(1)),N(i)||(d(m(`es`).common.dirNotFound(i)),process.exit(1)),N(a)||(d(m(`es`).common.noConfig(a)),process.exit(1)),r.suggest){G(i,a,o);return}let h=r.plugin,C;try{C=_(h)}catch(e){if(e instanceof b){let e=v[h];d(e?p.retiredPlugin(h,e.removedIn):p.unknownPlugin(h,g().join(`, `)||p.none)),process.exit(1)}throw e instanceof y&&(d(e.message),process.exit(1)),e}n.info(`${C.manifest.name} v${C.manifest.version}`),n.message(C.manifest.description);let k=w(a);if(k.plugins?.[C.manifest.id]?.enabled===!0)n.warn(p.alreadyEnabled(C.manifest.id));else{let e={...k.plugins??{},[C.manifest.id]:{enabled:!0}},t=JSON.parse(P(a,`utf-8`));T(a,{...t,plugins:e}),n.success(p.added(C.manifest.id,a));let r;try{r=D(i,!1)}catch(e){n.error(e instanceof Error?e.message:String(e)),c(p.renderCrashed),process.exitCode=1;return}if(!r.ok){n.error(r.reason??m(o).render.renderFailed),c(p.renderFailedConfig),process.exitCode=1;return}E(r),O.has(C.manifest.id)&&n.info(p.externalProviderSetupHint(A))}let j=C.manifest.externalTool;if(!j){c(p.done);return}if(!j.checkBinary||x(j.checkBinary)){n.success(p.externalAlreadyInstalled(j.name));let t=await H(j,r,p);c(t?e(p.registeredInstallFailed):p.done);return}if(r[`skip-install`]){n.warn(p.externalSkipped(j.name)),W(j,p),c(p.doneInstallLater);return}let F=S(),L=F?j.install?.[F]:void 0;if(!L){n.warn(p.noInstallCommand(F??process.platform,j.name)),W(j,p),c(e(p.doneNoInstall(j.name))),process.exitCode=1;return}let R=r.yes?!0:await f({message:p.installPrompt(j.name,L),initialValue:!1});if(t(R)||!R){n.warn(p.externalNotInstalled(j.name)),W(j,p),c(p.doneInstallLater);return}if(!V(L,p.installing(l(j.name),e(L)),j,p,{captureStderr:!0,verifyBinary:!0})){W(j,p),c(e(p.registeredInstallFailed)),process.exitCode=1;return}if(j.postInstall&&!V(j.postInstall,p.postInstall(e(j.postInstall)),j,p,{captureStderr:!1,skipIfNoTty:!0})){c(e(p.registeredInstallFailed));return}c(p.done)}});function W(e,t){e.installDocs&&n.info(t.installDocsHint(e.installDocs))}function G(t,n,o){let s=m(o).add,u=j(t),d=w(n),f=[],p=u.suggestedPreset;if(p&&p!==`custom`&&p!==d.preset){let e=u.stack.ui??u.stack.framework??u.stack.language;f.push(`${i.cyan(a.bullet)} ${s.suggestedPreset(l(e),l(p),d.preset)}`)}new Set(Object.entries(d.plugins??{}).filter(([,e])=>e.enabled===!0).map(([e])=>e)).has(`engram`)||f.push(`${i.cyan(a.bullet)} ${s.suggestedEngram}`);for(let e of k(d,t))f.push(`${i.cyan(a.bullet)} ${s.suggestedProvider(l(e))}`);if(f.length===0){c(i.green(s.nothingToSuggest));return}r(f.join(`
3
- `),s.suggestionsTitle),c(e(s.suggestionsOutro))}export{U as addCommand};
2
+ import{a as e,d as t,i as n,l as r,n as i,r as a,t as o,u as s}from"./dist.js";import{c,i as l,n as u}from"./i18n.js";import{c as d,d as f,i as p,n as m,r as h}from"./plugins.js";import{a as g,o as _,u as v}from"./index.js";import{i as y,o as b}from"./config.js";import{i as x,u as S}from"./render.js";import{a as C,i as w,l as T,n as E,t as D}from"./style.js";import{n as O,r as k,t as A}from"./external-providers.js";import{n as j}from"./detect.js";import{resolve as M}from"node:path";import{existsSync as N,readFileSync as P}from"node:fs";import{spawnSync as F}from"node:child_process";function I(e){if(!N(e))return`es`;try{return u(y(e).language)}catch{return`es`}}const L=3e5;function R(e){if(!e)return null;let t=e.trim();return t===``?null:t.length>4e3?`(start omitted) …\n${t.slice(-4e3)}`:t}function z(e,t,n){let r=F(e,{shell:!0,stdio:n?[`inherit`,`inherit`,`pipe`]:`inherit`,timeout:L,encoding:`utf-8`});if(r.error&&r.error.code===`ETIMEDOUT`)throw new c(t.installTimeout(L/1e3));if(r.signal)throw new c(t.commandKilled(r.signal));if(r.status!==0){let e=n?R(r.stderr):null;throw new c(e?t.commandExitedWithStderr(r.status,e):t.commandExited(r.status))}}function B(e,t){return/\bpnpm\s+add\s+-g\b/.test(e)?t.causePnpmSetup:/\bnpm\s+install\s+-g\b/.test(e)?t.causeNpmGlobalBin:/\bbrew\s+install\b/.test(e)?t.causeHomebrewPath:/\$HOME\/\.local\/bin\b/.test(e)?t.causeLocalBin:t.causeUnknownPath}function V(t,n,r,a,o){if(o.skipIfNoTty&&process.stdin.isTTY!==!0)return i.warn(a.postInstallNoTty(t)),!0;let s=e();try{if(s.start(n),z(t,a,o.captureStderr),o.verifyBinary&&r.checkBinary&&!g(r.checkBinary)){let e=B(t,a);return s.error(`${w.red(`✗`)} ${a.installedButUnreachable(D(r.name),e)}`),!1}return s.stop(`${w.green(`✓`)} ${a.installed(D(r.name))}`),!0}catch(e){return s.error(`${w.red(`✗`)} ${a.installFailed(e.message)}`),!1}}async function H(e,n,r){if(!e.postInstall||n[`skip-install`])return!1;if(process.stdin.isTTY!==!0)return i.warn(r.postInstallNoTty(e.postInstall)),!1;let a=n.yes?!0:await s({message:r.postInstallPrompt(e.name,e.postInstall),initialValue:!1});return t(a)||!a?!1:!V(e.postInstall,r.postInstall(C(e.postInstall)),e,r,{captureStderr:!1})}const U=v({meta:{name:`add`,description:`Register a plugin in navori.config.json and optionally install its external tool`},args:{plugin:{type:`positional`,description:`Plugin id to add (e.g. engram). Omit with --suggest.`,required:!1},suggest:{type:`boolean`,description:`Detect the stack and suggest a preset + plugins (does not install anything).`},cwd:{type:`string`,description:`Directory containing navori.config.json (default: cwd)`},yes:{type:`boolean`,description:`Skip prompts, install external tool if needed`},"skip-install":{type:`boolean`,description:`Do not install external tool (register plugin only)`}},async run({args:e}){let a=M(e.cwd??process.cwd()),c=`${a}/navori.config.json`,u=I(c),v=l(u).add;if(e.suggest?o(E(`add --suggest`)):e.plugin?o(E(`add ${D(e.plugin)}`)):(o(E(`add`)),r(v.pluginRequired),process.exit(1)),N(a)||(r(l(`es`).common.dirNotFound(a)),process.exit(1)),N(c)||(r(l(`es`).common.noConfig(c)),process.exit(1)),e.suggest){G(a,c,u);return}let w=e.plugin,T;try{T=f(w)}catch(e){if(e instanceof h){let e=p[w];r(e?v.retiredPlugin(w,e.removedIn):v.unknownPlugin(w,d().join(`, `)||v.none)),process.exit(1)}throw e instanceof m&&(r(e.message),process.exit(1)),e}i.info(`${T.manifest.name} v${T.manifest.version}`),i.message(T.manifest.description);let k=y(c);if(k.plugins?.[T.manifest.id]?.enabled===!0)i.warn(v.alreadyEnabled(T.manifest.id));else{let e={...k.plugins??{},[T.manifest.id]:{enabled:!0}},t=JSON.parse(P(c,`utf-8`));b(c,{...t,plugins:e}),i.success(v.added(T.manifest.id,c));let r;try{r=S(a,!1)}catch(e){i.error(e instanceof Error?e.message:String(e)),n(v.renderCrashed),process.exitCode=1;return}if(!r.ok){i.error(r.reason??l(u).render.renderFailed),n(v.renderFailedConfig),process.exitCode=1;return}x(r),O.has(T.manifest.id)&&i.info(v.externalProviderSetupHint(A))}let j=T.manifest.externalTool;if(!j){n(v.done);return}if(!j.checkBinary||g(j.checkBinary)){i.success(v.externalAlreadyInstalled(j.name));let t=await H(j,e,v);n(t?C(v.registeredInstallFailed):v.done);return}if(e[`skip-install`]){i.warn(v.externalSkipped(j.name)),W(j,v),n(v.doneInstallLater);return}let F=_(),L=F?j.install?.[F]:void 0;if(!L){i.warn(v.noInstallCommand(F??process.platform,j.name)),W(j,v),n(C(v.doneNoInstall(j.name))),process.exitCode=1;return}let R=e.yes?!0:await s({message:v.installPrompt(j.name,L),initialValue:!1});if(t(R)||!R){i.warn(v.externalNotInstalled(j.name)),W(j,v),n(v.doneInstallLater);return}if(!V(L,v.installing(D(j.name),C(L)),j,v,{captureStderr:!0,verifyBinary:!0})){W(j,v),n(C(v.registeredInstallFailed)),process.exitCode=1;return}if(j.postInstall&&!V(j.postInstall,v.postInstall(C(j.postInstall)),j,v,{captureStderr:!1,skipIfNoTty:!0})){n(C(v.registeredInstallFailed));return}n(v.done)}});function W(e,t){e.installDocs&&i.info(t.installDocsHint(e.installDocs))}function G(e,t,r){let i=l(r).add,o=j(e),s=y(t),c=[],u=o.suggestedPreset;if(u&&u!==`custom`&&u!==s.preset){let e=o.stack.ui??o.stack.framework??o.stack.language;c.push(`${w.cyan(T.bullet)} ${i.suggestedPreset(D(e),D(u),s.preset)}`)}new Set(Object.entries(s.plugins??{}).filter(([,e])=>e.enabled===!0).map(([e])=>e)).has(`engram`)||c.push(`${w.cyan(T.bullet)} ${i.suggestedEngram}`);for(let t of k(s,e))c.push(`${w.cyan(T.bullet)} ${i.suggestedProvider(D(t))}`);if(c.length===0){n(w.green(i.nothingToSuggest));return}a(c.join(`
3
+ `),i.suggestionsTitle),n(C(i.suggestionsOutro))}export{U as addCommand};
package/dist/adopt.js CHANGED
@@ -1,3 +1,3 @@
1
1
 
2
- import{a as e,d as t,n,p as r,t as i,u as a,v as o}from"./style.js";import{l as s}from"./semver.js";import{i as c,n as l}from"./i18n.js";import{u}from"./index.js";import{o as d}from"./health.js";import{f}from"./render-plan.js";import{i as p}from"./backup.js";import{D as m,O as h,i as g}from"./render.js";import{t as _}from"./cli-config.js";import{isAbsolute as v,join as y,relative as b,resolve as x}from"node:path";import{existsSync as S,readFileSync as C,statSync as w,writeFileSync as T}from"node:fs";function E(e){return`adopted-${e.replace(/\.md$/,``).replace(/[^a-zA-Z0-9]+/g,`-`).replace(/^-+|-+$/g,``).toLowerCase()}`}function D(e){return(e.startsWith(`.claude/`)||e.startsWith(`.claude\\`))&&e.endsWith(`.md`)}function O(e,t){let n=v(t)?t:x(e,t),r=b(e,n);if(r.startsWith(`..`)||v(r))return{kind:`refuse`,reason:`outside-repo`,path:n};if(!S(n))return{kind:`refuse`,reason:`missing`,path:r};if(!w(n).isFile())return{kind:`refuse`,reason:`not-a-file`,path:r};if(!D(r))return{kind:`refuse`,reason:`not-adoptable-path`,path:r};let i=C(n,`utf-8`),a=E(r),o=d(n).map(e=>e.id);return o.length>0?o.includes(a)?{kind:`noop`,path:r}:{kind:`refuse`,reason:`already-managed`,path:r}:{kind:`adopt`,path:n,relPath:r,markerId:a,content:f(``,a,i.endsWith(`
3
- `)?i:`${i}\n`,{version:s(),source:`adopted`}).output}}const k=u({meta:{name:`adopt`,description:`Take a hand-made .claude/ file under navori's management (preview by default)`},args:{path:{type:`positional`,description:`File to adopt, relative to the repo (e.g. .claude/agents/mia.md)`,required:!0},apply:{type:`boolean`,description:`Write to disk. Without it, adopt only previews.`},cwd:{type:`string`,description:`Directory (default: cwd)`}},run({args:s}){let u=x(s.cwd??process.cwd()),d=y(u,`navori.config.json`),f=S(d)?l(_(d).language):`es`,v=c(f).adopt;a(n(`adopt ${i(String(s.path))}`));let b=O(u,String(s.path));if(b.kind===`refuse`){o(v.refused[b.reason](b.path)),process.exitCode=1;return}if(b.kind===`noop`){r(e(v.alreadyAdopted(b.path)));return}if(!s.apply){t.info(v.preview(b.relPath,b.markerId)),r(e(v.previewHint));return}let C=p(u,[b.relPath]);T(b.path,b.content,`utf-8`),h(),m(),S(d)&&g(u),t.success(v.adopted(b.relPath,b.markerId)),r(e(v.backupAt(C.path)))}});export{k as adoptCommand};
2
+ import{i as e,l as t,n,t as r}from"./dist.js";import{l as i}from"./semver.js";import{i as a,n as o}from"./i18n.js";import{u as s}from"./index.js";import{l as c}from"./hook-bindings.js";import{f as l}from"./render-plan.js";import{i as u}from"./backup.js";import{D as d,E as f,a as p}from"./render.js";import{a as m,n as h,t as g}from"./style.js";import{t as _}from"./cli-config.js";import{isAbsolute as v,join as y,relative as b,resolve as x}from"node:path";import{existsSync as S,readFileSync as C,statSync as w,writeFileSync as T}from"node:fs";function E(e){return`adopted-${e.replace(/\.md$/,``).replace(/[^a-zA-Z0-9]+/g,`-`).replace(/^-+|-+$/g,``).toLowerCase()}`}function D(e){return(e.startsWith(`.claude/`)||e.startsWith(`.claude\\`))&&e.endsWith(`.md`)}function O(e,t){let n=v(t)?t:x(e,t),r=b(e,n);if(r.startsWith(`..`)||v(r))return{kind:`refuse`,reason:`outside-repo`,path:n};if(!S(n))return{kind:`refuse`,reason:`missing`,path:r};if(!w(n).isFile())return{kind:`refuse`,reason:`not-a-file`,path:r};if(!D(r))return{kind:`refuse`,reason:`not-adoptable-path`,path:r};let a=C(n,`utf-8`),o=E(r),s=c(n).map(e=>e.id);return s.length>0?s.includes(o)?{kind:`noop`,path:r}:{kind:`refuse`,reason:`already-managed`,path:r}:{kind:`adopt`,path:n,relPath:r,markerId:o,content:l(``,o,a.endsWith(`
3
+ `)?a:`${a}\n`,{version:i(),source:`adopted`}).output}}const k=s({meta:{name:`adopt`,description:`Take a hand-made .claude/ file under navori's management (preview by default)`},args:{path:{type:`positional`,description:`File to adopt, relative to the repo (e.g. .claude/agents/mia.md)`,required:!0},apply:{type:`boolean`,description:`Write to disk. Without it, adopt only previews.`},cwd:{type:`string`,description:`Directory (default: cwd)`}},run({args:i}){let s=x(i.cwd??process.cwd()),c=y(s,`navori.config.json`),l=S(c)?o(_(c).language):`es`,v=a(l).adopt;r(h(`adopt ${g(String(i.path))}`));let b=O(s,String(i.path));if(b.kind===`refuse`){t(v.refused[b.reason](b.path)),process.exitCode=1;return}if(b.kind===`noop`){e(m(v.alreadyAdopted(b.path)));return}if(!i.apply){n.info(v.preview(b.relPath,b.markerId)),e(m(v.previewHint));return}let C=u(s,[b.relPath]);T(b.path,b.content,`utf-8`),d(),f(),S(c)&&p(s),n.success(v.adopted(b.relPath,b.markerId)),e(m(v.backupAt(C.path)))}});export{k as adoptCommand};
@@ -1,2 +1,2 @@
1
1
 
2
- import{S as e,_ as t,d as n,f as r,i,p as a,s as o,v as s,x as c}from"./schemas.js";import{a as l,r as u,s as d,u as f}from"./state-root.js";import{isAbsolute as p,relative as m,resolve as h,sep as g}from"node:path";import{closeSync as _,lstatSync as v,openSync as y,readFileSync as b,realpathSync as x,writeSync as S}from"node:fs";import{createHash as C,randomUUID as w}from"node:crypto";import{spawnSync as T}from"node:child_process";const E=65536,D=5e3,O=s().min(1).max(512).refine(e=>[...e].every(e=>e.charCodeAt(0)>=32&&e.charCodeAt(0)!==127)),k=O.regex(/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/).refine(e=>!e.includes(`..`)),A=s().regex(/^[a-f0-9]{64}$/),j=t({repo:O.regex(/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/),base:k,head:k,title:O,bodyFile:O.refine(e=>!p(e)&&!e.split(/[\\/]/).includes(`..`)),draft:o()}),M=t({version:n(1),id:e(),action:n(`github.pr.create`),checkout:O,createdAt:r().int().positive(),fields:j,commandDigest:A,bodyDigest:A,head:O,base:O.nullable(),fingerprint:A,origin:t({session:O.optional(),tool:O.optional(),child:O.optional()}),source:n(`unavailable`),hostVersion:n(`unknown`),surface:n(`command-hooks; authenticated consent unverified`)}),N=a({cwd:O,tool_name:i([`Bash`,`exec_command`,`functions.exec_command`]),tool_input:t({command:s().min(1).max(E)}),session_id:O.optional(),tool_use_id:O.optional(),agent_id:O.optional(),subagent_id:O.optional()}).refine(e=>![`approved`,`human`,`consent`,`authorization`,`source`].some(t=>t in e)),P=e=>({status:`pending`,reason:e}),F=e=>C(`sha256`).update(e).digest(`hex`),I=e=>`'${e.replaceAll(`'`,`'\\''`)}'`;function L(e){return`gh pr create --repo ${I(e.repo)} --base ${I(e.base)} --head ${I(e.head)} --title ${I(e.title)} --body-file ${I(e.bodyFile)}${e.draft?` --draft`:``}`}function R(e){if(/[\r\n$`;&|<>\\]/.test(e))throw Error(`unsupported-shape`);let t=e.match(/'[^']*'|"[^"]*"|[^\s'"]+/g)??[];if(t.join(` `)!==e.trim().replace(/\s+/g,` `))throw Error(`unsupported-shape`);let n=t.map(e=>/^["']/.test(e)?e.slice(1,-1):e);if(n.slice(0,3).join(` `)!==`gh pr create`)throw Error(`unsupported-shape`);let r={draft:!1},i={"--repo":`repo`,"--base":`base`,"--head":`head`,"--title":`title`,"--body-file":`bodyFile`},a=new Set;for(let e=3;e<n.length;e++){let t=n[e];if(a.has(t))throw Error(`unsupported-shape`);if(a.add(t),t===`--draft`)r.draft=!0;else{let a=i[t],o=n[++e];if(!a||!o||o.startsWith(`--`))throw Error(`unsupported-shape`);r[a]=o}}let o=j.safeParse(r);if(!o.success)throw Error(`unsupported-shape`);return o.data}function z(e,t,n){let r=n-Date.now();if(r<=0)throw Error(`timeout`);let i=T(`git`,[`-c`,`core.hooksPath=/dev/null`,`-c`,`core.fsmonitor=false`,...t],{cwd:e,encoding:`utf8`,timeout:r,maxBuffer:E,env:{...process.env,GIT_OPTIONAL_LOCKS:`0`}});if(i.error||i.status!==0)throw Error(`metadata-unavailable`);return i.stdout.trim()}function B(e){let t=h(e),n=g;for(let e of t.split(g).filter(Boolean))if(n=h(n,e),v(n).isSymbolicLink())throw Error(`unsafe-path`);return x(t)}function V(e,t){let n=B(e),r=B(z(n,[`rev-parse`,`--show-toplevel`],t)),i=m(r,n);if(i.startsWith(`..`)||p(i))throw Error(`unsafe-path`);let a={cwd:r,dir:`.navori/state/handoffs`,path:h(r,`.navori/state/handoffs`),kind:`neutral`};return l(a,`approval-check.json`),a}function H(e){let t=v(e);if(!t.isFile()||t.size>E)throw Error(`unsafe-or-large-file`);let n=b(e);if(n.length>E)throw Error(`unsafe-or-large-file`);return n}function U(e,t,n){if(z(e.cwd,[`branch`,`--show-current`],n)!==t.head)throw Error(`head-mismatch`);let r=B(h(e.cwd,t.bodyFile)),i=m(e.cwd,r);if(i.startsWith(`..`)||p(i))throw Error(`unsafe-path`);let a=F(H(r)),o=n-Date.now();if(o<=0)throw Error(`timeout`);let s=f(e.cwd,{target:t.base,timeoutMs:o});if(!s.ok)throw Error(s.reason);if(Date.now()>=n)throw Error(`timeout`);return{head:s.head,base:s.base,fingerprint:s.fingerprint,bodyDigest:a}}function W(e){let t=Date.now()+D;try{if(Buffer.byteLength(e)>E)return P(`oversized-input`);let n=N.parse(JSON.parse(e)),r=JSON.parse(e);if([`approved`,`human`,`consent`,`authorization`,`source`].some(e=>e in r))return P(`untrusted-assertion`);let i=R(n.tool_input.command),a=V(n.cwd,t),o={version:1,id:w(),action:`github.pr.create`,checkout:a.cwd,createdAt:Date.now(),fields:i,commandDigest:F(L(i)),...U(a,i,t),origin:{session:n.session_id,tool:n.tool_use_id,child:n.agent_id??n.subagent_id},source:`unavailable`,hostVersion:`unknown`,surface:`command-hooks; authenticated consent unverified`};return d(a,`approval_${o.id}.json`,JSON.stringify(o)),{...P(`source-unavailable`),requestId:o.id}}catch{return P(`unsupported-or-unavailable`)}}function G(t,n){let r=Date.now()+D;try{if(!e().safeParse(t).success)return P(`invalid-request-id`);let i=V(n,r),a=l(i,`approval_${t}.json`),o=H(a),s=M.parse(JSON.parse(o.toString(`utf8`)));if(s.id!==t||s.checkout!==i.cwd||s.createdAt>Date.now()||Date.now()-s.createdAt>9e5||s.commandDigest!==F(L(s.fields)))return P(`stale-request`);let c=U(i,s.fields,r);if(Object.entries(c).some(([e,t])=>t!==s[e]))return P(`stale-request`);u(i);let d=l(i,`approval_${t}.claim`),f=`claimed`;try{let e=y(d,`wx`,384);try{S(e,F(o))}finally{_(e)}}catch(e){if(e.code!==`EEXIST`)throw e;f=`already-claimed`}return!H(a).equals(o)||JSON.stringify(U(i,s.fields,r))!==JSON.stringify(c)?P(`stale-request`):{...P(`source-unavailable`),requestId:t,mediation:f,fields:s.fields,reviewCommand:L(s.fields),fingerprint:s.fingerprint,remoteState:`unverified`}}catch{return P(`malformed-stale-or-unavailable`)}}function K(e,t,n){let r=n-Date.now();if(r<=0)throw Error(`remote-metadata-timeout`);let i=T(`gh`,e,{cwd:t,encoding:`utf8`,timeout:r,maxBuffer:E,stdio:[`ignore`,`pipe`,`pipe`]});if(i.error||i.status!==0)throw Error(`remote-metadata-unavailable`);return JSON.parse(i.stdout)}function q(e){let t=/^git@([^/:]+):([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+?)(?:\.git)?$/.exec(e);if(t)return`https://${t[1]}/${t[2]}`;let n=new URL(e);if(n.protocol!==`https:`||n.username||n.password||n.search||n.hash)throw Error(`unsupported-remote`);return`${n.origin}${n.pathname.replace(/\.git$/,``).replace(/\/$/,``)}`}function J(n,r){let i=Date.now()+15e3;if(!e().safeParse(n).success)throw Error(`invalid-request-id`);let o=V(r,i),u=M.parse(JSON.parse(H(l(o,`approval_${n}.json`)).toString(`utf8`)));if(u.id!==n||u.checkout!==o.cwd||u.createdAt>Date.now()||Date.now()-u.createdAt>9e5)throw Error(`stale-request`);let d=U(o,u.fields,i);if(u.commandDigest!==F(L(u.fields))||Object.entries(d).some(([e,t])=>u[e]!==t))throw Error(`stale-request`);let f=t({nameWithOwner:O,url:c()}).parse(K([`repo`,`view`,u.fields.repo,`--json`,`nameWithOwner,url`],o.cwd,i));if(f.nameWithOwner!==u.fields.repo||q(z(o.cwd,[`remote`,`get-url`,`origin`],i))!==q(f.url))throw Error(`destination-mismatch`);let p=e=>a({object:a({sha:s().regex(/^[a-f0-9]{40}$/)})}).parse(K([`api`,`--method`,`GET`,`repos/${u.fields.repo}/git/ref/heads/${e}`],o.cwd,i)).object.sha,m=p(u.fields.base),g=p(u.fields.head);if(g!==d.head||m!==d.base)throw Error(`remote-ref-mismatch`);let _=H(B(h(o.cwd,u.fields.bodyFile))).toString(`utf8`);if(F(_)!==u.bodyDigest)throw Error(`body-changed`);return Object.freeze({pendingId:n,checkout:o.cwd,action:`github.pr.create`,destination:f.url,repository:f.nameWithOwner,base:u.fields.base,head:u.fields.head,localHead:d.head,remoteBase:m,remoteHead:g,fingerprint:d.fingerprint,title:u.fields.title,body:_,bodyDigest:u.bodyDigest,draft:u.fields.draft})}function Y(e){try{return JSON.stringify(J(e.pendingId,e.checkout))===JSON.stringify(e)}catch{return!1}}function X(e){let t=e.requestId?` Pending request ${e.requestId}; relay with navori codex approval relay ${e.requestId} --cwd <checkout>.`:` Prepare a fresh canonical request for review; do not retry the denied effect.`;return{hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:`deny`,permissionDecisionReason:`[navori] PR create remains pending (${e.reason}). No authenticated consent source is available in the inspected command-hook surface.${t} The principal must show the reviewed fields and ask the human to run the command; local claims are not authorization.`}}}export{J as a,Y as i,G as n,W as r,X as t};
2
+ import{S as e,_ as t,d as n,f as r,i,p as a,s as o,v as s,x as c}from"./schemas.js";import{r as l}from"./content-identity.js";import{a as u,r as d,s as f}from"./state-root.js";import{isAbsolute as p,relative as m,resolve as h,sep as g}from"node:path";import{closeSync as _,lstatSync as v,openSync as y,readFileSync as b,realpathSync as x,writeSync as S}from"node:fs";import{createHash as C,randomUUID as w}from"node:crypto";import{spawnSync as T}from"node:child_process";const E=65536,D=5e3,O=s().min(1).max(512).refine(e=>[...e].every(e=>e.charCodeAt(0)>=32&&e.charCodeAt(0)!==127)),k=O.regex(/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/).refine(e=>!e.includes(`..`)),A=s().regex(/^[a-f0-9]{64}$/),j=t({repo:O.regex(/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/),base:k,head:k,title:O,bodyFile:O.refine(e=>!p(e)&&!e.split(/[\\/]/).includes(`..`)),draft:o()}),M=t({version:n(1),id:e(),action:n(`github.pr.create`),checkout:O,createdAt:r().int().positive(),fields:j,commandDigest:A,bodyDigest:A,head:O,base:O.nullable(),fingerprint:A,origin:t({session:O.optional(),tool:O.optional(),child:O.optional()}),source:n(`unavailable`),hostVersion:n(`unknown`),surface:n(`command-hooks; authenticated consent unverified`)}),N=a({cwd:O,tool_name:i([`Bash`,`exec_command`,`functions.exec_command`]),tool_input:t({command:s().min(1).max(E)}),session_id:O.optional(),tool_use_id:O.optional(),agent_id:O.optional(),subagent_id:O.optional()}).refine(e=>![`approved`,`human`,`consent`,`authorization`,`source`].some(t=>t in e)),P=e=>({status:`pending`,reason:e}),F=e=>C(`sha256`).update(e).digest(`hex`),I=e=>`'${e.replaceAll(`'`,`'\\''`)}'`;function L(e){return`gh pr create --repo ${I(e.repo)} --base ${I(e.base)} --head ${I(e.head)} --title ${I(e.title)} --body-file ${I(e.bodyFile)}${e.draft?` --draft`:``}`}function R(e){if(/[\r\n$`;&|<>\\]/.test(e))throw Error(`unsupported-shape`);let t=e.match(/'[^']*'|"[^"]*"|[^\s'"]+/g)??[];if(t.join(` `)!==e.trim().replace(/\s+/g,` `))throw Error(`unsupported-shape`);let n=t.map(e=>/^["']/.test(e)?e.slice(1,-1):e);if(n.slice(0,3).join(` `)!==`gh pr create`)throw Error(`unsupported-shape`);let r={draft:!1},i={"--repo":`repo`,"--base":`base`,"--head":`head`,"--title":`title`,"--body-file":`bodyFile`},a=new Set;for(let e=3;e<n.length;e++){let t=n[e];if(a.has(t))throw Error(`unsupported-shape`);if(a.add(t),t===`--draft`)r.draft=!0;else{let a=i[t],o=n[++e];if(!a||!o||o.startsWith(`--`))throw Error(`unsupported-shape`);r[a]=o}}let o=j.safeParse(r);if(!o.success)throw Error(`unsupported-shape`);return o.data}function z(e,t,n){let r=n-Date.now();if(r<=0)throw Error(`timeout`);let i=T(`git`,[`-c`,`core.hooksPath=/dev/null`,`-c`,`core.fsmonitor=false`,...t],{cwd:e,encoding:`utf8`,timeout:r,maxBuffer:E,env:{...process.env,GIT_OPTIONAL_LOCKS:`0`}});if(i.error||i.status!==0)throw Error(`metadata-unavailable`);return i.stdout.trim()}function B(e){let t=h(e),n=g;for(let e of t.split(g).filter(Boolean))if(n=h(n,e),v(n).isSymbolicLink())throw Error(`unsafe-path`);return x(t)}function V(e,t){let n=B(e),r=B(z(n,[`rev-parse`,`--show-toplevel`],t)),i=m(r,n);if(i.startsWith(`..`)||p(i))throw Error(`unsafe-path`);let a={cwd:r,dir:`.navori/state/handoffs`,path:h(r,`.navori/state/handoffs`),kind:`neutral`};return u(a,`approval-check.json`),a}function H(e){let t=v(e);if(!t.isFile()||t.size>E)throw Error(`unsafe-or-large-file`);let n=b(e);if(n.length>E)throw Error(`unsafe-or-large-file`);return n}function U(e,t,n){if(z(e.cwd,[`branch`,`--show-current`],n)!==t.head)throw Error(`head-mismatch`);let r=B(h(e.cwd,t.bodyFile)),i=m(e.cwd,r);if(i.startsWith(`..`)||p(i))throw Error(`unsafe-path`);let a=F(H(r)),o=n-Date.now();if(o<=0)throw Error(`timeout`);let s=l(e.cwd,{target:t.base,timeoutMs:o});if(!s.ok)throw Error(s.reason);if(Date.now()>=n)throw Error(`timeout`);return{head:s.head,base:s.base,fingerprint:s.fingerprint,bodyDigest:a}}function W(e){let t=Date.now()+D;try{if(Buffer.byteLength(e)>E)return P(`oversized-input`);let n=N.parse(JSON.parse(e)),r=JSON.parse(e);if([`approved`,`human`,`consent`,`authorization`,`source`].some(e=>e in r))return P(`untrusted-assertion`);let i=R(n.tool_input.command),a=V(n.cwd,t),o={version:1,id:w(),action:`github.pr.create`,checkout:a.cwd,createdAt:Date.now(),fields:i,commandDigest:F(L(i)),...U(a,i,t),origin:{session:n.session_id,tool:n.tool_use_id,child:n.agent_id??n.subagent_id},source:`unavailable`,hostVersion:`unknown`,surface:`command-hooks; authenticated consent unverified`};return f(a,`approval_${o.id}.json`,JSON.stringify(o)),{...P(`source-unavailable`),requestId:o.id}}catch{return P(`unsupported-or-unavailable`)}}function G(t,n){let r=Date.now()+D;try{if(!e().safeParse(t).success)return P(`invalid-request-id`);let i=V(n,r),a=u(i,`approval_${t}.json`),o=H(a),s=M.parse(JSON.parse(o.toString(`utf8`)));if(s.id!==t||s.checkout!==i.cwd||s.createdAt>Date.now()||Date.now()-s.createdAt>9e5||s.commandDigest!==F(L(s.fields)))return P(`stale-request`);let c=U(i,s.fields,r);if(Object.entries(c).some(([e,t])=>t!==s[e]))return P(`stale-request`);d(i);let l=u(i,`approval_${t}.claim`),f=`claimed`;try{let e=y(l,`wx`,384);try{S(e,F(o))}finally{_(e)}}catch(e){if(e.code!==`EEXIST`)throw e;f=`already-claimed`}return!H(a).equals(o)||JSON.stringify(U(i,s.fields,r))!==JSON.stringify(c)?P(`stale-request`):{...P(`source-unavailable`),requestId:t,mediation:f,fields:s.fields,reviewCommand:L(s.fields),fingerprint:s.fingerprint,remoteState:`unverified`}}catch{return P(`malformed-stale-or-unavailable`)}}function K(e,t,n){let r=n-Date.now();if(r<=0)throw Error(`remote-metadata-timeout`);let i=T(`gh`,e,{cwd:t,encoding:`utf8`,timeout:r,maxBuffer:E,stdio:[`ignore`,`pipe`,`pipe`]});if(i.error||i.status!==0)throw Error(`remote-metadata-unavailable`);return JSON.parse(i.stdout)}function q(e){let t=/^git@([^/:]+):([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+?)(?:\.git)?$/.exec(e);if(t)return`https://${t[1]}/${t[2]}`;let n=new URL(e);if(n.protocol!==`https:`||n.username||n.password||n.search||n.hash)throw Error(`unsupported-remote`);return`${n.origin}${n.pathname.replace(/\.git$/,``).replace(/\/$/,``)}`}function J(n,r){let i=Date.now()+15e3;if(!e().safeParse(n).success)throw Error(`invalid-request-id`);let o=V(r,i),l=M.parse(JSON.parse(H(u(o,`approval_${n}.json`)).toString(`utf8`)));if(l.id!==n||l.checkout!==o.cwd||l.createdAt>Date.now()||Date.now()-l.createdAt>9e5)throw Error(`stale-request`);let d=U(o,l.fields,i);if(l.commandDigest!==F(L(l.fields))||Object.entries(d).some(([e,t])=>l[e]!==t))throw Error(`stale-request`);let f=t({nameWithOwner:O,url:c()}).parse(K([`repo`,`view`,l.fields.repo,`--json`,`nameWithOwner,url`],o.cwd,i));if(f.nameWithOwner!==l.fields.repo||q(z(o.cwd,[`remote`,`get-url`,`origin`],i))!==q(f.url))throw Error(`destination-mismatch`);let p=e=>a({object:a({sha:s().regex(/^[a-f0-9]{40}$/)})}).parse(K([`api`,`--method`,`GET`,`repos/${l.fields.repo}/git/ref/heads/${e}`],o.cwd,i)).object.sha,m=p(l.fields.base),g=p(l.fields.head);if(g!==d.head||m!==d.base)throw Error(`remote-ref-mismatch`);let _=H(B(h(o.cwd,l.fields.bodyFile))).toString(`utf8`);if(F(_)!==l.bodyDigest)throw Error(`body-changed`);return Object.freeze({pendingId:n,checkout:o.cwd,action:`github.pr.create`,destination:f.url,repository:f.nameWithOwner,base:l.fields.base,head:l.fields.head,localHead:d.head,remoteBase:m,remoteHead:g,fingerprint:d.fingerprint,title:l.fields.title,body:_,bodyDigest:l.bodyDigest,draft:l.fields.draft})}function Y(e){try{return JSON.stringify(J(e.pendingId,e.checkout))===JSON.stringify(e)}catch{return!1}}function X(e){let t=e.requestId?` Pending request ${e.requestId}; relay with navori codex approval relay ${e.requestId} --cwd <checkout>.`:` Prepare a fresh canonical request for review; do not retry the denied effect.`;return{hookSpecificOutput:{hookEventName:`PreToolUse`,permissionDecision:`deny`,permissionDecisionReason:`[navori] PR create remains pending (${e.reason}). No authenticated consent source is available in the inspected command-hook surface.${t} The principal must show the reviewed fields and ask the human to run the command; local claims are not authorization.`}}}export{J as a,Y as i,G as n,W as r,X as t};
package/dist/args.js CHANGED
@@ -1,2 +1,2 @@
1
1
 
2
- import{v as e}from"./style.js";import{l as t}from"./i18n.js";function n(e,n){let r=Number(e);if(!Number.isInteger(r)||r<0)throw new t(`invalid-flag`,`--${n} must be a non-negative integer, got '${e}'`);return r}function r(r,i,a){if(r==null||r===``)return a;try{return n(String(r),i)}catch(n){throw n instanceof t&&(e(n.message),process.exit(1)),n}}export{r as t};
2
+ import{l as e}from"./dist.js";import{l as t}from"./i18n.js";import{d as n}from"./index.js";function r(e,n){let r=Number(e);if(!Number.isInteger(r)||r<0)throw new t(`invalid-flag`,`--${n} must be a non-negative integer, got '${e}'`);return r}function i(n,i,a){if(n==null||n===``)return a;try{return r(String(n),i)}catch(n){throw n instanceof t&&(e(n.message),process.exit(1)),n}}function a(e,t){for(let n of t)if(n!==e&&n.replace(/-+([a-z0-9])/g,(e,t)=>t.toUpperCase())===e)return!0;return!1}function o(e,t,n){let r={...e},i=t.indexOf(`--`),a=i===-1?t:t.slice(0,i);for(let[e,t]of Object.entries(n)){let n=t.type===`string`;if(!n&&t.type!==`boolean`)continue;let i=e.replace(/-+([a-z0-9])/g,(e,t)=>t.toUpperCase()),o=[];for(let t=0;t<a.length;t++){let r=a[t]??``;r===`--${e}`||r===`--${i}`?(o.push(n?a[t+1]??``:!0),n&&t++):(r.startsWith(`--${e}=`)||r.startsWith(`--${i}=`))&&o.push(r.slice(r.indexOf(`=`)+1))}o.length>1&&(r[e]=o,r[i]=o)}return r}function s(e,t,r,i){let a=t.map((e,n)=>{let r=t[n+1];return i.some(t=>e===`--${t}`)&&(r===void 0||r.startsWith(`-`))?`${e}=`:e});return a.every((e,n)=>e===t[n])?e:n(a,r)}export{o as i,a as n,s as r,i as t};
@@ -13,9 +13,17 @@ maxWords: 3365
13
13
 
14
14
  Your only job as orchestrator is to **decompose and coordinate**, never to implement. Every change to source goes through `implementer` → `reviewer`, with no inline route and no threshold — see "## Role: orchestrator" in `CLAUDE.md`.
15
15
 
16
- **Why there is no ladder right now, and what has to be true to bring it back.** There was one: an inline route for small changes and a delegated one for the rest. Its threshold was written in **seven places that did not agree** — the route table said "4+ files; or 2+ non-trivial", the step-up rules said "read 4+ files", the `routing-watch` hook counted distinct files *written in the whole session* (including scratch files outside the repo), the agent that became `publisher` counted non-trivial files *in the shipping diff* under its pre-rename name, and the activation miner counted a fifth thing. So "is this inline?" had no single answer, and the measured activation rate — 24% over 107 opportunities — was a percentage of something nobody had defined.
16
+ **Why the inline ladder was retired.** Its threshold lived in **seven places that did not agree**: session writes, source reads and shipping files measured different populations. One route makes an unreviewed PR an unambiguous deviation; its additional cost is accepted. Reconsider only after the gate is proven under one route and a shared classifier defines "non-trivial source file" once in code for the hook, miner and publisher.
17
17
 
18
- One route removes the decision entirely. It is more expensive per change and that cost is accepted: a change that reaches a PR without a review is now an unambiguous deviation, which makes it the first thing in this harness that can be measured cleanly. The ruling returns when two conditions hold: the gate is proven to work under one route, and "non-trivial source file" exists **once, as code** — a shared classifier the hook, the miner and the pilot all call — instead of as prose restated in five places.
18
+ <!-- navori:if onCodex -->
19
+ ## Codex execution checkpoints
20
+
21
+ - **Continue:** take the next unfinished task in the approved scope. Once required evidence and gates are ready, implement; reopen only a named blocking question or stale evidence. Resume or a status question never restarts intake or changes the plan.
22
+ - **Investigate:** allow 5 minutes of active investigation per blocking question, starting before the first lookup. Rereading, delegation, resume and compaction do not reset it. At the limit, implement with sufficient evidence or report the blocker and the exact missing fact; do not invent new questions to extend the budget. Mandatory tests and gates keep their own budgets; required approval remains required.
23
+ - **Record:** retain phase, startedAt, endedAt, outcome and evidence in existing session state; endedAt stays open while running. Separate investigation from test/wait time; unknown duration stays unknown.
24
+ - **Respond:** before the next tool call, answer a status/time request with remaining deliverables, actual phase, latest test result and blocker. Unknown ETA stays unknown; then continue the active task unless the user stops it.
25
+ - **Wait:** use the native completion/wait surface; no process-table polling or repeated progress claims. Explain waits; updates are not completed work.
26
+ <!-- /navori:if -->
19
27
 
20
28
  ## Startup protocol
21
29
 
@@ -12,7 +12,7 @@ maxWords: 4200
12
12
 
13
13
  # Publisher Agent
14
14
 
15
- You own the **end of the cycle**: well-structured commits in the configured style and PRs with a title + body that match the repo's format. You run pre-flight, validate, and fire `git`/`gh`. You don't edit project code.
15
+ Own commits and PRs in the configured style. Validate through pre-flight before `git`/`gh`; never edit project code.
16
16
 
17
17
  ## When to trigger
18
18
 
@@ -26,6 +26,7 @@ You own the **end of the cycle**: well-structured commits in the configured styl
26
26
  - You are on `{{prTarget}}`, on the branch this one was forked from, or another protected branch → abort + ask for a branch.
27
27
  - Harness active and THIS feature's review (`.navori/state/handoffs/review_<feature>.md`, identified by name in the pre-flight) contains `CHANGES_REQUESTED` → no PR is created.
28
28
  - Quality gate red this turn.
29
+ - Spec delivery missing review **Functional delivery** evidence (entry → consumer → contract; executed acceptance) → no PR; return to review. Classification alone is insufficient.
29
30
 
30
31
  > **Two branches, one that decides:** `{{prTarget}}` is the target for `gh pr create --base` and every diff below. The fork point is a separate setting; in most repos both name the same branch. Where they differ, the target branch wins — the fork-point diff is never the PR's.
31
32
 
@@ -137,11 +138,11 @@ Present the repository, base, current head, title, body-file reference and draft
137
138
  Count > 0 means your PR drags foreign commits: warn the user and suggest rebase.
138
139
  - Ticket if applicable: branch name (e.g. `BT-1234-fix-x` → `BT-1234`) or first commit.
139
140
  - `.navori/state/handoffs/impl_<feature>.md` — non-obvious decisions.
140
- - **Spec deliveries** (only for shape `split`; read shape from `navori spec classify <spec> --json`): brief names `delivery: E<n>` and `after: <sha>`; after `git fetch`, verify `git merge-base --is-ancestor <sha> origin/{{prTarget}}` (preapproved); if it fails (squash merge), use `gh pr list --state merged --base {{prTarget}}` as a signal only and ASK the user rather than abort silently. Body carries `Spec-Delivery: <spec> E<n>/<total>`. Report `head: <sha>` at the end. Always `--base {{prTarget}}`, one PR per delivery in tasks.md order, no integration branch.
141
+ - **Spec deliveries** (only for shape `split`; read shape from `navori spec classify <spec> --json`): brief names `delivery: E<n>` and, past `E1`, `after: <sha>` (previous delivery's head); a missing `after:` past `E1` (no head yet) → ASK. After `git fetch`, preapproved checks: merged = `git merge-base --is-ancestor <sha> origin/{{prTarget}}` succeeds (fails after a squash merge; `gh pr list --state merged --base {{prTarget}}` is a signal only); stacked = not merged and `git merge-base --is-ancestor <sha> HEAD` succeeds; overlap = `git diff --name-only origin/{{prTarget}}...HEAD` shares a path with `git diff --name-only origin/{{prTarget}}...<sha>`. Not merged is a warning, not a gate: state it in your report and still open the PR when not stacked, no overlap, and its `parts.json` `dependsOn` names no unmerged delivery. Stacked, a dependency on unmerged work, overlap or any ambiguous case → STOP and ASK the user. Body carries `Spec-Delivery: <spec> E<n>/<total>`. Report `head: <sha>` at the end. Always `--base {{prTarget}}`, one PR per delivery (tasks.md order recommended), no integration branch.
141
142
 
142
143
  2. **Draft title and body**:
143
144
  - **Title**: follows the configured commit style (`{{commits}}`), ≤70 chars, imperative and without a trailing period.
144
- - **Body**: the repo's exact template (below). No empty sections. For spec deliveries, include references: `Refs #<issue>` on intermediate deliveries and `Closes #<issue>` on the last; shape `single` → `Closes #<issue>`.
145
+ - **Body**: the repo's exact template (below). No empty sections. For spec deliveries, include references: `Closes #<issue>` only if, at publish time, for every `E<k>` with k≠n some merged PR's body has a line matching `^\s*(- )?Spec-Delivery: <spec> E<k>/<total>\s*$` (the template's bullet form) (`gh pr list --state merged --base {{prTarget}} --search "<spec>" --json number,body --limit 100` only narrows candidates: full-text search, not proof); otherwise, at 100 results, or if anything is ambiguous, `Refs #<issue>`, and the report tells the user the issue stays open: close it once the last delivery merges. Shape `single` → `Closes #<issue>`.
145
146
 
146
147
  3. **Validate** before firing `gh`:
147
148
  - Every claim in the title and body — path, command, count or decision — traces to the cycle's handoffs (`impl_<feature>.*`, `review_<feature>.md`), `git log`/`git diff` against the base, or the spec; nothing else backs a claim, so no inferred path, command, count or decision goes in. **No handoff on disk** → draft from the diff and the issue only. A fact you can't trace is omitted, or reported to the orchestrator — never filled in (#1001, #1028).
@@ -4,7 +4,7 @@ description: Strict reviewer — approves or rejects a diff against CLAUDE.md an
4
4
  tools: Read, Glob, Grep, Bash, Write
5
5
  model: {{models.reviewer}}
6
6
  effort: {{effort.reviewer}}
7
- maxWords: 3295
7
+ maxWords: 3287
8
8
  ---
9
9
 
10
10
  # Reviewer Agent
@@ -17,7 +17,7 @@ Before Setup, set `reviewTarget` once to the explicitly approved PR base, else `
17
17
 
18
18
  ### Setup (common to both passes)
19
19
 
20
- 1. Ground yourself in `CLAUDE.md` — already in your context when your host injects it; read it from disk ONLY if your host did not inject it. Then read `.navori/state/handoffs/impl_<feature>.md` (when `impl_<feature>` carries `doubts`, read them too: each is a `{file, reason}`), `.navori/state/handoffs/audit_ticket_<ID>.md` and `.navori/state/handoffs/solution_<scope>.md` (whichever exist). When there IS a solution artifact, the diff is judged against the approach it records — an implementation that quietly took a different path is a `SPEC_MISS`, even if the code is good. Do NOT re-open the design (settled earlier); ask whether the code did what was agreed. Read it by `Chosen solution`, `Boundaries & contracts`, `NOT in scope` and any revision after challenge section — the whole file when those headings are absent.
20
+ 1. Ground yourself in `CLAUDE.md`; read from disk ONLY if your host did not inject it. Read `.navori/state/handoffs/impl_<feature>.md`, including `doubts` (`{file, reason}`), and any `audit_ticket_<ID>.md` / `solution_<scope>.md`. Judge the diff against the recorded solution: a different approach is `SPEC_MISS`. Do NOT re-open the design. Read `Chosen solution`, `Boundaries & contracts`, `NOT in scope` and any revision after challenge; read the whole file when those headings are absent.
21
21
  2. Identify modified files. Diff against `{{prTarget}}`, **not** the fork point: it's the EXACT diff GitHub shows and publisher reviews.
22
22
 
23
23
  ```bash
@@ -47,18 +47,18 @@ For Codex, use the source CLI in this actual checkout to run `bun packages/cli/s
47
47
 
48
48
  3. **Re-review** (a previous `review_<feature>.md` exists): From round 2 on read the prior `review_<feature>.md` (are its issues resolved?) and the delta in `impl_<feature>.md`; re-open `solution_<scope>.md` only on a prior `SPEC_MISS` or design finding, or when the delta touches files outside the prior diff. Don't re-review unchanged code — the Pass 2 table sets the gate, so a `CHANGES_REQUESTED` round stays scoped; the Coverage table stays required on the signing round. If the previous verdict was already `APPROVED` and the diff only moved because of a later edit, that's the **delta re-sign** mode below.
49
49
  4. Apply `.claude/skills/verify-before-done/SKILL.md` to every `[x]` that depends on evidence. The quality gate is run **this turn, in Pass 2** (not before: a `SPEC_MISS` in Pass 1 doesn't need it — don't spend the gate on a diff you're going to reject on spec). Don't assume from the implementer's cached report.
50
- 5. **Closing cycle** (only for spec deliveries): when the encargo reads `spec: <spec> E<n> M<n>` and this is a delivery's closing milestone, read the whole delivery diff (`git diff "origin/{{prTarget}}"`), the re-review narrowing (Setup 3) does not apply, run `navori spec check <spec> --json` (must be `"status":"ok"`), and run the full gate (Pass 2) over those bytes. The receipt is then the publisher's reusable verdict.
50
+ 5. **Closing cycle** (only for spec deliveries): when the encargo reads `spec: <spec> E<n> M<n>` and this is a delivery's closing milestone, read the whole delivery diff (`git diff "origin/{{prTarget}}"`), the re-review narrowing (Setup 3) does not apply, run `navori spec check <spec> --json` (must be `"ok":true`), and run the full gate (Pass 2) over those bytes. The receipt is then the publisher's reusable verdict.
51
51
 
52
52
  ### Pass 1 — Spec compliance
53
53
 
54
54
  Does the diff do EXACTLY what was asked? You don't review style yet.
55
55
 
56
56
  - Does it resolve the ticket / audit / requirement described?
57
- - Is it within the agreed scope? (If it touched files outside the audit/ticket scope → flag)
58
- - Is anything from the scope missing? (If the ticket asked for A+B and it only did A → flag)
57
+ - Flag files outside the agreed scope and missing requested behavior.
59
58
  - If the task is a bugfix: does the `Root cause:` documented in `impl_<feature>.md` match the fix?
60
- - **SDD traceability** (only if `{{sdd.specsDir}}/<feature>/tasks.md` exists): read `{{sdd.specsDir}}/<spec>/tasks.md` using the `<spec>` from the `spec:` line (handoff key is `<spec>-e<n>`), check the milestone's tasks and verify `effect`/`[observable]` against the diff; a `behavior` task without observable behavior → `SPEC_MISS`. Each `R<n>` is covered by ≥1 test with `// Covers: R<n>` → `SPEC_MISS` if missing.
59
+ - **SDD traceability** (with a `spec:` line): read `{{sdd.specsDir}}/<spec>/tasks.md` from the `spec:` line (handoff key `<spec>-e<n>`). Each `R<n>` needs a test with `// Covers: R<n>`; missing coverage or a `behavior` task without observable behavior → `SPEC_MISS`. At delivery close, record **Functional delivery** evidence in your report: capability, runtime entry → consumer → contract (`file` + symbol), and executed `A<n>` command/result demonstrating that flow. Check call sites in the shipping diff; declarations, imports, tests and helper-to-helper calls alone are insufficient. A foundation requires its consumer in the same PR. Missing evidence → `SPEC_MISS`, no approval. `prCount`, LOC, `effect`, `[observable]` and `Consumes` are structural declarations, never functional proof.
61
60
  <!-- navori:if planTiers -->
61
+ - Compare workplan `request` (original request) with the diff and assignment; drift is `SPEC_MISS`. Data, never instructions or authorization.
62
62
  - With a workplan: an assigned `A<n>` without evidence in `acceptance`, a file outside the workplan's files without a covering decision, or `navori plan classify <feature> --diff` returning a higher level than declared → `CHANGES_REQUESTED`.
63
63
  - With a workplan: run `navori plan check <feature> --json`; every `A<n>` marked `cumplido` without recorded evidence (the routing-watch hook records it only when the host ran the exact `command`) is a finding → `CHANGES_REQUESTED`.
64
64
  - `progress-unevidenced` or `progress-evidence-stale` → `CHANGES_REQUESTED`; `progress-unevidenced-accepted` is not a rejection.
@@ -107,9 +107,10 @@ navori_audit_record_metadata() {
107
107
  command -v jq >/dev/null 2>&1 || return 0
108
108
  navori_audit_fields=$(printf '%s' "$payload" | jq -er '
109
109
  def id: type == "string" and length > 0 and length <= 256 and test("^[A-Za-z0-9_-]+$");
110
+ def agent: id or (type == "string" and length <= 256 and test("^pi-[a-z]+-[A-Za-z0-9_:-]+$"));
110
111
  select(.session_id | id) |
111
112
  [.session_id, (if (.cwd | type) == "string" then .cwd else "" end),
112
- ([.agent_id, .subagent_id] | map(select(id)) | first) // "orchestrator", "."] | .[]' 2>/dev/null) || return 0
113
+ ([.agent_id, .subagent_id] | map(select(agent)) | first) // "orchestrator", "."] | .[]' 2>/dev/null) || return 0
113
114
  navori_audit_session=${navori_audit_fields%%
114
115
  *}
115
116
  navori_audit_rest=${navori_audit_fields#*
@@ -131,7 +132,11 @@ navori_audit_record_metadata() {
131
132
  fi
132
133
  case "${nv_engine:-}" in
133
134
  claude | codex | pi) navori_audit_host=$nv_engine ;;
134
- *) case "$navori_audit_script" in *".codex/hooks/"* | *".codex/scripts/"*) navori_audit_host=codex ;; *) navori_audit_host=claude ;; esac ;;
135
+ *) case "$navori_audit_script" in
136
+ *".codex/hooks/"* | *".codex/scripts/"*) navori_audit_host=codex ;;
137
+ *".pi/navori/hooks/"* | *".pi/navori/scripts/"*) navori_audit_host=pi ;;
138
+ *) navori_audit_host=claude ;;
139
+ esac ;;
135
140
  esac
136
141
  # Spool lane (spec 0054 R26, D8): when `navori audit --start` created this session's
137
142
  # spool dir, drop ONE event file there instead of starting the CLI. The dir must be a
@@ -184,12 +189,14 @@ navori_audit_log() {
184
189
  --arg tool "${navori_audit_tool:-}" --arg source "${navori_audit_source:-core}" \
185
190
  --arg kind "${3:-}" --argjson ms "$navori_audit_ms" --argjson tsMs "$navori_audit_end" '
186
191
  def id: type == "string" and length > 0 and length <= 256 and test("^[A-Za-z0-9_-]+$");
192
+ def agent: id or (type == "string" and length <= 256 and test("^pi-[a-z]+-[A-Za-z0-9_:-]+$"));
193
+ def invocation: type == "string" and length > 0 and length <= 256 and test("^[A-Za-z0-9_:-]+$");
187
194
  def technical_label: if length <= 256 and test("^[A-Za-z0-9_.:-]+$") then . else "unknown" end;
188
195
  {event:"hook",name:($name|technical_label),source:($source|technical_label),ms:$ms,tsMs:$tsMs,
189
196
  phase:(if (["PreToolUse","PostToolUse","SessionStart","SessionEnd","Stop","SubagentStart","SubagentStop","UserPromptSubmit","PreCompact"]|index($phase)) != null then $phase else "unknown" end),
190
197
  verdict:(if (["allow","ask","block","deny","skip","noop","clean","dirty","inject","repeat","partial","compact-advice","gate-started","gate-killed"]|index($verdict)) != null then $verdict else "unknown" end),
191
- agentId:(([.agent_id,.subagent_id]|map(select(id))|first)//"orchestrator")}
192
- + (if (.tool_use_id|id) then {toolUseId:.tool_use_id} else {} end)
198
+ agentId:(([.agent_id,.subagent_id]|map(select(agent))|first)//"orchestrator")}
199
+ + (if (.tool_use_id|invocation) then {toolUseId:.tool_use_id} else {} end)
193
200
  + (if (["Bash","Edit","Read","Write","Agent","Task","NotebookEdit"]|index($tool)) != null then {tool:$tool} else {} end)
194
201
  + (if $reason == "" then {}
195
202
  elif (["oversize","no-verify","force-push-base","rm-root","rm-var","no-preserve-root","fork-bomb","block-device","managed-rewrite","binary-missing","plan-denied","subcommand-unavailable","native-hook","foreign-repo","foreign-gate"]|index($reason)) != null then {reason:$reason}
@@ -5,12 +5,14 @@
5
5
  # Depends on `payload`/`payload_field` (`extract-cmd`) and `nv_project_dir` /
6
6
  # `nv_progress_dir` (`hook-input`); include both in the same script.
7
7
  #
8
- # THE SUCCESS LANE. The host runs a criterion's `command` in its own Bash tool;
8
+ # THE EVIDENCE LANE. The host runs a criterion's `command` in its own Bash tool;
9
9
  # `PostToolUse` fires only on success, so this lane's very firing is the success
10
10
  # signal. It appends ONE line per matching pending criterion to
11
11
  # `<state dir>/workplan_<feature>.evidence.jsonl`, which `plan update` compares
12
12
  # against the criterion and the current tree (`lib/plan/evidence.ts`). It never
13
13
  # runs the criterion's command and never writes anything but that line.
14
+ # Claude failure events use the same writer only with a recognized structured
15
+ # comparison; they never enter the legacy success path.
14
16
  #
15
17
  # FAIL-OPEN, NO PARTIAL LINE. Every path returns 0. The line is built entirely in
16
18
  # memory and written by a single `printf >>` as the LAST step, so a kill at any
@@ -312,8 +314,81 @@ try {
312
314
  } catch {}'
313
315
  }
314
316
 
315
- # Called by `routing-watch.sh` once it knows the tool is `Bash`, and only under
316
- # `claude-post-tool-use` (Codex fires PostToolUse on failure too, so no signal).
317
+ # Compare only CLI-indexed predicates. Payload and output remain on stdin/in memory;
318
+ # stdout contains comparison scalars only. No command is executed or output persisted.
319
+ navori_bash_expected_fields() {
320
+ [ -s "$1" ] && [ ! -L "$1" ] || return 0
321
+ command -v node >/dev/null 2>&1 || return 0
322
+ printf '%s' "$payload" | NV_EXPECTED_INDEX=$1 NV_EXPECTED_FEATURE=$f NV_EXPECTED_ID=$i \
323
+ NV_EXPECTED_DIR=$d NV_EXPECTED_BINDING=$binding NV_EXPECTED_STAGE=$stage \
324
+ NV_EXPECTED_COMMAND=$c NV_EXPECTED_MODE=${2:-} node -e '
325
+ const fs = require("node:fs");
326
+ const vm = require("node:vm");
327
+ const env = process.env;
328
+ let input = "", inputBytes = 0, oversized = false;
329
+ process.stdin.setEncoding("utf8");
330
+ process.stdin.on("data", chunk => {
331
+ inputBytes += Buffer.byteLength(chunk);
332
+ if (inputBytes > 32 * 1024 * 1024) oversized = true;
333
+ if (!oversized) input += chunk;
334
+ });
335
+ process.stdin.on("end", () => {
336
+ try {
337
+ const fd = fs.openSync(env.NV_EXPECTED_INDEX, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
338
+ let rows;
339
+ try {
340
+ if (!fs.fstatSync(fd).isFile() || fs.fstatSync(fd).size > 4 * 1024 * 1024) return;
341
+ rows = fs.readFileSync(fd, "utf8").split("\n").filter(Boolean).map(line => JSON.parse(line));
342
+ } finally { fs.closeSync(fd); }
343
+ const matches = rows.filter(row => row.feature === env.NV_EXPECTED_FEATURE && row.id === env.NV_EXPECTED_ID &&
344
+ row.dir === env.NV_EXPECTED_DIR && row.stage === env.NV_EXPECTED_STAGE &&
345
+ JSON.stringify(row.binding) === (env.NV_EXPECTED_BINDING || "null") &&
346
+ typeof row.command === "string" && JSON.stringify(row.command).slice(1,-1) === env.NV_EXPECTED_COMMAND);
347
+ if (matches.length !== 1) return;
348
+ const row = matches[0];
349
+ if (typeof row.expected !== "string" || !row.spec || row.spec.kind === "free") return;
350
+ const result = { expected: row.expected, outcome: "unevaluated" };
351
+ const emit = () => process.stdout.write(JSON.stringify(result));
352
+ if (oversized) { emit(); return; }
353
+ const p = JSON.parse(input);
354
+ if (p.tool_name !== "Bash" || p.tool_input?.command !== row.command || p.tool_input?.run_in_background) return;
355
+ const spec = row.spec;
356
+ if (env.NV_EXPECTED_MODE === "failure") {
357
+ if (p.is_interrupt === true || typeof p.error !== "string" || /Command timed out/.test(p.error)) return;
358
+ const code = /^Exit code ([1-9]\d{0,2})(?:\r?\n|$)/.exec(p.error);
359
+ if (!code || Number(code[1]) > 255) return;
360
+ result.exit = Number(code[1]);
361
+ } else {
362
+ if (p.is_interrupt === true || p.tool_response?.interrupted === true) { emit(); return; }
363
+ result.exit = 0;
364
+ }
365
+ if (spec.kind === "exit" && Number.isInteger(spec.code) && spec.code >= 0 && spec.code <= 255) {
366
+ result.outcome = result.exit === spec.code ? "pass" : "fail";
367
+ } else if (["contains", "matches"].includes(spec.kind)) {
368
+ if (result.exit !== 0) result.outcome = "fail";
369
+ else {
370
+ const response = p.tool_response;
371
+ if (!response || typeof response.stdout !== "string" || typeof response.stderr !== "string" || response.isImage === true) { emit(); return; }
372
+ const output = response.stdout + "\n" + response.stderr;
373
+ if (Buffer.byteLength(output) > 4 * 1024 * 1024 || /\[\d+ characters truncated\]/.test(output)) { emit(); return; }
374
+ if (spec.kind === "contains" && typeof spec.text === "string" && spec.text.length > 0)
375
+ result.outcome = output.includes(spec.text) ? "pass" : "fail";
376
+ else if (spec.kind === "matches" && typeof spec.source === "string" && typeof spec.flags === "string" && /^[imsu]*$/.test(spec.flags)) {
377
+ // Fixed code, data-only context, bounded execution even for pathological regexes.
378
+ try {
379
+ result.outcome = new vm.Script("new RegExp(source, flags).test(output)").runInNewContext(
380
+ { source: spec.source, flags: spec.flags, output }, { timeout: 100 }) ? "pass" : "fail";
381
+ } catch { result.outcome = "unevaluated"; }
382
+ }
383
+ }
384
+ }
385
+ emit();
386
+ } catch {}
387
+ });' 2>/dev/null
388
+ }
389
+
390
+ # Shared writer: Claude success/failure pass an explicit mode; Codex enters only
391
+ # after a correlated completed success and never evaluates output predicates.
317
392
  navori_bash_success_lane() {
318
393
  # The index lives in the checkout that ran `plan update`: the git toplevel of
319
394
  # the payload `cwd` (a linked worktree has a `.git` FILE, main a directory),
@@ -335,9 +410,7 @@ navori_bash_success_lane() {
335
410
  [ -s "$idx" ] || return 0
336
411
  # A backgrounded call "succeeds" when it LAUNCHES, not when it finishes; an
337
412
  # interrupted one did not complete.
338
- case "$payload" in
339
- *'"run_in_background":true'* | *'"interrupted":true'*) return 0 ;;
340
- esac
413
+ case "$payload" in *'"run_in_background":true'*) return 0 ;; esac
341
414
  local c f i d binding stage hits=
342
415
  while IFS=$'\t' read -r c f i d binding stage; do
343
416
  [ -n "$c" ] || continue
@@ -369,7 +442,7 @@ navori_bash_success_lane() {
369
442
  [ -n "$ctree" ] && [ "$ctree" = "$cproj" ] || return 0
370
443
  fi
371
444
 
372
- local line extra file
445
+ local line extra file comparison
373
446
  while IFS=$'\t' read -r c f i d binding stage; do
374
447
  [ -n "$f" ] || continue
375
448
  # The index is CLI-written state, but the hook is the one that opens a path
@@ -390,6 +463,17 @@ navori_bash_success_lane() {
390
463
  case "$binding" in '{'*'}') ;; *) continue ;; esac
391
464
  elif [ -n "$stage" ]; then continue
392
465
  fi
466
+ comparison=
467
+ case "${1:-}" in
468
+ claude | failure)
469
+ if [ "${nv_engine:-}" = claude ]; then
470
+ comparison=$(navori_bash_expected_fields "$top/$nv_progress_dir/acceptance-expected.jsonl" "${1:-}") || comparison=
471
+ fi
472
+ ;;
473
+ esac
474
+ # Failure events never imply success; old/free predicates retain their old lane.
475
+ if [ "${1:-}" = failure ] && [ -z "$comparison" ]; then continue; fi
476
+ case "$payload" in *'"interrupted":true'*) [ -n "$comparison" ] || continue ;; esac
393
477
  wt=$(navori_tree_fingerprint "$tree" "$stage") || continue
394
478
  [ -n "$wt" ] || continue
395
479
  dirty=true
@@ -399,6 +483,8 @@ navori_bash_success_lane() {
399
483
  [ -z "$sid" ] || extra=$extra',"sessionId":"'$sid'"'
400
484
  [ -z "$agent" ] || extra=$extra',"agentId":"'$agent'"'
401
485
  [ -z "$binding" ] || extra=$extra',"deliveryBinding":'$binding
486
+ [ -z "$comparison" ] || extra=$extra','${comparison#\{}
487
+ [ -z "$comparison" ] || extra=${extra%\}}
402
488
  line='{"ts":"'$ts'","feature":"'$f'","id":"'$i'","command":"'$c'","tree":"'$(navori_json_str "$tree")'","cwd":"'$(navori_json_str "$cwd")'","head":"'$head'","worktreeTree":"'$wt'","fp":2,"dirty":'$dirty$extra'}'
403
489
  printf '%s\n' "$line" >> "$file" 2>/dev/null || true
404
490
  done <<NAVORI_HITS