arkgate 3.8.1 → 3.8.2
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 +28 -0
- package/README.md +4 -4
- 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/managed-upgrade.mjs +58 -16
- package/bin/lib/skill-install.mjs +115 -17
- package/bin/lib/upgrade-command.mjs +21 -5
- 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,34 @@ in the immutable pre-2.0 archive linked below.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 3.8.2 — 2026-07-22
|
|
9
|
+
|
|
10
|
+
Corrective **patch** over 3.8.1 from PREDIAL WEB field evidence. Aligns doctor skill freshness with
|
|
11
|
+
managed upgrade content identity, hardens upgrade preview honesty, clarifies doctor writePath
|
|
12
|
+
inventory vs this-invocation, ships the Y06 pure-layer opt-in advisory, and treats complete-catalog
|
|
13
|
+
Codex leftover prompts as safe-to-delete. **No required config migration.** Z09 / residual `RB-11`
|
|
14
|
+
remain open.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Doctor skill “stale” vs managed upgrade (field DX):** skill gap detection now uses the same
|
|
19
|
+
content-identity rules as `ark upgrade` (stamp-normalized). A skill whose body matches the
|
|
20
|
+
package template is not stale when only `arkVersion` lags. Stale copy says content-behind-package.
|
|
21
|
+
- **Upgrade preview phantom apply:** summary reports `managedAssets` / `wouldWrite` /
|
|
22
|
+
`customizedPreserved`; when `wouldWrite` is 0, prints “Nothing to apply” instead of urging
|
|
23
|
+
`--apply` as the primary next step (optional stamp-only apply remains digest-bound).
|
|
24
|
+
- **Doctor writePath honesty:** with `activeHost: unknown`, a `sessionNote` separates on-disk
|
|
25
|
+
inventory from this-invocation hardness; package `installed` is independent of host support.
|
|
26
|
+
Hard still requires runtime proof (Z10).
|
|
27
|
+
- **Codex legacy prompts:** complete `.agents/skills` with leftover `.codex/prompts` is an
|
|
28
|
+
advisory safe-to-delete signal (CLI, doctor, HTML), not an install-agent-gates gap.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Y06 pure-layer opt-in nudge:** when a golden pattern names pure modules and no layer sets
|
|
33
|
+
`pure: true`, doctor emits one advisory line (`doctor.pureLayerOptIn`). Never a blocker; never
|
|
34
|
+
auto-writes `pure: true`. Promoted from field evidence (PREDIAL WEB).
|
|
35
|
+
|
|
8
36
|
## 3.8.1 — 2026-07-22
|
|
9
37
|
|
|
10
38
|
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.2** is current stable: field DX on top of 3.8.1 — skill stale matches upgrade content
|
|
20
|
+
> identity, honest upgrade preview, doctor session notes, Y06 pure opt-in nudge, Codex legacy advisory.
|
|
21
|
+
> [Release notes](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/3.8.2.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.2) | [release notes](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/3.8.2.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/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)}` : '',
|
|
@@ -6,8 +6,10 @@ import { codexPrimaryTable, upsertCodexMcpTable } from './codex-home.mjs';
|
|
|
6
6
|
import { buildManagedAssetCatalog } from './install-migrate.mjs';
|
|
7
7
|
import {
|
|
8
8
|
KNOWN_TOOLS,
|
|
9
|
+
arkPackageVersion,
|
|
9
10
|
detectActiveAgentHost,
|
|
10
11
|
normalizeToolsList,
|
|
12
|
+
skillContentIdentity,
|
|
11
13
|
} from './skill-install.mjs';
|
|
12
14
|
|
|
13
15
|
export const MANAGED_MANIFEST_PATH = 'ark.managed.json';
|
|
@@ -36,21 +38,17 @@ function hash(content) {
|
|
|
36
38
|
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
function normalizedIdentityContent(content
|
|
40
|
-
|
|
41
|
-
if (kind !== 'skill') return text;
|
|
42
|
-
const lines = text.split('\n');
|
|
43
|
-
if (lines[0] !== '---') return text;
|
|
44
|
-
const end = lines.indexOf('---', 1);
|
|
45
|
-
if (end < 0) return text;
|
|
46
|
-
for (let index = 1; index < end; index += 1) {
|
|
47
|
-
if (/^arkVersion:/.test(lines[index])) lines[index] = 'arkVersion:<managed>';
|
|
48
|
-
}
|
|
49
|
-
return lines.join('\n');
|
|
41
|
+
function normalizedIdentityContent(content) {
|
|
42
|
+
return String(content).replace(/\r\n/g, '\n');
|
|
50
43
|
}
|
|
51
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Content identity for managed assets. Skill kind delegates to skill-install so
|
|
47
|
+
* doctor stale detection and upgrade classify never drift (single hasher).
|
|
48
|
+
*/
|
|
52
49
|
export function managedContentIdentity(content, kind = 'gate') {
|
|
53
|
-
return
|
|
50
|
+
if (kind === 'skill') return skillContentIdentity(content);
|
|
51
|
+
return hash(Buffer.from(normalizedIdentityContent(content)));
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
function isSafeRelativePath(relativePath) {
|
|
@@ -271,12 +269,22 @@ function serializeManifest(value) {
|
|
|
271
269
|
function summaryFor(assets, manifestChanged) {
|
|
272
270
|
const states = {};
|
|
273
271
|
for (const asset of assets) states[asset.state] = (states[asset.state] ?? 0) + 1;
|
|
274
|
-
const
|
|
272
|
+
const applying = assets.filter((asset) => asset.willApply);
|
|
273
|
+
// Content writes (stale/missing/conflicted accepted) — not version-stamp metadata-only.
|
|
274
|
+
const wouldWrite = applying.filter((asset) => asset.action !== 'refresh-metadata').length;
|
|
275
|
+
const metadataRefresh = applying.filter((asset) => asset.action === 'refresh-metadata').length;
|
|
276
|
+
const customizedPreserved = assets.filter((asset) => asset.state === 'customized').length;
|
|
277
|
+
const fileChanges = applying.length;
|
|
275
278
|
return {
|
|
276
279
|
total: assets.length,
|
|
280
|
+
managedAssets: assets.length,
|
|
277
281
|
states,
|
|
282
|
+
wouldWrite,
|
|
283
|
+
metadataRefresh,
|
|
284
|
+
customizedPreserved,
|
|
278
285
|
fileChanges,
|
|
279
286
|
manifestChanged,
|
|
287
|
+
// Full apply count still includes optional stamp refresh + manifest bookkeeping.
|
|
280
288
|
changed: fileChanges + (manifestChanged ? 1 : 0),
|
|
281
289
|
blocked: assets.filter((asset) => asset.blocked).length,
|
|
282
290
|
};
|
|
@@ -616,7 +624,41 @@ export function renderManagedUpgrade(plan, options = {}) {
|
|
|
616
624
|
const consent = asset.requiresConsent ? ' (consent required)' : '';
|
|
617
625
|
console.log(` ${asset.state.padEnd(10)} ${asset.path}${consent}`);
|
|
618
626
|
}
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
627
|
+
const summary = plan.summary;
|
|
628
|
+
const managedAssets = summary.managedAssets ?? summary.total ?? plan.assets.length;
|
|
629
|
+
const wouldWrite = summary.wouldWrite ?? 0;
|
|
630
|
+
const metadataRefresh = summary.metadataRefresh ?? 0;
|
|
631
|
+
const customizedPreserved = summary.customizedPreserved ?? summary.states?.customized ?? 0;
|
|
632
|
+
const blocked = summary.blocked ?? 0;
|
|
633
|
+
console.log(
|
|
634
|
+
`Managed assets: ${managedAssets}; would write: ${wouldWrite}; ` +
|
|
635
|
+
`customized preserved: ${customizedPreserved}; blocked conflicts/deletions: ${blocked}` +
|
|
636
|
+
(metadataRefresh > 0 ? `; optional stamp refresh: ${metadataRefresh}` : '') +
|
|
637
|
+
'.'
|
|
638
|
+
);
|
|
639
|
+
if (plan.applied) {
|
|
640
|
+
console.log(`Applied changes: ${summary.changed}.`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
// Content already matches package templates — do not urge --apply as the primary next step.
|
|
644
|
+
if (wouldWrite === 0 && blocked === 0) {
|
|
645
|
+
const ver = options.packageVersion ?? arkPackageVersion();
|
|
646
|
+
const verLabel = ver ? `arkgate@${ver}` : 'the installed arkgate package';
|
|
647
|
+
console.log(
|
|
648
|
+
`Nothing to apply — managed content matches ${verLabel} (${customizedPreserved} customized preserved).`
|
|
649
|
+
);
|
|
650
|
+
if (metadataRefresh > 0) {
|
|
651
|
+
console.log(
|
|
652
|
+
`Optional: ${metadataRefresh} skill stamp(s) lag package version while content is already current.`
|
|
653
|
+
);
|
|
654
|
+
const stampCmd = options.optionalStampApply ?? options.next;
|
|
655
|
+
if (stampCmd) {
|
|
656
|
+
console.log(`Optional stamp-only apply (not required): ${stampCmd}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
console.log(`Planned writes: ${wouldWrite}; blocked conflicts/deletions: ${blocked}.`);
|
|
662
|
+
if (options.next) console.log(options.next);
|
|
663
|
+
else console.log('Apply the exact preview with: ark upgrade --apply --no-install');
|
|
622
664
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool detection, skill templates, stamping, and skill freshness gaps.
|
|
3
3
|
*/
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
import fs from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { arkCommand } from '../ark-shared.mjs';
|
|
@@ -206,7 +207,12 @@ export function installedSkillVersion(filePath) {
|
|
|
206
207
|
} catch {
|
|
207
208
|
return null;
|
|
208
209
|
}
|
|
209
|
-
|
|
210
|
+
return skillVersionFromContent(content);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function skillVersionFromContent(content) {
|
|
214
|
+
if (content == null) return null;
|
|
215
|
+
const match = String(content).match(/^arkVersion:\s*(.+)$/m);
|
|
210
216
|
return match ? match[1].trim() : null;
|
|
211
217
|
}
|
|
212
218
|
|
|
@@ -225,6 +231,63 @@ export function isVersionOlder(a, b) {
|
|
|
225
231
|
return false;
|
|
226
232
|
}
|
|
227
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Content identity for managed skills — arkVersion stamp is normalized so a lagging
|
|
236
|
+
* header alone never diverges from the package template (matches managed-upgrade).
|
|
237
|
+
* @param {string|null|undefined} content
|
|
238
|
+
* @returns {string|null}
|
|
239
|
+
*/
|
|
240
|
+
export function skillContentIdentity(content) {
|
|
241
|
+
if (content == null) return null;
|
|
242
|
+
let text = String(content).replace(/\r\n/g, '\n');
|
|
243
|
+
const lines = text.split('\n');
|
|
244
|
+
if (lines[0] === '---') {
|
|
245
|
+
const end = lines.indexOf('---', 1);
|
|
246
|
+
if (end >= 0) {
|
|
247
|
+
for (let index = 1; index < end; index += 1) {
|
|
248
|
+
if (/^arkVersion:/.test(lines[index])) lines[index] = 'arkVersion:<managed>';
|
|
249
|
+
}
|
|
250
|
+
text = lines.join('\n');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return `sha256:${createHash('sha256').update(text).digest('hex')}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* True when installed skill body matches the package template for that skill
|
|
258
|
+
* (version stamp ignored). Used so doctor "stale" aligns with managed upgrade.
|
|
259
|
+
*
|
|
260
|
+
* Templates ship without arkVersion; installs are stamped. Identity normalizes
|
|
261
|
+
* the stamp value, so compare against both the raw template and a stamped copy.
|
|
262
|
+
* @param {string} installedContent
|
|
263
|
+
* @param {string|undefined|null} templateContent
|
|
264
|
+
*/
|
|
265
|
+
export function skillContentMatchesTemplate(installedContent, templateContent) {
|
|
266
|
+
if (templateContent == null || installedContent == null) return false;
|
|
267
|
+
const installedId = skillContentIdentity(installedContent);
|
|
268
|
+
if (installedId === skillContentIdentity(templateContent)) return true;
|
|
269
|
+
// Installed skills are stamped; templates are not — stamp with a dummy version
|
|
270
|
+
// so arkVersion:<managed> lines align under skillContentIdentity.
|
|
271
|
+
return installedId === skillContentIdentity(stampSkill(templateContent, '0.0.0'));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** @returns {Record<string, string>} skill name → template body from package */
|
|
275
|
+
export function skillTemplateBodies() {
|
|
276
|
+
return Object.fromEntries(skillTemplates());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Count a present skill as stale only when content differs from the package
|
|
281
|
+
* template AND the arkVersion stamp is missing or older than the package.
|
|
282
|
+
* Content identity match → never stale (even if header lags).
|
|
283
|
+
*/
|
|
284
|
+
function isInstalledSkillStale(installedContent, templateContent, packageVersion) {
|
|
285
|
+
if (!packageVersion) return false;
|
|
286
|
+
if (skillContentMatchesTemplate(installedContent, templateContent)) return false;
|
|
287
|
+
const installed = skillVersionFromContent(installedContent);
|
|
288
|
+
return installed === null || isVersionOlder(installed, packageVersion);
|
|
289
|
+
}
|
|
290
|
+
|
|
228
291
|
export function skillTemplates() {
|
|
229
292
|
const dir = path.join(__packageRoot, 'templates', 'skills');
|
|
230
293
|
// A missing/mispackaged templates dir would otherwise install zero skills with
|
|
@@ -263,23 +326,31 @@ export function skillTemplateNames() {
|
|
|
263
326
|
|
|
264
327
|
/**
|
|
265
328
|
* Count present / stale / legacy-only skill files for one catalog root.
|
|
329
|
+
* "stale" means content behind the package template (identity mismatch) with a
|
|
330
|
+
* missing/older arkVersion stamp — not merely a lagging version header when the
|
|
331
|
+
* body still matches the template (aligned with managed-upgrade classify).
|
|
266
332
|
* @param {string[]} skillNames
|
|
267
333
|
* @param {(name: string) => string} skillFile path builder
|
|
268
334
|
* @param {string|null} packageVersion
|
|
269
|
-
* @param {{ legacyFile?: (name: string) => string }} [opts]
|
|
335
|
+
* @param {{ legacyFile?: (name: string) => string, templateBodies?: Record<string, string> }} [opts]
|
|
270
336
|
*/
|
|
271
337
|
export function assessSkillCatalogParity(skillNames, skillFile, packageVersion, opts = {}) {
|
|
272
338
|
const expectedCount = skillNames.length;
|
|
339
|
+
const templates = opts.templateBodies ?? skillTemplateBodies();
|
|
273
340
|
const present = [];
|
|
274
341
|
let stale = 0;
|
|
275
342
|
for (const name of skillNames) {
|
|
276
343
|
const file = skillFile(name);
|
|
277
344
|
if (!fs.existsSync(file)) continue;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
345
|
+
let content;
|
|
346
|
+
try {
|
|
347
|
+
content = fs.readFileSync(file, 'utf8');
|
|
348
|
+
} catch {
|
|
349
|
+
// Unreadable path is not a usable install — count as missing (matches detectSkillGaps).
|
|
350
|
+
continue;
|
|
282
351
|
}
|
|
352
|
+
present.push(name);
|
|
353
|
+
if (isInstalledSkillStale(content, templates[name], packageVersion)) stale += 1;
|
|
283
354
|
}
|
|
284
355
|
let legacyCount = 0;
|
|
285
356
|
if (typeof opts.legacyFile === 'function') {
|
|
@@ -291,6 +362,7 @@ export function assessSkillCatalogParity(skillNames, skillFile, packageVersion,
|
|
|
291
362
|
const missing = expectedCount - presentCount;
|
|
292
363
|
const legacyPromptsOnly = presentCount === 0 && legacyCount > 0;
|
|
293
364
|
const hasLegacyPrompts = legacyCount > 0;
|
|
365
|
+
// Legacy prompts beside a complete modern catalog are not catalog debt.
|
|
294
366
|
const ok = missing === 0 && stale === 0 && !legacyPromptsOnly;
|
|
295
367
|
return {
|
|
296
368
|
ok,
|
|
@@ -302,6 +374,7 @@ export function assessSkillCatalogParity(skillNames, skillFile, packageVersion,
|
|
|
302
374
|
legacyPromptsOnly,
|
|
303
375
|
hasLegacyPrompts,
|
|
304
376
|
legacyCount,
|
|
377
|
+
catalogComplete: missing === 0 && stale === 0 && presentCount === expectedCount,
|
|
305
378
|
};
|
|
306
379
|
}
|
|
307
380
|
|
|
@@ -490,6 +563,7 @@ export function detectSkillGaps(root) {
|
|
|
490
563
|
if (fs.existsSync(path.join(root, 'templates', 'skills'))) return [];
|
|
491
564
|
const skillNames = skillTemplateNames();
|
|
492
565
|
if (skillNames.length === 0) return [];
|
|
566
|
+
const templates = skillTemplateBodies();
|
|
493
567
|
const detected = [];
|
|
494
568
|
if (fs.existsSync(path.join(root, '.claude'))) detected.push('claude');
|
|
495
569
|
if (fs.existsSync(path.join(root, '.cursor'))) detected.push('cursor');
|
|
@@ -510,16 +584,21 @@ export function detectSkillGaps(root) {
|
|
|
510
584
|
const file = path.join(root, target(name));
|
|
511
585
|
if (!fs.existsSync(file)) {
|
|
512
586
|
missing += 1;
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
let content;
|
|
590
|
+
try {
|
|
591
|
+
content = fs.readFileSync(file, 'utf8');
|
|
592
|
+
} catch {
|
|
593
|
+
missing += 1;
|
|
594
|
+
continue;
|
|
519
595
|
}
|
|
596
|
+
// Content identity match with package template → not stale (version header may lag).
|
|
597
|
+
if (isInstalledSkillStale(content, templates[name], version)) stale += 1;
|
|
520
598
|
}
|
|
521
599
|
let legacyPromptsOnly = false;
|
|
522
600
|
let hasLegacyPrompts = false;
|
|
601
|
+
let legacyAdvisory = false;
|
|
523
602
|
if (tool === 'codex') {
|
|
524
603
|
const legacyCount = skillNames.filter((name) =>
|
|
525
604
|
fs.existsSync(path.join(root, '.codex', 'prompts', `${name}.md`))
|
|
@@ -527,14 +606,20 @@ export function detectSkillGaps(root) {
|
|
|
527
606
|
hasLegacyPrompts = legacyCount > 0;
|
|
528
607
|
// Flat prompts without any SKILL.md catalog entries are not loadable.
|
|
529
608
|
legacyPromptsOnly = hasLegacyPrompts && missing === skillNames.length;
|
|
609
|
+
// Modern catalog complete + leftover flat prompts → advisory only (safe delete).
|
|
610
|
+
legacyAdvisory =
|
|
611
|
+
hasLegacyPrompts && !legacyPromptsOnly && missing === 0 && stale === 0;
|
|
530
612
|
}
|
|
531
|
-
if (missing > 0 || stale > 0 || legacyPromptsOnly) {
|
|
613
|
+
if (missing > 0 || stale > 0 || legacyPromptsOnly || legacyAdvisory) {
|
|
532
614
|
gaps.push({
|
|
533
615
|
tool,
|
|
534
616
|
missing,
|
|
535
617
|
stale,
|
|
536
618
|
...(legacyPromptsOnly ? { legacyPromptsOnly: true } : {}),
|
|
537
619
|
...(hasLegacyPrompts ? { hasLegacyPrompts: true } : {}),
|
|
620
|
+
...(legacyAdvisory
|
|
621
|
+
? { legacyAdvisory: true, catalogComplete: true }
|
|
622
|
+
: {}),
|
|
538
623
|
});
|
|
539
624
|
}
|
|
540
625
|
}
|
|
@@ -550,8 +635,14 @@ export function printSkillAndCodexGapHints(root, opts) {
|
|
|
550
635
|
const { skillGaps, codexHomeGap, codexRepoSkillGap, codexSessionActive, color } = opts;
|
|
551
636
|
if (skillGaps?.length > 0) {
|
|
552
637
|
const legacyCodex = skillGaps.some((gap) => gap.tool === 'codex' && gap.legacyPromptsOnly);
|
|
638
|
+
const legacyAdvisory = skillGaps.some(
|
|
639
|
+
(gap) => gap.tool === 'codex' && gap.legacyAdvisory && gap.catalogComplete
|
|
640
|
+
);
|
|
553
641
|
// Report Codex legacy separately; never suppress missing/stale for other hosts.
|
|
554
|
-
const remaining = skillGaps.filter(
|
|
642
|
+
const remaining = skillGaps.filter(
|
|
643
|
+
(gap) =>
|
|
644
|
+
!(gap.tool === 'codex' && (gap.legacyPromptsOnly || gap.legacyAdvisory))
|
|
645
|
+
);
|
|
555
646
|
const missingTotal = remaining.reduce((sum, gap) => sum + gap.missing, 0);
|
|
556
647
|
const staleTotal = remaining.reduce((sum, gap) => sum + gap.stale, 0);
|
|
557
648
|
const tools = remaining.map((gap) => gap.tool).join(', ');
|
|
@@ -563,6 +654,13 @@ export function printSkillAndCodexGapHints(root, opts) {
|
|
|
563
654
|
)
|
|
564
655
|
);
|
|
565
656
|
}
|
|
657
|
+
if (legacyAdvisory) {
|
|
658
|
+
console.log(
|
|
659
|
+
color.dim(
|
|
660
|
+
'Codex .agents/skills catalog is complete; leftover .codex/prompts/ark-*.md are not loadable and safe to delete (not required).'
|
|
661
|
+
)
|
|
662
|
+
);
|
|
663
|
+
}
|
|
566
664
|
if (missingTotal > 0) {
|
|
567
665
|
console.log(
|
|
568
666
|
color.dim(
|
|
@@ -574,7 +672,7 @@ export function printSkillAndCodexGapHints(root, opts) {
|
|
|
574
672
|
if (staleTotal > 0) {
|
|
575
673
|
console.log(
|
|
576
674
|
color.dim(
|
|
577
|
-
`${staleTotal} /ark-* skill(s)
|
|
675
|
+
`${staleTotal} /ark-* skill(s) content behind this Ark package for ${tools}. ` +
|
|
578
676
|
`Refresh: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --force')}`
|
|
579
677
|
)
|
|
580
678
|
);
|
|
@@ -584,7 +682,7 @@ export function printSkillAndCodexGapHints(root, opts) {
|
|
|
584
682
|
const parts = [];
|
|
585
683
|
if (codexHomeGap.legacyPromptsOnly) parts.push('legacy-prompts-only');
|
|
586
684
|
if (codexHomeGap.missing > 0) parts.push(`${codexHomeGap.missing} missing`);
|
|
587
|
-
if (codexHomeGap.stale > 0) parts.push(`${codexHomeGap.stale}
|
|
685
|
+
if (codexHomeGap.stale > 0) parts.push(`${codexHomeGap.stale} content-behind-package`);
|
|
588
686
|
const deferred = !codexSessionActive;
|
|
589
687
|
const deferredNote = deferred
|
|
590
688
|
? ' Deferred unless you use Codex — not a blocker for Grok/Claude/Cursor. '
|
|
@@ -600,7 +698,7 @@ export function printSkillAndCodexGapHints(root, opts) {
|
|
|
600
698
|
const parts = [];
|
|
601
699
|
if (codexRepoSkillGap.legacyPromptsOnly) parts.push('legacy-prompts-only');
|
|
602
700
|
if (codexRepoSkillGap.missing > 0) parts.push(`${codexRepoSkillGap.missing} missing`);
|
|
603
|
-
if (codexRepoSkillGap.stale > 0) parts.push(`${codexRepoSkillGap.stale}
|
|
701
|
+
if (codexRepoSkillGap.stale > 0) parts.push(`${codexRepoSkillGap.stale} content-behind-package`);
|
|
604
702
|
console.log(
|
|
605
703
|
color.yellow(
|
|
606
704
|
`Codex repo skill catalog (.agents/skills) needs refresh (${parts.join(', ')}). ` +
|