arkgate 3.8.1 → 3.8.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,62 @@ in the immutable pre-2.0 archive linked below.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 3.8.3 — 2026-07-22
9
+
10
+ Corrective **patch** over 3.8.2 from multi-repo field evidence (PROPIA pnpm workspace upgrade;
11
+ Amarilla greenfield `start`). **No required config migration.** Z09 / residual `RB-11` remain open.
12
+
13
+ ### Fixed
14
+
15
+ - **pnpm workspace upgrade install:** `packageInstallArgv` emits `pnpm add -D arkgate@… -w` on
16
+ workspace roots (`pnpm-workspace.yaml`). Yarn workspaces get `-W`. npm single-package path
17
+ unchanged.
18
+ - **Upgrade re-install when already current:** skip package-manager install when
19
+ `node_modules/arkgate` already matches this CLI version; go straight to managed preview.
20
+ - **Install failure recovery copy:** prints the exact failed install command and a
21
+ `--no-install` re-run path.
22
+ - **`start` package pin by default:** `start --apply` pins `arkgate` in `devDependencies`
23
+ unless `--no-install` (was: only with explicit `--install`).
24
+ - **Compact start always writes `.mcp.json`:** every host gets project MCP registration;
25
+ setup budget raised to 8 files / 32 KB so MCP fits with host hooks.
26
+ - **Unbound upgrade apply when content matches:** `--apply` without `--plan-digest` is a
27
+ successful no-op when `wouldWrite` is 0 (optional stamp refresh still needs the digest).
28
+ - **Upgrade applied copy:** distinguishes content writes vs stamp/metadata refresh instead of
29
+ a single “Applied N changes” when only stamps moved.
30
+
31
+ ### Tests
32
+
33
+ - `tests/unit/static-check/fieldJourney383.test.ts` — workspace argv, skip-when-current, start pin.
34
+ - Z06 / O03 / installFieldFixes expectations updated for default pin and MCP-in-compact.
35
+
36
+ ## 3.8.2 — 2026-07-22
37
+
38
+ Corrective **patch** over 3.8.1 from PREDIAL WEB field evidence. Aligns doctor skill freshness with
39
+ managed upgrade content identity, hardens upgrade preview honesty, clarifies doctor writePath
40
+ inventory vs this-invocation, ships the Y06 pure-layer opt-in advisory, and treats complete-catalog
41
+ Codex leftover prompts as safe-to-delete. **No required config migration.** Z09 / residual `RB-11`
42
+ remain open.
43
+
44
+ ### Fixed
45
+
46
+ - **Doctor skill “stale” vs managed upgrade (field DX):** skill gap detection now uses the same
47
+ content-identity rules as `ark upgrade` (stamp-normalized). A skill whose body matches the
48
+ package template is not stale when only `arkVersion` lags. Stale copy says content-behind-package.
49
+ - **Upgrade preview phantom apply:** summary reports `managedAssets` / `wouldWrite` /
50
+ `customizedPreserved`; when `wouldWrite` is 0, prints “Nothing to apply” instead of urging
51
+ `--apply` as the primary next step (optional stamp-only apply remains digest-bound).
52
+ - **Doctor writePath honesty:** with `activeHost: unknown`, a `sessionNote` separates on-disk
53
+ inventory from this-invocation hardness; package `installed` is independent of host support.
54
+ Hard still requires runtime proof (Z10).
55
+ - **Codex legacy prompts:** complete `.agents/skills` with leftover `.codex/prompts` is an
56
+ advisory safe-to-delete signal (CLI, doctor, HTML), not an install-agent-gates gap.
57
+
58
+ ### Added
59
+
60
+ - **Y06 pure-layer opt-in nudge:** when a golden pattern names pure modules and no layer sets
61
+ `pure: true`, doctor emits one advisory line (`doctor.pureLayerOptIn`). Never a blocker; never
62
+ auto-writes `pure: true`. Promoted from field evidence (PREDIAL WEB).
63
+
8
64
  ## 3.8.1 — 2026-07-22
9
65
 
10
66
  Corrective **patch** over 3.8.0. Closes silent fail-open on peerIsolation incomplete evidence,
package/README.md CHANGED
@@ -16,9 +16,9 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
- > **ArkGate 3.8.1** is current stable: 3.8.0 complete analysis / resolved verdict / managed upgrades
20
- > / design-delta, plus peerIsolation fail-closed and pure-IR require/type-only accuracy.
21
- > [Release notes](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/3.8.1.md).
19
+ > **ArkGate 3.8.3** is current stable: field journey on top of 3.8.2 pnpm/yarn workspace install,
20
+ > default `start` package pin, compact `.mcp.json`, upgrade recovery/UX.
21
+ > [Release notes](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/3.8.3.md).
22
22
 
23
23
  > **TypeScript 7 distribution note:** ArkGate prefers a usable project compiler API, then its
24
24
  > exact, physically distinct TypeScript 6 analysis host. Analysis reports required
