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
|
@@ -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
|
};
|
|
@@ -505,7 +513,20 @@ export function applyManagedUpgrade(root, plan, expectedPlanDigest) {
|
|
|
505
513
|
const resolvedRoot = path.resolve(root);
|
|
506
514
|
if (resolvedRoot !== plan.root) throw new Error('managed upgrade plan root mismatch');
|
|
507
515
|
if (plan.summary.blocked > 0) return publicPlan(plan, { blocked: true });
|
|
516
|
+
const wouldWrite = plan.summary.wouldWrite ?? 0;
|
|
517
|
+
const metadataRefresh = plan.summary.metadataRefresh ?? 0;
|
|
518
|
+
// Content already matches: unbound --apply is a no-op (exit success), not a digest error.
|
|
519
|
+
// Optional stamp-only refresh still requires the preview's exact --plan-digest.
|
|
508
520
|
if (!expectedPlanDigest || expectedPlanDigest !== plan.planDigest) {
|
|
521
|
+
if (wouldWrite === 0 && (plan.summary.blocked ?? 0) === 0 && !expectedPlanDigest) {
|
|
522
|
+
return publicPlan(plan, {
|
|
523
|
+
readOnly: true,
|
|
524
|
+
applied: false,
|
|
525
|
+
blocked: false,
|
|
526
|
+
nothingToApply: true,
|
|
527
|
+
optionalStampRefresh: metadataRefresh,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
509
530
|
throw new Error('managed upgrade plan digest mismatch; run a new preview and use its exact nextCommand');
|
|
510
531
|
}
|
|
511
532
|
|
|
@@ -616,7 +637,55 @@ export function renderManagedUpgrade(plan, options = {}) {
|
|
|
616
637
|
const consent = asset.requiresConsent ? ' (consent required)' : '';
|
|
617
638
|
console.log(` ${asset.state.padEnd(10)} ${asset.path}${consent}`);
|
|
618
639
|
}
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
640
|
+
const summary = plan.summary;
|
|
641
|
+
const managedAssets = summary.managedAssets ?? summary.total ?? plan.assets.length;
|
|
642
|
+
const wouldWrite = summary.wouldWrite ?? 0;
|
|
643
|
+
const metadataRefresh = summary.metadataRefresh ?? 0;
|
|
644
|
+
const customizedPreserved = summary.customizedPreserved ?? summary.states?.customized ?? 0;
|
|
645
|
+
const blocked = summary.blocked ?? 0;
|
|
646
|
+
console.log(
|
|
647
|
+
`Managed assets: ${managedAssets}; would write: ${wouldWrite}; ` +
|
|
648
|
+
`customized preserved: ${customizedPreserved}; blocked conflicts/deletions: ${blocked}` +
|
|
649
|
+
(metadataRefresh > 0 ? `; optional stamp refresh: ${metadataRefresh}` : '') +
|
|
650
|
+
'.'
|
|
651
|
+
);
|
|
652
|
+
if (plan.applied) {
|
|
653
|
+
// Distinguish content writes from optional stamp/metadata bookkeeping.
|
|
654
|
+
if (wouldWrite === 0 && metadataRefresh > 0) {
|
|
655
|
+
console.log(
|
|
656
|
+
`Refreshed ${metadataRefresh} version stamp(s)` +
|
|
657
|
+
(summary.manifestChanged ? ' and managed manifest' : '') +
|
|
658
|
+
' (no content body changes).'
|
|
659
|
+
);
|
|
660
|
+
} else {
|
|
661
|
+
console.log(
|
|
662
|
+
`Applied ${wouldWrite} content write(s)` +
|
|
663
|
+
(metadataRefresh > 0 ? `, ${metadataRefresh} stamp refresh(es)` : '') +
|
|
664
|
+
(summary.manifestChanged ? ', managed manifest' : '') +
|
|
665
|
+
'.'
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
// Content already matches package templates — do not urge --apply as the primary next step.
|
|
671
|
+
if (wouldWrite === 0 && blocked === 0) {
|
|
672
|
+
const ver = options.packageVersion ?? arkPackageVersion();
|
|
673
|
+
const verLabel = ver ? `arkgate@${ver}` : 'the installed arkgate package';
|
|
674
|
+
console.log(
|
|
675
|
+
`Nothing to apply — managed content matches ${verLabel} (${customizedPreserved} customized preserved).`
|
|
676
|
+
);
|
|
677
|
+
if (metadataRefresh > 0) {
|
|
678
|
+
console.log(
|
|
679
|
+
`Optional: ${metadataRefresh} skill stamp(s) lag package version while content is already current.`
|
|
680
|
+
);
|
|
681
|
+
const stampCmd = options.optionalStampApply ?? options.next;
|
|
682
|
+
if (stampCmd) {
|
|
683
|
+
console.log(`Optional stamp-only apply (not required): ${stampCmd}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
console.log(`Planned writes: ${wouldWrite}; blocked conflicts/deletions: ${blocked}.`);
|
|
689
|
+
if (options.next) console.log(options.next);
|
|
690
|
+
else console.log('Apply the exact preview with: ark upgrade --apply --no-install');
|
|
622
691
|
}
|
|
@@ -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(', ')}). ` +
|
|
@@ -70,12 +70,14 @@ function setupBudget(changes) {
|
|
|
70
70
|
(total, item) => total + (item.afterBase64 ? Buffer.from(item.afterBase64, 'base64').length : 0),
|
|
71
71
|
0
|
|
72
72
|
);
|
|
73
|
+
// Compact start includes shared MCP + one host registration + CI + AGENTS + config.
|
|
74
|
+
// Budget raised from 5→8 so .mcp.json always fits (field: grok compact hit the old ceiling).
|
|
73
75
|
return {
|
|
74
76
|
files: generatedChanges.length,
|
|
75
77
|
bytes,
|
|
76
|
-
maxFiles:
|
|
77
|
-
maxBytes:
|
|
78
|
-
ok: generatedChanges.length <=
|
|
78
|
+
maxFiles: 8,
|
|
79
|
+
maxBytes: 32 * 1024,
|
|
80
|
+
ok: generatedChanges.length <= 8 && bytes < 32 * 1024,
|
|
79
81
|
};
|
|
80
82
|
}
|
|
81
83
|
|
|
@@ -84,7 +86,8 @@ function commands(root, args, helpers) {
|
|
|
84
86
|
return [`ark start --root ${root} --tools ${args.removeHost} --apply`];
|
|
85
87
|
}
|
|
86
88
|
const result = [];
|
|
87
|
-
|
|
89
|
+
// Default install=true: surface the package pin/install command unless --no-install.
|
|
90
|
+
if (args.install !== false && fs.existsSync(path.join(root, 'package.json'))) {
|
|
88
91
|
const [command, commandArgs] = helpers.packageInstallArgv(root, `^${helpers.cliVersion()}`);
|
|
89
92
|
result.push(`${command} ${commandArgs.join(' ')}`);
|
|
90
93
|
}
|
|
@@ -216,8 +219,9 @@ export async function planStart(args, helpers) {
|
|
|
216
219
|
if (args.yes) childArgs.push('--yes');
|
|
217
220
|
if (args.force) childArgs.push('--force');
|
|
218
221
|
if (!args.strict) childArgs.push('--no-strict');
|
|
219
|
-
|
|
220
|
-
if (args.
|
|
222
|
+
// Propagate install intent into the shadow plan so package.json pin is in the diff by default.
|
|
223
|
+
if (args.install === false) childArgs.push('--no-install');
|
|
224
|
+
else childArgs.push('--install');
|
|
221
225
|
if (args.tools) childArgs.push('--tools', args.tools);
|
|
222
226
|
if (args.requireWriteHook) childArgs.push('--require-write-hook', args.requireWriteHook);
|
|
223
227
|
const planned = spawnSync(process.execPath, [helpers.cliPath, ...childArgs], {
|
|
@@ -55,23 +55,50 @@ function verify(root, json, arkCheck, runArkCheck) {
|
|
|
55
55
|
export function runUpgradeCommand(args, dependencies) {
|
|
56
56
|
const root = args.root;
|
|
57
57
|
if (args.apply && args.install) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
const skip =
|
|
59
|
+
typeof dependencies.shouldSkipArkgateInstall === 'function'
|
|
60
|
+
? dependencies.shouldSkipArkgateInstall(root, dependencies.cliVersion)
|
|
61
|
+
: { skip: false };
|
|
62
|
+
if (skip.skip) {
|
|
63
|
+
if (!args.json) {
|
|
64
|
+
console.log(
|
|
65
|
+
`Package already at arkgate@${skip.installedVersion}; skipping install and recomputing managed preview.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
const [command, commandArgs] = dependencies.packageInstallArgv(root);
|
|
70
|
+
if (!args.json) console.log(`Updating ArkGate: ${command} ${commandArgs.join(' ')}`);
|
|
71
|
+
const install = spawnSync(command, commandArgs, {
|
|
72
|
+
cwd: root,
|
|
73
|
+
stdio: args.json ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
});
|
|
76
|
+
const exitCode = install.status ?? 1;
|
|
77
|
+
if (exitCode !== 0) {
|
|
78
|
+
if (args.json && install.stderr) console.error(install.stderr.trim());
|
|
79
|
+
const recovery = `${command} ${commandArgs.join(' ')}`;
|
|
80
|
+
console.error(
|
|
81
|
+
`Package update failed (exit ${exitCode}). Fix the install and re-run:\n` +
|
|
82
|
+
` ${recovery}\n` +
|
|
83
|
+
`Then: ark upgrade --no-install --root ${JSON.stringify(root)}` +
|
|
84
|
+
(args.tools ? ` --tools ${args.tools}` : '') +
|
|
85
|
+
(!args.strict ? ' --no-strict' : '') +
|
|
86
|
+
(args.json ? ' --json' : '')
|
|
87
|
+
);
|
|
88
|
+
return exitCode;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Re-enter via installed CLI so the managed plan uses the newly installed package bytes.
|
|
92
|
+
let cli;
|
|
93
|
+
try {
|
|
94
|
+
cli = installedCli(root);
|
|
95
|
+
} catch {
|
|
68
96
|
console.error(
|
|
69
|
-
|
|
70
|
-
'`ark upgrade --no-install` against the installed version.'
|
|
97
|
+
'arkgate is not installed in this project after the package step. Install it, then re-run with --no-install.'
|
|
71
98
|
);
|
|
72
|
-
return
|
|
99
|
+
return 1;
|
|
73
100
|
}
|
|
74
|
-
return spawnSync(process.execPath, [
|
|
101
|
+
return spawnSync(process.execPath, [cli, ...previewArgs(args)], {
|
|
75
102
|
cwd: root,
|
|
76
103
|
stdio: 'inherit',
|
|
77
104
|
encoding: 'utf8',
|
|
@@ -83,19 +110,42 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
83
110
|
acceptConflicts: args.acceptConflicts,
|
|
84
111
|
});
|
|
85
112
|
if (!args.apply) {
|
|
113
|
+
const wouldWrite = plan.summary?.wouldWrite ?? 0;
|
|
114
|
+
const blocked = plan.summary?.blocked ?? 0;
|
|
115
|
+
const needsApply = wouldWrite > 0 || blocked > 0;
|
|
86
116
|
const command = nextCommand(args, plan.planDigest);
|
|
87
|
-
if (args.json)
|
|
88
|
-
|
|
117
|
+
if (args.json) {
|
|
118
|
+
// Always expose nextCommand for digest-bound apply (metadata/manifest optional);
|
|
119
|
+
// nothingToApply flags when content writes are zero so UIs do not urge apply.
|
|
120
|
+
console.log(
|
|
121
|
+
managedUpgradeJson(plan, {
|
|
122
|
+
nextCommand: command,
|
|
123
|
+
...(needsApply ? {} : { nothingToApply: true }),
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
} else {
|
|
127
|
+
const metadataRefresh = plan.summary?.metadataRefresh ?? 0;
|
|
89
128
|
renderManagedUpgrade(plan, {
|
|
90
|
-
next:
|
|
91
|
-
?
|
|
92
|
-
|
|
129
|
+
next: needsApply
|
|
130
|
+
? args.install
|
|
131
|
+
? `Update the package and recompute this preview with: ${command}`
|
|
132
|
+
: `Apply the exact preview with: ${command}`
|
|
133
|
+
: undefined,
|
|
134
|
+
// Human path: optional digest-bound stamp refresh without urging content apply.
|
|
135
|
+
...( !needsApply && metadataRefresh > 0 ? { optionalStampApply: command } : {}),
|
|
93
136
|
});
|
|
94
137
|
}
|
|
95
138
|
return 0;
|
|
96
139
|
}
|
|
97
140
|
|
|
98
|
-
|
|
141
|
+
let applied;
|
|
142
|
+
try {
|
|
143
|
+
applied = applyManagedUpgrade(root, plan, args.planDigest);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
146
|
+
console.error(message);
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
99
149
|
if (applied.blocked) {
|
|
100
150
|
if (args.json) console.log(JSON.stringify(applied, null, 2));
|
|
101
151
|
else renderManagedUpgrade(applied, {
|
|
@@ -103,6 +153,14 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
103
153
|
});
|
|
104
154
|
return 1;
|
|
105
155
|
}
|
|
156
|
+
if (applied.nothingToApply && !applied.applied) {
|
|
157
|
+
if (args.json) console.log(JSON.stringify(applied, null, 2));
|
|
158
|
+
else {
|
|
159
|
+
renderManagedUpgrade(applied);
|
|
160
|
+
console.log('No managed content writes pending (optional stamp refresh needs --plan-digest).');
|
|
161
|
+
}
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
106
164
|
const verification = args.strict
|
|
107
165
|
? { mode: 'strict-merge', ...verify(root, args.json, dependencies.arkCheck, dependencies.runArkCheck) }
|
|
108
166
|
: { mode: 'skipped', exitCode: 0 };
|
|
@@ -15,6 +15,43 @@ function installToolsForHost(activeHost) {
|
|
|
15
15
|
: activeHost;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Human/JSON note when doctor runs outside an agent host: inventory on disk vs
|
|
20
|
+
* this-invocation hardness (Z10 — never claim hard from assets alone).
|
|
21
|
+
* @param {{ hosts?: Record<string, unknown>, capabilities?: Record<string, boolean> }|null|undefined} inventory
|
|
22
|
+
* @param {Record<string, boolean>} capabilities active-host projection
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export function buildUnknownHostSessionNote(inventory, capabilities = {}) {
|
|
26
|
+
const hosts = inventory?.hosts && typeof inventory.hosts === 'object' ? inventory.hosts : {};
|
|
27
|
+
const onDiskHosts = Object.entries(hosts)
|
|
28
|
+
.filter(([, record]) => {
|
|
29
|
+
if (!record || typeof record !== 'object') return false;
|
|
30
|
+
const caps = /** @type {{ [k: string]: boolean }} */ (record).capabilities ?? {};
|
|
31
|
+
const configured = Boolean(/** @type {{ configured?: boolean }} */ (record).configured);
|
|
32
|
+
return (
|
|
33
|
+
configured ||
|
|
34
|
+
Boolean(caps['hard-write']) ||
|
|
35
|
+
Boolean(caps['advisory-write']) ||
|
|
36
|
+
Boolean(caps['repair-payload'])
|
|
37
|
+
);
|
|
38
|
+
})
|
|
39
|
+
.map(([name]) => name)
|
|
40
|
+
.sort();
|
|
41
|
+
const parts = [];
|
|
42
|
+
if (onDiskHosts.length > 0) {
|
|
43
|
+
parts.push(`On-disk hosts with write-path assets: ${onDiskHosts.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
if (inventory?.capabilities?.['merge-gate'] || capabilities['merge-gate']) {
|
|
46
|
+
parts.push('CI merge gate configured on disk');
|
|
47
|
+
}
|
|
48
|
+
parts.push(
|
|
49
|
+
'This invocation has no active hard-write guarantee (activeHost unknown); ' +
|
|
50
|
+
'required-status remains unverified without provider evidence'
|
|
51
|
+
);
|
|
52
|
+
return `${parts.join('. ')}.`;
|
|
53
|
+
}
|
|
54
|
+
|
|
18
55
|
export function detectWritePathCapabilities(root, explicitHost, attempt) {
|
|
19
56
|
const model = buildWritePathCapabilityModel(root, explicitHost, attempt);
|
|
20
57
|
const { activeHost, support, capabilities, capabilityEvidence, enforcementLadder, enforcementState, inventory } = model;
|
|
@@ -92,6 +129,12 @@ export function detectWritePathCapabilities(root, explicitHost, attempt) {
|
|
|
92
129
|
};
|
|
93
130
|
}
|
|
94
131
|
|
|
132
|
+
/** @type {string|null} */
|
|
133
|
+
const sessionNote =
|
|
134
|
+
activeHost === 'unknown'
|
|
135
|
+
? buildUnknownHostSessionNote(inventory, capabilities)
|
|
136
|
+
: null;
|
|
137
|
+
|
|
95
138
|
return {
|
|
96
139
|
activeHost,
|
|
97
140
|
support,
|
|
@@ -101,6 +144,8 @@ export function detectWritePathCapabilities(root, explicitHost, attempt) {
|
|
|
101
144
|
enforcementLadder,
|
|
102
145
|
enforcementState,
|
|
103
146
|
inventory,
|
|
147
|
+
// Configured inventory (on-disk hosts) vs this-invocation projection (activeHost).
|
|
148
|
+
...(sessionNote ? { sessionNote } : {}),
|
|
104
149
|
// Compatibility projection for existing doctor/API consumers.
|
|
105
150
|
mode,
|
|
106
151
|
prepareWrite: advisoryWrite,
|