arkgate 4.1.0 → 4.2.0
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 +124 -2
- package/README.md +24 -14
- package/bin/ark-check-runtime.mjs +16 -5
- package/bin/ark-mcp-runtime.mjs +766 -64
- package/bin/lib/agent-gates.mjs +1 -0
- package/bin/lib/ark-gitignore.mjs +88 -0
- package/bin/lib/ci-and-commands.mjs +33 -8
- package/bin/lib/codex-home.mjs +90 -8
- package/bin/lib/design-smells.mjs +71 -9
- package/bin/lib/doctor-plan.mjs +47 -39
- package/bin/lib/effective-contract-load.mjs +73 -9
- package/bin/lib/enforcement-honesty.mjs +78 -22
- package/bin/lib/enforcement-state.mjs +1 -1
- package/bin/lib/gate-files.mjs +441 -9
- package/bin/lib/github-enforcement.mjs +168 -7
- package/bin/lib/hook-templates.mjs +12 -11
- package/bin/lib/host-support-matrix.mjs +91 -17
- package/bin/lib/html-report-depth.mjs +13 -2
- package/bin/lib/html-report-evolution.mjs +114 -0
- package/bin/lib/html-report.mjs +18 -97
- 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/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/bin/lib/write-path-detect.mjs +2 -2
- package/dist/index.cjs +13 -13
- package/dist/index.d.ts +194 -2
- package/dist/index.js +13 -13
- package/docs/README.md +6 -4
- package/docs/agent-guide.md +115 -17
- package/docs/ai-gates.md +133 -25
- package/docs/assets/ark-write-gate.svg +2 -2
- package/docs/develop.md +16 -6
- package/docs/enthusiast/how-to-agent-gates.md +6 -0
- package/docs/package-surface.md +16 -9
- package/docs/product-voice.md +22 -4
- package/docs/use.md +3 -1
- package/package.json +3 -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
package/bin/lib/html-report.mjs
CHANGED
|
@@ -21,6 +21,11 @@ import {
|
|
|
21
21
|
} from './html-report-depth.mjs';
|
|
22
22
|
import { FIX_HINTS } from './violations.mjs';
|
|
23
23
|
import { capabilityBadgesFor, renderAdvisorySections } from './html-report-advisories.mjs';
|
|
24
|
+
import { renderEvolutionSection } from './html-report-evolution.mjs';
|
|
25
|
+
import { arkGitignoreAppendDecision } from './ark-gitignore.mjs';
|
|
26
|
+
import { captureGitSnapshot } from './report-snapshot-context.mjs';
|
|
27
|
+
|
|
28
|
+
export { arkGitignoreAppendDecision, gitignoreCoversArkState, gitignoreHasArkNegationException } from './ark-gitignore.mjs';
|
|
24
29
|
|
|
25
30
|
export function detectEnforcement(root) {
|
|
26
31
|
const has = (rel) => fs.existsSync(path.join(root, rel));
|
|
@@ -158,6 +163,7 @@ export function buildReportSnapshot({
|
|
|
158
163
|
return path.basename(root);
|
|
159
164
|
}
|
|
160
165
|
})(),
|
|
166
|
+
git: captureGitSnapshot(root),
|
|
161
167
|
ok: Boolean(ok),
|
|
162
168
|
mode: mode ?? null,
|
|
163
169
|
score: score ?? null,
|
|
@@ -336,20 +342,16 @@ export function archiveReportSnapshots(root, { html, snapshot, resetOrigin = fal
|
|
|
336
342
|
}
|
|
337
343
|
}
|
|
338
344
|
|
|
339
|
-
//
|
|
345
|
+
// EH03: cover .ark reports in .gitignore without defeating ! exceptions.
|
|
340
346
|
const gitignore = path.join(root, '.gitignore');
|
|
341
347
|
if (fs.existsSync(gitignore)) {
|
|
342
348
|
const text = fs.readFileSync(gitignore, 'utf8');
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
const t = line.trim();
|
|
346
|
-
return t === '.ark/' || t === '.ark' || t === '/.ark/' || t === '**/.ark/';
|
|
347
|
-
});
|
|
348
|
-
if (!hasArk) {
|
|
349
|
+
const decision = arkGitignoreAppendDecision(text);
|
|
350
|
+
if (decision.append && decision.rule) {
|
|
349
351
|
const suffix = text.endsWith('\n') || text.length === 0 ? '' : '\n';
|
|
350
352
|
fs.writeFileSync(
|
|
351
353
|
gitignore,
|
|
352
|
-
`${text}${suffix}\n# Ark generated reports / local state\n.
|
|
354
|
+
`${text}${suffix}\n# Ark generated reports / local state\n${decision.rule}\n`
|
|
353
355
|
);
|
|
354
356
|
}
|
|
355
357
|
}
|
|
@@ -1229,95 +1231,14 @@ export function renderHtmlReport({
|
|
|
1229
1231
|
<div class="gates">${enforcementRows}</div>
|
|
1230
1232
|
</div>
|
|
1231
1233
|
|
|
1232
|
-
${(
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
(<code>.ark/reports/origin.json</code> + <code>origin.html</code>).
|
|
1241
|
-
Future reports will show deltas against this starting point so you can prove evolution.
|
|
1242
|
-
</p>
|
|
1243
|
-
</div>`;
|
|
1244
|
-
}
|
|
1245
|
-
const rows = [
|
|
1246
|
-
['Ark score', originSnapshot.score, currentSnapshot.score, ''],
|
|
1247
|
-
['Governed %', originSnapshot.governedPercent, currentSnapshot.governedPercent, 'pp'],
|
|
1248
|
-
['Files in scope', originSnapshot.totalFiles, currentSnapshot.totalFiles, ''],
|
|
1249
|
-
['Classified files', originSnapshot.classifiedFiles, currentSnapshot.classifiedFiles, ''],
|
|
1250
|
-
['Active violations', originSnapshot.activeViolations, currentSnapshot.activeViolations, ''],
|
|
1251
|
-
['Value violations', originSnapshot.valueViolations, currentSnapshot.valueViolations, ''],
|
|
1252
|
-
['Type-only violations', originSnapshot.typeOnlyViolations, currentSnapshot.typeOnlyViolations, ''],
|
|
1253
|
-
['Layers', originSnapshot.layerCount, currentSnapshot.layerCount, ''],
|
|
1254
|
-
['Deny rules', originSnapshot.denyRules, currentSnapshot.denyRules, ''],
|
|
1255
|
-
['Gates configured', originSnapshot.gatesOn, currentSnapshot.gatesOn, ''],
|
|
1256
|
-
];
|
|
1257
|
-
const originDate = (originSnapshot.generatedAt || '').slice(0, 10) || 'origin';
|
|
1258
|
-
const nowDate = (currentSnapshot.generatedAt || '').slice(0, 10) || 'now';
|
|
1259
|
-
const tr = rows
|
|
1260
|
-
.map(([label, from, to, unit]) => {
|
|
1261
|
-
const d =
|
|
1262
|
-
typeof from === 'number' && typeof to === 'number' ? to - from : null;
|
|
1263
|
-
const good =
|
|
1264
|
-
label.includes('violation') || label.includes('Violation')
|
|
1265
|
-
? d != null && d <= 0
|
|
1266
|
-
: label.includes('Governed') || label.includes('score') || label.includes('Classified') || label.includes('Gates')
|
|
1267
|
-
? d != null && d >= 0
|
|
1268
|
-
: null;
|
|
1269
|
-
const cls =
|
|
1270
|
-
d == null || d === 0 ? 'flat' : good === true ? 'up' : good === false ? 'down' : 'flat';
|
|
1271
|
-
const delta =
|
|
1272
|
-
d == null
|
|
1273
|
-
? '—'
|
|
1274
|
-
: unit === 'pp'
|
|
1275
|
-
? formatDelta(Math.round(d * 10) / 10, { suffix: ' pp' })
|
|
1276
|
-
: formatDelta(d);
|
|
1277
|
-
return `<tr>
|
|
1278
|
-
<td>${esc(label)}</td>
|
|
1279
|
-
<td class="num">${from ?? '—'}</td>
|
|
1280
|
-
<td class="num">${to ?? '—'}</td>
|
|
1281
|
-
<td class="num delta ${cls}">${esc(delta)}</td>
|
|
1282
|
-
</tr>`;
|
|
1283
|
-
})
|
|
1284
|
-
.join('\n');
|
|
1285
|
-
// Layer file deltas
|
|
1286
|
-
const originLayers = originSnapshot.layerFiles || {};
|
|
1287
|
-
const currentLayers = currentSnapshot.layerFiles || {};
|
|
1288
|
-
const layerKeys = [...new Set([...Object.keys(originLayers), ...Object.keys(currentLayers)])].sort();
|
|
1289
|
-
const layerTr = layerKeys
|
|
1290
|
-
.map((name) => {
|
|
1291
|
-
const from = originLayers[name] || 0;
|
|
1292
|
-
const to = currentLayers[name] || 0;
|
|
1293
|
-
const d = to - from;
|
|
1294
|
-
const cls = d === 0 ? 'flat' : d > 0 ? 'up' : 'down';
|
|
1295
|
-
return `<tr>
|
|
1296
|
-
<td class="ln">${esc(name)}</td>
|
|
1297
|
-
<td class="num">${from}</td>
|
|
1298
|
-
<td class="num">${to}</td>
|
|
1299
|
-
<td class="num delta ${cls}">${esc(formatDelta(d))}</td>
|
|
1300
|
-
</tr>`;
|
|
1301
|
-
})
|
|
1302
|
-
.join('\n');
|
|
1303
|
-
return `<div class="section card evolve">
|
|
1304
|
-
<h2>Evolution vs origin</h2>
|
|
1305
|
-
<p class="dim" style="margin:.15rem 0 .75rem;font-size:.88rem">
|
|
1306
|
-
Origin snapshot <code>${esc(originDate)}</code> → this report <code>${esc(nowDate)}</code>
|
|
1307
|
-
· frozen at <code>.ark/reports/origin.*</code> · reopen origin HTML anytime for the starting picture.
|
|
1308
|
-
</p>
|
|
1309
|
-
<table class="layers">
|
|
1310
|
-
<tr><th>Metric</th><th>Origin</th><th>Now</th><th>Δ</th></tr>
|
|
1311
|
-
${tr}
|
|
1312
|
-
</table>
|
|
1313
|
-
<h3>Files per layer</h3>
|
|
1314
|
-
<table class="layers">
|
|
1315
|
-
<tr><th>Layer</th><th>Origin</th><th>Now</th><th>Δ</th></tr>
|
|
1316
|
-
${layerTr || '<tr><td colspan="4" class="dim">No layer file data in snapshots.</td></tr>'}
|
|
1317
|
-
</table>
|
|
1318
|
-
<p class="legend">Green Δ = improvement for that metric (↑ coverage/score/gates, ↓ violations). History JSON under <code>.ark/reports/history/</code> (last ${ARK_REPORT_HISTORY_MAX}).</p>
|
|
1319
|
-
</div>`;
|
|
1320
|
-
})()}
|
|
1234
|
+
${renderEvolutionSection({
|
|
1235
|
+
originSnapshot,
|
|
1236
|
+
currentSnapshot,
|
|
1237
|
+
originJustCreated,
|
|
1238
|
+
esc,
|
|
1239
|
+
formatDelta,
|
|
1240
|
+
historyMax: ARK_REPORT_HISTORY_MAX,
|
|
1241
|
+
})}
|
|
1321
1242
|
|
|
1322
1243
|
<div class="section card senior">
|
|
1323
1244
|
<h2>Senior diagnostics</h2>
|
|
@@ -35,32 +35,55 @@ export function readTsconfigAliases(ts, root) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function canonicalPathWithMissingTail(value) {
|
|
39
|
+
const resolved = path.resolve(value);
|
|
40
|
+
let current = resolved;
|
|
41
|
+
const tail = [];
|
|
42
|
+
while (true) {
|
|
43
|
+
try {
|
|
44
|
+
return path.join(fs.realpathSync(current), ...tail.reverse());
|
|
45
|
+
} catch {
|
|
46
|
+
const parent = path.dirname(current);
|
|
47
|
+
if (parent === current) return resolved;
|
|
48
|
+
tail.push(path.basename(current));
|
|
49
|
+
current = parent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
/**
|
|
39
55
|
* Resolve an import specifier to a repo-relative path.
|
|
40
56
|
* Relative + tsconfig-aliased only; bare packages → undefined.
|
|
41
57
|
*/
|
|
42
58
|
export function resolveSpecifierToRel(specifier, fromFilePath, root, tsAliases) {
|
|
59
|
+
const canonicalRoot = canonicalPathWithMissingTail(root);
|
|
43
60
|
let abs;
|
|
44
61
|
if (specifier.startsWith('./') || specifier.startsWith('../')) {
|
|
45
62
|
if (!fromFilePath) return undefined;
|
|
46
|
-
const fromAbs =
|
|
47
|
-
? fromFilePath
|
|
48
|
-
|
|
49
|
-
abs = path.resolve(path.dirname(fromAbs), specifier);
|
|
63
|
+
const fromAbs = canonicalPathWithMissingTail(
|
|
64
|
+
path.isAbsolute(fromFilePath) ? fromFilePath : path.resolve(root, fromFilePath)
|
|
65
|
+
);
|
|
66
|
+
abs = canonicalPathWithMissingTail(path.resolve(path.dirname(fromAbs), specifier));
|
|
50
67
|
} else {
|
|
51
68
|
const alias = tsAliases.aliases.find((a) => specifier.startsWith(a.from));
|
|
52
69
|
if (!alias) return undefined;
|
|
53
|
-
abs =
|
|
70
|
+
abs = canonicalPathWithMissingTail(
|
|
71
|
+
path.resolve(tsAliases.baseUrl, `${alias.to}${specifier.slice(alias.from.length)}`)
|
|
72
|
+
);
|
|
54
73
|
}
|
|
55
|
-
const
|
|
56
|
-
|
|
74
|
+
const relative = path.relative(canonicalRoot, abs);
|
|
75
|
+
if (path.isAbsolute(relative) || relative.startsWith('..')) return undefined;
|
|
76
|
+
return relative.split(path.sep).join('/');
|
|
57
77
|
}
|
|
58
78
|
|
|
59
79
|
function filePathToRel(filePath, root) {
|
|
60
80
|
if (!filePath || typeof filePath !== 'string') return undefined;
|
|
61
|
-
const abs =
|
|
62
|
-
|
|
63
|
-
|
|
81
|
+
const abs = canonicalPathWithMissingTail(
|
|
82
|
+
path.isAbsolute(filePath) ? filePath : path.resolve(root, filePath)
|
|
83
|
+
);
|
|
84
|
+
const relative = path.relative(canonicalPathWithMissingTail(root), abs);
|
|
85
|
+
if (path.isAbsolute(relative) || relative.startsWith('..')) return undefined;
|
|
86
|
+
return relative.split(path.sep).join('/');
|
|
64
87
|
}
|
|
65
88
|
|
|
66
89
|
function classifyProbe(root, rel, layers) {
|
|
@@ -130,4 +153,3 @@ export function createImportTargetResolver(ts, root, config) {
|
|
|
130
153
|
return undefined;
|
|
131
154
|
};
|
|
132
155
|
}
|
|
133
|
-
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { arkCommand } from '../ark-shared.mjs';
|
|
4
|
+
import { codexProjectMcpIsValid } from './codex-home.mjs';
|
|
5
|
+
import { codexRuntimeActivation } from './enforcement-state.mjs';
|
|
6
|
+
|
|
7
|
+
export function inspectCodexInstallActivation(root, enabled) {
|
|
8
|
+
let configuredOnDisk = false;
|
|
9
|
+
if (enabled) {
|
|
10
|
+
try {
|
|
11
|
+
configuredOnDisk = codexProjectMcpIsValid(
|
|
12
|
+
fs.readFileSync(path.join(root, '.codex', 'config.toml'), 'utf8'),
|
|
13
|
+
root
|
|
14
|
+
);
|
|
15
|
+
} catch {
|
|
16
|
+
// Missing/unreadable configuration remains explicitly unverified.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
codexProjectConfigured: configuredOnDisk,
|
|
21
|
+
runtimeActivation: codexRuntimeActivation({
|
|
22
|
+
configuredOnDisk,
|
|
23
|
+
restartRequired: configuredOnDisk,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function reportPartialInstall({
|
|
29
|
+
root,
|
|
30
|
+
tools,
|
|
31
|
+
results,
|
|
32
|
+
homeResults,
|
|
33
|
+
earlyWritten,
|
|
34
|
+
codexMcp,
|
|
35
|
+
runtimeActivation,
|
|
36
|
+
}) {
|
|
37
|
+
const failed = [...results, ...homeResults]
|
|
38
|
+
.filter((result) => result.status === 'failed')
|
|
39
|
+
.map((result) => ({
|
|
40
|
+
target: result.relativePath ?? '(unknown template)',
|
|
41
|
+
reason: 'write failed',
|
|
42
|
+
}));
|
|
43
|
+
if (codexMcp?.status === 'failed') {
|
|
44
|
+
failed.push({
|
|
45
|
+
target: codexMcp.file,
|
|
46
|
+
reason: codexMcp.message ?? 'Codex MCP registration failed',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (failed.length === 0) return false;
|
|
50
|
+
|
|
51
|
+
const written = new Set([
|
|
52
|
+
...earlyWritten,
|
|
53
|
+
...[...results, ...homeResults]
|
|
54
|
+
.filter((result) => result.status === 'written' || result.status === 'merged')
|
|
55
|
+
.map((result) => result.relativePath),
|
|
56
|
+
]);
|
|
57
|
+
console.error('\nINSTALL PARTIAL — some artifacts were written; activation is not complete.');
|
|
58
|
+
console.error('Written:');
|
|
59
|
+
if (written.size === 0) console.error(' - (none)');
|
|
60
|
+
for (const relativePath of written) console.error(` - ${relativePath}`);
|
|
61
|
+
console.error('Failed:');
|
|
62
|
+
for (const failure of failed) console.error(` - ${failure.target}: ${failure.reason}`);
|
|
63
|
+
console.error('Recovery:');
|
|
64
|
+
console.error(
|
|
65
|
+
` - Fix the listed path or permission error, then re-run ${arkCommand(root, 'ark-check', `--install-agent-gates${tools.size > 0 ? ` --tools ${[...tools].join(',')}` : ''}`)}.`
|
|
66
|
+
);
|
|
67
|
+
console.error(' - Existing customized files were preserved; no destructive rollback was attempted.');
|
|
68
|
+
if (tools.has('codex')) {
|
|
69
|
+
console.error(` - Runtime activation: ${JSON.stringify(runtimeActivation)}`);
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function printCodexActivationHandoff(root, configuredOnDisk, runtimeActivation) {
|
|
75
|
+
console.log(
|
|
76
|
+
configuredOnDisk
|
|
77
|
+
? ' CODEX MCP CONFIGURED — RUNTIME NOT VERIFIED'
|
|
78
|
+
: ' CODEX MCP CONFIGURATION UNRESOLVED — RUNTIME NOT VERIFIED'
|
|
79
|
+
);
|
|
80
|
+
console.log(` Runtime activation: ${JSON.stringify(runtimeActivation)}`);
|
|
81
|
+
console.log(
|
|
82
|
+
configuredOnDisk
|
|
83
|
+
? ` Restart Codex, then call ark_identity with expectedRoot "${path.resolve(root)}".`
|
|
84
|
+
: ' Repair `.codex/config.toml`, then restart Codex and call ark_identity.'
|
|
85
|
+
);
|
|
86
|
+
console.log(' Do not trust MCP verdicts before the project identity matches.');
|
|
87
|
+
}
|
|
@@ -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}.`);
|