arkgate 3.0.4 → 3.1.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/README.md +29 -9
  3. package/bin/ark-check.mjs +69 -54
  4. package/bin/ark-mcp.mjs +267 -26
  5. package/bin/ark.mjs +50 -3
  6. package/bin/lib/adapter-contract.mjs +27 -1
  7. package/bin/lib/agent-gates.mjs +9 -0
  8. package/bin/lib/analysis-engine.mjs +7 -1169
  9. package/bin/lib/ci-and-commands.mjs +4 -0
  10. package/bin/lib/codex-home.mjs +10 -1
  11. package/bin/lib/doctor-plan.mjs +37 -9
  12. package/bin/lib/host-support-matrix.mjs +6 -2
  13. package/bin/lib/install-migrate.mjs +81 -25
  14. package/bin/lib/mcp-adoption.mjs +8 -0
  15. package/bin/lib/policy-delta-io.mjs +161 -0
  16. package/bin/lib/prepare-change.mjs +186 -0
  17. package/bin/lib/remediation.mjs +24 -0
  18. package/bin/lib/skill-install.mjs +302 -22
  19. package/bin/lib/violations.mjs +2 -2
  20. package/bin/lib/weakest-link.mjs +61 -12
  21. package/bin/lib/write-path-capabilities.mjs +70 -2
  22. package/bin/lib/write-path-detect.mjs +18 -11
  23. package/dist/eslint/index.cjs +3 -977
  24. package/dist/eslint/index.js +3 -931
  25. package/dist/index.cjs +6 -1960
  26. package/dist/index.d.cts +152 -5
  27. package/dist/index.d.ts +152 -5
  28. package/dist/index.js +6 -1908
  29. package/docs/agent-guide.md +16 -2
  30. package/docs/ai-gates.md +35 -3
  31. package/docs/configuration.md +44 -0
  32. package/docs/package-surface.md +8 -1
  33. package/docs/threat-model.md +7 -4
  34. package/package.json +6 -5
  35. package/schemas/ark.analysis-result.schema.json +5 -1
  36. package/schemas/ark.change-map.schema.json +77 -0
  37. package/server.json +2 -2
  38. package/templates/skills/ark-upgrade.md +9 -5
  39. package/docs/ark-check-example.json +0 -87
  40. package/docs/demos/03-copilot-autopilot.md +0 -93
  41. package/docs/migrate-from-ark-runtime-kernel.md +0 -174
  42. package/docs/production-hardening.md +0 -100
@@ -48,6 +48,30 @@ export function detectPreCommitArk(root) {
48
48
  return { present, arkAware, path: hit };
49
49
  }
50
50
 
51
+ /**
52
+ * Classify ark-check flags in a workflow or package script body.
53
+ * CLI: `--strict` and `--strict-merge` both set strictConfig + requireGates (fail-closed).
54
+ * `--strict-config` alone does not require gate files.
55
+ *
56
+ * @param {string} text
57
+ * @returns {{ hasFailClosedFlag: boolean, hasStrictConfigOnly: boolean, hasStrictFlag: boolean }}
58
+ */
59
+ export function classifyArkCheckFlags(text) {
60
+ if (!text || typeof text !== 'string') {
61
+ return { hasFailClosedFlag: false, hasStrictConfigOnly: false, hasStrictFlag: false };
62
+ }
63
+ const hasStrictMerge = /--strict-merge\b/.test(text);
64
+ const hasRequireGates = /--require-gates\b/.test(text);
65
+ // Bare --strict (alias of --strict-merge), not --strict-config / already-matched merge.
66
+ const withoutLong = text.replace(/--strict-merge\b/g, ' ').replace(/--strict-config\b/g, ' ');
67
+ const hasBareStrict = /--strict\b/.test(withoutLong);
68
+ const hasFailClosedFlag = hasStrictMerge || hasRequireGates || hasBareStrict;
69
+ const hasStrictConfig = /--strict-config\b/.test(text);
70
+ const hasStrictConfigOnly = hasStrictConfig && !hasFailClosedFlag;
71
+ const hasStrictFlag = hasFailClosedFlag || hasStrictConfigOnly;
72
+ return { hasFailClosedFlag, hasStrictConfigOnly, hasStrictFlag };
73
+ }
74
+
51
75
  /**
52
76
  * @param {string} root
53
77
  * @returns {{
@@ -56,6 +80,9 @@ export function detectPreCommitArk(root) {
56
80
  * arkWorkflowFiles: string[],
57
81
  * hasArkCheckWorkflow: boolean,
58
82
  * hasStrictFlag: boolean,
83
+ * hasFailClosedFlag: boolean,
84
+ * hasStrictConfigOnly: boolean,
85
+ * failClosed: boolean,
59
86
  * hasArchitectureJobName: boolean,
60
87
  * }}
61
88
  */
