arkgate 4.1.1 → 4.2.1
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 +107 -3
- package/README.md +16 -4
- package/bin/ark-check-runtime.mjs +16 -5
- package/bin/ark-mcp-runtime.mjs +766 -64
- package/bin/ark-shared.mjs +16 -4
- package/bin/lib/agent-gates.mjs +1 -0
- package/bin/lib/ci-and-commands.mjs +16 -7
- package/bin/lib/codex-home.mjs +90 -8
- package/bin/lib/design-smells.mjs +71 -9
- package/bin/lib/doctor-plan.mjs +36 -36
- package/bin/lib/effective-contract-load.mjs +73 -9
- package/bin/lib/enforcement-state.mjs +1 -1
- package/bin/lib/gate-files.mjs +441 -9
- package/bin/lib/github-enforcement.mjs +16 -3
- package/bin/lib/hook-templates.mjs +12 -11
- package/bin/lib/html-report-evolution.mjs +114 -0
- package/bin/lib/html-report.mjs +11 -89
- package/bin/lib/import-resolve.mjs +33 -11
- package/bin/lib/install-activation.mjs +87 -0
- package/bin/lib/install-migrate.mjs +66 -50
- package/bin/lib/managed-upgrade.mjs +10 -41
- package/bin/lib/mcp-adoption.mjs +15 -5
- package/bin/lib/physical-cohesion.mjs +2 -1
- package/bin/lib/pilot-loop.mjs +25 -8
- package/bin/lib/project-identity.mjs +103 -0
- package/bin/lib/report-snapshot-context.mjs +28 -0
- package/bin/lib/resident-hook.mjs +33 -9
- package/bin/lib/rules-inventory.mjs +100 -8
- package/bin/lib/skill-install.mjs +272 -22
- package/bin/lib/skill-write.mjs +899 -0
- package/bin/lib/start-preview.mjs +84 -1
- package/bin/lib/upgrade-command.mjs +2 -5
- package/dist/index.cjs +13 -13
- package/dist/index.d.ts +194 -2
- package/dist/index.js +13 -13
- package/docs/README.md +5 -3
- package/docs/agent-guide.md +110 -14
- package/docs/ai-gates.md +103 -18
- package/docs/assets/ark-write-gate.svg +2 -2
- package/docs/enthusiast/how-to-agent-gates.md +6 -0
- package/docs/package-surface.md +15 -9
- package/docs/product-voice.md +13 -1
- package/package.json +7 -1
- package/schemas/ark.project-identity.schema.json +116 -0
- package/server.json +2 -2
- package/templates/skills/ark-adopt.md +9 -0
- package/templates/skills/ark-architect.md +12 -2
- package/templates/skills/ark-autopilot.md +9 -0
- package/templates/skills/ark-contract.md +11 -1
- package/templates/skills/ark-coverage.md +9 -0
- package/templates/skills/ark-explain.md +13 -1
- package/templates/skills/ark-explore.md +9 -0
- package/templates/skills/ark-fix.md +10 -1
- package/templates/skills/ark-loop.md +11 -2
- package/templates/skills/ark-place.md +17 -6
- package/templates/skills/ark-runtime.md +8 -0
- package/templates/skills/ark-think.md +14 -2
- package/templates/skills/ark-upgrade.md +9 -0
|
@@ -63,12 +63,11 @@ import {
|
|
|
63
63
|
SKILL_TOOL_TARGETS,
|
|
64
64
|
skillTemplates,
|
|
65
65
|
stampSkill,
|
|
66
|
-
installedSkillVersion,
|
|
67
|
-
isVersionOlder,
|
|
68
66
|
detectSkillGaps,
|
|
69
67
|
arkPackageVersion,
|
|
70
68
|
verifyHostSkillCatalog,
|
|
71
69
|
} from './skill-install.mjs';
|
|
70
|
+
import { installRepoSkillFile, installSkillCatalog, skillInstallLine, skillInstallNote } from './skill-write.mjs';
|
|
72
71
|
import { detectDeployPathQuality } from './deploy-path.mjs';
|
|
73
72
|
import {
|
|
74
73
|
stripMcpServerArgs,
|
|
@@ -78,6 +77,7 @@ import {
|
|
|
78
77
|
PREFERRED_CHECK_BIN,
|
|
79
78
|
RUNNER_BEFORE_ARK,
|
|
80
79
|
} from './mcp-adoption.mjs';
|
|
80
|
+
import { inspectCodexInstallActivation, printCodexActivationHandoff, reportPartialInstall } from './install-activation.mjs';
|
|
81
81
|
import {
|
|
82
82
|
hasHardWriteHook,
|
|
83
83
|
validateHardWriteRequest,
|
|
@@ -181,7 +181,7 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
|
|
|
181
181
|
compact ? compactAgentInstructions(root, compactHost) : agentInstructions(root)
|
|
182
182
|
);
|
|
183
183
|
// Always write project MCP registration — compact hosts other than Claude still need
|
|
184
|
-
//
|
|
184
|
+
// project-bound ark_identity/ark_manifest (field: compact Grok start left doctor reporting Missing .mcp.json
|
|
185
185
|
// when AGENTS lost compact markers or hosts were mixed).
|
|
186
186
|
add('.mcp.json', mcpJson(root));
|
|
187
187
|
const deploy = detectDeployPathQuality(root);
|
|
@@ -416,9 +416,11 @@ export function runInstallAgentGates(args) {
|
|
|
416
416
|
// settings.json, CI workflow, rules) are the ones users customize, so a plain
|
|
417
417
|
// `--force` clobbers them — this is the safe way to pick up new skill versions.
|
|
418
418
|
// Do not mutate package.json under --skills-only (typecheck bootstrap is gates/CI).
|
|
419
|
+
const earlyWritten = new Set();
|
|
419
420
|
if (!args.skillsOnly) {
|
|
420
421
|
// S4.4: always write check:architecture on gate install / start (including compact).
|
|
421
422
|
const checkBootstrap = ensureCheckArchitectureScript(root, { write: true });
|
|
423
|
+
if (checkBootstrap.changed) earlyWritten.add('package.json');
|
|
422
424
|
if (checkBootstrap.changed && !args.json) {
|
|
423
425
|
console.log(
|
|
424
426
|
`Added package.json script "check:architecture": "${checkBootstrap.script}" (local/CI parity).`
|
|
@@ -426,6 +428,7 @@ export function runInstallAgentGates(args) {
|
|
|
426
428
|
}
|
|
427
429
|
// Bootstrap typecheck before CI template so generated workflow includes the step.
|
|
428
430
|
const typecheckBootstrap = ensureTypecheckScript(root, { write: !args.compact });
|
|
431
|
+
if (typecheckBootstrap.changed && !args.compact) earlyWritten.add('package.json');
|
|
429
432
|
if (typecheckBootstrap.changed && !args.compact && !args.json) {
|
|
430
433
|
console.log(
|
|
431
434
|
`Added package.json script "typecheck": "${typecheckBootstrap.script}" (tsconfig present; local/CI parity).`
|
|
@@ -438,7 +441,7 @@ export function runInstallAgentGates(args) {
|
|
|
438
441
|
compact: args.compact,
|
|
439
442
|
skillsOnly: args.skillsOnly,
|
|
440
443
|
});
|
|
441
|
-
const { skills,
|
|
444
|
+
const { skills, version } = catalog;
|
|
442
445
|
const assetKindByPath = new Map(
|
|
443
446
|
catalog.assets.map((asset) => [asset.relativePath, asset.kind ?? 'gate'])
|
|
444
447
|
);
|
|
@@ -450,7 +453,10 @@ export function runInstallAgentGates(args) {
|
|
|
450
453
|
if (args.compact && priorCompactHost === 'none' && [...tools][0]) {
|
|
451
454
|
const genericMcp = path.join(root, '.mcp.json');
|
|
452
455
|
try {
|
|
453
|
-
if (fs.readFileSync(genericMcp, 'utf8') === mcpJson(root))
|
|
456
|
+
if (fs.readFileSync(genericMcp, 'utf8') === mcpJson(root)) {
|
|
457
|
+
fs.rmSync(genericMcp);
|
|
458
|
+
earlyWritten.add('.mcp.json');
|
|
459
|
+
}
|
|
454
460
|
} catch {
|
|
455
461
|
// Missing or customized user MCP configuration is deliberately retained.
|
|
456
462
|
}
|
|
@@ -459,6 +465,9 @@ export function runInstallAgentGates(args) {
|
|
|
459
465
|
const results = templates.map(([relativePath, content]) => {
|
|
460
466
|
// S4.1: preserve content-identity customized/conflicted managed assets even under --force.
|
|
461
467
|
const kind = assetKindByPath.get(relativePath) ?? 'gate';
|
|
468
|
+
if (kind === 'skill') {
|
|
469
|
+
return installRepoSkillFile(root, relativePath, content, version, args.force);
|
|
470
|
+
}
|
|
462
471
|
if (
|
|
463
472
|
args.force &&
|
|
464
473
|
managedPresent &&
|
|
@@ -477,7 +486,14 @@ export function runInstallAgentGates(args) {
|
|
|
477
486
|
}
|
|
478
487
|
const tableStart = content.indexOf('[mcp_servers.ark]');
|
|
479
488
|
const generatedPrelude = tableStart > 0 ? content.slice(0, tableStart) : '';
|
|
480
|
-
const mergeBase = generatedPrelude
|
|
489
|
+
const mergeBase = generatedPrelude
|
|
490
|
+
? existing
|
|
491
|
+
.replace(generatedPrelude, '')
|
|
492
|
+
.replace(
|
|
493
|
+
/^# Generated by ark-check --install-agent-gates \(Codex project scope\)\.\n# Restart Codex after changes; MCP servers are loaded when the project session starts\.\n/m,
|
|
494
|
+
''
|
|
495
|
+
)
|
|
496
|
+
: existing;
|
|
481
497
|
const merged = upsertCodexMcpTable(mergeBase, 'ark', content);
|
|
482
498
|
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
483
499
|
return writeTemplate(root, relativePath, merged, true);
|
|
@@ -527,7 +543,9 @@ export function runInstallAgentGates(args) {
|
|
|
527
543
|
);
|
|
528
544
|
});
|
|
529
545
|
|
|
530
|
-
console.log(
|
|
546
|
+
console.log(
|
|
547
|
+
`Ark agent gate templates (scope=repo; source=${version ? `arkgate@${version}` : 'arkgate@unknown'}):`
|
|
548
|
+
);
|
|
531
549
|
let staleSkipped = 0;
|
|
532
550
|
let preservedCustomized = 0;
|
|
533
551
|
for (const result of results) {
|
|
@@ -549,13 +567,10 @@ export function runInstallAgentGates(args) {
|
|
|
549
567
|
if (result.status === 'skipped-customized') {
|
|
550
568
|
preservedCustomized += 1;
|
|
551
569
|
note = ' (customized — content-identity preserved)';
|
|
552
|
-
} else if (result.status === 'skipped' &&
|
|
553
|
-
|
|
554
|
-
if (
|
|
570
|
+
} else if (result.status === 'skipped' && result.skillPlan) {
|
|
571
|
+
note = ` (${skillInstallNote(result.skillPlan)})`;
|
|
572
|
+
if (result.skillPlan.reason === 'existing-preserved') {
|
|
555
573
|
staleSkipped += 1;
|
|
556
|
-
note = ` (stale: ${installed ?? 'no stamp'} < ${version})`;
|
|
557
|
-
} else {
|
|
558
|
-
note = ' (up to date)';
|
|
559
574
|
}
|
|
560
575
|
}
|
|
561
576
|
console.log(` ${marker.padEnd(7)} ${result.relativePath}${note}`);
|
|
@@ -581,36 +596,29 @@ export function runInstallAgentGates(args) {
|
|
|
581
596
|
if (args.codexHome) {
|
|
582
597
|
const dir = codexSkillsDir();
|
|
583
598
|
console.log('');
|
|
584
|
-
console.log(
|
|
599
|
+
console.log(
|
|
600
|
+
`Codex home skills (scope=home-shared; source=${version ? `arkgate@${version}` : 'arkgate@unknown'}; target=${dir}/<name>/SKILL.md):`
|
|
601
|
+
);
|
|
602
|
+
console.log(
|
|
603
|
+
' Compatibility: monotonic downgrade protection requires every shared-catalog writer ' +
|
|
604
|
+
'to use ArkGate 4.2.0+; pre-4.2 --codex-home ignores this catalog. Upgrade legacy repos first.'
|
|
605
|
+
);
|
|
585
606
|
try {
|
|
586
607
|
fs.mkdirSync(dir, { recursive: true });
|
|
587
608
|
} catch (error) {
|
|
588
609
|
console.error(` FAILED to create ${dir} (${error.message})`);
|
|
589
|
-
homeResults.push({ status: 'failed' });
|
|
610
|
+
homeResults.push({ relativePath: dir, status: 'failed' });
|
|
590
611
|
}
|
|
591
612
|
if (homeResults.length === 0) {
|
|
592
|
-
for (const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
console.log(` ${'skipped'.padEnd(7)} ${name}/SKILL.md${note}`);
|
|
602
|
-
homeResults.push({ status: 'skipped' });
|
|
603
|
-
continue;
|
|
604
|
-
}
|
|
605
|
-
try {
|
|
606
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
607
|
-
fs.writeFileSync(file, content);
|
|
608
|
-
console.log(` ${'wrote'.padEnd(7)} ${name}/SKILL.md`);
|
|
609
|
-
homeResults.push({ status: 'written' });
|
|
610
|
-
} catch (error) {
|
|
611
|
-
console.log(` ${'FAILED'.padEnd(7)} ${name}/SKILL.md (${error.message})`);
|
|
612
|
-
homeResults.push({ status: 'failed' });
|
|
613
|
-
}
|
|
613
|
+
for (const result of installSkillCatalog({
|
|
614
|
+
directory: dir,
|
|
615
|
+
skills,
|
|
616
|
+
packageVersion: version,
|
|
617
|
+
force: args.force,
|
|
618
|
+
scope: 'home',
|
|
619
|
+
})) {
|
|
620
|
+
console.log(skillInstallLine(result));
|
|
621
|
+
homeResults.push(result);
|
|
614
622
|
}
|
|
615
623
|
}
|
|
616
624
|
}
|
|
@@ -643,26 +651,28 @@ export function runInstallAgentGates(args) {
|
|
|
643
651
|
const verb = codexMcp.status === 'updated' ? 'updated' : 'wrote';
|
|
644
652
|
console.log(` ${verb.padEnd(7)} [mcp_servers.ark] with absolute paths`);
|
|
645
653
|
console.log(' RESTART Codex — it does not hot-load MCP servers.');
|
|
646
|
-
console.log(
|
|
654
|
+
console.log(
|
|
655
|
+
' Then expect: tools ark_identity, ark_manifest, validate_code, ark_check, ark_coverage, ark_place.'
|
|
656
|
+
);
|
|
647
657
|
}
|
|
648
658
|
} else if (skipHomeWire) {
|
|
649
659
|
codexMcp = { status: 'skipped', file: codexConfigPath(), reason: 'temp-root' };
|
|
650
660
|
}
|
|
651
661
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
662
|
+
const { codexProjectConfigured, runtimeActivation } =
|
|
663
|
+
inspectCodexInstallActivation(root, tools.has('codex') && !args.skillsOnly);
|
|
664
|
+
if (reportPartialInstall({
|
|
665
|
+
root,
|
|
666
|
+
tools,
|
|
667
|
+
results,
|
|
668
|
+
homeResults,
|
|
669
|
+
earlyWritten,
|
|
670
|
+
codexMcp,
|
|
671
|
+
runtimeActivation,
|
|
672
|
+
})) {
|
|
658
673
|
process.exitCode = 1;
|
|
659
674
|
return;
|
|
660
675
|
}
|
|
661
|
-
if (codexMcp?.status === 'failed') {
|
|
662
|
-
console.error(
|
|
663
|
-
`\nWarning: Codex home MCP registration failed (${codexMcp.message}). Repo gates were written; fix ~/.codex access or re-run with --codex-home --force.`
|
|
664
|
-
);
|
|
665
|
-
}
|
|
666
676
|
if (writeRequest.host) {
|
|
667
677
|
if (!hasHardWriteHook(root, writeRequest.host)) {
|
|
668
678
|
console.error(`\nFailed to verify the ${writeRequest.host} hard-write hook after install.`);
|
|
@@ -731,8 +741,14 @@ export function runInstallAgentGates(args) {
|
|
|
731
741
|
}
|
|
732
742
|
if ((tools.has('codex') || args.codexHome)) {
|
|
733
743
|
console.log('');
|
|
744
|
+
if (tools.has('codex') && !args.skillsOnly) {
|
|
745
|
+
printCodexActivationHandoff(root, codexProjectConfigured, runtimeActivation);
|
|
746
|
+
}
|
|
734
747
|
if (codexMcp && codexMcp.status !== 'failed') {
|
|
735
|
-
console.log(
|
|
748
|
+
console.log(
|
|
749
|
+
` Codex: ark MCP registered in ${codexMcp.file} — restart Codex, then bind with ` +
|
|
750
|
+
'`ark_identity` and read the contract with `ark_manifest`.'
|
|
751
|
+
);
|
|
736
752
|
}
|
|
737
753
|
if (tools.has('codex') && !args.compact) {
|
|
738
754
|
console.log(' Codex write path (honest):');
|
|
@@ -371,9 +371,9 @@ function summaryFor(assets, manifestChanged) {
|
|
|
371
371
|
const states = {};
|
|
372
372
|
for (const asset of assets) states[asset.state] = (states[asset.state] ?? 0) + 1;
|
|
373
373
|
const applying = assets.filter((asset) => asset.willApply);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const metadataRefresh =
|
|
374
|
+
const wouldWrite = applying.length;
|
|
375
|
+
// Public-summary compatibility: stamp-only writes are no longer scheduled.
|
|
376
|
+
const metadataRefresh = 0;
|
|
377
377
|
const customizedPreserved = assets.filter((asset) => asset.state === 'customized').length;
|
|
378
378
|
const fileChanges = applying.length;
|
|
379
379
|
return {
|
|
@@ -385,7 +385,6 @@ function summaryFor(assets, manifestChanged) {
|
|
|
385
385
|
customizedPreserved,
|
|
386
386
|
fileChanges,
|
|
387
387
|
manifestChanged,
|
|
388
|
-
// Full apply count still includes optional stamp refresh + manifest bookkeeping.
|
|
389
388
|
changed: fileChanges + (manifestChanged ? 1 : 0),
|
|
390
389
|
blocked: assets.filter((asset) => asset.blocked).length,
|
|
391
390
|
};
|
|
@@ -447,12 +446,7 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
447
446
|
kind: catalogAsset.kind,
|
|
448
447
|
});
|
|
449
448
|
const accepted = options.acceptConflicts === true;
|
|
450
|
-
const refreshMetadata =
|
|
451
|
-
classified.state === 'current' &&
|
|
452
|
-
catalogAsset.kind === 'skill' &&
|
|
453
|
-
currentScoped !== desiredScoped;
|
|
454
449
|
const canApply =
|
|
455
|
-
refreshMetadata ||
|
|
456
450
|
classified.state === 'stale' ||
|
|
457
451
|
(classified.state === 'missing' && (!recorded || accepted)) ||
|
|
458
452
|
(classified.state === 'conflicted' && accepted);
|
|
@@ -465,7 +459,7 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
465
459
|
scope: catalogAsset.scope,
|
|
466
460
|
...classified,
|
|
467
461
|
...(unparsedScope ? { reason: 'unparsed managed TOML scope preserved' } : {}),
|
|
468
|
-
action:
|
|
462
|
+
action: canApply ? (currentScoped == null ? 'create' : 'update') : 'none',
|
|
469
463
|
willApply: canApply,
|
|
470
464
|
blocked,
|
|
471
465
|
beforeHash: hash(currentScoped == null ? null : Buffer.from(currentScoped)),
|
|
@@ -615,9 +609,7 @@ export function applyManagedUpgrade(root, plan, expectedPlanDigest) {
|
|
|
615
609
|
if (resolvedRoot !== plan.root) throw new Error('managed upgrade plan root mismatch');
|
|
616
610
|
if (plan.summary.blocked > 0) return publicPlan(plan, { blocked: true });
|
|
617
611
|
const wouldWrite = plan.summary.wouldWrite ?? 0;
|
|
618
|
-
const metadataRefresh = plan.summary.metadataRefresh ?? 0;
|
|
619
612
|
// Content already matches: unbound --apply is a no-op (exit success), not a digest error.
|
|
620
|
-
// Optional stamp-only refresh still requires the preview's exact --plan-digest.
|
|
621
613
|
if (!expectedPlanDigest || expectedPlanDigest !== plan.planDigest) {
|
|
622
614
|
if (wouldWrite === 0 && (plan.summary.blocked ?? 0) === 0 && !expectedPlanDigest) {
|
|
623
615
|
return publicPlan(plan, {
|
|
@@ -625,7 +617,6 @@ export function applyManagedUpgrade(root, plan, expectedPlanDigest) {
|
|
|
625
617
|
applied: false,
|
|
626
618
|
blocked: false,
|
|
627
619
|
nothingToApply: true,
|
|
628
|
-
optionalStampRefresh: metadataRefresh,
|
|
629
620
|
});
|
|
630
621
|
}
|
|
631
622
|
throw new Error('managed upgrade plan digest mismatch; run a new preview and use its exact nextCommand');
|
|
@@ -741,31 +732,18 @@ export function renderManagedUpgrade(plan, options = {}) {
|
|
|
741
732
|
const summary = plan.summary;
|
|
742
733
|
const managedAssets = summary.managedAssets ?? summary.total ?? plan.assets.length;
|
|
743
734
|
const wouldWrite = summary.wouldWrite ?? 0;
|
|
744
|
-
const metadataRefresh = summary.metadataRefresh ?? 0;
|
|
745
735
|
const customizedPreserved = summary.customizedPreserved ?? summary.states?.customized ?? 0;
|
|
746
736
|
const blocked = summary.blocked ?? 0;
|
|
747
737
|
console.log(
|
|
748
738
|
`Managed assets: ${managedAssets}; would write: ${wouldWrite}; ` +
|
|
749
|
-
`customized preserved: ${customizedPreserved}; blocked conflicts/deletions: ${blocked}
|
|
750
|
-
(metadataRefresh > 0 ? `; optional stamp refresh: ${metadataRefresh}` : '') +
|
|
751
|
-
'.'
|
|
739
|
+
`customized preserved: ${customizedPreserved}; blocked conflicts/deletions: ${blocked}.`
|
|
752
740
|
);
|
|
753
741
|
if (plan.applied) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
' (no content body changes).'
|
|
760
|
-
);
|
|
761
|
-
} else {
|
|
762
|
-
console.log(
|
|
763
|
-
`Applied ${wouldWrite} content write(s)` +
|
|
764
|
-
(metadataRefresh > 0 ? `, ${metadataRefresh} stamp refresh(es)` : '') +
|
|
765
|
-
(summary.manifestChanged ? ', managed manifest' : '') +
|
|
766
|
-
'.'
|
|
767
|
-
);
|
|
768
|
-
}
|
|
742
|
+
console.log(
|
|
743
|
+
`Applied ${wouldWrite} content write(s)` +
|
|
744
|
+
(summary.manifestChanged ? ', managed manifest' : '') +
|
|
745
|
+
'.'
|
|
746
|
+
);
|
|
769
747
|
return;
|
|
770
748
|
}
|
|
771
749
|
// Content already matches package templates — do not urge --apply as the primary next step.
|
|
@@ -775,15 +753,6 @@ export function renderManagedUpgrade(plan, options = {}) {
|
|
|
775
753
|
console.log(
|
|
776
754
|
`Nothing to apply — managed content matches ${verLabel} (${customizedPreserved} customized preserved).`
|
|
777
755
|
);
|
|
778
|
-
if (metadataRefresh > 0) {
|
|
779
|
-
console.log(
|
|
780
|
-
`Optional: ${metadataRefresh} skill stamp(s) lag package version while content is already current.`
|
|
781
|
-
);
|
|
782
|
-
const stampCmd = options.optionalStampApply ?? options.next;
|
|
783
|
-
if (stampCmd) {
|
|
784
|
-
console.log(`Optional stamp-only apply (not required): ${stampCmd}`);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
756
|
return;
|
|
788
757
|
}
|
|
789
758
|
console.log(`Planned writes: ${wouldWrite}; blocked conflicts/deletions: ${blocked}.`);
|
package/bin/lib/mcp-adoption.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { detectWritePathCapabilities } from './write-path-detect.mjs';
|
|
|
16
16
|
import { detectActiveAgentHost, skillTemplateNames } from './skill-install.mjs';
|
|
17
17
|
import { detectDeployPathQuality } from './deploy-path.mjs';
|
|
18
18
|
import { collectWeakestLinkGaps } from './weakest-link.mjs';
|
|
19
|
-
import { withCiProviderEvidence } from './enforcement-state.mjs';
|
|
19
|
+
import { codexRuntimeActivation, withCiProviderEvidence } from './enforcement-state.mjs';
|
|
20
20
|
|
|
21
21
|
export { detectDeployPathQuality };
|
|
22
22
|
|
|
@@ -120,7 +120,7 @@ export function brokenMcpGateFiles(root) {
|
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
122
|
* Adoption completeness (separate from 0–100 fitness). Pure-ish: filesystem + config.
|
|
123
|
-
* @returns {{ gaps: object[], hosts: object[], mcp: object, codexHome: object|null, coreOptional: object[], originReport: object, baseline: object, layerBalance: object|null, deployPath: object|null, writePath: object }}
|
|
123
|
+
* @returns {{ gaps: object[], hosts: object[], mcp: object, codexHome: object|null, coreOptional: object[], originReport: object, baseline: object, layerBalance: object|null, deployPath: object|null, writePath: object, runtimeActivation: object }}
|
|
124
124
|
*/
|
|
125
125
|
export function collectAdoptionGaps(root, config, coverage) {
|
|
126
126
|
const gaps = [];
|
|
@@ -259,7 +259,11 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
259
259
|
return false;
|
|
260
260
|
}
|
|
261
261
|
})();
|
|
262
|
-
|
|
262
|
+
const runtimeActivation = codexRuntimeActivation({
|
|
263
|
+
configuredOnDisk: codexProjectMcp,
|
|
264
|
+
restartRequired: codexProjectMcp,
|
|
265
|
+
});
|
|
266
|
+
if (adopted && !isProducer) {
|
|
263
267
|
const codexFile = codexConfigPath();
|
|
264
268
|
let toml = '';
|
|
265
269
|
try {
|
|
@@ -278,6 +282,8 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
278
282
|
needsRewrite: assessed.needsRewrite,
|
|
279
283
|
multiProject: assessed.multiProject,
|
|
280
284
|
scopedTable: assessed.scopedTable,
|
|
285
|
+
projectConfiguredOnDisk: codexProjectMcp,
|
|
286
|
+
runtimeIdentityVerified: false,
|
|
281
287
|
};
|
|
282
288
|
if (assessed.gap) {
|
|
283
289
|
// Temp/upgrade MCP roots stay urgent (fail-closed). Non-temp Codex-home debt
|
|
@@ -288,9 +294,12 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
288
294
|
const deferred =
|
|
289
295
|
!tempUrgent && activeHost != null && activeHost !== 'codex';
|
|
290
296
|
const severity = deferred ? 'info' : assessed.gap.severity;
|
|
297
|
+
const localRisk = codexProjectMcp
|
|
298
|
+
? 'Project config exists on disk, but the active runtime identity is unverified. '
|
|
299
|
+
: '';
|
|
291
300
|
const message = deferred
|
|
292
|
-
? `Deferred (fix when using Codex): ${assessed.gap.message}`
|
|
293
|
-
: assessed.gap.message
|
|
301
|
+
? `Deferred (fix when using Codex): ${localRisk}${assessed.gap.message}`
|
|
302
|
+
: `${localRisk}${assessed.gap.message}`;
|
|
294
303
|
gaps.push({
|
|
295
304
|
id: assessed.gap.id,
|
|
296
305
|
severity,
|
|
@@ -526,6 +535,7 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
526
535
|
deployPath,
|
|
527
536
|
contractFalseGreen,
|
|
528
537
|
writePath,
|
|
538
|
+
runtimeActivation,
|
|
529
539
|
enforcement: {
|
|
530
540
|
ci: weakest.ci,
|
|
531
541
|
preCommit: weakest.preCommit,
|
|
@@ -24,7 +24,8 @@ const MAX_FINDINGS = 5;
|
|
|
24
24
|
const MAX_ANCHORS = 4;
|
|
25
25
|
const MAX_MOVE_SAMPLE = 5;
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// `proxy` = Next.js 16 network-boundary rename of middleware (still framework-owned).
|
|
28
|
+
const FRAMEWORK_FILES = /^(route|page|layout|index|loading|error|template|default|not-found|middleware|proxy|actions?|handler)$/i;
|
|
28
29
|
const SKIP_SEGMENT = /^(\[.*\]|\(.*\)|src|app|apps|api|lib|libs|pages|packages|modules|components|utils|helpers|hooks|server|client|shared|common|__tests__|tests?|e2e|examples?|dist|build)$/i;
|
|
29
30
|
const NOISE_TOKEN = /^(use|api|get|set|app|lib|the|new)$/;
|
|
30
31
|
/** ADR 0010 D7 — framework-owned anchors never move. */
|
package/bin/lib/pilot-loop.mjs
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* extraction-card payload, and compares residual after re-doctor on pilot paths.
|
|
6
6
|
* Judgment only — never mechanical-safe; never multi-pilot batch apply.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildPatternBetsFromSmells,
|
|
10
|
+
isNonProductionPilotPath,
|
|
11
|
+
} from './design-smells.mjs';
|
|
9
12
|
|
|
10
13
|
/** Stable product id for JSON / tests. */
|
|
11
14
|
export const PILOT_LOOP_ID = 'one-pilot-redoctor';
|
|
@@ -44,14 +47,28 @@ export function fileEvidencePaths(evidence = []) {
|
|
|
44
47
|
);
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
function pilotFilesForBet(bet, preferredFiles) {
|
|
51
|
+
const rawFiles = preferredFiles?.length
|
|
52
|
+
? preferredFiles
|
|
53
|
+
: fileEvidencePaths(bet?.evidence);
|
|
54
|
+
if (bet?.smellId !== 'god-module') return rawFiles;
|
|
55
|
+
const files = rawFiles.filter((file) => !isNonProductionPilotPath(file));
|
|
56
|
+
const excludedPilot =
|
|
57
|
+
rawFiles.length === 0 &&
|
|
58
|
+
typeof bet.pilot === 'string' &&
|
|
59
|
+
isNonProductionPilotPath(bet.pilot);
|
|
60
|
+
return (rawFiles.length > 0 && files.length === 0) || excludedPilot
|
|
61
|
+
? null
|
|
62
|
+
: files;
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
/**
|
|
48
66
|
* Score a pattern bet for "do this pilot first".
|
|
49
67
|
* Prefers concrete src/ files and higher-impact smell ids.
|
|
50
68
|
* @param {object} bet
|
|
51
69
|
* @param {number} index
|
|
52
70
|
*/
|
|
53
|
-
function scoreBet(bet, index) {
|
|
54
|
-
const files = fileEvidencePaths(bet?.evidence);
|
|
71
|
+
function scoreBet(bet, index, files = fileEvidencePaths(bet?.evidence)) {
|
|
55
72
|
const smellPri = SMELL_PRIORITY[bet?.smellId] ?? 50;
|
|
56
73
|
// Higher score wins; concrete files dominate; then smell priority; stable by index.
|
|
57
74
|
return files.length * 100 - smellPri * 10 - index;
|
|
@@ -64,9 +81,8 @@ function scoreBet(bet, index) {
|
|
|
64
81
|
*/
|
|
65
82
|
export function extractionCardFromBet(bet, preferredFiles) {
|
|
66
83
|
if (!bet || typeof bet !== 'object') return null;
|
|
67
|
-
const files = preferredFiles
|
|
68
|
-
|
|
69
|
-
: fileEvidencePaths(bet.evidence);
|
|
84
|
+
const files = pilotFilesForBet(bet, preferredFiles);
|
|
85
|
+
if (files === null) return null;
|
|
70
86
|
const evidence = files.length ? files : (bet.evidence || []).slice(0, 8);
|
|
71
87
|
const pilotTarget =
|
|
72
88
|
files[0] ||
|
|
@@ -123,8 +139,9 @@ export function selectNextPilot(patternBets, options = {}) {
|
|
|
123
139
|
if (!bet || bet.neverMechanicalSafe === false) continue;
|
|
124
140
|
// Skip anything that claims mechanical-safe (honesty).
|
|
125
141
|
if (bet.class === 'mechanical-safe') continue;
|
|
126
|
-
const files =
|
|
127
|
-
|
|
142
|
+
const files = pilotFilesForBet(bet);
|
|
143
|
+
if (files === null) continue;
|
|
144
|
+
const sc = scoreBet(bet, i, files);
|
|
128
145
|
if (sc > bestScore) {
|
|
129
146
|
bestScore = sc;
|
|
130
147
|
best = { bet, files };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/projectIdentity.ts
|
|
5
|
+
* Regenerate: node scripts/generate-cli-pure.mjs
|
|
6
|
+
* Drift check: node scripts/generate-cli-pure.mjs --check
|
|
7
|
+
*
|
|
8
|
+
* Pure CLI helper (bin/lib/project-identity.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const ARK_PROJECT_IDENTITY_SCHEMA_VERSION = '1.0';
|
|
12
|
+
export const ARK_PROJECT_IDENTITY_SCHEMA_URL = 'https://unpkg.com/arkgate@4/schemas/ark.project-identity.schema.json';
|
|
13
|
+
const sha256Pattern = '^sha256:[a-f0-9]{64}$';
|
|
14
|
+
export const PROJECT_EXPECTATION_SCHEMA = {
|
|
15
|
+
type: 'object',
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
expectedRoot: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
minLength: 1,
|
|
21
|
+
description: 'Absolute expected workspace/project directory. The initial authoritative handshake ' +
|
|
22
|
+
'requires the exact project root; descendant calls also require expectedProjectId.',
|
|
23
|
+
},
|
|
24
|
+
expectedProjectId: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
pattern: sha256Pattern,
|
|
27
|
+
description: 'Project id previously returned by ark_identity or ark_manifest.',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export const PROJECT_BINDING_SCHEMA = {
|
|
32
|
+
type: 'object',
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
required: ['status', 'authoritative'],
|
|
35
|
+
properties: {
|
|
36
|
+
status: { enum: ['matched', 'unverified', 'mismatch'] },
|
|
37
|
+
authoritative: { type: 'boolean' },
|
|
38
|
+
expectedRoot: { type: 'string', minLength: 1 },
|
|
39
|
+
expectedProjectId: { type: 'string', pattern: sha256Pattern },
|
|
40
|
+
code: {
|
|
41
|
+
enum: [
|
|
42
|
+
'PROJECT_ROOT_MISMATCH',
|
|
43
|
+
'PROJECT_ID_MISMATCH',
|
|
44
|
+
'INVALID_PROJECT_EXPECTATION',
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
message: { type: 'string', minLength: 1 },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
export const ARK_PROJECT_IDENTITY_SCHEMA = {
|
|
51
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
52
|
+
$id: ARK_PROJECT_IDENTITY_SCHEMA_URL,
|
|
53
|
+
title: 'ArkGate MCP project identity',
|
|
54
|
+
description: 'Stable project binding plus separate runtime and architecture-contract evidence.',
|
|
55
|
+
type: 'object',
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
required: [
|
|
58
|
+
'schemaVersion',
|
|
59
|
+
'projectId',
|
|
60
|
+
'resolvedRoot',
|
|
61
|
+
'resolvedConfigPath',
|
|
62
|
+
'arkgateVersion',
|
|
63
|
+
'contractHash',
|
|
64
|
+
'contractSource',
|
|
65
|
+
'runtimeId',
|
|
66
|
+
'processStartedAt',
|
|
67
|
+
],
|
|
68
|
+
properties: {
|
|
69
|
+
schemaVersion: { const: ARK_PROJECT_IDENTITY_SCHEMA_VERSION },
|
|
70
|
+
projectId: { type: 'string', pattern: sha256Pattern },
|
|
71
|
+
resolvedRoot: { type: 'string', minLength: 1 },
|
|
72
|
+
resolvedConfigPath: { type: 'string', minLength: 1 },
|
|
73
|
+
arkgateVersion: { type: 'string', minLength: 1 },
|
|
74
|
+
contractHash: { type: 'string', pattern: sha256Pattern },
|
|
75
|
+
contractSource: { enum: ['project', 'default-profile', 'manifest'] },
|
|
76
|
+
runtimeId: { type: 'string', minLength: 1 },
|
|
77
|
+
processStartedAt: { type: 'string', format: 'date-time' },
|
|
78
|
+
},
|
|
79
|
+
$defs: {
|
|
80
|
+
expectation: PROJECT_EXPECTATION_SCHEMA,
|
|
81
|
+
binding: PROJECT_BINDING_SCHEMA,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Stable identity: contract edits and MCP restarts must not change which project
|
|
86
|
+
* this is. Callers must pass canonical real paths and a SHA-256 hex function.
|
|
87
|
+
*/
|
|
88
|
+
export function createProjectId(resolvedRoot, resolvedConfigPath, sha256Hex) {
|
|
89
|
+
if (!resolvedRoot || !resolvedConfigPath) {
|
|
90
|
+
throw new Error('Project identity requires resolvedRoot and resolvedConfigPath.');
|
|
91
|
+
}
|
|
92
|
+
const digest = sha256Hex(JSON.stringify({ resolvedRoot, resolvedConfigPath })).toLowerCase();
|
|
93
|
+
if (!/^[a-f0-9]{64}$/.test(digest)) {
|
|
94
|
+
throw new Error('Project identity hash adapter must return 64 hexadecimal SHA-256 characters.');
|
|
95
|
+
}
|
|
96
|
+
return `sha256:${digest}`;
|
|
97
|
+
}
|
|
98
|
+
export function createProjectIdentity(input) {
|
|
99
|
+
return {
|
|
100
|
+
schemaVersion: ARK_PROJECT_IDENTITY_SCHEMA_VERSION,
|
|
101
|
+
...input,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/** Best-effort, shell-free Git/worktree evidence for report snapshots. */
|
|
4
|
+
export function captureGitSnapshot(root) {
|
|
5
|
+
const run = (args) =>
|
|
6
|
+
execFileSync('git', ['-C', root, ...args], {
|
|
7
|
+
encoding: 'utf8',
|
|
8
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
9
|
+
}).trim();
|
|
10
|
+
try {
|
|
11
|
+
const headSha = run(['rev-parse', '--verify', 'HEAD']);
|
|
12
|
+
let branch = null;
|
|
13
|
+
try {
|
|
14
|
+
branch = run(['symbolic-ref', '--quiet', '--short', 'HEAD']) || null;
|
|
15
|
+
} catch {
|
|
16
|
+
// Detached HEAD is valid release/report evidence.
|
|
17
|
+
}
|
|
18
|
+
let dirty = null;
|
|
19
|
+
try {
|
|
20
|
+
dirty = run(['status', '--porcelain=v1', '--untracked-files=normal']).length > 0;
|
|
21
|
+
} catch {
|
|
22
|
+
// Keep the commit identity even when worktree state is unavailable.
|
|
23
|
+
}
|
|
24
|
+
return { available: true, headSha, branch, dirty };
|
|
25
|
+
} catch {
|
|
26
|
+
return { available: false, headSha: null, branch: null, dirty: null };
|
|
27
|
+
}
|
|
28
|
+
}
|