arkgate 4.5.5 → 4.5.6
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 +45 -2
- package/README.md +4 -5
- package/bin/ark-mcp-runtime.mjs +49 -0
- package/bin/ark-shared.mjs +8 -29
- package/bin/ark.mjs +7 -3
- package/bin/lib/managed-upgrade.mjs +198 -1
- package/bin/lib/rules-inventory.mjs +23 -0
- package/bin/lib/upgrade-command.mjs +132 -11
- package/bin/lib/upgrade-package-decision.mjs +241 -0
- package/bin/lib/upgrade-whats-new.mjs +25 -0
- package/dist/index.cjs +21 -21
- package/dist/index.d.ts +1 -1
- package/dist/index.js +19 -19
- package/docs/README.md +2 -3
- package/docs/agent-guide.md +21 -0
- package/docs/package-surface.md +5 -3
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-upgrade/SKILL.md +48 -16
- package/templates/skills/ark-upgrade.md +48 -16
|
@@ -4,7 +4,11 @@ import fs from 'node:fs';
|
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
arkCommand,
|
|
9
|
+
buildPackageInstallSkipPayload,
|
|
10
|
+
formatPackageInstallDecisionHuman,
|
|
11
|
+
} from '../ark-shared.mjs';
|
|
8
12
|
import { describePackageVersionDualTruth } from './field-install.mjs';
|
|
9
13
|
import { __packageRoot } from './gate-files.mjs';
|
|
10
14
|
import {
|
|
@@ -12,6 +16,9 @@ import {
|
|
|
12
16
|
managedUpgradeJson,
|
|
13
17
|
planManagedUpgrade,
|
|
14
18
|
renderManagedUpgrade,
|
|
19
|
+
buildSkillDriftSummary,
|
|
20
|
+
buildPostUpgradeChecks,
|
|
21
|
+
buildHostSelectionHonesty,
|
|
15
22
|
} from './managed-upgrade.mjs';
|
|
16
23
|
|
|
17
24
|
function installedCli(root) {
|
|
@@ -277,6 +284,7 @@ function previewArgs(args) {
|
|
|
277
284
|
const next = ['upgrade', '--root', args.root, '--no-install'];
|
|
278
285
|
if (args.tools) next.push('--tools', args.tools);
|
|
279
286
|
if (args.acceptConflicts) next.push('--accept-conflicts');
|
|
287
|
+
if (args.refreshSkills) next.push('--refresh-skills');
|
|
280
288
|
if (!args.strict) next.push('--no-strict');
|
|
281
289
|
if (args.json) next.push('--json');
|
|
282
290
|
return next;
|
|
@@ -299,6 +307,7 @@ export function buildUpgradeNextCommand(args, planDigest) {
|
|
|
299
307
|
if (!args.install && planDigest) flagParts.push('--plan-digest', planDigest);
|
|
300
308
|
if (args.tools) flagParts.push('--tools', args.tools);
|
|
301
309
|
if (args.acceptConflicts) flagParts.push('--accept-conflicts');
|
|
310
|
+
if (args.refreshSkills) flagParts.push('--refresh-skills');
|
|
302
311
|
if (!args.strict) flagParts.push('--no-strict');
|
|
303
312
|
if (args.json) flagParts.push('--json');
|
|
304
313
|
const argsStr = flagParts.map(quote).join(' ');
|
|
@@ -357,19 +366,91 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
357
366
|
}
|
|
358
367
|
|
|
359
368
|
if (args.apply && args.install) {
|
|
360
|
-
|
|
369
|
+
// FX01–FX02: registry-aware skip + structured skip truth (injectable probe for tests).
|
|
370
|
+
const skipOptions = {
|
|
371
|
+
...(dependencies.registryLatest !== undefined
|
|
372
|
+
? { registryLatest: dependencies.registryLatest }
|
|
373
|
+
: {}),
|
|
374
|
+
...(typeof dependencies.getRegistryLatest === 'function'
|
|
375
|
+
? { getRegistryLatest: dependencies.getRegistryLatest }
|
|
376
|
+
: {}),
|
|
377
|
+
...(dependencies.skipRegistryProbe === true ? { skipRegistryProbe: true } : {}),
|
|
378
|
+
};
|
|
379
|
+
const decisionRaw =
|
|
361
380
|
typeof dependencies.shouldSkipArkgateInstall === 'function'
|
|
362
|
-
? dependencies.shouldSkipArkgateInstall(root, dependencies.cliVersion)
|
|
363
|
-
: {
|
|
364
|
-
|
|
381
|
+
? dependencies.shouldSkipArkgateInstall(root, dependencies.cliVersion, skipOptions)
|
|
382
|
+
: {
|
|
383
|
+
skip: false,
|
|
384
|
+
reasonCode: 'NOT_INSTALLED',
|
|
385
|
+
installedVersion: null,
|
|
386
|
+
registryLatest: null,
|
|
387
|
+
cliVersion: dependencies.cliVersion ?? null,
|
|
388
|
+
reason: 'not-installed',
|
|
389
|
+
};
|
|
390
|
+
// Normalize injectable mocks that only return { skip, installedVersion }.
|
|
391
|
+
const decision = {
|
|
392
|
+
...decisionRaw,
|
|
393
|
+
reasonCode:
|
|
394
|
+
decisionRaw.reasonCode ||
|
|
395
|
+
(decisionRaw.skip
|
|
396
|
+
? 'ALREADY_CURRENT'
|
|
397
|
+
: decisionRaw.installedVersion
|
|
398
|
+
? 'VERSION_DIFFERS'
|
|
399
|
+
: 'NOT_INSTALLED'),
|
|
400
|
+
reason:
|
|
401
|
+
decisionRaw.reason ||
|
|
402
|
+
(decisionRaw.skip ? 'already-current' : 'version-differs'),
|
|
403
|
+
cliVersion: decisionRaw.cliVersion ?? dependencies.cliVersion ?? null,
|
|
404
|
+
registryLatest: decisionRaw.registryLatest ?? null,
|
|
405
|
+
};
|
|
406
|
+
const installArgv =
|
|
407
|
+
typeof dependencies.packageInstallArgv === 'function'
|
|
408
|
+
? dependencies.packageInstallArgv
|
|
409
|
+
: null;
|
|
410
|
+
const targetSpec =
|
|
411
|
+
decision.registryLatest && decision.reasonCode === 'BEHIND_REGISTRY'
|
|
412
|
+
? decision.registryLatest
|
|
413
|
+
: 'latest';
|
|
414
|
+
function fallbackPayload(spec = targetSpec, skipped = decision.skip === true) {
|
|
415
|
+
return {
|
|
416
|
+
schemaVersion: '1.0',
|
|
417
|
+
notAScore: true,
|
|
418
|
+
packageInstallSkipped: skipped,
|
|
419
|
+
reasonCode: decision.reasonCode || 'UNKNOWN',
|
|
420
|
+
reason: decision.reason || null,
|
|
421
|
+
installedVersion: decision.installedVersion,
|
|
422
|
+
cliVersion: decision.cliVersion,
|
|
423
|
+
registryLatest: decision.registryLatest,
|
|
424
|
+
suggestedInstallCmd: `npm install -D arkgate@${spec}`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (decision.skip) {
|
|
428
|
+
// Skip path must not call packageInstallArgv or spawn install (legacy contract).
|
|
429
|
+
// Recovery command uses a portable default; agents can still read reasonCode.
|
|
365
430
|
if (!args.json) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
431
|
+
for (const line of formatPackageInstallDecisionHuman(fallbackPayload())) {
|
|
432
|
+
console.log(line);
|
|
433
|
+
}
|
|
369
434
|
}
|
|
370
435
|
} else {
|
|
371
|
-
const [command, commandArgs] =
|
|
372
|
-
|
|
436
|
+
const [command, commandArgs] = installArgv
|
|
437
|
+
? installArgv(root, targetSpec)
|
|
438
|
+
: ['npm', ['install', '-D', `arkgate@${targetSpec}`]];
|
|
439
|
+
const payload = installArgv
|
|
440
|
+
? {
|
|
441
|
+
...buildPackageInstallSkipPayload(decision, root, installArgv),
|
|
442
|
+
packageInstallSkipped: false,
|
|
443
|
+
suggestedInstallCmd: `${command} ${commandArgs.join(' ')}`,
|
|
444
|
+
}
|
|
445
|
+
: {
|
|
446
|
+
...fallbackPayload(targetSpec, false),
|
|
447
|
+
suggestedInstallCmd: `${command} ${commandArgs.join(' ')}`,
|
|
448
|
+
};
|
|
449
|
+
if (!args.json) {
|
|
450
|
+
for (const line of formatPackageInstallDecisionHuman(payload)) {
|
|
451
|
+
console.log(line);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
373
454
|
const install = spawnSync(command, commandArgs, {
|
|
374
455
|
cwd: root,
|
|
375
456
|
stdio: args.json ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
@@ -378,6 +459,19 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
378
459
|
const exitCode = install.status ?? 1;
|
|
379
460
|
if (exitCode !== 0) {
|
|
380
461
|
if (args.json && install.stderr) console.error(install.stderr.trim());
|
|
462
|
+
if (args.json) {
|
|
463
|
+
console.log(
|
|
464
|
+
JSON.stringify(
|
|
465
|
+
{
|
|
466
|
+
packageInstallFailed: true,
|
|
467
|
+
exitCode,
|
|
468
|
+
...payload,
|
|
469
|
+
},
|
|
470
|
+
null,
|
|
471
|
+
2
|
|
472
|
+
)
|
|
473
|
+
);
|
|
474
|
+
}
|
|
381
475
|
const recovery = `${command} ${commandArgs.join(' ')}`;
|
|
382
476
|
const rePreview = arkCommand(
|
|
383
477
|
root,
|
|
@@ -420,18 +514,24 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
420
514
|
const plan = planManagedUpgrade(root, {
|
|
421
515
|
tools: args.tools,
|
|
422
516
|
acceptConflicts: args.acceptConflicts,
|
|
517
|
+
refreshSkills: args.refreshSkills === true,
|
|
423
518
|
});
|
|
424
519
|
if (!args.apply) {
|
|
425
520
|
const wouldWrite = plan.summary?.wouldWrite ?? 0;
|
|
426
521
|
const blocked = plan.summary?.blocked ?? 0;
|
|
427
522
|
const needsApply = wouldWrite > 0 || blocked > 0;
|
|
428
523
|
const command = buildUpgradeNextCommand(args, plan.planDigest);
|
|
524
|
+
const skillDrift = buildSkillDriftSummary(plan);
|
|
525
|
+
const hostHonesty = buildHostSelectionHonesty(plan);
|
|
429
526
|
if (args.json) {
|
|
430
527
|
// Always expose nextCommand for digest-bound content/manifest apply;
|
|
431
528
|
// nothingToApply flags when content writes are zero so UIs do not urge apply.
|
|
529
|
+
// FX03 skillDrift + FX07 hostSelection + FX08 whatsNew always on preview.
|
|
432
530
|
console.log(
|
|
433
531
|
managedUpgradeJson(plan, {
|
|
434
532
|
nextCommand: command,
|
|
533
|
+
skillDrift,
|
|
534
|
+
hostSelection: hostHonesty,
|
|
435
535
|
...(needsApply ? {} : { nothingToApply: true }),
|
|
436
536
|
// Surface dual-truth when managed assets refresh without a package pin bump.
|
|
437
537
|
...(args.install === false
|
|
@@ -449,6 +549,8 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
449
549
|
? `Update the package and recompute this preview with: ${command}`
|
|
450
550
|
: `Apply the exact preview with: ${command}`
|
|
451
551
|
: undefined,
|
|
552
|
+
skillDrift,
|
|
553
|
+
hostSelection: hostHonesty,
|
|
452
554
|
});
|
|
453
555
|
if (args.install === false) {
|
|
454
556
|
console.log(
|
|
@@ -486,12 +588,23 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
486
588
|
? { mode: 'strict-merge', ...verify(root, args.json, dependencies.arkCheck, dependencies.runArkCheck) }
|
|
487
589
|
: { mode: 'skipped', exitCode: 0 };
|
|
488
590
|
const dualTruth = describePackageVersionDualTruth(root);
|
|
591
|
+
const skillDrift = buildSkillDriftSummary(applied);
|
|
592
|
+
const hostHonesty = buildHostSelectionHonesty(applied);
|
|
593
|
+
// FX05: post-upgrade verification block (advisory, notAScore).
|
|
594
|
+
const postUpgradeChecks = buildPostUpgradeChecks(root, {
|
|
595
|
+
cliVersion: dependencies.cliVersion,
|
|
596
|
+
verification,
|
|
597
|
+
dualTruth,
|
|
598
|
+
});
|
|
489
599
|
if (args.json) {
|
|
490
600
|
console.log(
|
|
491
601
|
JSON.stringify(
|
|
492
602
|
{
|
|
493
603
|
...applied,
|
|
494
604
|
verification,
|
|
605
|
+
skillDrift,
|
|
606
|
+
hostSelection: hostHonesty,
|
|
607
|
+
postUpgradeChecks,
|
|
495
608
|
...(args.install === false || dualTruth.dualTruth
|
|
496
609
|
? {
|
|
497
610
|
packageInstallSkipped: args.install === false,
|
|
@@ -513,7 +626,7 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
513
626
|
)
|
|
514
627
|
);
|
|
515
628
|
} else {
|
|
516
|
-
renderManagedUpgrade(applied);
|
|
629
|
+
renderManagedUpgrade(applied, { skillDrift, hostSelection: hostHonesty });
|
|
517
630
|
if (!args.strict) console.log('Architecture verification skipped (--no-strict).');
|
|
518
631
|
if (args.install === false || dualTruth.dualTruth) {
|
|
519
632
|
console.log(
|
|
@@ -522,6 +635,14 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
522
635
|
: 'Note: --no-install left package.json arkgate pin unchanged. Managed assets match this CLI; bump the pin (or re-run without --no-install) so CI resolves the same version.'
|
|
523
636
|
);
|
|
524
637
|
}
|
|
638
|
+
console.log('Post-upgrade checks (advisory — not a score; never flips the gate):');
|
|
639
|
+
for (const check of postUpgradeChecks.checks ?? []) {
|
|
640
|
+
const mark = check.ok === true ? 'ok' : check.ok === false ? 'attention' : 'note';
|
|
641
|
+
console.log(` [${mark}] ${check.id}: ${check.detail}`);
|
|
642
|
+
}
|
|
643
|
+
if (postUpgradeChecks.mcpNote) {
|
|
644
|
+
console.log(` [note] mcp: ${postUpgradeChecks.mcpNote}`);
|
|
645
|
+
}
|
|
525
646
|
}
|
|
526
647
|
return verification.exitCode;
|
|
527
648
|
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FX01–FX02 — package install decision for `ark upgrade`.
|
|
3
|
+
*
|
|
4
|
+
* Pure-ish helpers: registry version is injectable so unit tests need no network.
|
|
5
|
+
* Production may pass `getRegistryLatest` that runs `npm view arkgate version`.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Compare numeric major.minor.patch cores (prerelease / build ignored).
|
|
13
|
+
* @returns {-1|0|1}
|
|
14
|
+
*/
|
|
15
|
+
export function compareSemverCore(a, b) {
|
|
16
|
+
const parse = (value) => {
|
|
17
|
+
const core = String(value ?? '')
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/^v/i, '')
|
|
20
|
+
.split(/[-+]/)[0];
|
|
21
|
+
const parts = core.split('.').map((part) => {
|
|
22
|
+
const n = Number.parseInt(part, 10);
|
|
23
|
+
return Number.isFinite(n) ? n : 0;
|
|
24
|
+
});
|
|
25
|
+
return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
|
|
26
|
+
};
|
|
27
|
+
const left = parse(a);
|
|
28
|
+
const right = parse(b);
|
|
29
|
+
for (let i = 0; i < 3; i += 1) {
|
|
30
|
+
if (left[i] < right[i]) return -1;
|
|
31
|
+
if (left[i] > right[i]) return 1;
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Best-effort registry latest (npm view). Returns null on failure.
|
|
38
|
+
* @param {{ timeoutMs?: number, run?: Function }} [opts]
|
|
39
|
+
*/
|
|
40
|
+
export function probeRegistryArkgateLatest(opts = {}) {
|
|
41
|
+
const timeout = Number(opts.timeoutMs) > 0 ? Number(opts.timeoutMs) : 8000;
|
|
42
|
+
const run =
|
|
43
|
+
typeof opts.run === 'function'
|
|
44
|
+
? opts.run
|
|
45
|
+
: () =>
|
|
46
|
+
spawnSync('npm', ['view', 'arkgate', 'version'], {
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
timeout,
|
|
49
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
const result = run();
|
|
53
|
+
if (!result || result.status !== 0) return null;
|
|
54
|
+
const v = String(result.stdout || '')
|
|
55
|
+
.trim()
|
|
56
|
+
.split(/\s+/)[0];
|
|
57
|
+
return v || null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether install of arkgate can be skipped.
|
|
65
|
+
*
|
|
66
|
+
* FX01: when CLI == installed, still install if registryLatest > installed
|
|
67
|
+
* (unless registry probe skipped/unavailable — then fail open to install only if
|
|
68
|
+
* explicitly behind CLI; if equal and no registry, stay skip with honesty).
|
|
69
|
+
*
|
|
70
|
+
* @param {string} root
|
|
71
|
+
* @param {string} [cliVersion]
|
|
72
|
+
* @param {{
|
|
73
|
+
* registryLatest?: string|null,
|
|
74
|
+
* getRegistryLatest?: () => string|null|undefined,
|
|
75
|
+
* skipRegistryProbe?: boolean,
|
|
76
|
+
* }} [options]
|
|
77
|
+
* @returns {{
|
|
78
|
+
* skip: boolean,
|
|
79
|
+
* installedVersion: string|null,
|
|
80
|
+
* reason: string,
|
|
81
|
+
* reasonCode: string,
|
|
82
|
+
* registryLatest: string|null,
|
|
83
|
+
* cliVersion: string|null,
|
|
84
|
+
* }}
|
|
85
|
+
*/
|
|
86
|
+
export function shouldSkipArkgateInstall(root, cliVersion, options = {}) {
|
|
87
|
+
const cli = typeof cliVersion === 'string' && cliVersion.trim() ? cliVersion.trim() : null;
|
|
88
|
+
const pkgPath = path.join(root, 'node_modules', 'arkgate', 'package.json');
|
|
89
|
+
if (!fs.existsSync(pkgPath)) {
|
|
90
|
+
return {
|
|
91
|
+
skip: false,
|
|
92
|
+
installedVersion: null,
|
|
93
|
+
reason: 'not-installed',
|
|
94
|
+
reasonCode: 'NOT_INSTALLED',
|
|
95
|
+
registryLatest: null,
|
|
96
|
+
cliVersion: cli,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
let installedVersion = null;
|
|
100
|
+
try {
|
|
101
|
+
installedVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? null;
|
|
102
|
+
} catch {
|
|
103
|
+
return {
|
|
104
|
+
skip: false,
|
|
105
|
+
installedVersion: null,
|
|
106
|
+
reason: 'unreadable',
|
|
107
|
+
reasonCode: 'UNREADABLE',
|
|
108
|
+
registryLatest: null,
|
|
109
|
+
cliVersion: cli,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let registryLatest =
|
|
114
|
+
options.registryLatest !== undefined ? options.registryLatest : undefined;
|
|
115
|
+
if (registryLatest === undefined && options.skipRegistryProbe !== true) {
|
|
116
|
+
if (typeof options.getRegistryLatest === 'function') {
|
|
117
|
+
try {
|
|
118
|
+
registryLatest = options.getRegistryLatest() ?? null;
|
|
119
|
+
} catch {
|
|
120
|
+
registryLatest = null;
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
registryLatest = probeRegistryArkgateLatest();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (registryLatest === undefined) registryLatest = null;
|
|
127
|
+
|
|
128
|
+
// Behind CLI version → always install
|
|
129
|
+
if (cli && installedVersion && installedVersion !== cli) {
|
|
130
|
+
return {
|
|
131
|
+
skip: false,
|
|
132
|
+
installedVersion,
|
|
133
|
+
reason: 'version-differs',
|
|
134
|
+
reasonCode: 'VERSION_DIFFERS',
|
|
135
|
+
registryLatest,
|
|
136
|
+
cliVersion: cli,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Same as CLI (or no CLI): check registry
|
|
141
|
+
if (installedVersion && registryLatest && compareSemverCore(installedVersion, registryLatest) < 0) {
|
|
142
|
+
return {
|
|
143
|
+
skip: false,
|
|
144
|
+
installedVersion,
|
|
145
|
+
reason: 'behind-registry',
|
|
146
|
+
reasonCode: 'BEHIND_REGISTRY',
|
|
147
|
+
registryLatest,
|
|
148
|
+
cliVersion: cli,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (cli && installedVersion && installedVersion === cli) {
|
|
153
|
+
if (registryLatest == null && options.skipRegistryProbe !== true) {
|
|
154
|
+
// Probe failed: do not false-skip forever — still report honesty; skip only when
|
|
155
|
+
// we cannot know registry (offline). Field preferred install when unsure is worse
|
|
156
|
+
// for offline CI; document REGISTRY_UNAVAILABLE and skip with reason.
|
|
157
|
+
return {
|
|
158
|
+
skip: true,
|
|
159
|
+
installedVersion,
|
|
160
|
+
reason: 'already-current-registry-unknown',
|
|
161
|
+
reasonCode: 'REGISTRY_UNAVAILABLE',
|
|
162
|
+
registryLatest: null,
|
|
163
|
+
cliVersion: cli,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
skip: true,
|
|
168
|
+
installedVersion,
|
|
169
|
+
reason: 'already-current',
|
|
170
|
+
reasonCode: 'ALREADY_CURRENT',
|
|
171
|
+
registryLatest: registryLatest ?? installedVersion,
|
|
172
|
+
cliVersion: cli,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
skip: false,
|
|
178
|
+
installedVersion,
|
|
179
|
+
reason: 'version-differs',
|
|
180
|
+
reasonCode: 'VERSION_DIFFERS',
|
|
181
|
+
registryLatest,
|
|
182
|
+
cliVersion: cli,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Machine-readable install decision + human recovery (FX02).
|
|
188
|
+
* @param {ReturnType<typeof shouldSkipArkgateInstall>} decision
|
|
189
|
+
* @param {string} root
|
|
190
|
+
* @param {(root: string, spec?: string) => [string, string[]]} packageInstallArgv
|
|
191
|
+
*/
|
|
192
|
+
export function buildPackageInstallSkipPayload(decision, root, packageInstallArgv) {
|
|
193
|
+
const targetSpec =
|
|
194
|
+
decision.registryLatest && compareSemverCore(decision.installedVersion || '0.0.0', decision.registryLatest) < 0
|
|
195
|
+
? decision.registryLatest
|
|
196
|
+
: 'latest';
|
|
197
|
+
const [command, commandArgs] = packageInstallArgv(root, targetSpec);
|
|
198
|
+
const suggestedInstallCmd = `${command} ${commandArgs.join(' ')}`.trim();
|
|
199
|
+
return {
|
|
200
|
+
schemaVersion: '1.0',
|
|
201
|
+
notAScore: true,
|
|
202
|
+
packageInstallSkipped: decision.skip === true,
|
|
203
|
+
reasonCode: decision.reasonCode || 'UNKNOWN',
|
|
204
|
+
reason: decision.reason || null,
|
|
205
|
+
installedVersion: decision.installedVersion,
|
|
206
|
+
cliVersion: decision.cliVersion,
|
|
207
|
+
registryLatest: decision.registryLatest,
|
|
208
|
+
suggestedInstallCmd,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Human lines for skip / behind-registry install decision.
|
|
214
|
+
* @param {ReturnType<typeof buildPackageInstallSkipPayload>} payload
|
|
215
|
+
*/
|
|
216
|
+
export function formatPackageInstallDecisionHuman(payload) {
|
|
217
|
+
if (!payload) return [];
|
|
218
|
+
if (payload.packageInstallSkipped && payload.reasonCode === 'ALREADY_CURRENT') {
|
|
219
|
+
return [
|
|
220
|
+
`Package already at arkgate@${payload.installedVersion}` +
|
|
221
|
+
(payload.registryLatest ? ` (registry ${payload.registryLatest})` : '') +
|
|
222
|
+
'; skipping install and recomputing managed preview.',
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
if (payload.packageInstallSkipped && payload.reasonCode === 'REGISTRY_UNAVAILABLE') {
|
|
226
|
+
return [
|
|
227
|
+
`Package at arkgate@${payload.installedVersion} matches this CLI; registry latest unknown (offline or npm view failed). Skipping install.`,
|
|
228
|
+
`If you know a newer release exists, run: ${payload.suggestedInstallCmd}`,
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
if (!payload.packageInstallSkipped && payload.reasonCode === 'BEHIND_REGISTRY') {
|
|
232
|
+
return [
|
|
233
|
+
`Installed arkgate@${payload.installedVersion} is behind registry ${payload.registryLatest}; installing update.`,
|
|
234
|
+
` ${payload.suggestedInstallCmd}`,
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
if (!payload.packageInstallSkipped) {
|
|
238
|
+
return [`Updating ArkGate (${payload.reasonCode}): ${payload.suggestedInstallCmd}`];
|
|
239
|
+
}
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
@@ -74,6 +74,31 @@ export function buildUpgradeWhatsNewSuggestions() {
|
|
|
74
74
|
why:
|
|
75
75
|
'After managed upgrade, learn write-path hard|advisory|unavailable per host and whether customized content was preserved — without a maintainer ticket. Soft hosts never claim hard.',
|
|
76
76
|
},
|
|
77
|
+
{
|
|
78
|
+
id: 'registry-aware-upgrade',
|
|
79
|
+
title: 'Registry-aware package upgrade (FX field truth)',
|
|
80
|
+
try: 'npx arkgate upgrade --apply',
|
|
81
|
+
inspect:
|
|
82
|
+
'packageInstallSkipped / reasonCode / registryLatest / suggestedInstallCmd (JSON)',
|
|
83
|
+
why:
|
|
84
|
+
'When CLI version equals node_modules but npm registry is ahead, upgrade no longer false-skips. Offline/registry-unknown stays honest with a copy-paste install command.',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'skill-drift-refresh',
|
|
88
|
+
title: 'Skill content drift + opt-in refresh',
|
|
89
|
+
try: 'npx arkgate upgrade --json',
|
|
90
|
+
inspect: 'skillDrift (+ --refresh-skills for customized skill rewrite consent)',
|
|
91
|
+
why:
|
|
92
|
+
'See stale/customized/missing skill counts. Customized skills stay preserved unless you pass --refresh-skills; never silent overwrite of true edits.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'mcp-multi-project',
|
|
96
|
+
title: 'Multi-project MCP process honesty',
|
|
97
|
+
try: 'ark_identity with project.expectedRoot; read processPackage on every tool',
|
|
98
|
+
inspect: 'processPackage.processPackageMismatch / processStale + nextAction',
|
|
99
|
+
why:
|
|
100
|
+
'One user, many checkouts: after package bump, restart MCP so process arkgateVersion matches install. Prefer project-local CLI until identity matched and versions align.',
|
|
101
|
+
},
|
|
77
102
|
],
|
|
78
103
|
};
|
|
79
104
|
}
|