@@ -422,7 +422,7 @@ for real systems. Details: [production-hardening.md](https://github.com/pedrokni
422
422
  | Security reporting | [SECURITY.md](SECURITY.md) |
423
423
  | Demos | [docs/demos/](https://github.com/pedroknigge/arkgate/tree/main/docs/demos) |
424
424
  | Examples | [examples/](https://github.com/pedroknigge/arkgate/blob/main/examples/README.md) |
425
- | Latest release (3.8.1) | [release notes](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/3.8.1.md) · [3.0.0 baseline](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/3.0.0.md) |
425
+ | Latest release (3.8.3) | [release notes](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/3.8.3.md) · [3.0.0 baseline](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/3.0.0.md) |
426
426
  | Roadmap and decisions | [ROADMAP.md](https://github.com/pedroknigge/arkgate/blob/main/ROADMAP.md) · [ADRs](https://github.com/pedroknigge/arkgate/tree/main/docs/adr) · [Changelog](CHANGELOG.md) |
427
427
 
428
428
  ---
@@ -618,11 +618,98 @@ export function execCommandParts(root, bin, binArgs = []) {
618
618
  return { command: 'npx', args: [bin, ...binArgs] };
619
619
  }
620
620
 
621
+ /**
622
+ * True when this directory is a pnpm workspace root (needs `pnpm add -w` for root deps).
623
+ * Nested packages under the workspace are not roots.
624
+ */
625
+ export function isPnpmWorkspaceRoot(root) {
626
+ return fs.existsSync(path.join(root, 'pnpm-workspace.yaml'));
627
+ }
628
+
629
+ /**
630
+ * True when package.json declares npm/yarn workspaces (yarn classic needs `-W` at root).
631
+ */
632
+ export function isNpmYarnWorkspaceRoot(root) {
633
+ const pkg = readPackageJson(root);
634
+ if (!pkg) return false;
635
+ const ws = pkg.workspaces;
636
+ return Array.isArray(ws) || (ws && typeof ws === 'object' && Array.isArray(ws.packages));
637
+ }
638
+
639
+ /**
640
+ * Normalize a version/range/spec into an installable package argument for arkgate.
641
+ * Accepts `latest`, `^3.8.2`, `arkgate@latest`, or a full package name.
642
+ */
643
+ export function normalizeArkgateInstallSpec(versionSpec) {
644
+ const raw = typeof versionSpec === 'string' && versionSpec.trim() ? versionSpec.trim() : 'latest';
645
+ if (raw.startsWith('arkgate@') || raw === 'arkgate') return raw === 'arkgate' ? 'arkgate@latest' : raw;
646
+ if (raw.includes('/') || raw.startsWith('file:') || raw.startsWith('link:')) return raw;
647
+ return `arkgate@${raw}`;
648
+ }
649
+
650
+ /**
651
+ * Package-manager argv to add a dev dependency (e.g. arkgate@latest).
652
+ * pnpm workspace roots get `-w`; yarn classic workspaces get `-W`.
653
+ *
654
+ * @param {string} root
655
+ * @param {string} [versionSpec] package name or name@version (default arkgate@latest)
656
+ * @returns {[string, string[]]}
657
+ */
658
+ export function packageInstallArgv(root, versionSpec = 'latest') {
659
+ const pkgSpec = normalizeArkgateInstallSpec(versionSpec);
660
+ const pm = detectPackageManager(root);
661
+ if (pm === 'pnpm') {
662
+ const args = ['add', '-D', pkgSpec];
663
+ if (isPnpmWorkspaceRoot(root)) args.push('-w');
664
+ return ['pnpm', args];
665
+ }
666
+ if (pm === 'yarn') {
667
+ const args = ['add', '-D', pkgSpec];
668
+ if (isNpmYarnWorkspaceRoot(root)) args.push('-W');
669
+ return ['yarn', args];
670
+ }
671
+ return ['npm', ['install', '-D', pkgSpec]];
672
+ }
673
+
674
+ /**
675
+ * Whether an install of arkgate@latest can be skipped because node_modules already
676
+ * resolves the same version as this CLI package.
677
+ *
678
+ * @param {string} root
679
+ * @param {string} [cliVersion] this binary's package version
680
+ * @returns {{ skip: boolean, installedVersion: string|null, reason: string }}
681
+ */
682
+ export function shouldSkipArkgateInstall(root, cliVersion) {
683
+ const pkgPath = path.join(root, 'node_modules', 'arkgate', 'package.json');
684
+ if (!fs.existsSync(pkgPath)) {
685
+ return { skip: false, installedVersion: null, reason: 'not-installed' };
686
+ }
687
+ let installedVersion = null;
688
+ try {
689
+ installedVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? null;
690
+ } catch {
691
+ return { skip: false, installedVersion: null, reason: 'unreadable' };
692
+ }
693
+ if (
694
+ typeof cliVersion === 'string' &&
695
+ cliVersion &&
696
+ installedVersion &&
697
+ installedVersion === cliVersion
698
+ ) {
699
+ return { skip: true, installedVersion, reason: 'already-current' };
700
+ }
701
+ return { skip: false, installedVersion, reason: 'version-differs' };
702
+ }
703
+
621
704
  /** Package-manager aware "install a dev dependency" hint (e.g. for a missing typescript). */
622
705
  export function installDevHint(root, pkg) {
623
706
  const pm = detectPackageManager(root);
624
- if (pm === 'pnpm') return `pnpm add -D ${pkg}`;
625
- if (pm === 'yarn') return `yarn add -D ${pkg}`;
707
+ if (pm === 'pnpm') {
708
+ return isPnpmWorkspaceRoot(root) ? `pnpm add -D ${pkg} -w` : `pnpm add -D ${pkg}`;
709
+ }
710
+ if (pm === 'yarn') {
711
+ return isNpmYarnWorkspaceRoot(root) ? `yarn add -D ${pkg} -W` : `yarn add -D ${pkg}`;
712
+ }
626
713
  return `npm install -D ${pkg}`;
627
714
  }
628
715
 
package/bin/ark.mjs CHANGED
@@ -14,8 +14,10 @@ import {
14
14
  INIT_WIZARD_CHOICES,
15
15
  isValidArchetypeId,
16
16
  mapWizardChoiceToArchetype,
17
+ packageInstallArgv,
17
18
  resolveArchetypePreset,
18
19
  resolveOperatingMode,
20
+ shouldSkipArkgateInstall,
19
21
  } from './ark-shared.mjs';
20
22
  import { pinArkgateDevDependency, FALSE_GREEN_GAP_ID } from './lib/field-install.mjs';
21
23
  import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
@@ -155,9 +157,9 @@ Options:
155
157
  (Also the implicit default when stdin/stdout are not a TTY — agents never hang on prompts.)
156
158
  --force Allow generated files to overwrite existing files.
157
159
  --no-strict Skip the final strict ark-check run.
158
- --install Add arkgate to package.json explicitly before applying a start plan.
160
+ --install Pin and install arkgate as a project devDependency (default for start).
159
161
  --no-install Skip adding/installing arkgate as a project devDependency (start/upgrade).
160
- --apply Apply a start plan; for upgrade, update/repreview or apply with --no-install.
162
+ --apply Apply a start plan; for upgrade, update/repreview or apply managed bytes.
161
163
  --accept-conflicts
162
164
  Allow upgrade to recreate deleted managed assets or replace recorded conflicts.
163
165
  --plan-digest Digest emitted by an upgrade preview; required to apply managed bytes.
@@ -189,20 +191,7 @@ function cliVersion() {
189
191
  }
190
192
  }
191
193
 
192
- // The package-manager command that adds arkgate as a dev dependency.
193
- // Prefer an explicit version/range when pin already chose one (avoid pin=^2.9.0 then
194
- // `npm i arkgate@latest` rewriting package.json to a different range).
195
- function packageInstallArgv(root, versionSpec) {
196
- const range =
197
- typeof versionSpec === 'string' && versionSpec.trim()
198
- ? versionSpec.trim()
199
- : 'latest';
200
- const spec = range.startsWith('arkgate@') ? range : `arkgate@${range}`;
201
- const pm = detectPackageManager(root);
202
- if (pm === 'pnpm') return ['pnpm', ['add', '-D', spec]];
203
- if (pm === 'yarn') return ['yarn', ['add', '-D', spec]];
204
- return ['npm', ['install', '-D', spec]];
205
- }
194
+ // packageInstallArgv is imported from ark-shared (workspace-aware -w / -W).
206
195
 
207
196
  function runCommand(command, commandArgs, cwd) {
208
197
  const result = spawnSync(command, commandArgs, { cwd, stdio: 'inherit', encoding: 'utf8' });
@@ -438,6 +427,32 @@ async function start(args) {
438
427
  if (!args.apply) return 0;
439
428
  applyStartPreview(args.root, preview);
440
429
  if (!args.json) console.log(`Applied ${preview.changes.length} previewed mutation(s).`);
430
+ // After applying exact preview bytes, install the pinned package when requested
431
+ // (preview itself never runs the package manager — field: start left pin without node_modules).
432
+ if (
433
+ args.install &&
434
+ !args.skipPackageManager &&
435
+ fs.existsSync(path.join(args.root, 'package.json'))
436
+ ) {
437
+ const skip = shouldSkipArkgateInstall(args.root, cliVersion());
438
+ if (!skip.skip) {
439
+ const [command, commandArgs] = packageInstallArgv(args.root, `^${cliVersion()}`);
440
+ if (!args.json) console.log(`Installing package: ${command} ${commandArgs.join(' ')}`);
441
+ // Keep stdout clean for --json consumers (package managers are chatty on stdout).
442
+ const status = args.json
443
+ ? (spawnSync(command, commandArgs, {
444
+ cwd: args.root,
445
+ stdio: ['ignore', 'pipe', 'pipe'],
446
+ encoding: 'utf8',
447
+ }).status ?? 1)
448
+ : runCommand(command, commandArgs, args.root);
449
+ if (status !== 0 && !args.json) {
450
+ console.log(
451
+ `Package manager exited ${status}. package.json is pinned; run the install command when online.`
452
+ );
453
+ }
454
+ }
455
+ }
441
456
  return 0;
442
457
  }
443
458
  const root = args.root;
@@ -492,7 +507,8 @@ async function start(args) {
492
507
  }
493
508
 
494
509
  // 2b) Pin arkgate as a project devDependency so CI/npx do not depend on a stale global.
495
- if (args.installExplicit && args.install && fs.existsSync(path.join(root, 'package.json'))) {
510
+ // Default install=true; only --no-install skips. (installExplicit tracks user override for copy.)
511
+ if (args.install && fs.existsSync(path.join(root, 'package.json'))) {
496
512
  const { pinned, installStatus } = ensureProjectArkgateDependency(root, {
497
513
  install: true,
498
514
  runPackageManager: !args.skipPackageManager,
@@ -507,7 +523,7 @@ async function start(args) {
507
523
  } else if (pinned.reason === 'already-present') {
508
524
  console.log(` arkgate already in package.json (${pinned.version}).`);
509
525
  }
510
- } else if (args.installExplicit && args.install === false) {
526
+ } else if (!args.install) {
511
527
  console.log(' Skipping arkgate package pin (--no-install).');
512
528
  }
513
529
 
@@ -789,7 +805,13 @@ async function main() {
789
805
 
790
806
  if (args.command === 'upgrade' || args.command === 'update') {
791
807
  try {
792
- return runUpgradeCommand(args, { arkCheck, packageInstallArgv, runArkCheck });
808
+ return runUpgradeCommand(args, {
809
+ arkCheck,
810
+ packageInstallArgv,
811
+ runArkCheck,
812
+ cliVersion: cliVersion(),
813
+ shouldSkipArkgateInstall,
814
+ });
793
815
  } catch (error) {
794
816
  console.error(error instanceof Error ? error.message : String(error));
795
817
  return 2;
@@ -87,8 +87,11 @@ export {
87
87
  stampSkill,
88
88
  installedSkillVersion,
89
89
  isVersionOlder,
90
+ skillContentIdentity,
91
+ skillContentMatchesTemplate,
90
92
  skillTemplates,
91
93
  skillTemplateNames,
94
+ skillTemplateBodies,
92
95
  detectCodexHomeGap,
93
96
  detectCodexRepoSkillGap,
94
97
  assessCodexSkillParity,
@@ -36,7 +36,11 @@ import {
36
36
  mergePostGreenTopActions,
37
37
  isDoctorHealthyNothingToDo,
38
38
  } from './post-green-path.mjs';
39
- import { loadGoldenPattern, summarizeGoldenPattern } from './golden-pattern.mjs';
39
+ import {
40
+ computePureLayerOptInNudge,
41
+ loadGoldenPattern,
42
+ summarizeGoldenPattern,
43
+ } from './golden-pattern.mjs';
40
44
  import { summarizePilotLoop } from './pilot-loop.mjs';
41
45
  import { computeDoctorAdvisories, printDoctorAdvisories } from './doctor-advisories.mjs';
42
46
  import { ANALYSIS_COMPLETENESS, analysisIncompleteStatement, normalizeAnalysisCompleteness } from './analysis-completeness.mjs';
@@ -418,7 +422,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
418
422
  // Q01 — single post-green door when design-weak (map → B; no skill shopping).
419
423
  const postGreenPath = buildPostGreenNextAction(designFitness);
420
424
  // Q03 — optional golden pattern for NEW code (advisory; never clears design-weak).
421
- const goldenPattern = summarizeGoldenPattern(loadGoldenPattern(root));
425
+ const goldenLoad = loadGoldenPattern(root);
426
+ const goldenPattern = summarizeGoldenPattern(goldenLoad);
427
+ // Y06 — pure-layer opt-in when golden names pure modules but no pure:true layer.
428
+ const pureLayerOptIn = computePureLayerOptInNudge(config, goldenLoad);
422
429
  // Q04 — one next pilot (extraction card) when design-weak.
423
430
  const patternBetsForLoop = buildPatternBetsFromSmells(designSmells);
424
431
  const pilotLoop = summarizePilotLoop({
@@ -463,6 +470,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
463
470
  : {}),
464
471
  // Q03: advisory golden for new-code placement (absent = no claim).
465
472
  goldenPattern,
473
+ // Y06: advisory pure-layer opt-in (null when not applicable).
474
+ pureLayerOptIn,
466
475
  // Q04: one-pilot loop (extraction card → re-doctor).
467
476
  pilotLoop,
468
477
  // Advisories, never a verdict: W01 contract health, U05 ambient state,
@@ -507,6 +516,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
507
516
  hookRepair: writePath.hookRepair,
508
517
  mcpPresent: writePath.mcpPresent,
509
518
  evidence: writePath.evidence,
519
+ ...(writePath.sessionNote ? { sessionNote: writePath.sessionNote } : {}),
510
520
  ...(writePath.gap
511
521
  ? {
512
522
  gap: {
@@ -655,6 +665,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
655
665
  'Fix or remove it — absence is fine; a bad file is not guidance.'
656
666
  );
657
667
  }
668
+ // Y06 — one-line pure-layer opt-in (U05 voice; never blocker).
669
+ if (pureLayerOptIn) {
670
+ line(' ', color.dim(pureLayerOptIn.message));
671
+ }
658
672
 
659
673
  printDoctorAdvisories(doctorAdvisories, { line, warn, color }); // advisory sections
660
674
 
@@ -756,6 +770,9 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
756
770
  line(' ', `Active host: ${writePath.activeHost}`);
757
771
  line(' ', `Supported profile: ${writePath.supportSummary}`);
758
772
  line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`);
773
+ if (writePath.sessionNote) {
774
+ line(warn, writePath.sessionNote);
775
+ }
759
776
  const enforcement = writePath.enforcementState;
760
777
  for (const row of enforcementDoctorLines(enforcement)) line(row.level === 'ok' ? ok : row.level === 'bad' ? bad : warn, row.text);
761
778
  line(
@@ -779,7 +796,12 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
779
796
  }
780
797
  // Report Codex legacy prompts and other-host missing/stale independently (never exclusive).
781
798
  const legacyCodex = skillGaps.some((g) => g.tool === 'codex' && g.legacyPromptsOnly);
782
- const remainingGaps = skillGaps.filter((g) => !(g.tool === 'codex' && g.legacyPromptsOnly));
799
+ const codexLegacySafeDelete = skillGaps.some(
800
+ (g) => g.tool === 'codex' && g.legacyAdvisory && g.catalogComplete
801
+ );
802
+ const remainingGaps = skillGaps.filter(
803
+ (g) => !(g.tool === 'codex' && (g.legacyPromptsOnly || g.legacyAdvisory))
804
+ );
783
805
  const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0);
784
806
  const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0);
785
807
  if (remMiss + remStale === 0 && !legacyCodex) line(ok, '/ark-* skills current for detected tools');
@@ -787,8 +809,19 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
787
809
  line(warn, 'Codex: legacy flat .codex/prompts only (not a loadable skill catalog)');
788
810
  actions.push('install Codex SKILL.md catalog (--install-agent-gates --skills-only --tools codex --force)');
789
811
  }
812
+ if (codexLegacySafeDelete) {
813
+ line(
814
+ ' ',
815
+ color.dim(
816
+ 'Codex catalog complete — leftover .codex/prompts/ark-*.md are safe to delete (not loadable; not required).'
817
+ )
818
+ );
819
+ }
790
820
  if (remMiss + remStale > 0) {
791
- line(warn, `${remMiss} missing / ${remStale} outdated /ark-* skill(s) for ${remainingGaps.map((g) => g.tool).join(', ')}`);
821
+ line(
822
+ warn,
823
+ `${remMiss} missing / ${remStale} content-behind-package /ark-* skill(s) for ${remainingGaps.map((g) => g.tool).join(', ')}`
824
+ );
792
825
  actions.push('refresh /ark-* skills (--install-agent-gates --skills-only --force)');
793
826
  }
794
827
  const codexHomeGap = detectCodexHomeGap(root);
@@ -796,7 +829,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
796
829
  const parts = [
797
830
  codexHomeGap.legacyPromptsOnly ? 'legacy-prompts-only' : null,
798
831
  codexHomeGap.missing > 0 ? `${codexHomeGap.missing} missing` : null,
799
- codexHomeGap.stale > 0 ? `${codexHomeGap.stale} outdated` : null,
832
+ codexHomeGap.stale > 0 ? `${codexHomeGap.stale} content-behind-package` : null,
800
833
  ].filter(Boolean);
801
834
  const deferred = !codexConcernIsActive();
802
835
  // Deferred home debt is dim/info (not warn) so non-Codex sessions are not "incomplete".
@@ -1,2 +1,2 @@
1
1
  // Generated from enforcement-state.source.mjs — run npm run generate:packaged-tooling.
2
- import b from"node:fs";import{createRequire as q}from"node:module";import h from"node:path";const n="unverified";function M(a){const e=h.join(a,"package.json");try{if(JSON.parse(b.readFileSync(e,"utf8"))?.name==="arkgate"&&b.statSync(h.join(a,"bin","ark-check.mjs"),{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"package.json + bin/ark-check.mjs (self-host)"}}catch{}try{const r=q(e).resolve("arkgate/package.json"),o=h.dirname(r),t=JSON.parse(b.readFileSync(r,"utf8")),l=h.join(o,"bin","ark-check.mjs");if(t?.name==="arkgate"&&b.statSync(l,{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"arkgate/package.json via project resolver"}}catch{}return{installed:!1,source:"arkgate/package.json unresolved from project"}}function S(a){return a.length>0?a:["filesystem scan (no matching configuration)"]}function g({supported:a,configuredPaths:e,installed:r,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,sources:s}){const p=e.length>0;return{supported:a,analyzed:!0,configured:p,installed:a&&r.installed,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,evidence:[...S(e).map(v=>({field:"configured",source:v,value:p})),{field:"installed",source:r.source,value:a&&r.installed},{field:"active",source:s.active,value:o},{field:"runtimeObserved",source:s.runtimeObserved,value:t},{field:"operationCoverage",source:s.operationCoverage,value:d},{field:"bypassable",source:s.bypassable,value:u},{field:"required",source:s.required,value:c},{field:"hard",source:s.hard,value:i}]}}function w(a,e){const r=M(a),o=!!e.support?.capabilities?.["hard-write"],t=!!e.support?.capabilities?.["advisory-write"],l=e.capabilityEvidence["hard-write"],d=e.capabilityEvidence["advisory-write"],u=e.capabilityEvidence["merge-gate"],c=e.enforcementLadder.localWrite,i=typeof c.operationCovered=="boolean",s=i?c.operationCovered:n,p=i&&s===!0,v=!!(o&&p&&c.hard===!0),m=i?p:o&&l.length>0&&r.installed?n:!1,k=t&&d.length>0&&r.installed?n:!1,f=!!(e.ci?.failClosed&&u.length>0),C=f&&r.installed?n:!1;return{schemaVersion:"1.1",activeHost:e.activeHost,localWrite:g({supported:o,configuredPaths:l,installed:r,active:m,runtimeObserved:i,operation:i?c.operation??null:null,operationCoverage:s,bypassable:v?!1:o&&!i?n:!0,required:n,hard:v,sources:{active:i?"observed PreToolUse attempt":"runtime observation unavailable",runtimeObserved:i?"fresh PreToolUse invocation":"runtime observation unavailable",operationCoverage:i?"active-host operation matcher":"operation not observed",bypassable:v?"observed hard write boundary":"host runtime bypass evidence unavailable",required:"local host policy unavailable",hard:v?"fresh covered active-host invocation":"hardness not proven for this invocation"}}),advisoryMcp:g({supported:t,configuredPaths:d,installed:r,active:k,runtimeObserved:!1,operation:null,operationCoverage:n,bypassable:!0,required:n,hard:!1,sources:{active:"MCP runtime observation unavailable",runtimeObserved:"doctor did not observe an MCP tool invocation",operationCoverage:"advisory MCP is caller-invoked",bypassable:"advisory MCP does not intercept every write",required:"local host policy unavailable",hard:"MCP presence is advisory and never proves a hard boundary"}}),ciMerge:g({supported:!0,configuredPaths:f?u:[],installed:r,active:C,runtimeObserved:!1,operation:"merge",operationCoverage:f?n:!1,bypassable:f?n:!0,required:n,hard:!1,sources:{active:"CI run and provider enforcement not observed",runtimeObserved:"provider evidence unavailable",operationCoverage:"required-status operation coverage unavailable",bypassable:"branch-protection evidence unavailable",required:"branch-protection evidence unavailable",hard:"merge hardness requires fresh provider evidence"}})}}function j(a,e,r,o){return{...a,...o,evidence:[...a.evidence.filter(t=>!e.includes(t.field)),...e.map(t=>({field:t,source:r,value:o[t]}))]}}function x(a,e){if(!e?.available)return a;const r=typeof e.arkCheckRequired=="boolean"?e.arkCheckRequired:n,o=!!(a.enforcementState.ciMerge.configured&&a.enforcementState.ciMerge.installed),t=r===!0?o:r===!1?!1:o?n:!1,l=t===!0?e.arkCheckSourceBound===!1?!0:n:r===!1?!0:o?n:!0,d=`GitHub branch protection (${e.repo??"repository"}:${e.branch??"default"})`,u=!0,c=r,i=t===!0&&l===!1&&c===!0,s=j(a.enforcementState.ciMerge,["active","runtimeObserved","operationCoverage","bypassable","required","hard"],d,{active:t,runtimeObserved:u,operationCoverage:c,bypassable:l,required:r,hard:i});return{...a,enforcementState:{...a.enforcementState,ciMerge:s},enforcementLadder:{...a.enforcementLadder,ciMerge:{...a.enforcementLadder.ciMerge,requiredStatus:r}}}}function y(a,e){const r=o=>o===!0?"yes":o===!1?"no":String(o);return`${a} \u2014 supported: ${r(e.supported)} \xB7 analyzed: ${r(e.analyzed)} \xB7 configured: ${r(e.configured)} \xB7 installed: ${r(e.installed)} \xB7 runtime observed: ${r(e.runtimeObserved)} \xB7 operation: ${e.operation??"none"} \xB7 operation covered: ${r(e.operationCoverage)} \xB7 active: ${r(e.active)} \xB7 bypassable: ${r(e.bypassable)} \xB7 required: ${r(e.required)} \xB7 hard: ${r(e.hard)}`}function L(a){const e=[{level:a.localWrite.active===!0?"ok":"warn",text:y("Local write",a.localWrite)},{level:"warn",text:y("Advisory MCP",a.advisoryMcp)},{level:a.ciMerge.required===!0?"ok":"warn",text:y("CI merge",a.ciMerge)}];return a.localWrite.active===n&&a.localWrite.hard===!1&&e.push({level:"bad",text:"RED FLAG: local hook assets exist, but this active-host operation was not observed at runtime; hard blocking is unverified."}),e}export{w as buildEnforcementState,L as enforcementDoctorLines,x as withCiProviderEvidence};
2
+ import b from"node:fs";import{createRequire as q}from"node:module";import h from"node:path";const n="unverified";function S(a){const e=h.join(a,"package.json");try{if(JSON.parse(b.readFileSync(e,"utf8"))?.name==="arkgate"&&b.statSync(h.join(a,"bin","ark-check.mjs"),{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"package.json + bin/ark-check.mjs (self-host)"}}catch{}try{const r=q(e).resolve("arkgate/package.json"),o=h.dirname(r),t=JSON.parse(b.readFileSync(r,"utf8")),l=h.join(o,"bin","ark-check.mjs");if(t?.name==="arkgate"&&b.statSync(l,{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"arkgate/package.json via project resolver"}}catch{}return{installed:!1,source:"arkgate/package.json unresolved from project"}}function M(a){return a.length>0?a:["filesystem scan (no matching configuration)"]}function y({supported:a,configuredPaths:e,installed:r,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,sources:s}){const p=e.length>0,v=!!r.installed;return{supported:a,analyzed:!0,configured:p,installed:v,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,evidence:[...M(e).map(g=>({field:"configured",source:g,value:p})),{field:"installed",source:r.source,value:v},{field:"active",source:s.active,value:o},{field:"runtimeObserved",source:s.runtimeObserved,value:t},{field:"operationCoverage",source:s.operationCoverage,value:d},{field:"bypassable",source:s.bypassable,value:u},{field:"required",source:s.required,value:c},{field:"hard",source:s.hard,value:i}]}}function O(a,e){const r=S(a),o=!!e.support?.capabilities?.["hard-write"],t=!!e.support?.capabilities?.["advisory-write"],l=e.capabilityEvidence["hard-write"],d=e.capabilityEvidence["advisory-write"],u=e.capabilityEvidence["merge-gate"],c=e.enforcementLadder.localWrite,i=typeof c.operationCovered=="boolean",s=i?c.operationCovered:n,p=i&&s===!0,v=!!(o&&p&&c.hard===!0),g=i?p:o&&l.length>0&&r.installed?n:!1,k=t&&d.length>0&&r.installed?n:!1,f=!!(e.ci?.failClosed&&u.length>0),C=f&&r.installed?n:!1;return{schemaVersion:"1.1",activeHost:e.activeHost,localWrite:y({supported:o,configuredPaths:l,installed:r,active:g,runtimeObserved:i,operation:i?c.operation??null:null,operationCoverage:s,bypassable:v?!1:o&&!i?n:!0,required:n,hard:v,sources:{active:i?"observed PreToolUse attempt":"runtime observation unavailable",runtimeObserved:i?"fresh PreToolUse invocation":"runtime observation unavailable",operationCoverage:i?"active-host operation matcher":"operation not observed",bypassable:v?"observed hard write boundary":"host runtime bypass evidence unavailable",required:"local host policy unavailable",hard:v?"fresh covered active-host invocation":"hardness not proven for this invocation"}}),advisoryMcp:y({supported:t,configuredPaths:d,installed:r,active:k,runtimeObserved:!1,operation:null,operationCoverage:n,bypassable:!0,required:n,hard:!1,sources:{active:"MCP runtime observation unavailable",runtimeObserved:"doctor did not observe an MCP tool invocation",operationCoverage:"advisory MCP is caller-invoked",bypassable:"advisory MCP does not intercept every write",required:"local host policy unavailable",hard:"MCP presence is advisory and never proves a hard boundary"}}),ciMerge:y({supported:!0,configuredPaths:f?u:[],installed:r,active:C,runtimeObserved:!1,operation:"merge",operationCoverage:f?n:!1,bypassable:f?n:!0,required:n,hard:!1,sources:{active:"CI run and provider enforcement not observed",runtimeObserved:"provider evidence unavailable",operationCoverage:"required-status operation coverage unavailable",bypassable:"branch-protection evidence unavailable",required:"branch-protection evidence unavailable",hard:"merge hardness requires fresh provider evidence"}})}}function w(a,e,r,o){return{...a,...o,evidence:[...a.evidence.filter(t=>!e.includes(t.field)),...e.map(t=>({field:t,source:r,value:o[t]}))]}}function x(a,e){if(!e?.available)return a;const r=typeof e.arkCheckRequired=="boolean"?e.arkCheckRequired:n,o=!!(a.enforcementState.ciMerge.configured&&a.enforcementState.ciMerge.installed),t=r===!0?o:r===!1?!1:o?n:!1,l=t===!0?e.arkCheckSourceBound===!1?!0:n:r===!1?!0:o?n:!0,d=`GitHub branch protection (${e.repo??"repository"}:${e.branch??"default"})`,u=!0,c=r,i=t===!0&&l===!1&&c===!0,s=w(a.enforcementState.ciMerge,["active","runtimeObserved","operationCoverage","bypassable","required","hard"],d,{active:t,runtimeObserved:u,operationCoverage:c,bypassable:l,required:r,hard:i});return{...a,enforcementState:{...a.enforcementState,ciMerge:s},enforcementLadder:{...a.enforcementLadder,ciMerge:{...a.enforcementLadder.ciMerge,requiredStatus:r}}}}function m(a,e){const r=o=>o===!0?"yes":o===!1?"no":String(o);return`${a} \u2014 supported: ${r(e.supported)} \xB7 analyzed: ${r(e.analyzed)} \xB7 configured: ${r(e.configured)} \xB7 installed: ${r(e.installed)} \xB7 runtime observed: ${r(e.runtimeObserved)} \xB7 operation: ${e.operation??"none"} \xB7 operation covered: ${r(e.operationCoverage)} \xB7 active: ${r(e.active)} \xB7 bypassable: ${r(e.bypassable)} \xB7 required: ${r(e.required)} \xB7 hard: ${r(e.hard)}`}function P(a){const e=[{level:a.localWrite.active===!0?"ok":"warn",text:m("Local write",a.localWrite)},{level:"warn",text:m("Advisory MCP",a.advisoryMcp)},{level:a.ciMerge.required===!0?"ok":"warn",text:m("CI merge",a.ciMerge)}];return a.localWrite.active===n&&a.localWrite.hard===!1&&e.push({level:"bad",text:"RED FLAG: local hook assets exist, but this active-host operation was not observed at runtime; hard blocking is unverified."}),a.activeHost==="unknown"&&e.push({level:"warn",text:"Active host unknown for this invocation \u2014 enforcementState is session projection only. See writePath.inventory for on-disk host hooks; hard write is never claimed without runtime proof."}),e}export{O as buildEnforcementState,P as enforcementDoctorLines,x as withCiProviderEvidence};
@@ -116,6 +116,33 @@ export function loadGoldenPattern(root) {
116
116
  return { ok: true, present: true, path: rel, golden };
117
117
  }
118
118
 
119
+ /**
120
+ * Y06 — advisory opt-in nudge when the golden pattern names pure modules but no
121
+ * layer declares `pure: true`. Never a blocker; never auto-writes pure:true.
122
+ *
123
+ * @param {{ layers?: Array<{ pure?: boolean }> }|null|undefined} config
124
+ * @param {GoldenPatternLoadResult|null|undefined} goldenResult
125
+ * @returns {null | { id: string, advisory: true, message: string }}
126
+ */
127
+ export function computePureLayerOptInNudge(config, goldenResult) {
128
+ const layers = Array.isArray(config?.layers) ? config.layers : [];
129
+ if (layers.some((layer) => layer?.pure === true)) return null;
130
+ if (!goldenResult?.present || !goldenResult.golden) return null;
131
+ const g = goldenResult.golden;
132
+ const text = [g.name, g.norm, g.newCodeHome, g.examplePath]
133
+ .filter((part) => typeof part === 'string' && part.trim())
134
+ .join('\n');
135
+ // Require explicit purity language in the golden pattern (not mere "Domain").
136
+ if (!/\bpure\b/i.test(text)) return null;
137
+ return {
138
+ id: 'pure-layer-opt-in',
139
+ advisory: true,
140
+ message:
141
+ 'Golden pattern references pure modules, but no layer sets pure: true. ' +
142
+ 'Opt in on Domain (or equivalent) to enable ambient-state diagnostics — advisory only; never auto-applied.',
143
+ };
144
+ }
145
+
119
146
  /**
120
147
  * One-line guidance for agents / placement notes.
121
148
  * @param {GoldenPatternLoadResult} result
@@ -809,10 +809,33 @@ export function renderHtmlReport({
809
809
  )
810
810
  .join('\n');
811
811
 
812
- const skillsNote =
813
- skillGaps.length === 0
814
- ? '<div class="pill good">Agent skills current for detected tools</div>'
815
- : `<div class="pill warn">${skillGaps.length} skill gap(s) — run ark upgrade / --install-agent-gates</div>`;
812
+ // Actionable catalog debt only — Codex leftover flat prompts with a complete
813
+ // .agents/skills catalog (legacyAdvisory) are cleanup advisories, not install gaps.
814
+ const actionableSkillGaps = skillGaps.filter(
815
+ (gap) =>
816
+ (gap?.missing ?? 0) > 0 ||
817
+ (gap?.stale ?? 0) > 0 ||
818
+ gap?.legacyPromptsOnly === true
819
+ );
820
+ const legacyAdvisoryOnly = skillGaps.some(
821
+ (gap) => gap?.legacyAdvisory === true && gap?.catalogComplete === true
822
+ );
823
+ const skillsParts = [];
824
+ if (actionableSkillGaps.length === 0) {
825
+ skillsParts.push(
826
+ '<div class="pill good">Agent skills current for detected tools</div>'
827
+ );
828
+ } else {
829
+ skillsParts.push(
830
+ `<div class="pill warn">${actionableSkillGaps.length} skill gap(s) — run ark upgrade / --install-agent-gates</div>`
831
+ );
832
+ }
833
+ if (legacyAdvisoryOnly) {
834
+ skillsParts.push(
835
+ '<div class="pill" style="opacity:.85">Codex leftover .codex/prompts are safe to delete (catalog complete; not required)</div>'
836
+ );
837
+ }
838
+ const skillsNote = skillsParts.join('\n');
816
839
 
817
840
  const meta = [
818
841
  version ? `ark-check v${esc(version)}` : '',
@@ -149,7 +149,10 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
149
149
  'AGENTS.md',
150
150
  compact ? compactAgentInstructions(root, compactHost) : agentInstructions(root)
151
151
  );
152
- if (!compact || !compactHost || compactHost === 'claude') add('.mcp.json', mcpJson(root));
152
+ // Always write project MCP registration compact hosts other than Claude still need
153
+ // ark://manifest for agents (field: compact grok start left doctor reporting Missing .mcp.json
154
+ // when AGENTS lost compact markers or hosts were mixed).
155
+ add('.mcp.json', mcpJson(root));
153
156
  const deploy = detectDeployPathQuality(root);
154
157
  add(
155
158
  '.github/workflows/ark-check.yml',