arkgate 4.8.7 → 4.8.9
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 +92 -3
- package/README.md +41 -6
- package/bin/ark-check-runtime.mjs +22 -0
- package/bin/ark-dashboard.mjs +423 -0
- package/bin/ark-layer-match.mjs +25 -10
- package/bin/ark.mjs +51 -3
- package/bin/lib/agent-homes.mjs +1 -1
- package/bin/lib/analysis-engine.mjs +8 -8
- package/bin/lib/ark-order-sensors.mjs +103 -3
- package/bin/lib/config-extras.mjs +1 -0
- package/bin/lib/contract-smells.mjs +12 -6
- package/bin/lib/doctor-human.mjs +32 -7
- package/bin/lib/doctor-next-actions.mjs +21 -2
- package/bin/lib/gate-files.mjs +108 -22
- package/bin/lib/managed-upgrade.mjs +9 -1
- package/bin/lib/upgrade-command.mjs +17 -4
- package/dist/{configTypes-0eHpocR3.d.ts → configTypes-j7so8B4O.d.ts} +12 -0
- package/dist/{diagnosticCatalog-wDAH08gH.d.ts → diagnosticCatalog-BrkOiwCk.d.ts} +3 -3
- package/dist/eslint/index.cjs +5 -5
- package/dist/eslint/index.d.ts +6 -4
- package/dist/eslint/index.js +5 -5
- package/dist/index.cjs +30 -30
- package/dist/index.d.ts +13 -4
- package/dist/index.js +31 -31
- package/dist/nestjs/index.cjs +5 -5
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +5 -5
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.ts +6 -2
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +15 -15
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +15 -15
- package/dist/{types-BK47clMl.d.ts → types-Djbs3KjE.d.ts} +1 -1
- package/dist/{types-CwZ_oz1N.d.ts → types-tGhZUiGX.d.ts} +106 -2
- package/docs/README.md +13 -4
- package/docs/agent-guide.md +38 -2
- package/docs/ai-gates.md +13 -1
- package/docs/arkorder.md +41 -8
- package/docs/configuration.md +47 -12
- package/docs/develop.md +17 -8
- package/docs/enthusiast/README.md +13 -2
- package/docs/package-surface.md +20 -6
- package/docs/product-voice.md +25 -2
- package/docs/use.md +11 -3
- package/package.json +3 -1
- package/schemas/ark.config.schema.json +9 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +1 -0
- package/templates/agent-skills/ark-autopilot/SKILL.md +1 -1
- package/templates/agent-skills/ark-contract/SKILL.md +1 -1
- package/templates/agent-skills/ark-explore/SKILL.md +2 -2
- package/templates/agent-skills/ark-place/SKILL.md +1 -0
- package/templates/skills/ark-adopt.md +1 -0
- package/templates/skills/ark-autopilot.md +1 -1
- package/templates/skills/ark-contract.md +1 -1
- package/templates/skills/ark-explore.md +2 -2
- package/templates/skills/ark-place.md +1 -0
|
@@ -11,6 +11,102 @@
|
|
|
11
11
|
import { extractArkOrderGenericUpdatesFromSource, extractArkOrderIngestWritesXiFromSource, extractArkOrderPlaneCallsFromSource, extractArkOrderReleaseKeyCountsFromSource, extractArkOrderXiFieldWritesFromSource, isArkOrderModuleSpecifier, } from './ark-order-facts.mjs';
|
|
12
12
|
import { extraMergeTeethAllowed, } from './extra-merge-teeth.mjs';
|
|
13
13
|
import { deterministicNextAction } from './remediation.mjs';
|
|
14
|
+
/**
|
|
15
|
+
* XIWRITE-001: same engine as `globToRegExp` in src/domain/layerMatch.ts.
|
|
16
|
+
* Inlined so generate:cli-pure emits a self-contained bin/lib/ark-order-sensors.mjs
|
|
17
|
+
* (layerMatch is derived to bin/ark-layer-match.mjs, not a bin/lib sibling).
|
|
18
|
+
*/
|
|
19
|
+
const appliesToRegexpCache = new Map();
|
|
20
|
+
function escapeAppliesToLiteral(ch) {
|
|
21
|
+
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
22
|
+
}
|
|
23
|
+
function normalizeAppliesToGlob(pattern) {
|
|
24
|
+
let out = '';
|
|
25
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
26
|
+
const c = pattern[i];
|
|
27
|
+
if (c === '\\' && i + 1 < pattern.length) {
|
|
28
|
+
const next = pattern[i + 1];
|
|
29
|
+
if ('*?{}[],'.includes(next) || next === '\\') {
|
|
30
|
+
out += '\\' + next;
|
|
31
|
+
i += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
out += '/';
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
out += c;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function appliesToBracesBalanced(glob) {
|
|
42
|
+
let depth = 0;
|
|
43
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
44
|
+
const c = glob[i];
|
|
45
|
+
if (c === '\\') {
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (c === '{')
|
|
50
|
+
depth += 1;
|
|
51
|
+
else if (c === '}') {
|
|
52
|
+
depth -= 1;
|
|
53
|
+
if (depth < 0)
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return depth === 0;
|
|
58
|
+
}
|
|
59
|
+
function globToRegExp(pattern) {
|
|
60
|
+
const cached = appliesToRegexpCache.get(pattern);
|
|
61
|
+
if (cached)
|
|
62
|
+
return cached;
|
|
63
|
+
const glob = normalizeAppliesToGlob(pattern);
|
|
64
|
+
const useBraces = appliesToBracesBalanced(glob);
|
|
65
|
+
let out = '';
|
|
66
|
+
let braceDepth = 0;
|
|
67
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
68
|
+
const c = glob[i];
|
|
69
|
+
if (c === '\\' && i + 1 < glob.length) {
|
|
70
|
+
out += escapeAppliesToLiteral(glob[i + 1]);
|
|
71
|
+
i += 1;
|
|
72
|
+
}
|
|
73
|
+
else if (c === '*') {
|
|
74
|
+
if (glob[i + 1] === '*') {
|
|
75
|
+
if (glob[i + 2] === '/') {
|
|
76
|
+
out += '(?:.*/)?';
|
|
77
|
+
i += 2;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
out += '.*';
|
|
81
|
+
i += 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
out += '[^/]*';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (c === '?') {
|
|
89
|
+
out += '[^/]';
|
|
90
|
+
}
|
|
91
|
+
else if (c === '{' && useBraces) {
|
|
92
|
+
out += '(?:';
|
|
93
|
+
braceDepth += 1;
|
|
94
|
+
}
|
|
95
|
+
else if (c === '}' && useBraces && braceDepth > 0) {
|
|
96
|
+
out += ')';
|
|
97
|
+
braceDepth -= 1;
|
|
98
|
+
}
|
|
99
|
+
else if (c === ',' && useBraces && braceDepth > 0) {
|
|
100
|
+
out += '|';
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
out += escapeAppliesToLiteral(c);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const re = new RegExp(`^${out}$`);
|
|
107
|
+
appliesToRegexpCache.set(pattern, re);
|
|
108
|
+
return re;
|
|
109
|
+
}
|
|
14
110
|
export const ARKORDER_TIER1_SENSOR_IDS = [
|
|
15
111
|
'arkorder-missing-plane',
|
|
16
112
|
'arkorder-kernel-in-domain',
|
|
@@ -31,6 +127,11 @@ export const ARKORDER_RULE_IDS = {
|
|
|
31
127
|
'arkorder-information-budget': 'ARKORDER_INFORMATION_BUDGET',
|
|
32
128
|
'arkorder-xi-ttl': 'ARKORDER_XI_TTL',
|
|
33
129
|
};
|
|
130
|
+
function matchesArkOrderAppliesTo(file, appliesTo) {
|
|
131
|
+
if (!appliesTo || appliesTo.length === 0)
|
|
132
|
+
return true;
|
|
133
|
+
return appliesTo.some((pattern) => globToRegExp(pattern).test(file));
|
|
134
|
+
}
|
|
34
135
|
function isDomainRoleLayer(layer, intentPrefixes = []) {
|
|
35
136
|
const name = layer.trim();
|
|
36
137
|
if (/^domain(?:model)?$/i.test(name) || /^domain(?=[A-Z_\-\s])/i.test(name))
|
|
@@ -103,9 +204,6 @@ export function evaluateArkOrderSensors(input) {
|
|
|
103
204
|
findings.push(finding(extra, 'arkorder-generic-update', update.file, update.line, `Generic ${update.method}() on the order plane rewrites ξ; Haken forbids it.`, { target: update.method }, teethAllowed));
|
|
104
205
|
}
|
|
105
206
|
const xiKeys = extra.xiKeys ?? [];
|
|
106
|
-
if (xiKeys.length > extra.maxXiKeys) {
|
|
107
|
-
findings.push(finding(extra, 'arkorder-too-many-params', 'ark.config.json', 1, `arkOrder.xiKeys has ${xiKeys.length} keys; maxXiKeys is ${extra.maxXiKeys} (few slow modes).`, { target: String(xiKeys.length) }, teethAllowed));
|
|
108
|
-
}
|
|
109
207
|
for (const release of input.releaseKeyCounts ?? []) {
|
|
110
208
|
if (release.keyCount <= extra.maxXiKeys)
|
|
111
209
|
continue;
|
|
@@ -119,6 +217,8 @@ export function evaluateArkOrderSensors(input) {
|
|
|
119
217
|
const fromLayer = input.layerForFile(write.file);
|
|
120
218
|
if (!fromLayer || !managed.has(fromLayer))
|
|
121
219
|
continue;
|
|
220
|
+
if (!matchesArkOrderAppliesTo(write.file, extra.appliesTo))
|
|
221
|
+
continue;
|
|
122
222
|
findings.push(finding(extra, 'arkorder-xi-field-write', write.file, write.line, `File writes slow key ${JSON.stringify(write.key)} through a persistence driver; route the field through ingest or a pattern change through proposeRelease.`, { fromLayer, target: write.key }, teethAllowed));
|
|
123
223
|
}
|
|
124
224
|
findings.sort((left, right) => left.file.localeCompare(right.file) ||
|
|
@@ -42,6 +42,7 @@ export const ARK_ORDER_SCHEMA_DEF = {
|
|
|
42
42
|
managedLayers: { ...stringArraySchema, default: [] },
|
|
43
43
|
maxXiKeys: { type: 'integer', minimum: 1, default: 7 },
|
|
44
44
|
xiKeys: { ...stringArraySchema, default: [] },
|
|
45
|
+
appliesTo: { ...stringArraySchema, default: [] },
|
|
45
46
|
},
|
|
46
47
|
};
|
|
47
48
|
function isObject(value) {
|
|
@@ -41,7 +41,7 @@ export const CONTRACT_SMELL_OUTCOMES = Object.freeze({
|
|
|
41
41
|
'contract-lateral-adapter-allow':
|
|
42
42
|
'One adapter layer may import another adapter family directly — shared mappers/aliases will pile up in the wrong place. Move shared shapes into Domain (or a shared kernel) instead of adapter-to-adapter reach.',
|
|
43
43
|
'contract-dead-rule':
|
|
44
|
-
'A rule enforces nothing: it points at a layer that matches no files or does not exist, or
|
|
44
|
+
'A rule enforces nothing: it points at a layer that matches no files or does not exist, or is a same-layer allow (classic same-layer deny is also a no-op). A same-layer peerIsolation deny is a live wall, not a dead rule.',
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
export const CONTRACT_SMELL_ACKS_PATH = '.ark/contract-smell-acks.json';
|
|
@@ -277,14 +277,20 @@ export function analyzeContractSmells(
|
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
// 4) Dead rules: self
|
|
281
|
-
// or — when coverage is known — layers matching
|
|
280
|
+
// 4) Dead rules: self-allow and classic same-layer deny (the gate ignores
|
|
281
|
+
// those), unknown layers, or — when coverage is known — layers matching
|
|
282
|
+
// zero files (optional layers exempt). `peerIsolation: true` + `allowed:
|
|
283
|
+
// false` is a live self-edge wall, not a dead rule.
|
|
282
284
|
for (const r of rules) {
|
|
283
285
|
const edge = `${r.from}->${r.to}`;
|
|
284
286
|
const ackEdge = ackMatchable(r.from, r.to) ? edge : null;
|
|
285
287
|
if (r.from === r.to) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
+
const livePeerIsolationWall = r.peerIsolation === true && r.allowed === false;
|
|
289
|
+
if (!livePeerIsolationWall) {
|
|
290
|
+
add('contract-dead-rule', ackEdge, `rule:${edge} (self edge has no effect)`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
// Live slice wall on a self-edge — still check unknown/empty layers.
|
|
288
294
|
}
|
|
289
295
|
for (const side of [r.from, r.to]) {
|
|
290
296
|
if (side.length === 0) continue;
|
|
@@ -409,7 +415,7 @@ function fixFor(id) {
|
|
|
409
415
|
case 'contract-lateral-adapter-allow':
|
|
410
416
|
return `Move shared shapes into Domain/shared kernel and drop the lateral allow (/ark-adopt), or acknowledge with a reason in ${CONTRACT_SMELL_ACKS_PATH}.`;
|
|
411
417
|
case 'contract-dead-rule':
|
|
412
|
-
return 'Fix the layer patterns so the layer matches real files, or delete
|
|
418
|
+
return 'Fix the layer patterns so the layer matches real files, or delete a stale self-allow / classic self-deny via /ark-adopt. Never delete a live peerIsolation wall (`peerIsolation: true` with `allowed: false`).';
|
|
413
419
|
default:
|
|
414
420
|
return 'Review the contract edge via /ark-adopt; never weaken the gate to silence a smell.';
|
|
415
421
|
}
|
package/bin/lib/doctor-human.mjs
CHANGED
|
@@ -13,6 +13,23 @@ import { enforcementDoctorLines } from './enforcement-state.mjs';
|
|
|
13
13
|
import { analysisIncompleteStatement } from './analysis-completeness.mjs';
|
|
14
14
|
import { skillGapsForActiveHost, detectCodexHomeGap, codexConcernIsActive } from './agent-gates.mjs';
|
|
15
15
|
import { agentHomeConcernIsActive } from './agent-homes.mjs';
|
|
16
|
+
import { REQUIRED_GATE_WORKFLOW } from './gate-files.mjs';
|
|
17
|
+
|
|
18
|
+
function displayedMissingGates(gatesMissing, view) {
|
|
19
|
+
const list = Array.isArray(gatesMissing) ? gatesMissing : [];
|
|
20
|
+
const hideGlob = Boolean(
|
|
21
|
+
view?.ciNotFailClosed?.workflowFile ||
|
|
22
|
+
view?.ciNotFailClosed?.error === 'ci-not-fail-closed' ||
|
|
23
|
+
view?.ciMergeBoundary?.ci?.workflowPresent
|
|
24
|
+
);
|
|
25
|
+
return hideGlob ? list.filter((item) => item !== REQUIRED_GATE_WORKFLOW) : list;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ciNotFailClosedNotice(view) {
|
|
29
|
+
const file = view?.ciNotFailClosed?.workflowFile;
|
|
30
|
+
if (!file) return null;
|
|
31
|
+
return `CI not fail-closed: ${file} — remove the skippable if:, or write .ark/adoption-stance.json with stance: advisory-only`;
|
|
32
|
+
}
|
|
16
33
|
|
|
17
34
|
function lineWith(ok, warn, bad, color) {
|
|
18
35
|
return (mark, text) => console.log(` ${mark} ${text}`);
|
|
@@ -52,6 +69,8 @@ export function printDoctorCompactHuman(view) {
|
|
|
52
69
|
gatesMissing,
|
|
53
70
|
violations,
|
|
54
71
|
} = view;
|
|
72
|
+
const listedMissing = displayedMissingGates(gatesMissing, view);
|
|
73
|
+
const skippableCi = ciNotFailClosedNotice(view);
|
|
55
74
|
|
|
56
75
|
console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
|
|
57
76
|
if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness));
|
|
@@ -123,15 +142,17 @@ export function printDoctorCompactHuman(view) {
|
|
|
123
142
|
}
|
|
124
143
|
|
|
125
144
|
const hostRed =
|
|
126
|
-
|
|
145
|
+
listedMissing.length > 0 ||
|
|
127
146
|
Boolean(writePath.gap) ||
|
|
128
|
-
writePathHonesty?.softWriteHost === true
|
|
147
|
+
writePathHonesty?.softWriteHost === true ||
|
|
148
|
+
Boolean(skippableCi);
|
|
129
149
|
if (hostRed) {
|
|
130
150
|
console.log('');
|
|
131
151
|
console.log(color.bold('Host / CI'));
|
|
132
152
|
if (writePath.activeHost) line(' ', `Active host: ${writePath.activeHost}`);
|
|
133
|
-
if (
|
|
134
|
-
|
|
153
|
+
if (listedMissing.length > 0) line(bad, `Missing gates: ${listedMissing.join(', ')}`);
|
|
154
|
+
if (skippableCi) line(warn, skippableCi);
|
|
155
|
+
else if (listedMissing.length === 0 && (writePath.gap || writePathHonesty?.softWriteHost)) {
|
|
135
156
|
line(warn, 'Local writes are advisory; required CI is the merge boundary.');
|
|
136
157
|
}
|
|
137
158
|
}
|
|
@@ -212,6 +233,8 @@ export function printDoctorDetailsHuman(view) {
|
|
|
212
233
|
staleRunners,
|
|
213
234
|
adoption,
|
|
214
235
|
} = view;
|
|
236
|
+
const listedMissing = displayedMissingGates(gatesMissing, view);
|
|
237
|
+
const skippableCi = ciNotFailClosedNotice(view);
|
|
215
238
|
const modeTitle = operatingModeTitle(operatingMode, designFitness.designWeak, stewardUnfinished);
|
|
216
239
|
|
|
217
240
|
console.log('');
|
|
@@ -385,9 +408,11 @@ export function printDoctorDetailsHuman(view) {
|
|
|
385
408
|
|
|
386
409
|
console.log('');
|
|
387
410
|
console.log(color.bold('Gates & skills'));
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
411
|
+
if (listedMissing.length === 0 && !skippableCi) {
|
|
412
|
+
line(ok, 'Shared gate artifacts found on disk (AGENTS.md, .mcp.json, CI); runtime activation is reported separately');
|
|
413
|
+
} else {
|
|
414
|
+
if (listedMissing.length > 0) line(bad, `Missing gates: ${listedMissing.join(', ')}`);
|
|
415
|
+
if (skippableCi) line(warn, skippableCi);
|
|
391
416
|
}
|
|
392
417
|
const humanSkillGaps = skillGapsForActiveHost(skillGaps);
|
|
393
418
|
const legacyCodex = humanSkillGaps.some((g) => g.tool === 'codex' && g.legacyPromptsOnly);
|
|
@@ -7,10 +7,20 @@ import { skillGapsForActiveHost } from './agent-gates.mjs';
|
|
|
7
7
|
import { agentHomeConcernIsActive, agentHomeRefreshCommand } from './agent-homes.mjs';
|
|
8
8
|
import { mergePostGreenTopActions } from './post-green-path.mjs';
|
|
9
9
|
import { ADOPTED_NOT, NOT_ADOPTED_NEXT_ACTION } from './adoption-stance.mjs';
|
|
10
|
+
import { REQUIRED_GATE_WORKFLOW } from './gate-files.mjs';
|
|
11
|
+
|
|
12
|
+
function missingGateFiles(ctx) {
|
|
13
|
+
const list = Array.isArray(ctx.gatesMissing) ? ctx.gatesMissing : [];
|
|
14
|
+
if (ctx.ciNotFailClosed) {
|
|
15
|
+
return list.filter((item) => item !== REQUIRED_GATE_WORKFLOW);
|
|
16
|
+
}
|
|
17
|
+
return list;
|
|
18
|
+
}
|
|
10
19
|
|
|
11
20
|
export function collectDoctorNextActions(ctx) {
|
|
12
21
|
const actions = [];
|
|
13
|
-
const
|
|
22
|
+
const missingFiles = missingGateFiles(ctx);
|
|
23
|
+
const gatesInstalled = missingFiles.length === 0;
|
|
14
24
|
const planAEmpty = !ctx.activeCount;
|
|
15
25
|
const notAdopted = ctx.adopted !== 'required-merge' && ctx.adopted !== 'advisory-only-acked';
|
|
16
26
|
if (notAdopted || ctx.adopted === ADOPTED_NOT || ctx.adopted == null) {
|
|
@@ -55,9 +65,18 @@ export function collectDoctorNextActions(ctx) {
|
|
|
55
65
|
);
|
|
56
66
|
}
|
|
57
67
|
if (ctx.writePath?.gap?.fix && !gatesInstalled) actions.push(ctx.writePath.gap.fix);
|
|
58
|
-
if (!gatesInstalled &&
|
|
68
|
+
if (!gatesInstalled && missingFiles.length > 0) {
|
|
59
69
|
actions.push(`install gates (${arkCommand(ctx.root, 'ark-check', '--install-agent-gates')})`);
|
|
60
70
|
}
|
|
71
|
+
if (ctx.ciNotFailClosed) {
|
|
72
|
+
const file = ctx.ciNotFailClosed.workflowFile;
|
|
73
|
+
actions.push(
|
|
74
|
+
ctx.ciNotFailClosed.nextAction ||
|
|
75
|
+
(file
|
|
76
|
+
? `Remove the skippable if: in ${file}, or write .ark/adoption-stance.json with stance: advisory-only`
|
|
77
|
+
: 'Remove the skippable if:, or write .ark/adoption-stance.json with stance: advisory-only')
|
|
78
|
+
);
|
|
79
|
+
}
|
|
61
80
|
const humanSkillGaps = skillGapsForActiveHost(ctx.skillGaps);
|
|
62
81
|
const legacyCodex = humanSkillGaps.some((g) => g.tool === 'codex' && g.legacyPromptsOnly);
|
|
63
82
|
const remainingGaps = humanSkillGaps.filter(
|
package/bin/lib/gate-files.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { codexProjectMcpIsValid } from './codex-home.mjs';
|
|
8
|
-
import { enforcingArkRunText } from './github-enforcement.mjs';
|
|
8
|
+
import { enforcingArkRunText, runsArkCheck } from './github-enforcement.mjs';
|
|
9
9
|
|
|
10
10
|
export const __packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
11
11
|
export const __arkCheckCli = path.join(__packageRoot, 'bin', 'ark-check.mjs');
|
|
@@ -120,7 +120,8 @@ export const REQUIRED_GATE_FILES = [
|
|
|
120
120
|
'AGENTS.md',
|
|
121
121
|
'.mcp.json',
|
|
122
122
|
];
|
|
123
|
-
const REQUIRED_GATE_WORKFLOW = '.github/workflows/*.yml running ark-check';
|
|
123
|
+
export const REQUIRED_GATE_WORKFLOW = '.github/workflows/*.yml running ark-check';
|
|
124
|
+
export const CI_NOT_FAIL_CLOSED_ERROR = 'ci-not-fail-closed';
|
|
124
125
|
const COMPACT_ROUTER = /<!--\s*arkgate:compact-router host=([a-z]+)\s*-->/;
|
|
125
126
|
const FAIL_CLOSED_ARK_FLAG = /(?:^|\s)--(?:strict|strict-merge|require-gates)(?=\s|$)/;
|
|
126
127
|
|
|
@@ -584,27 +585,112 @@ function withVerifiedDependencyJobs(content) {
|
|
|
584
585
|
return lines.join('\n');
|
|
585
586
|
}
|
|
586
587
|
|
|
587
|
-
|
|
588
|
+
function isGuaranteedJobCondition(value) {
|
|
589
|
+
const text = unquoteYamlScalar(value);
|
|
590
|
+
return (
|
|
591
|
+
/^(?:\$\{\{\s*)?always\(\)(?:\s*\}\})?$/i.test(text) ||
|
|
592
|
+
/^(?:true|\$\{\{\s*true\s*\}\})$/i.test(text)
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function neutralizeSkippableJobControls(content) {
|
|
597
|
+
const { lines, jobs } = workflowJobSections(content);
|
|
598
|
+
for (const job of jobs) {
|
|
599
|
+
const condition = jobProperty(lines, job, 'if');
|
|
600
|
+
if (condition && !isGuaranteedJobCondition(condition.value)) {
|
|
601
|
+
lines[condition.index] = lines[condition.index].replace(
|
|
602
|
+
/^(\s*(?:"if"|'if'|if):\s*).*$/i,
|
|
603
|
+
'$1true'
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
const continuation = jobProperty(lines, job, 'continue-on-error');
|
|
607
|
+
if (continuation && !/^['"]?false['"]?$/i.test(unquoteYamlScalar(continuation.value))) {
|
|
608
|
+
lines[continuation.index] = lines[continuation.index].replace(
|
|
609
|
+
/^(\s*(?:"continue-on-error"|'continue-on-error'|continue-on-error):\s*).*$/i,
|
|
610
|
+
'$1false'
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return lines.join('\n');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function workflowMentionsArkAction(content) {
|
|
618
|
+
return /^\s*(?:-\s+)?uses:\s*['"]?pedroknigge\/arkgate@/im.test(String(content));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function listWorkflowYamlFiles(root) {
|
|
588
622
|
const workflowsDir = path.join(root, '.github', 'workflows');
|
|
589
|
-
if (!fs.existsSync(workflowsDir)) return
|
|
623
|
+
if (!fs.existsSync(workflowsDir)) return [];
|
|
624
|
+
try {
|
|
625
|
+
return fs.readdirSync(workflowsDir).filter((file) => /\.ya?ml$/i.test(file));
|
|
626
|
+
} catch {
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Presence vs fail-closed. A skippable `if:` still means the YAML exists.
|
|
633
|
+
* `failClosed` matches `hasArkWorkflow` (merge line). `present` is any
|
|
634
|
+
* ark-check / arkgate action workflow, including draft-skip jobs.
|
|
635
|
+
*/
|
|
636
|
+
export function inspectArkCiGate(root) {
|
|
637
|
+
const failClosedFiles = [];
|
|
638
|
+
const presentFiles = [];
|
|
590
639
|
const declaredScript = architectureScript(root);
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
640
|
+
const failClosedScript = isFailClosedArchitectureScript(declaredScript)
|
|
641
|
+
? declaredScript
|
|
642
|
+
: '';
|
|
643
|
+
const workflowsDir = path.join(root, '.github', 'workflows');
|
|
644
|
+
for (const file of listWorkflowYamlFiles(root)) {
|
|
645
|
+
let content = '';
|
|
646
|
+
try {
|
|
647
|
+
content = fs.readFileSync(path.join(workflowsDir, file), 'utf8');
|
|
648
|
+
} catch {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const prepared = withVerifiedDependencyJobs(withFailClosedArkActions(content));
|
|
652
|
+
const failClosed = FAIL_CLOSED_ARK_FLAG.test(
|
|
653
|
+
enforcingArkRunText(prepared, failClosedScript)
|
|
654
|
+
);
|
|
655
|
+
const visible = withVerifiedDependencyJobs(
|
|
656
|
+
neutralizeSkippableJobControls(withFailClosedArkActions(content))
|
|
657
|
+
);
|
|
658
|
+
const present =
|
|
659
|
+
failClosed ||
|
|
660
|
+
runsArkCheck(visible, declaredScript) ||
|
|
661
|
+
workflowMentionsArkAction(content);
|
|
662
|
+
const relativePath = `.github/workflows/${file}`;
|
|
663
|
+
if (failClosed) failClosedFiles.push(relativePath);
|
|
664
|
+
if (present) presentFiles.push(relativePath);
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
failClosed: failClosedFiles.length > 0,
|
|
668
|
+
present: presentFiles.length > 0,
|
|
669
|
+
failClosedFiles,
|
|
670
|
+
presentFiles,
|
|
671
|
+
workflowFile:
|
|
672
|
+
presentFiles.find((file) => !failClosedFiles.includes(file)) ??
|
|
673
|
+
presentFiles[0] ??
|
|
674
|
+
null,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export function hasArkWorkflow(root) {
|
|
679
|
+
return inspectArkCiGate(root).failClosed;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function ciNotFailClosed(root) {
|
|
683
|
+
const ci = inspectArkCiGate(root);
|
|
684
|
+
if (!ci.present || ci.failClosed) return null;
|
|
685
|
+
const workflowFile = ci.workflowFile;
|
|
686
|
+
const named = workflowFile || REQUIRED_GATE_WORKFLOW;
|
|
687
|
+
return {
|
|
688
|
+
error: CI_NOT_FAIL_CLOSED_ERROR,
|
|
689
|
+
workflowFile,
|
|
690
|
+
workflowFiles: ci.presentFiles,
|
|
691
|
+
nextAction: `Remove the skippable if: in ${named}, or write .ark/adoption-stance.json with stance: advisory-only`,
|
|
692
|
+
message: `CI is not fail-closed. Workflow ${named} runs ark-check but a skippable if: is not a merge line.`,
|
|
693
|
+
};
|
|
608
694
|
}
|
|
609
695
|
|
|
610
696
|
export function missingGates(root) {
|
|
@@ -615,7 +701,7 @@ export function missingGates(root) {
|
|
|
615
701
|
if (compactHost && !hasCompactHostRegistration(root, compactHost)) {
|
|
616
702
|
missing.push(`compact host registration (${compactHost})`);
|
|
617
703
|
}
|
|
618
|
-
if (!
|
|
704
|
+
if (!inspectArkCiGate(root).present) missing.push(REQUIRED_GATE_WORKFLOW);
|
|
619
705
|
return missing;
|
|
620
706
|
}
|
|
621
707
|
|
|
@@ -494,8 +494,12 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
494
494
|
action: canApply ? (currentScoped == null ? 'create' : 'update') : 'none',
|
|
495
495
|
willApply: canApply,
|
|
496
496
|
blocked,
|
|
497
|
+
// Raw scoped bytes. state/willApply use identity, not these hashes.
|
|
497
498
|
beforeHash: hash(currentScoped == null ? null : Buffer.from(currentScoped)),
|
|
498
499
|
afterHash: hash(Buffer.from(desiredScoped)),
|
|
500
|
+
beforeIdentity:
|
|
501
|
+
currentScoped == null ? null : managedContentIdentity(currentScoped, catalogAsset.kind),
|
|
502
|
+
afterIdentity: managedContentIdentity(desiredScoped, catalogAsset.kind),
|
|
499
503
|
containerBeforeHash: hash(currentFile),
|
|
500
504
|
[AFTER_CONTENT]: desiredFile,
|
|
501
505
|
};
|
|
@@ -531,6 +535,8 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
531
535
|
blocked: false,
|
|
532
536
|
beforeHash: recorded.baseHash,
|
|
533
537
|
afterHash: null,
|
|
538
|
+
beforeIdentity: recorded.contentIdentity ?? null,
|
|
539
|
+
afterIdentity: null,
|
|
534
540
|
containerBeforeHash: null,
|
|
535
541
|
});
|
|
536
542
|
}
|
|
@@ -834,7 +840,9 @@ function assertAssetUnchanged(root, asset) {
|
|
|
834
840
|
export function applyManagedUpgrade(root, plan, expectedPlanDigest) {
|
|
835
841
|
const resolvedRoot = path.resolve(root);
|
|
836
842
|
if (resolvedRoot !== plan.root) throw new Error('managed upgrade plan root mismatch');
|
|
837
|
-
if (plan.summary.blocked > 0)
|
|
843
|
+
if (plan.summary.blocked > 0) {
|
|
844
|
+
return publicPlan(plan, { blocked: true, reasonCode: 'managed-consent-required' });
|
|
845
|
+
}
|
|
838
846
|
const wouldWrite = plan.summary.wouldWrite ?? 0;
|
|
839
847
|
// Content already matches: unbound --apply is a no-op (exit success), not a digest error.
|
|
840
848
|
if (!expectedPlanDigest || expectedPlanDigest !== plan.planDigest) {
|
|
@@ -603,10 +603,23 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
603
603
|
return 2;
|
|
604
604
|
}
|
|
605
605
|
if (applied.blocked) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
606
|
+
const command = buildUpgradeNextCommand(
|
|
607
|
+
{ ...args, acceptConflicts: true },
|
|
608
|
+
applied.planDigest
|
|
609
|
+
);
|
|
610
|
+
if (args.json) {
|
|
611
|
+
console.log(
|
|
612
|
+
managedUpgradeJson(applied, {
|
|
613
|
+
blocked: true,
|
|
614
|
+
reasonCode: applied.reasonCode ?? 'managed-consent-required',
|
|
615
|
+
nextCommand: command,
|
|
616
|
+
})
|
|
617
|
+
);
|
|
618
|
+
} else {
|
|
619
|
+
renderManagedUpgrade(applied, {
|
|
620
|
+
next: 'Preview again with --accept-conflicts, then use that preview\'s exact next command.',
|
|
621
|
+
});
|
|
622
|
+
}
|
|
610
623
|
return 1;
|
|
611
624
|
}
|
|
612
625
|
if (applied.nothingToApply && !applied.applied) {
|
|
@@ -105,12 +105,24 @@ type ArkConfigArkOrder = {
|
|
|
105
105
|
mode: ArkConfigArkOrderMode;
|
|
106
106
|
planeRoots: string[];
|
|
107
107
|
managedLayers: string[];
|
|
108
|
+
/**
|
|
109
|
+
* Haken cap on one `release()` / `assertXiKeyCap` (default 7).
|
|
110
|
+
* Not a cap on the `xiKeys` watchlist length.
|
|
111
|
+
*/
|
|
108
112
|
maxXiKeys: number;
|
|
109
113
|
/**
|
|
110
114
|
* Slow product keys the team can already name (plan, cost code, protocol).
|
|
111
115
|
* Optional. Empty → `ARKORDER_XI_FIELD_WRITE` stays silent.
|
|
116
|
+
* Repo-wide watchlist — not compared to `maxXiKeys`.
|
|
112
117
|
*/
|
|
113
118
|
xiKeys: string[];
|
|
119
|
+
/**
|
|
120
|
+
* Optional globs that narrow ξ field-write observation inside `managedLayers`.
|
|
121
|
+
* Same glob engine as `layers[].patterns`. Absence or empty → every file in
|
|
122
|
+
* those layers. Non-empty → emit only when the layer is managed AND the file
|
|
123
|
+
* matches at least one glob.
|
|
124
|
+
*/
|
|
125
|
+
appliesTo?: string[];
|
|
114
126
|
};
|
|
115
127
|
type ArkConfig = {
|
|
116
128
|
$schema: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { e as CreateArchitectureProfileOptions, b as ArchitectureProfile, d as ArkCheckConfig, C as CreateArchitectureProfileFromArkConfigOptions, f as CreateElevenLayerArkConfigOptions, i as Policy, j as IntentCreator, I as IntentName } from './types-
|
|
2
|
-
import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-
|
|
1
|
+
import { e as CreateArchitectureProfileOptions, b as ArchitectureProfile, d as ArkCheckConfig, C as CreateArchitectureProfileFromArkConfigOptions, f as CreateElevenLayerArkConfigOptions, i as Policy, j as IntentCreator, I as IntentName } from './types-Djbs3KjE.js';
|
|
2
|
+
import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-j7so8B4O.js';
|
|
3
3
|
|
|
4
4
|
/** Versioned public result contract shared by every ArkGate enforcement adapter. */
|
|
5
5
|
/**
|
|
@@ -409,7 +409,7 @@ declare const ARK_ANALYSIS_RESULT_SCHEMA: {
|
|
|
409
409
|
};
|
|
410
410
|
|
|
411
411
|
/** ArkGate library version — single source of truth. */
|
|
412
|
-
declare const version = "4.8.
|
|
412
|
+
declare const version = "4.8.9";
|
|
413
413
|
|
|
414
414
|
/**
|
|
415
415
|
* AI Code Gate (basic).
|