@@ -67,6 +94,9 @@ export function detectCiEnforcement(root) {
67
94
  arkWorkflowFiles: [],
68
95
  hasArkCheckWorkflow: false,
69
96
  hasStrictFlag: false,
97
+ hasFailClosedFlag: false,
98
+ hasStrictConfigOnly: false,
99
+ failClosed: false,
70
100
  hasArchitectureJobName: false,
71
101
  };
72
102
  if (!out.hasWorkflowsDir) return out;
@@ -77,6 +107,19 @@ export function detectCiEnforcement(root) {
77
107
  return out;
78
108
  }
79
109
  out.workflowFiles = files;
110
+
111
+ let checkArchScript = '';
112
+ try {
113
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
114
+ checkArchScript =
115
+ typeof pkg?.scripts?.['check:architecture'] === 'string'
116
+ ? pkg.scripts['check:architecture']
117
+ : '';
118
+ } catch {
119
+ checkArchScript = '';
120
+ }
121
+ const scriptFlags = classifyArkCheckFlags(checkArchScript);
122
+
80
123
  for (const f of files) {
81
124
  let text = '';
82
125
  try {
@@ -92,9 +135,19 @@ export function detectCiEnforcement(root) {
92
135
  if (mentionsArk) {
93
136
  out.hasArkCheckWorkflow = true;
94
137
  out.arkWorkflowFiles.push(`.github/workflows/${f}`);
95
- if (/--strict\b/.test(text) || /check:architecture/.test(text)) {
138
+ const flags = classifyArkCheckFlags(text);
139
+ const viaScript = /check:architecture/.test(text) && scriptFlags.hasFailClosedFlag;
140
+ if (flags.hasFailClosedFlag || viaScript) {
141
+ out.hasFailClosedFlag = true;
142
+ out.failClosed = true;
143
+ out.hasStrictFlag = true;
144
+ } else if (flags.hasStrictConfigOnly) {
145
+ out.hasStrictConfigOnly = true;
146
+ out.hasStrictFlag = true;
147
+ } else if (flags.hasStrictFlag) {
96
148
  out.hasStrictFlag = true;
97
149
  }
150
+ // check:architecture without fail-closed flags in the script is NOT fail-closed.
98
151
  if (/architecture|ark-check|arkgate-check/i.test(f) || /name:\s*.*ark/i.test(text)) {
99
152
  out.hasArchitectureJobName = true;
100
153
  }
@@ -339,18 +392,14 @@ export function collectWeakestLinkGaps(root, opts = {}) {
339
392
  'CI workflows exist but none run ark-check / arkgate-check / check:architecture',
340
393
  fix: arkCommand(root, 'ark-check', '--install-agent-gates'),
341
394
  });
342
- } else if (
343
- adopted &&
344
- !isProducer &&
345
- ci.hasArkCheckWorkflow &&
346
- !ci.hasStrictFlag
347
- ) {
395
+ } else if (adopted && !isProducer && ci.hasArkCheckWorkflow && !ci.failClosed) {
348
396
  gaps.push({
349
- id: 'enforcement-ci-not-strict',
350
- severity: 'info',
351
- message:
352
- 'Architecture CI job found but does not pass --strict / check:architecture (weaker than recommended)',
353
- fix: 'Add --strict (or npm run check:architecture) to the architecture workflow step',
397
+ id: 'enforcement-ci-not-fail-closed',
398
+ severity: 'warn',
399
+ message: ci.hasStrictConfigOnly
400
+ ? 'Architecture CI uses --strict-config only (config coverage without gate-file presence). Prefer the fail-closed profile.'
401
+ : 'Architecture CI job found but does not use the fail-closed profile (--strict-merge / --strict / --require-gates)',
402
+ fix: 'ark-check --root . --config ark.config.json --strict-merge --baseline .ark-baseline.json',
354
403
  });
355
404
  }
356
405
 
@@ -25,6 +25,66 @@ export const WRITE_CAPABILITY_NAMES = [
25
25
  'repair-payload',
26
26
  ];
27
27
 
28
+ function boundaryState({ supported, evidence, active, bypassable, hard = false, extra = {} }) {
29
+ return {
30
+ supported,
31
+ installed: evidence.length > 0,
32
+ active,
33
+ bypassable,
34
+ hard,
35
+ evidence: [...evidence],
36
+ ...extra,
37
+ };
38
+ }
39
+
40
+ function operationCovered(profile, operation) {
41
+ if (!profile || typeof operation !== 'string') return false;
42
+ const normalized = operation.trim().toLowerCase();
43
+ return profile.hookOperations.some((candidate) => candidate.toLowerCase() === normalized);
44
+ }
45
+
46
+ function buildEnforcementLadder(activeHost, support, evidence, attempt) {
47
+ const localInstalled = evidence['hard-write'].length > 0;
48
+ const observedPreTool = attempt?.boundary === 'pre-tool';
49
+ const covered = observedPreTool && operationCovered(support, attempt.operation);
50
+ const hard = Boolean(
51
+ support?.capabilities['hard-write'] && (localInstalled || observedPreTool) && covered
52
+ );
53
+ const inferredActive = (installed) => (installed ? 'unverified' : false);
54
+ return {
55
+ schemaVersion: '1.0',
56
+ activeHost,
57
+ localWrite: boundaryState({
58
+ supported: Boolean(support?.capabilities['hard-write']),
59
+ evidence: evidence['hard-write'],
60
+ active: observedPreTool ? covered : inferredActive(localInstalled),
61
+ bypassable: !hard,
62
+ hard,
63
+ extra: {
64
+ installed: localInstalled || observedPreTool,
65
+ completePatch: Boolean(covered && attempt?.completePatch),
66
+ coverage: covered && attempt?.completePatch ? 'complete-patch' : support?.hookSurface ?? null,
67
+ ...(observedPreTool
68
+ ? { operation: attempt.operation, operationCovered: covered }
69
+ : { operationCovered: 'unverified' }),
70
+ },
71
+ }),
72
+ advisoryMcp: boundaryState({
73
+ supported: Boolean(support?.capabilities['advisory-write']),
74
+ evidence: evidence['advisory-write'],
75
+ active: inferredActive(evidence['advisory-write'].length > 0),
76
+ bypassable: true,
77
+ }),
78
+ ciMerge: boundaryState({
79
+ supported: true,
80
+ evidence: evidence['merge-gate'],
81
+ active: inferredActive(evidence['merge-gate'].length > 0),
82
+ bypassable: 'unknown',
83
+ extra: { requiredStatus: 'unverified' },
84
+ }),
85
+ };
86
+ }
87
+
28
88
  const KNOWN_HOSTS = HOST_SUPPORT_HOSTS;
29
89
 
30
90
  function unique(values) {
@@ -121,7 +181,9 @@ function hostRecord(hard, advisory, repair, merge) {
121
181
  }
122
182
 
123
183
  export function detectWritePathInventory(root) {
124
- const merge = detectCiEnforcement(root).arkWorkflowFiles;
184
+ // Merge-gate evidence only when CI uses the fail-closed profile (not bare ark-check).
185
+ const ci = detectCiEnforcement(root);
186
+ const merge = ci.failClosed ? ci.arkWorkflowFiles : [];
125
187
  const claudeHook = hookEvidence(root, '.claude/settings.json');
126
188
  const grokHook = hookEvidence(root, '.grok/hooks/ark-write-gate.json');
127
189
  const hosts = {
@@ -162,7 +224,7 @@ export function detectWritePathInventory(root) {
162
224
  };
163
225
  }
164
226
 
165
- export function buildWritePathCapabilityModel(root, explicitHost) {
227
+ export function buildWritePathCapabilityModel(root, explicitHost, attempt) {
166
228
  const inventory = detectWritePathInventory(root);
167
229
  const detectedHost = explicitHost ?? detectActiveAgentHost();
168
230
  const activeHost = KNOWN_HOSTS.includes(detectedHost) ? detectedHost : 'unknown';
@@ -181,6 +243,12 @@ export function buildWritePathCapabilityModel(root, explicitHost) {
181
243
  support: getHostSupportProfile(activeHost),
182
244
  capabilities: capabilityMap(capabilityEvidence),
183
245
  capabilityEvidence,
246
+ enforcementLadder: buildEnforcementLadder(
247
+ activeHost,
248
+ getHostSupportProfile(activeHost),
249
+ capabilityEvidence,
250
+ attempt
251
+ ),
184
252
  inventory,
185
253
  };
186
254
  }
@@ -15,9 +15,9 @@ function installToolsForHost(activeHost) {
15
15
  : activeHost;
16
16
  }
17
17
 
18
- export function detectWritePathCapabilities(root, explicitHost) {
19
- const model = buildWritePathCapabilityModel(root, explicitHost);
20
- const { activeHost, support, capabilities, capabilityEvidence, inventory } = model;
18
+ export function detectWritePathCapabilities(root, explicitHost, attempt) {
19
+ const model = buildWritePathCapabilityModel(root, explicitHost, attempt);
20
+ const { activeHost, support, capabilities, capabilityEvidence, enforcementLadder, inventory } = model;
21
21
  const hardWrite = capabilities['hard-write'];
22
22
  const advisoryWrite = capabilities['advisory-write'];
23
23
  const repairPayload = capabilities['repair-payload'];
@@ -72,17 +72,23 @@ export function detectWritePathCapabilities(root, explicitHost) {
72
72
  ),
73
73
  };
74
74
  } else if (mode === 'mcp-only') {
75
+ const codexHonesty =
76
+ activeHost === 'codex'
77
+ ? 'Codex local write is advisory (MCP + best-effort hooks.json — not a hard boundary; ' +
78
+ 'not equivalent to Claude/Grok PreToolUse hard-write + repair). ' +
79
+ 'The hard merge backstop is CI --strict-merge plus a required status check.'
80
+ : `Active host ${activeHost} has advisory prepare-write/autoPatch tools, ` +
81
+ 'but no hard write boundary; the CI check can still reject the change before merge.';
75
82
  gap = {
76
83
  id: 'write-path-mcp-only',
77
84
  severity: 'info',
78
- message:
79
- `Active host ${activeHost} has advisory prepare-write/autoPatch tools, ` +
80
- 'but no hard write boundary; the CI check can still reject the change before merge.',
81
- fix: arkCommand(
82
- root,
83
- 'ark-check',
84
- `--install-agent-gates --tools ${tools}`
85
- ),
85
+ host: activeHost,
86
+ message: codexHonesty,
87
+ fix:
88
+ activeHost === 'codex'
89
+ ? 'Keep CI on --strict-merge and require the ark-check status on the default branch; ' +
90
+ `refresh Codex MCP/skills with ${arkCommand(root, 'ark-check', '--install-agent-gates --tools codex')}`
91
+ : arkCommand(root, 'ark-check', `--install-agent-gates --tools ${tools}`),
86
92
  };
87
93
  }
88
94
 
@@ -92,6 +98,7 @@ export function detectWritePathCapabilities(root, explicitHost) {
92
98
  supportSummary: formatHostSupportSummary(support),
93
99
  capabilities,
94
100
  capabilityEvidence,
101
+ enforcementLadder,
95
102
  inventory,
96
103
  // Compatibility projection for existing doctor/API consumers.
97
104
  mode,