arkgate 4.5.5 → 4.5.7
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 +68 -2
- package/README.md +8 -9
- package/bin/ark-check-runtime.mjs +6 -4
- package/bin/ark-mcp-runtime.mjs +91 -21
- package/bin/ark-shared.mjs +8 -29
- package/bin/ark.mjs +9 -5
- package/bin/lib/agent-gates.mjs +2 -0
- package/bin/lib/ci-and-commands.mjs +2 -1
- package/bin/lib/gate-files.mjs +1 -1
- package/bin/lib/hook-templates.mjs +13 -11
- package/bin/lib/host-support-matrix.mjs +31 -11
- package/bin/lib/install-migrate.mjs +24 -0
- package/bin/lib/managed-upgrade.mjs +204 -2
- package/bin/lib/mcp-adoption.mjs +6 -1
- package/bin/lib/mcp-process-package.mjs +95 -0
- package/bin/lib/rules-inventory.mjs +23 -0
- package/bin/lib/start-preview.mjs +5 -1
- 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/bin/lib/write-path-capabilities.mjs +62 -1
- 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 +29 -6
- package/docs/ai-gates.md +24 -14
- package/docs/enthusiast/how-to-agent-gates.md +4 -3
- 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
|
@@ -70,9 +70,27 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
|
|
|
70
70
|
true,
|
|
71
71
|
true
|
|
72
72
|
),
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
// Cursor: official preToolUse deny / exit 2 is a hard block for matched tools when
|
|
74
|
+
// `.cursor/hooks.json` is installed + trusted. Claim hard only for Write|StrReplace;
|
|
75
|
+
// Shell/Tab/human edits still rely on CI. Repair envelope may emit; Write updated_input
|
|
76
|
+
// reinjection is not guaranteed on Cursor (agent_message + retry is the supported path).
|
|
77
|
+
cursor: hostProfile(
|
|
78
|
+
'Cursor',
|
|
79
|
+
'.cursor/hooks.json',
|
|
80
|
+
'preToolUse `Write` / `StrReplace`',
|
|
81
|
+
['Write', 'StrReplace'],
|
|
82
|
+
true,
|
|
83
|
+
false,
|
|
84
|
+
{
|
|
85
|
+
repairEnvelopeEmitted: true,
|
|
86
|
+
operationCoverage: {
|
|
87
|
+
Write: true,
|
|
88
|
+
StrReplace: true,
|
|
89
|
+
shell: false,
|
|
90
|
+
'pre-commit': false,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
),
|
|
76
94
|
codex: hostProfile(
|
|
77
95
|
'OpenAI Codex',
|
|
78
96
|
'.codex/hooks.json',
|
|
@@ -134,14 +152,15 @@ export function renderHostSupportMatrixMarkdown() {
|
|
|
134
152
|
const rows = HOST_SUPPORT_HOSTS.map((host) => {
|
|
135
153
|
const profile = HOST_SUPPORT_MATRIX[host];
|
|
136
154
|
const capabilities = profile.capabilities;
|
|
137
|
-
// Fail-closed honesty:
|
|
138
|
-
//
|
|
155
|
+
// Fail-closed honesty: Codex/OpenCode never claim hard write; CI is required-status.
|
|
156
|
+
// Cursor claims hard only for listed preToolUse ops when hooks are installed + trusted.
|
|
157
|
+
// hookSurface already includes "PreToolUse/preToolUse …" — do not prefix again.
|
|
139
158
|
let local;
|
|
140
159
|
if (capabilities['hard-write']) {
|
|
141
160
|
local = `**Hard** block for listed ops (${profile.hookSurface}) when installed + trusted`;
|
|
142
161
|
} else if (host === 'codex') {
|
|
143
162
|
local =
|
|
144
|
-
'**Advisory / best-effort** at write (not equivalent to Claude/Grok hard block)';
|
|
163
|
+
'**Advisory / best-effort** at write (not equivalent to Claude/Grok/Cursor hard block)';
|
|
145
164
|
} else if (host === 'opencode') {
|
|
146
165
|
local =
|
|
147
166
|
'**Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary)';
|
|
@@ -169,7 +188,8 @@ ${rows}
|
|
|
169
188
|
|
|
170
189
|
**Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
|
|
171
190
|
GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
|
|
172
|
-
|
|
191
|
+
Codex/OpenCode never get a fake hard write claim. Cursor hard write covers only listed
|
|
192
|
+
\`preToolUse\` ops when \`.cursor/hooks.json\` is installed and trusted — Shell/Tab/human edits still rely on CI.
|
|
173
193
|
|
|
174
194
|
This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run \`arkgate-check --doctor\` (or \`ark-check --doctor\`) for the evidence actually detected in the current repository.`;
|
|
175
195
|
}
|
|
@@ -203,14 +223,14 @@ export function doctorWritePathHonestyMessage(activeHost, hardWriteActive) {
|
|
|
203
223
|
// EH07: distinguish CLI command (arkgate-check / ark-check) from the GitHub required status context name.
|
|
204
224
|
const mergeBoundary =
|
|
205
225
|
'Required CI hard merge boundary = a required GitHub status context that runs arkgate-check --strict-merge (alias ark-check --strict-merge)';
|
|
206
|
-
if (host === 'cursor') {
|
|
207
|
-
return `Cursor:
|
|
226
|
+
if (host === 'cursor' && !hardWriteActive) {
|
|
227
|
+
return `Cursor: hard preToolUse is supported for Write/StrReplace when .cursor/hooks.json is installed + trusted; without runtime-observed hook evidence, hard is unverified. ${mergeBoundary}.`;
|
|
208
228
|
}
|
|
209
229
|
if (host === 'codex') {
|
|
210
|
-
return `Codex: write path is advisory / best-effort at write (not Claude/Grok hard). ${mergeBoundary}.`;
|
|
230
|
+
return `Codex: write path is advisory / best-effort at write (not Claude/Grok/Cursor hard). ${mergeBoundary}.`;
|
|
211
231
|
}
|
|
212
232
|
if (host === 'opencode') {
|
|
213
|
-
return `OpenCode: write path is advisory / best-effort (MCP + optional plugin; not Claude/Grok/Antigravity hard). ${mergeBoundary}.`;
|
|
233
|
+
return `OpenCode: write path is advisory / best-effort (MCP + optional plugin; not Claude/Grok/Antigravity/Cursor hard). ${mergeBoundary}.`;
|
|
214
234
|
}
|
|
215
235
|
if ((host === 'claude' || host === 'grok' || host === 'antigravity') && !hardWriteActive) {
|
|
216
236
|
const label =
|
|
@@ -25,9 +25,11 @@ import {
|
|
|
25
25
|
claudeSettings,
|
|
26
26
|
codexHooks,
|
|
27
27
|
codexProjectConfig,
|
|
28
|
+
cursorHooks,
|
|
28
29
|
grokHooks,
|
|
29
30
|
grokProjectConfig,
|
|
30
31
|
mergeAntigravityArkHook,
|
|
32
|
+
mergeCursorArkHook,
|
|
31
33
|
mergeOpencodeArkMcp,
|
|
32
34
|
opencodeProjectConfig,
|
|
33
35
|
} from './hook-templates.mjs';
|
|
@@ -194,6 +196,9 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
|
|
|
194
196
|
);
|
|
195
197
|
if (selectedTools.has('cursor')) {
|
|
196
198
|
add('.cursor/mcp.json', mcpJson(root));
|
|
199
|
+
// whole-file for managed upgrade/manifest (json-merge is install-time only via
|
|
200
|
+
// mergeCursorArkHook below — same pattern as Antigravity hooks).
|
|
201
|
+
add('.cursor/hooks.json', cursorHooks(root));
|
|
197
202
|
if (!compact) add('.cursor/rules/ark.mdc', cursorRule(root));
|
|
198
203
|
}
|
|
199
204
|
if (selectedTools.has('claude')) add('.claude/settings.json', claudeSettings(root));
|
|
@@ -516,6 +521,25 @@ export function runInstallAgentGates(args) {
|
|
|
516
521
|
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
517
522
|
return writeTemplate(root, relativePath, merged, true);
|
|
518
523
|
}
|
|
524
|
+
if (relativePath === '.cursor/hooks.json') {
|
|
525
|
+
const fullPath = path.join(root, relativePath);
|
|
526
|
+
let existing = '';
|
|
527
|
+
try {
|
|
528
|
+
existing = fs.readFileSync(fullPath, 'utf8');
|
|
529
|
+
} catch {
|
|
530
|
+
// Missing hooks file → write generated Cursor preToolUse gate.
|
|
531
|
+
}
|
|
532
|
+
if (!existing) {
|
|
533
|
+
return writeTemplate(root, relativePath, content, true);
|
|
534
|
+
}
|
|
535
|
+
const merged = mergeCursorArkHook(existing, content);
|
|
536
|
+
if (merged == null) {
|
|
537
|
+
return { relativePath, status: 'skipped-non-ark' };
|
|
538
|
+
}
|
|
539
|
+
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
540
|
+
// Upsert Ark preToolUse without requiring --force; never wipe sibling hooks.
|
|
541
|
+
return writeTemplate(root, relativePath, merged, true);
|
|
542
|
+
}
|
|
519
543
|
if (relativePath === '.agents/hooks.json') {
|
|
520
544
|
const fullPath = path.join(root, relativePath);
|
|
521
545
|
let existing = '';
|
|
@@ -126,7 +126,12 @@ function hasArkText(root, relativePath) {
|
|
|
126
126
|
|
|
127
127
|
const HOST_SIGNALS = {
|
|
128
128
|
claude: ['.claude/settings.json', '.claude/skills/ark-upgrade/SKILL.md'],
|
|
129
|
-
cursor: [
|
|
129
|
+
cursor: [
|
|
130
|
+
'.cursor/mcp.json',
|
|
131
|
+
'.cursor/hooks.json',
|
|
132
|
+
'.cursor/rules/ark.mdc',
|
|
133
|
+
'.cursor/commands/ark-upgrade.md',
|
|
134
|
+
],
|
|
130
135
|
codex: ['.codex/hooks.json', '.codex/config.toml', '.agents/skills/ark-upgrade/SKILL.md'],
|
|
131
136
|
grok: ['.grok/config.toml', '.grok/hooks/ark-write-gate.json', '.grok/skills/ark-upgrade/SKILL.md'],
|
|
132
137
|
antigravity: ['.agents/hooks.json', '.agents/skills/ark-upgrade/SKILL.md'],
|
|
@@ -465,11 +470,19 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
465
470
|
kind: catalogAsset.kind,
|
|
466
471
|
});
|
|
467
472
|
const accepted = options.acceptConflicts === true;
|
|
473
|
+
// FX04: --refresh-skills opt-in rewrites customized *skill* assets to package
|
|
474
|
+
// templates. Conflicted still needs --accept-conflicts. Never silent default.
|
|
475
|
+
const refreshSkills = options.refreshSkills === true;
|
|
476
|
+
const skillRefresh =
|
|
477
|
+
refreshSkills &&
|
|
478
|
+
catalogAsset.kind === 'skill' &&
|
|
479
|
+
classified.state === 'customized';
|
|
468
480
|
const canApply =
|
|
469
481
|
classified.state === 'stale' ||
|
|
482
|
+
skillRefresh ||
|
|
470
483
|
(classified.state === 'missing' && (!recorded || accepted)) ||
|
|
471
484
|
(classified.state === 'conflicted' && accepted);
|
|
472
|
-
const blocked = classified.requiresConsent && !accepted;
|
|
485
|
+
const blocked = classified.requiresConsent && !accepted && !skillRefresh;
|
|
473
486
|
const desiredFile = afterFileContent(catalogAsset, currentFile, desiredScoped);
|
|
474
487
|
const asset = {
|
|
475
488
|
path: catalogAsset.relativePath,
|
|
@@ -542,6 +555,7 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
542
555
|
profile: selection.profile,
|
|
543
556
|
hosts: selection.hosts,
|
|
544
557
|
acceptConflicts: options.acceptConflicts === true,
|
|
558
|
+
refreshSkills: options.refreshSkills === true,
|
|
545
559
|
assets,
|
|
546
560
|
summary,
|
|
547
561
|
nextManifest,
|
|
@@ -552,6 +566,183 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
552
566
|
return plan;
|
|
553
567
|
}
|
|
554
568
|
|
|
569
|
+
/**
|
|
570
|
+
* FX03 — skill content drift honesty (counts by state + sample paths).
|
|
571
|
+
* Skills only; never claims "skills upgraded" when only package pin moved.
|
|
572
|
+
*/
|
|
573
|
+
export function buildSkillDriftSummary(plan) {
|
|
574
|
+
const assets = Array.isArray(plan?.assets) ? plan.assets : [];
|
|
575
|
+
const skills = assets.filter((a) => a?.kind === 'skill');
|
|
576
|
+
const byState = {};
|
|
577
|
+
for (const skill of skills) {
|
|
578
|
+
const state = typeof skill.state === 'string' ? skill.state : 'unknown';
|
|
579
|
+
byState[state] = (byState[state] ?? 0) + 1;
|
|
580
|
+
}
|
|
581
|
+
const sample = (state, limit = 5) =>
|
|
582
|
+
skills
|
|
583
|
+
.filter((s) => s.state === state)
|
|
584
|
+
.map((s) => s.path)
|
|
585
|
+
.sort()
|
|
586
|
+
.slice(0, limit);
|
|
587
|
+
const customized = byState.customized ?? 0;
|
|
588
|
+
const stale = byState.stale ?? 0;
|
|
589
|
+
const missing = byState.missing ?? 0;
|
|
590
|
+
const current = byState.current ?? 0;
|
|
591
|
+
const wouldRefresh = skills.filter((s) => s.willApply === true).length;
|
|
592
|
+
return {
|
|
593
|
+
schemaVersion: '1.0',
|
|
594
|
+
notAScore: true,
|
|
595
|
+
skillCount: skills.length,
|
|
596
|
+
byState,
|
|
597
|
+
stale,
|
|
598
|
+
customized,
|
|
599
|
+
missing,
|
|
600
|
+
current,
|
|
601
|
+
wouldRefresh,
|
|
602
|
+
samplePaths: {
|
|
603
|
+
stale: sample('stale'),
|
|
604
|
+
customized: sample('customized'),
|
|
605
|
+
missing: sample('missing'),
|
|
606
|
+
},
|
|
607
|
+
note:
|
|
608
|
+
customized > 0 && wouldRefresh === 0
|
|
609
|
+
? 'Skills on disk differ from package templates (customized preserved). Use --refresh-skills to opt in to rewrite customized skills; never silent overwrite.'
|
|
610
|
+
: stale > 0
|
|
611
|
+
? 'Some skills are stale vs package templates and will refresh on apply.'
|
|
612
|
+
: skills.length === 0
|
|
613
|
+
? 'No managed skill assets in this upgrade selection.'
|
|
614
|
+
: 'Skill content matches package templates or is scheduled for write.',
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* FX07 — active host vs managed --tools / manifest hosts.
|
|
620
|
+
*/
|
|
621
|
+
export function buildHostSelectionHonesty(plan) {
|
|
622
|
+
const hosts = Array.isArray(plan?.hosts) ? plan.hosts.map((h) => String(h).toLowerCase()) : [];
|
|
623
|
+
let active = null;
|
|
624
|
+
try {
|
|
625
|
+
active = detectActiveAgentHost();
|
|
626
|
+
} catch {
|
|
627
|
+
active = null;
|
|
628
|
+
}
|
|
629
|
+
const activeNorm =
|
|
630
|
+
typeof active === 'string' && active.trim() ? active.trim().toLowerCase() : null;
|
|
631
|
+
const known = activeNorm && KNOWN_TOOLS.includes(activeNorm);
|
|
632
|
+
const inSelection = Boolean(activeNorm && hosts.includes(activeNorm));
|
|
633
|
+
const note =
|
|
634
|
+
known && !inSelection
|
|
635
|
+
? `Detected host "${activeNorm}" is not in managed tools [${hosts.join(', ') || 'none'}]. Re-run with --tools ${[...new Set([...hosts, activeNorm])].sort().join(',')} so that host's skills/hooks are in the plan.`
|
|
636
|
+
: known && inSelection
|
|
637
|
+
? `Detected host "${activeNorm}" is in the managed selection.`
|
|
638
|
+
: activeNorm
|
|
639
|
+
? `Detected host "${activeNorm}" is outside the known managed tool set.`
|
|
640
|
+
: 'No active agent host detected for this process.';
|
|
641
|
+
return {
|
|
642
|
+
schemaVersion: '1.0',
|
|
643
|
+
notAScore: true,
|
|
644
|
+
activeHost: activeNorm,
|
|
645
|
+
managedHosts: hosts,
|
|
646
|
+
activeInSelection: inSelection,
|
|
647
|
+
suggestTools:
|
|
648
|
+
known && !inSelection
|
|
649
|
+
? [...new Set([...hosts, activeNorm])].sort().join(',')
|
|
650
|
+
: null,
|
|
651
|
+
note,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* FX05 — post-upgrade verification block (advisory sensors only).
|
|
657
|
+
*/
|
|
658
|
+
export function buildPostUpgradeChecks(root, options = {}) {
|
|
659
|
+
const resolvedRoot = path.resolve(root);
|
|
660
|
+
const checks = [];
|
|
661
|
+
let projectVersion = null;
|
|
662
|
+
try {
|
|
663
|
+
const pkgPath = path.join(resolvedRoot, 'node_modules', 'arkgate', 'package.json');
|
|
664
|
+
if (fs.existsSync(pkgPath)) {
|
|
665
|
+
projectVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? null;
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
projectVersion = null;
|
|
669
|
+
}
|
|
670
|
+
const cli = typeof options.cliVersion === 'string' ? options.cliVersion : arkPackageVersion();
|
|
671
|
+
const pinOk =
|
|
672
|
+
projectVersion != null && cli != null ? projectVersion === cli : null;
|
|
673
|
+
checks.push({
|
|
674
|
+
id: 'package-pin-cli',
|
|
675
|
+
ok: pinOk,
|
|
676
|
+
detail:
|
|
677
|
+
pinOk === true
|
|
678
|
+
? `Installed arkgate@${projectVersion} matches CLI ${cli}.`
|
|
679
|
+
: pinOk === false
|
|
680
|
+
? `Installed arkgate@${projectVersion} ≠ CLI ${cli}; re-run install or restart using project-local CLI.`
|
|
681
|
+
: `Could not compare pin (installed=${projectVersion ?? 'missing'}, cli=${cli ?? 'unknown'}).`,
|
|
682
|
+
});
|
|
683
|
+
checks.push({
|
|
684
|
+
id: 'architecture-verification',
|
|
685
|
+
ok:
|
|
686
|
+
options.verification?.mode === 'skipped'
|
|
687
|
+
? null
|
|
688
|
+
: options.verification?.exitCode === 0,
|
|
689
|
+
detail:
|
|
690
|
+
options.verification?.mode === 'skipped'
|
|
691
|
+
? 'Strict architecture verification was skipped (--no-strict).'
|
|
692
|
+
: options.verification?.exitCode === 0
|
|
693
|
+
? 'Strict-merge architecture verification passed.'
|
|
694
|
+
: `Architecture verification exit ${options.verification?.exitCode ?? 'unknown'}.`,
|
|
695
|
+
});
|
|
696
|
+
checks.push({
|
|
697
|
+
id: 'package-version-truth',
|
|
698
|
+
ok: options.dualTruth?.dualTruth === true ? false : options.dualTruth ? true : null,
|
|
699
|
+
detail:
|
|
700
|
+
options.dualTruth?.dualTruth === true
|
|
701
|
+
? options.dualTruth.note || 'Package pin dual-truth detected.'
|
|
702
|
+
: options.dualTruth
|
|
703
|
+
? 'Package pin truth is consistent for this apply.'
|
|
704
|
+
: 'Package version truth not evaluated.',
|
|
705
|
+
});
|
|
706
|
+
checks.push({
|
|
707
|
+
id: 'doctor-compass-coach',
|
|
708
|
+
ok: null,
|
|
709
|
+
detail:
|
|
710
|
+
'Run `npx arkgate-check --doctor --json` and confirm doctor.improvementCompass + doctor.deepModuleCoach (notAScore).',
|
|
711
|
+
});
|
|
712
|
+
checks.push({
|
|
713
|
+
id: 'agents-md-projection',
|
|
714
|
+
ok: null,
|
|
715
|
+
detail: 'Run `npx arkgate agents-md --check` (or --write) so AGENTS.md matches the package projection.',
|
|
716
|
+
});
|
|
717
|
+
checks.push({
|
|
718
|
+
id: 'status-mode',
|
|
719
|
+
ok: null,
|
|
720
|
+
detail: 'Run `npx arkgate status --json` and read honesty mode; incomplete facts never invent green residual.',
|
|
721
|
+
});
|
|
722
|
+
return {
|
|
723
|
+
schemaVersion: '1.0',
|
|
724
|
+
notAScore: true,
|
|
725
|
+
neverGateInput: true,
|
|
726
|
+
checks,
|
|
727
|
+
mcpNote:
|
|
728
|
+
'If you used Ark MCP this session: restart/retarget MCP after package bump so process arkgateVersion matches project install; always pass project.expectedRoot + expectedProjectId (WI01). Prefer project-local CLI until identity matched and versions align.',
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function formatSkillDriftHuman(skillDrift) {
|
|
733
|
+
if (!skillDrift) return [];
|
|
734
|
+
const lines = [
|
|
735
|
+
`Skill drift: ${skillDrift.skillCount} skill(s) — current ${skillDrift.current}, stale ${skillDrift.stale}, customized ${skillDrift.customized}, missing ${skillDrift.missing}, would refresh ${skillDrift.wouldRefresh}.`,
|
|
736
|
+
];
|
|
737
|
+
if (skillDrift.note) lines.push(` ${skillDrift.note}`);
|
|
738
|
+
return lines;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export function formatHostSelectionHuman(hostSelection) {
|
|
742
|
+
if (!hostSelection?.note) return [];
|
|
743
|
+
return [`Host selection: ${hostSelection.note}`];
|
|
744
|
+
}
|
|
745
|
+
|
|
555
746
|
function publicPlan(plan, overrides = {}) {
|
|
556
747
|
const assets = plan.assets.map(
|
|
557
748
|
({ containerBeforeHash: _container, [AFTER_CONTENT]: _content, ...asset }) => asset
|
|
@@ -784,6 +975,17 @@ export function renderManagedUpgrade(plan, options = {}) {
|
|
|
784
975
|
for (const line of formatManagedUpgradeSelfServiceHonesty(honesty)) {
|
|
785
976
|
console.log(line);
|
|
786
977
|
}
|
|
978
|
+
const skillDrift =
|
|
979
|
+
options.skillDrift ?? plan.skillDrift ?? buildSkillDriftSummary(plan);
|
|
980
|
+
for (const line of formatSkillDriftHuman(skillDrift)) {
|
|
981
|
+
console.log(line);
|
|
982
|
+
}
|
|
983
|
+
const hostSelection =
|
|
984
|
+
options.hostSelection ?? plan.hostSelection ?? buildHostSelectionHonesty(plan);
|
|
985
|
+
for (const line of formatHostSelectionHuman(hostSelection)) {
|
|
986
|
+
console.log(line);
|
|
987
|
+
}
|
|
988
|
+
// FX08: whatsNew always on preview/apply human path (including nothing-to-apply).
|
|
787
989
|
const whatsNew = plan.whatsNew ?? buildUpgradeWhatsNewSuggestions();
|
|
788
990
|
for (const line of formatUpgradeWhatsNewSuggestions(whatsNew)) {
|
|
789
991
|
console.log(line);
|
package/bin/lib/mcp-adoption.mjs
CHANGED
|
@@ -27,7 +27,12 @@ export const COMMAND_GATE_TEXT_FILES = [
|
|
|
27
27
|
'.grok/hooks/ark-write-gate.json', '.grok/config.toml', '.codex/config.toml',
|
|
28
28
|
'.agents/hooks.json',
|
|
29
29
|
];
|
|
30
|
-
export const COMMAND_GATE_JSON_FILES = [
|
|
30
|
+
export const COMMAND_GATE_JSON_FILES = [
|
|
31
|
+
'.mcp.json',
|
|
32
|
+
'.cursor/mcp.json',
|
|
33
|
+
'.cursor/hooks.json',
|
|
34
|
+
'opencode.json',
|
|
35
|
+
];
|
|
31
36
|
// Primary CLI names (product) + one-major aliases. migrate-commands must strip ALL of these
|
|
32
37
|
// before re-emitting a single preferred bin — otherwise a partial rename leaves
|
|
33
38
|
// args: ["ark-mcp", "arkgate-mcp", ...] which breaks stdio MCP hosts.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FX06 — MCP process package vs project install honesty.
|
|
3
|
+
*
|
|
4
|
+
* Pure-ish helpers so unit tests need no MCP server. Production MCP runtime
|
|
5
|
+
* calls buildProcessPackageHonesty with the process version + project root.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve installed arkgate version for a project root (shallow node_modules,
|
|
13
|
+
* then Node module resolution for hoisted monorepos).
|
|
14
|
+
* @param {string} root
|
|
15
|
+
* @returns {string|null}
|
|
16
|
+
*/
|
|
17
|
+
export function readProjectInstalledArkgateVersion(root) {
|
|
18
|
+
const resolvedRoot = path.resolve(root);
|
|
19
|
+
try {
|
|
20
|
+
const shallow = path.join(resolvedRoot, 'node_modules', 'arkgate', 'package.json');
|
|
21
|
+
if (fs.existsSync(shallow)) {
|
|
22
|
+
const v = JSON.parse(fs.readFileSync(shallow, 'utf8')).version;
|
|
23
|
+
return typeof v === 'string' && v.trim() ? v.trim() : null;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
/* fall through */
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const requireFromProject = createRequire(path.join(resolvedRoot, 'package.json'));
|
|
30
|
+
const pkgJson = requireFromProject.resolve('arkgate/package.json');
|
|
31
|
+
const v = JSON.parse(fs.readFileSync(pkgJson, 'utf8')).version;
|
|
32
|
+
return typeof v === 'string' && v.trim() ? v.trim() : null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {{
|
|
40
|
+
* processVersion?: string|null,
|
|
41
|
+
* projectInstalledVersion?: string|null,
|
|
42
|
+
* root?: string,
|
|
43
|
+
* }} input
|
|
44
|
+
* @returns {{
|
|
45
|
+
* schemaVersion: '1.0',
|
|
46
|
+
* notAScore: true,
|
|
47
|
+
* processArkgateVersion: string|null,
|
|
48
|
+
* projectInstalledVersion: string|null,
|
|
49
|
+
* processPackageMismatch: boolean,
|
|
50
|
+
* processStale: boolean,
|
|
51
|
+
* nextAction: string,
|
|
52
|
+
* }}
|
|
53
|
+
*/
|
|
54
|
+
export function buildProcessPackageHonesty(input = {}) {
|
|
55
|
+
const processVersion =
|
|
56
|
+
typeof input.processVersion === 'string' && input.processVersion.trim()
|
|
57
|
+
? input.processVersion.trim()
|
|
58
|
+
: null;
|
|
59
|
+
let projectInstalledVersion =
|
|
60
|
+
input.projectInstalledVersion !== undefined
|
|
61
|
+
? input.projectInstalledVersion
|
|
62
|
+
: null;
|
|
63
|
+
if (
|
|
64
|
+
projectInstalledVersion === null &&
|
|
65
|
+
input.projectInstalledVersion === undefined &&
|
|
66
|
+
typeof input.root === 'string' &&
|
|
67
|
+
input.root
|
|
68
|
+
) {
|
|
69
|
+
projectInstalledVersion = readProjectInstalledArkgateVersion(input.root);
|
|
70
|
+
}
|
|
71
|
+
if (typeof projectInstalledVersion === 'string') {
|
|
72
|
+
projectInstalledVersion = projectInstalledVersion.trim() || null;
|
|
73
|
+
} else if (projectInstalledVersion !== null) {
|
|
74
|
+
projectInstalledVersion = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const mismatch =
|
|
78
|
+
processVersion != null &&
|
|
79
|
+
projectInstalledVersion != null &&
|
|
80
|
+
processVersion !== projectInstalledVersion;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
schemaVersion: '1.0',
|
|
84
|
+
notAScore: true,
|
|
85
|
+
processArkgateVersion: processVersion,
|
|
86
|
+
projectInstalledVersion,
|
|
87
|
+
processPackageMismatch: mismatch,
|
|
88
|
+
processStale: mismatch,
|
|
89
|
+
nextAction: mismatch
|
|
90
|
+
? 'Restart or retarget the Ark MCP server so process arkgateVersion matches the project install. Prefer project-local CLI (`npx arkgate` / `npx arkgate-check`) until identity is matched and versions align. Multi-checkout users: one expectedRoot per project; never reuse another checkout’s projectId.'
|
|
91
|
+
: projectInstalledVersion == null
|
|
92
|
+
? 'Project has no resolvable node_modules/arkgate; install the package or use CLI from a project that pins arkgate.'
|
|
93
|
+
: 'Process package version matches project install for this MCP root.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -136,8 +136,29 @@ export function buildRulesInventory(input) {
|
|
|
136
136
|
/_(?:OID|OIDS)$/i.test(name) ||
|
|
137
137
|
/^(?:INT2|INT4|INT8|FLOAT4|FLOAT8|NUMERIC|DATE|TIME|TIMESTAMP|TIMESTAMPTZ|JSON|JSONB|UUID)OID$/i.test(name) ||
|
|
138
138
|
/(?:^|_)(?:SCHEMA|PROTOCOL|RESOLVER|FORMAT)_(?:URL|URI|VERSION|ID|IDENTITY)$/i.test(name);
|
|
139
|
+
/**
|
|
140
|
+
* FX09 — pure UX copy / error-message string constants crowd inventory pilots.
|
|
141
|
+
* Downrank (skip) sentence-like strings and message-named identifiers; keep
|
|
142
|
+
* numeric thresholds and domain status tokens for adopt/contract pilots.
|
|
143
|
+
*/
|
|
144
|
+
const isUxMessageConstant = (name, rawValue) => {
|
|
145
|
+
if (/^(?:ERROR|SUCCESS|WARNING|INFO|HINT|HELP|EMPTY|TOAST|SNACK|ALERT|BANNER|DIALOG|MODAL|TOOLTIP|CAPTION|SUBTITLE|HEADLINE|USER|UI|DISPLAY|FEEDBACK)_(?:MSG|MESSAGE|TEXT|COPY|LABEL|TITLE|BODY|DESC|DESCRIPTION|HINT|HELP)?/i.test(name) ||
|
|
146
|
+
/_(?:MSG|MESSAGE|TEXT|COPY|TOAST|SNACK|ALERT|BANNER|CAPTION|HINT|HELP_TEXT|ERROR_TEXT|EMPTY_TEXT|PLACEHOLDER_TEXT|USER_MESSAGE|FEEDBACK)$/i.test(name)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
const unquoted = rawValue.replace(/^['"]|['"]$/g, '');
|
|
150
|
+
// Sentence-like string values (spaces or terminal punctuation) are UX copy,
|
|
151
|
+
// not behavioral business limits — unless the name is a clear domain status seed.
|
|
152
|
+
if (/^['"]/.test(rawValue) &&
|
|
153
|
+
(/\s/.test(unquoted) || /[.!?…]$/.test(unquoted)) &&
|
|
154
|
+
!/^(?:STATUS|STATE|PHASE|ROLE|TYPE|KIND|ORDER|PAYMENT|CART|INVOICE|POLICY)_[A-Z0-9_]+$/i.test(name)) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
};
|
|
139
159
|
while ((magic = magicRe.exec(content)) !== null) {
|
|
140
160
|
const name = magic[2];
|
|
161
|
+
const rawValue = magic[3] ?? '';
|
|
141
162
|
// With governed layer evidence, generic Tooling/Kernel constants are not
|
|
142
163
|
// business-rule candidates. Controller-shaped boundaries stay eligible
|
|
143
164
|
// because business policy can leak into them.
|
|
@@ -145,6 +166,8 @@ export function buildRulesInventory(input) {
|
|
|
145
166
|
continue;
|
|
146
167
|
if (isInfraMagicName(name))
|
|
147
168
|
continue;
|
|
169
|
+
if (isUxMessageConstant(name, rawValue))
|
|
170
|
+
continue;
|
|
148
171
|
// P2-N: skip remaining ALL_CAPS noise only on clear UI chrome (not all of app/).
|
|
149
172
|
if (isUiChrome && !isDomain)
|
|
150
173
|
continue;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
claudeSettings,
|
|
11
11
|
codexHooks,
|
|
12
12
|
codexProjectConfig,
|
|
13
|
+
cursorHooks,
|
|
13
14
|
grokHooks,
|
|
14
15
|
grokProjectConfig,
|
|
15
16
|
opencodeProjectConfig,
|
|
@@ -30,7 +31,10 @@ const COMPACT_HOST_TEMPLATES = {
|
|
|
30
31
|
['.agents/hooks.json', antigravityHooks(root)],
|
|
31
32
|
['.mcp.json', mcpJson(root)],
|
|
32
33
|
],
|
|
33
|
-
cursor: (root) => [
|
|
34
|
+
cursor: (root) => [
|
|
35
|
+
['.cursor/mcp.json', mcpJson(root)],
|
|
36
|
+
['.cursor/hooks.json', cursorHooks(root)],
|
|
37
|
+
],
|
|
34
38
|
codex: (root) => [
|
|
35
39
|
['.codex/hooks.json', codexHooks(root)],
|
|
36
40
|
['.codex/config.toml', codexProjectConfig(root)],
|