arkgate 4.8.16 → 4.8.18

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 (46) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +10 -7
  3. package/bin/ark-check-runtime.mjs +5 -1
  4. package/bin/ark-mcp-runtime.mjs +14 -7
  5. package/bin/ark-shared.mjs +10 -4
  6. package/bin/ark.mjs +6 -0
  7. package/bin/lib/adr-path.mjs +116 -0
  8. package/bin/lib/adr-presence.mjs +3 -2
  9. package/bin/lib/agent-gates.mjs +2 -0
  10. package/bin/lib/analysis-engine.mjs +6 -6
  11. package/bin/lib/doctor-human.mjs +2 -2
  12. package/bin/lib/doctor-next-actions.mjs +117 -17
  13. package/bin/lib/first-run-help.mjs +6 -2
  14. package/bin/lib/html-report.mjs +1 -1
  15. package/bin/lib/policy-delta-io.mjs +19 -10
  16. package/bin/lib/presets.mjs +15 -21
  17. package/bin/lib/skill-install.mjs +42 -4
  18. package/bin/lib/start-preview.mjs +75 -3
  19. package/dist/{diagnosticCatalog-KWvGLI1U.d.ts → diagnosticCatalog-BNxKdcN4.d.ts} +7 -1
  20. package/dist/index.cjs +9 -9
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +11 -11
  23. package/dist/nestjs/index.cjs +1 -1
  24. package/dist/nestjs/index.js +1 -1
  25. package/dist/runtime/index.cjs +6 -6
  26. package/dist/runtime/index.d.ts +1 -1
  27. package/dist/runtime/index.js +6 -6
  28. package/docs/README.md +2 -2
  29. package/docs/agent-guide.md +9 -2
  30. package/docs/ai-gates.md +4 -0
  31. package/docs/configuration.md +6 -3
  32. package/docs/enthusiast/how-to-agent-gates.md +3 -1
  33. package/docs/enthusiast/how-to-pick-shape.md +1 -1
  34. package/docs/package-surface.md +1 -1
  35. package/docs/use.md +9 -4
  36. package/package.json +1 -1
  37. package/server.json +2 -2
  38. package/templates/agent-skills/README.md +1 -1
  39. package/templates/agent-skills/ark-adopt/SKILL.md +8 -5
  40. package/templates/agent-skills/ark-explain/SKILL.md +3 -0
  41. package/templates/agent-skills/ark-explore/SKILL.md +3 -1
  42. package/templates/agent-skills/ark-upgrade/SKILL.md +1 -0
  43. package/templates/skills/ark-adopt.md +8 -5
  44. package/templates/skills/ark-explain.md +3 -0
  45. package/templates/skills/ark-explore.md +3 -1
  46. package/templates/skills/ark-upgrade.md +1 -0
package/CHANGELOG.md CHANGED
@@ -96,6 +96,87 @@ in the immutable pre-2.0 archive linked below.
96
96
  Existing configs keep whatever URL they already have. Editor completion
97
97
  matches the 4.x line you just installed.
98
98
 
