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 +56 -0
- package/README.md +4 -4
- package/bin/ark-shared.mjs +89 -2
- package/bin/ark.mjs +41 -19
- package/bin/lib/agent-gates.mjs +3 -0
- package/bin/lib/doctor-plan.mjs +38 -5
- package/bin/lib/enforcement-state.mjs +1 -1
- package/bin/lib/golden-pattern.mjs +27 -0
- package/bin/lib/html-report.mjs +27 -4
- package/bin/lib/install-migrate.mjs +4 -1
- package/bin/lib/managed-upgrade.mjs +85 -16
- package/bin/lib/skill-install.mjs +115 -17
- package/bin/lib/start-preview.mjs +10 -6
- package/bin/lib/upgrade-command.mjs +78 -20
- package/bin/lib/write-path-detect.mjs +45 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/package-surface.md +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
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.
|
|
20
|
-
>
|
|
21
|
-
> [Release notes](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/3.8.
|
|
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.
|
|
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
|
---
|
package/bin/ark-shared.mjs
CHANGED
|
@@ -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')
|
|
625
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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, {
|
|
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;
|
package/bin/lib/agent-gates.mjs
CHANGED
|
@@ -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,
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -36,7 +36,11 @@ import {
|
|
|
36
36
|
mergePostGreenTopActions,
|
|
37
37
|
isDoctorHealthyNothingToDo,
|
|
38
38
|
} from './post-green-path.mjs';
|
|
39
|
-
import {
|
|
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
|
|
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
|
|
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(
|
|
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}
|
|
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
|
|
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
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -809,10 +809,33 @@ export function renderHtmlReport({
|
|
|
809
809
|
)
|
|
810
810
|
.join('\n');
|
|
811
811
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
|
|
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',
|