99
+ ## 4.8.18 — 2026-09-18
100
+
101
+ **Patch** over **4.8.17**. Daily accumulate (2026-09-18 ART) of the six
102
+ ships that landed after 4.8.17 published: monorepo DomainModel globs
103
+ ([#269](https://github.com/pedroknigge/arkgate/pull/269)), packed
104
+ README latest-truth
105
+ ([#270](https://github.com/pedroknigge/arkgate/issues/270) /
106
+ [#272](https://github.com/pedroknigge/arkgate/pull/272)), deep pstack
107
+ audit ([#273](https://github.com/pedroknigge/arkgate/pull/273)), doctor
108
+ #1 dual-match honesty
109
+ ([#275](https://github.com/pedroknigge/arkgate/pull/275)), compact-start
110
+ size refuse ([#274](https://github.com/pedroknigge/arkgate/pull/274)),
111
+ and stale-skill Guiar
112
+ ([#278](https://github.com/pedroknigge/arkgate/pull/278)).
113
+ **Write. Check. Ship.** **No required config migration.** No
114
+ `schemaVersion` bump. Does not close `K01` / `Z09`. This mother
115
+ `ark.config.json` still does **not** turn `arkOrder` on.
116
+
117
+ **Status: published** (npm `latest` is **4.8.18**).
118
+
119
+ ### Added
120
+ - Doctor and `ark-check` warn when installed `/ark-*` skills are behind
121
+ this package (canonical `.agents/skills` included, even with no host
122
+ marker dir). Primary next action is skills-only refresh
123
+ (`--install-agent-gates --skills-only --force`) — not a full re-adopt
124
+ ([#278](https://github.com/pedroknigge/arkgate/pull/278)).
125
+ - Maintainer pstack audit report (core + add-ons) at
126
+ `docs/audit/2026-09-17-pstack-deep-product-audit.md`
127
+ ([#273](https://github.com/pedroknigge/arkgate/pull/273)). Report
128
+ only. No product code.
129
+
130
+ ### Fixed
131
+ - Monorepo `arkgate start` no longer puts whole-app roots (`api/**`,
132
+ `client/**`) on DomainModel next to the finer globs. Domain stays
133
+ package-scoped (`packages/*/src/**`, `**/domain/**`). When dual-match
134
+ is huge, doctor #1 is “fix overlapping layer globs” with one example,
135
+ not only “make CI required”
136
+ ([#269](https://github.com/pedroknigge/arkgate/pull/269)).
137
+ - Packed README / docs hub no longer pin npm `latest` to an older
138
+ version than this tarball. `check:package-files` and the
139
+ packed-artifact path refuse that waiting-room pin
140
+ ([#270](https://github.com/pedroknigge/arkgate/issues/270) /
141
+ [#272](https://github.com/pedroknigge/arkgate/pull/272)).
142
+ - Doctor #1 no longer treats intentional Domain+Tooling file+glob
143
+ dual-lists as a glob leak, and overlapping-glob copy names the tree's
144
+ real roots instead of a canned `api/**` sermon
145
+ ([#275](https://github.com/pedroknigge/arkgate/pull/275)).
146
+ - `doctorResidentWarm` CI ceiling is 800ms after docs-only main already
147
+ missed 680ms (p95 697–707ms). Same miss as #270 / the audit tip
148
+ ([#275](https://github.com/pedroknigge/arkgate/pull/275)).
149
+ - Cold `arkgate start --apply` now refuses a plan that is too big for
150
+ compact start **before** any “writing…” copy, names the file/byte
151
+ numbers, and prints one next action (`arkgate-check --init`).
152
+ `--force` does not unlock the size lock
153
+ ([#274](https://github.com/pedroknigge/arkgate/pull/274)).
154
+
155
+ ## 4.8.17 — 2026-09-17
156
+
157
+ **Patch** over **4.8.16**. Daily accumulate of the two ships that landed
158
+ after 4.8.16 published: ADR path on policy weaken / new layer edge
159
+ ([#264](https://github.com/pedroknigge/arkgate/pull/264)) and the
160
+ policy-delta-ack-match confidence retarget
161
+ ([#265](https://github.com/pedroknigge/arkgate/pull/265)).
162
+ **Write. Check. Ship.** **No required config migration.** No
163
+ `schemaVersion` bump. Does not close `K01` / `Z09`. This mother
164
+ `ark.config.json` still does **not** turn `arkOrder` on.
165
+
166
+ **Status: published** (npm `latest` is **4.8.17**).
167
+
168
+ ### Added
169
+ - A weaken, new layer, or new allow edge must name a short ADR /
170
+ decision-note path on `--policy-ack` (`adrPath`). A free-floating
171
+ reason is not enough
172
+ ([#264](https://github.com/pedroknigge/arkgate/pull/264)).
173
+
174
+ ### Fixed
175
+ - Retarget `policy-delta-ack-match` onto the current
176
+ `policyDeltaAcknowledgementMatches` body so the 90% confidence
177
+ floor still holds after #264
178
+ ([#265](https://github.com/pedroknigge/arkgate/pull/265)).
179
+
99
180
  ## 4.8.16 — 2026-09-16
100
181
 
101
182
  **Patch** over **4.8.15**. Daily accumulate of the five ships that landed
package/README.md CHANGED
@@ -39,7 +39,7 @@ Works with Cursor, Claude, Codex, and Grok.
39
39
 
40
40
  </div>
41
41
 
42
- > **ArkGate 4.8.16** is prepared on this tree; npm `latest` remains **4.8.15** until `publish-npm` for `v4.8.16`.
42
+ > **ArkGate 4.8.18** is on npm `latest`.
43
43
  > Write. Check. Ship. Adopted = required GitHub
44
44
  > status running `arkgate-check --strict-merge`, or an explicit `advisory-only` stance.
45
45
  > Status is compact (`arkgate-check --doctor`; `--all` for Details). Optional **ArkRun**
@@ -47,7 +47,7 @@ Works with Cursor, Claude, Codex, and Grok.
47
47
  > (`arkgate/order`) stops rewriting a big product choice — like the billing plan —
48
48
  > as if it were a seat count. Change those choices through a valve, not a generic update.
49
49
  > `@arkgate/runtime` is deprecated.
50
- > [4.8.16 prepared](CHANGELOG.md) · [4.8.11 published](docs/releases/4.8.11.md) · [4.8.10](docs/releases/4.8.10.md) · [4.8.9](docs/releases/4.8.9.md) · [4.8.8](docs/releases/4.8.8.md) · [4.8.7](docs/releases/4.8.7.md) · [4.8.6](docs/releases/4.8.6.md) · [4.8.5](docs/releases/4.8.5.md) · [4.8.4](docs/releases/4.8.4.md) · [4.8.3](docs/releases/4.8.3.md) · [4.8.2](docs/releases/4.8.2.md) · [4.8.1](docs/releases/4.8.1.md) · [4.8.0](docs/releases/4.8.0.md) · [Docs hub](docs/README.md) · [Voice](docs/product-voice.md)
50
+ > [4.8.18](CHANGELOG.md) · [4.8.11](docs/releases/4.8.11.md) · [4.8.10](docs/releases/4.8.10.md) · [4.8.9](docs/releases/4.8.9.md) · [4.8.8](docs/releases/4.8.8.md) · [4.8.7](docs/releases/4.8.7.md) · [4.8.6](docs/releases/4.8.6.md) · [4.8.5](docs/releases/4.8.5.md) · [4.8.4](docs/releases/4.8.4.md) · [4.8.3](docs/releases/4.8.3.md) · [4.8.2](docs/releases/4.8.2.md) · [4.8.1](docs/releases/4.8.1.md) · [4.8.0](docs/releases/4.8.0.md) · [Docs hub](docs/README.md) · [Voice](docs/product-voice.md)
51
51
 
52
52
  ---
53
53
 
@@ -76,9 +76,12 @@ npx --package=arkgate arkgate-check --doctor --all # full details
76
76
  `arkgate-check` is a command in the `arkgate` package, not its own npm package. After a local install, `npx arkgate-check --doctor` also works.
77
77
 
78
78
  `start --apply` refuses when projected governed coverage is below 50% or
79
- shape confidence is weak (below 0.6 with coverage under 80%). That lock is
80
- deliberate. Lock the shape with `--archetype <id>`, `--preset <name>`, or
81
- `--force`, or inspect ranked shapes with `npx arkgate-check --recommend`.
79
+ shape confidence is weak (below 0.6 with coverage under 80%), or when the
80
+ planned write is too big for compact start (more than 8 gate files or 32 KB).
81
+ That lock is deliberate. Coverage/shape: lock with `--archetype <id>`,
82
+ `--preset <name>`, or `--force`. Size: `--force` does not unlock. Next:
83
+ `npx arkgate-check --init`. Or inspect ranked shapes with
84
+ `npx arkgate-check --recommend`.
82
85
 
83
86
  That is the product. Stuck? Run status (`--doctor`) and do action **#1**.
84
87
 
@@ -344,8 +347,8 @@ Compact starters leave the extra off. Details: [ArkOrder](docs/arkorder.md).
344
347
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
345
348
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
346
349
  | Security | [SECURITY.md](SECURITY.md) |
347
- | Prepared (4.8.16; not published) | [CHANGELOG](CHANGELOG.md) |
348
- | Current published (4.8.11 on npm `latest`) | [docs/releases/4.8.11.md](docs/releases/4.8.11.md) · [CHANGELOG](CHANGELOG.md) |
350
+ | Current published (4.8.18 on npm `latest`) | [CHANGELOG](CHANGELOG.md) |
351
+ | Prior published (4.8.11) | [docs/releases/4.8.11.md](docs/releases/4.8.11.md) · [CHANGELOG](CHANGELOG.md) |
349
352
  | Prior published (4.8.10) | [docs/releases/4.8.10.md](docs/releases/4.8.10.md) · [CHANGELOG](CHANGELOG.md) |
350
353
  | Prior published (4.8.9) | [docs/releases/4.8.9.md](docs/releases/4.8.9.md) · [CHANGELOG](CHANGELOG.md) |
351
354
  | Prior published (4.8.7) | [docs/releases/4.8.7.md](docs/releases/4.8.7.md) · [CHANGELOG](CHANGELOG.md) |
@@ -1959,9 +1959,13 @@ async function main() {
1959
1959
  );
1960
1960
  console.error(` Next: ${finding.nextAction}`);
1961
1961
  }
1962
+ if (policyDelta.adrNote?.missing) {
1963
+ console.error(policyDelta.adrNote.ask);
1964
+ console.error(`Next: ${policyDelta.adrNote.nextAction}`);
1965
+ }
1962
1966
  console.error(
1963
1967
  `Policy transition blocked (${policyDelta.basePolicyHash} → ${policyDelta.candidatePolicyHash}). ` +
1964
- 'Provide --policy-ack with the exact hashes, finding ids, and a non-empty reason.'
1968
+ 'Provide --policy-ack with the exact hashes, finding ids, a non-empty reason, and adrPath to a short note under docs/adr/.'
1965
1969
  );
1966
1970
  }
1967
1971
  if (designCheck.failureText()) console.error(designCheck.failureText());
@@ -107,6 +107,7 @@ import {
107
107
  evaluateWriteDesignDelta,
108
108
  formatDesignDeltaBlock,
109
109
  } from './lib/design-delta.mjs';
110
+ import { attachPolicyAdrNote } from './lib/adr-path.mjs';
110
111
 
111
112
  const arkCheckBin = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
112
113
  const arkMcpLauncher = fileURLToPath(new URL('./ark-mcp.mjs', import.meta.url));
@@ -1995,7 +1996,8 @@ export async function runArkMcp({ hookInput } = {}) {
1995
1996
  'judgment-required, or weakening. Pass the previous baseConfig and optional ' +
1996
1997
  'candidateConfig (defaults to this project contract). Weakening and judgment-required ' +
1997
1998
  'results set isError unless acknowledgement exactly matches both policy hashes and all ' +
1998
- 'blocking finding ids. Read-only; never edits the contract.',
1999
+ 'blocking finding ids, and adrPath names a short note under docs/adr/ or docs/decisions/. ' +
2000
+ 'Read-only; never edits the contract.',
1999
2001
  inputSchema: {
2000
2002
  type: 'object',
2001
2003
  properties: {
@@ -2010,7 +2012,8 @@ export async function runArkMcp({ hookInput } = {}) {
2010
2012
  acknowledgement: {
2011
2013
  type: 'object',
2012
2014
  description:
2013
- 'Optional schemaVersion/basePolicyHash/candidatePolicyHash/findingIds/reason object.',
2015
+ 'Optional schemaVersion/basePolicyHash/candidatePolicyHash/findingIds/reason object. ' +
2016
+ 'When the transition needs an acknowledgement, adrPath must name a short note under docs/adr/ or docs/decisions/.',
2014
2017
  },
2015
2018
  },
2016
2019
  required: ['baseConfig'],
@@ -2468,11 +2471,15 @@ export async function runArkMcp({ hookInput } = {}) {
2468
2471
  };
2469
2472
  }
2470
2473
  try {
2471
- const result = ark.analyzePolicyDelta({
2472
- baseConfig,
2473
- candidateConfig: params?.arguments?.candidateConfig ?? config,
2474
- acknowledgement: params?.arguments?.acknowledgement,
2475
- });
2474
+ const acknowledgement = params?.arguments?.acknowledgement;
2475
+ const result = attachPolicyAdrNote(
2476
+ ark.analyzePolicyDelta({
2477
+ baseConfig,
2478
+ candidateConfig: params?.arguments?.candidateConfig ?? config,
2479
+ acknowledgement,
2480
+ }),
2481
+ { root: args.root, acknowledgement, failClosed: true }
2482
+ );
2476
2483
  return {
2477
2484
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
2478
2485
  structuredContent: result,
@@ -1740,7 +1740,12 @@ export function buildArchitectureRecommendation(root, options = {}) {
1740
1740
  // a thin slice and can mis-flag framework internals, so steer these to the adoption flow.
1741
1741
  mature: signals.sourceFileCount >= MATURE_REPO_FILE_THRESHOLD,
1742
1742
  initCommand: `${arkCommand(root, 'ark', `init --archetype ${result.archetype} --yes`)}`,
1743
- firstCommand: `${arkCommand(root, 'ark', `start --apply --archetype ${result.archetype}`)}`,
1743
+ // Greenfield: compact start. Mature: `--init` writes the rules file without the
1744
+ // compact-start size lock that cold `start --apply` can hit on large trees.
1745
+ firstCommand:
1746
+ signals.sourceFileCount >= MATURE_REPO_FILE_THRESHOLD
1747
+ ? arkCommand(root, 'ark-check', '--init')
1748
+ : `${arkCommand(root, 'ark', `start --apply --archetype ${result.archetype}`)}`,
1744
1749
  adoptCommand: arkCommand(root, 'ark-check', '--recommend --write-plan'),
1745
1750
  recommendCommand: arkCommand(root, 'ark-check', '--recommend'),
1746
1751
  checkCommand: arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config'),
@@ -1817,10 +1822,11 @@ export function resolveStartInitPreset(root, rec = {}, archetype = rec.archetype
1817
1822
  return preset ?? null;
1818
1823
  }
1819
1824
 
1820
- /** One-minute / preview footer when the coverage·confidence gate may refuse apply. */
1825
+ /** One-minute / preview footer when apply may refuse (coverage·shape or compact size). */
1821
1826
  export const START_APPLY_REFUSE_FOOTER = [
1822
- 'If apply refuses (coverage below 50% or weak shape), that lock is deliberate.',
1823
- 'Lock the shape: --archetype <id> · --preset <name> · --force',
1827
+ 'If apply refuses (coverage below 50%, weak shape, or too big for compact start), that lock is deliberate.',
1828
+ 'Coverage/shape: --archetype <id> · --preset <name> · --force',
1829
+ 'Too big for compact start: --force does not unlock. Next: arkgate-check --init',
1824
1830
  'Inspect ranked shapes: arkgate-check --recommend',
1825
1831
  ].join('\n');
1826
1832
 
package/bin/ark.mjs CHANGED
@@ -24,6 +24,8 @@ import { pinArkgateDevDependency, FALSE_GREEN_GAP_ID } from './lib/field-install
24
24
  import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
25
25
  import {
26
26
  applyStartPreview,
27
+ emitStartSetupBudgetRefuse,
28
+ evaluateStartSetupBudgetGate,
27
29
  formatStartPackageInstallFailure,
28
30
  planStart,
29
31
  renderStartPreview,
@@ -470,6 +472,10 @@ async function start(args) {
470
472
  }
471
473
  return 2;
472
474
  }
475
+ if (!evaluateStartSetupBudgetGate(preview.setupBudget).ok) {
476
+ emitStartSetupBudgetRefuse(preview, args.json);
477
+ return 2;
478
+ }
473
479
  }
474
480
  if (args.json) console.log(JSON.stringify(preview, null, 2));
475
481
  else if (!args.apply) renderStartPreview(preview);
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Policy-ack ADR path tooth (AP02). Tooling I/O on the existing
3
+ * --policy-ack / --strict-merge plane. Doctor does not import this file.
4
+ */
5
+
6
+ import path from 'node:path';
7
+ import { ADR_PRESENCE_HOMES, isNonEmptyMarkdownFile } from './adr-presence.mjs';
8
+
9
+ export const ADR_PATH_ASK =
10
+ 'This change loosens a rule or adds a layer edge, but the acknowledgement has no decision-note path.';
11
+
12
+ export const ADR_PATH_NEXT =
13
+ 'Add a short note under docs/adr/ (or docs/decisions/) and put that file path in --policy-ack as adrPath.';
14
+
15
+ export const ADR_PATH_MISSING_FILE_ASK =
16
+ 'The acknowledgement names a decision note that is missing or empty.';
17
+
18
+ export const ADR_PATH_MISSING_FILE_NEXT =
19
+ 'Write that note (or fix adrPath) under docs/adr/ or docs/decisions/, then run ArkGate again.';
20
+
21
+ /**
22
+ * Relative path under a conventional decision-note home. No I/O.
23
+ *
24
+ * @param {unknown} value
25
+ * @returns {string | null}
26
+ */
27
+ export function canonicalizeAdrPath(value) {
28
+ if (typeof value !== 'string') return null;
29
+ const rel = value.trim().replace(/\\/g, '/');
30
+ if (!rel || rel.startsWith('/') || /^[A-Za-z]:\//.test(rel) || rel.includes('\0')) return null;
31
+ const parts = [];
32
+ for (const segment of rel.split('/')) {
33
+ if (!segment || segment === '.') continue;
34
+ if (segment === '..') return null;
35
+ parts.push(segment);
36
+ }
37
+ return parts.join('/') || null;
38
+ }
39
+
40
+ /**
41
+ * @param {unknown} value
42
+ * @returns {boolean}
43
+ */
44
+ export function isConventionalAdrPath(value) {
45
+ const rel = canonicalizeAdrPath(value);
46
+ if (!rel) return false;
47
+ for (const home of ADR_PRESENCE_HOMES) {
48
+ if (home.endsWith('.md')) {
49
+ if (rel === home) return true;
50
+ continue;
51
+ }
52
+ if (rel.startsWith(`${home}/`) && rel.toLowerCase().endsWith('.md') && rel.length > home.length + 4) {
53
+ return true;
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+
59
+ /**
60
+ * Conventional path that already has a non-empty markdown file.
61
+ *
62
+ * @param {string} root
63
+ * @param {unknown} adrPath
64
+ * @returns {string | null}
65
+ */
66
+ export function resolveAdrNotePath(root, adrPath) {
67
+ const rel = isConventionalAdrPath(adrPath) ? canonicalizeAdrPath(adrPath) : null;
68
+ if (!rel || typeof root !== 'string' || root.length === 0) return null;
69
+ return isNonEmptyMarkdownFile(path.join(root, rel)) ? rel : null;
70
+ }
71
+
72
+ /**
73
+ * Residual when a weaken / new-edge needs a tied note path.
74
+ * `needed` is policyDelta.requiresAcknowledgement. Off → null.
75
+ *
76
+ * @param {{ root?: string, needed?: boolean, adrPath?: unknown }} [input]
77
+ * @returns {{ missing: true, ask: string, nextAction: string, path?: string } | null}
78
+ */
79
+ export function collectAdrPathResidual(input = {}) {
80
+ if (input.needed !== true) return null;
81
+ const rel = canonicalizeAdrPath(input.adrPath);
82
+ if (!rel) {
83
+ return { missing: true, ask: ADR_PATH_ASK, nextAction: ADR_PATH_NEXT };
84
+ }
85
+ if (resolveAdrNotePath(input.root ?? '', input.adrPath)) return null;
86
+ return {
87
+ missing: true,
88
+ path: rel,
89
+ ask: isConventionalAdrPath(input.adrPath) ? ADR_PATH_MISSING_FILE_ASK : ADR_PATH_ASK,
90
+ nextAction: isConventionalAdrPath(input.adrPath) ? ADR_PATH_MISSING_FILE_NEXT : ADR_PATH_NEXT,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Attach `adrNote` to a policy-delta result. Flips `valid` only when
96
+ * `failClosed` (existing policy-ack / --strict-merge plane) and the path
97
+ * is missing. Domain hash match stays I/O-free.
98
+ *
99
+ * @param {object | undefined} result
100
+ * @param {{ root?: string, acknowledgement?: { adrPath?: unknown }, failClosed?: boolean }} [input]
101
+ */
102
+ export function attachPolicyAdrNote(result, input = {}) {
103
+ if (!result || result.requiresAcknowledgement !== true) return result;
104
+ const residual = collectAdrPathResidual({
105
+ root: input.root,
106
+ needed: true,
107
+ adrPath: input.acknowledgement?.adrPath,
108
+ });
109
+ const adrNote = residual
110
+ ? residual
111
+ : { missing: false, path: canonicalizeAdrPath(input.acknowledgement?.adrPath) };
112
+ if (!residual || input.failClosed !== true) {
113
+ return { ...result, adrNote };
114
+ }
115
+ return { ...result, adrNote, valid: false };
116
+ }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Soft ADR / decision-note presence when gates are demanded.
3
3
  * Tooling I/O. Never a gate fail. Absence is silent unless require-gates
4
- * or adopted-strict (required-merge) is on.
4
+ * or adopted-strict (required-merge) is on. The policy-ack path tooth lives
5
+ * in adr-path.mjs so doctor does not parse it.
5
6
  */
6
7
 
7
8
  import fs from 'node:fs';
@@ -23,7 +24,7 @@ export const ADR_PRESENCE_ASK =
23
24
  export const ADR_PRESENCE_NEXT =
24
25
  'Add a short note under docs/adr/ (or docs/decisions/) when you loosen a rule or add a real gate. Not every change.';
25
26
 
26
- function isNonEmptyMarkdownFile(file) {
27
+ export function isNonEmptyMarkdownFile(file) {
27
28
  if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return false;
28
29
  try {
29
30
  return fs.readFileSync(file, 'utf8').trim().length > 0;
@@ -106,6 +106,8 @@ export {
106
106
  assessSkillCatalogParity,
107
107
  detectSkillGaps,
108
108
  skillGapsForActiveHost,
109
+ skillGapToolLabel,
110
+ SKILL_CANONICAL_TOOL,
109
111
  agentsMdSkillRefs,
110
112
  verifyHostSkillCatalog,
111
113
  printSkillAndCodexGapHints,