arkgate 4.6.7 → 4.7.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +20 -9
  3. package/SECURITY.md +1 -1
  4. package/bin/ark-check-runtime.mjs +13 -1
  5. package/bin/ark-mcp-runtime.mjs +65 -3
  6. package/bin/lib/adapter-contract.mjs +17 -36
  7. package/bin/lib/agent-skills-package.mjs +29 -0
  8. package/bin/lib/analysis-engine.mjs +6 -6
  9. package/bin/lib/ark-run-doctor.mjs +144 -0
  10. package/bin/lib/ark-run-facts.mjs +472 -0
  11. package/bin/lib/ark-run-report.mjs +57 -0
  12. package/bin/lib/ark-run-sensors.mjs +309 -0
  13. package/bin/lib/check-args.mjs +1 -0
  14. package/bin/lib/config-contract.mjs +86 -11
  15. package/bin/lib/diagnostic-catalog.mjs +8 -0
  16. package/bin/lib/doctor-advisories.mjs +45 -8
  17. package/bin/lib/doctor-human.mjs +10 -0
  18. package/bin/lib/doctor-next-actions.mjs +5 -1
  19. package/bin/lib/doctor-plan.mjs +20 -16
  20. package/bin/lib/extra-merge-teeth.mjs +187 -0
  21. package/bin/lib/first-run-help.mjs +2 -2
  22. package/bin/lib/html-report-advisories.mjs +2 -0
  23. package/bin/lib/html-report-depth.mjs +22 -2
  24. package/bin/lib/html-report.mjs +16 -0
  25. package/bin/lib/install-migrate.mjs +29 -57
  26. package/bin/lib/managed-upgrade.mjs +2 -7
  27. package/bin/lib/remediation.mjs +132 -0
  28. package/bin/lib/resolved-candidate-facts.mjs +67 -2
  29. package/bin/lib/rules-under-contract.mjs +37 -89
  30. package/bin/lib/skill-catalog-apply.mjs +126 -0
  31. package/bin/lib/skill-install.mjs +264 -33
  32. package/bin/lib/skill-write.mjs +3 -0
  33. package/bin/lib/snippet-analysis.mjs +43 -2
  34. package/bin/lib/status-command.mjs +28 -0
  35. package/bin/lib/status-manifest.mjs +23 -0
  36. package/dist/{configTypes-l6XiwiC1.d.ts → configTypes-CgJimx9o.d.ts} +17 -3
  37. package/dist/eslint/index.cjs +6 -2
  38. package/dist/eslint/index.d.ts +70 -2
  39. package/dist/eslint/index.js +6 -2
  40. package/dist/index.cjs +35 -35
  41. package/dist/index.d.ts +804 -272
  42. package/dist/index.js +35 -35
  43. package/docs/README.md +3 -3
  44. package/docs/agent-guide.md +22 -16
  45. package/docs/ai-gates.md +15 -2
  46. package/docs/configuration.md +24 -11
  47. package/docs/develop.md +12 -3
  48. package/docs/diagnostics.md +75 -0
  49. package/docs/enthusiast/README.md +4 -3
  50. package/docs/enthusiast/how-to-agent-gates.md +1 -1
  51. package/docs/package-surface.md +16 -13
  52. package/docs/product-voice.md +6 -3
  53. package/docs/threat-model.md +1 -1
  54. package/docs/use.md +5 -4
  55. package/package.json +1 -1
  56. package/schemas/ark.config.schema.json +41 -2
  57. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  58. package/schemas/ark.status-manifest.schema.json +47 -0
  59. package/server.json +2 -2
  60. package/templates/agent-skills/README.md +1 -1
  61. package/templates/agent-skills/ark-adopt/SKILL.md +23 -2
  62. package/templates/agent-skills/ark-contract/SKILL.md +8 -7
  63. package/templates/agent-skills/ark-place/SKILL.md +26 -2
  64. package/templates/agent-skills/ark-runtime/SKILL.md +66 -24
  65. package/templates/skills/ark-adopt.md +23 -2
  66. package/templates/skills/ark-contract.md +8 -7
  67. package/templates/skills/ark-place.md +26 -2
  68. package/templates/skills/ark-runtime.md +66 -24
@@ -8,7 +8,7 @@
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
 
11
- import { isScanExcludedRelative } from '../ark-shared.mjs';
11
+ import { globToRegExp, isScanExcludedRelative } from '../ark-shared.mjs';
12
12
  import {
13
13
  AMBIENT_CAPABILITY_ENTRIES,
14
14
  collectCapabilityUses,
@@ -35,6 +35,11 @@ import {
35
35
  } from './ast-scan.mjs';
36
36
  import { provePortProofInject } from './port-proof.mjs';
37
37
  import { extractClassShapesFromSource } from './arkrules-sensors.mjs';
38
+ import {
39
+ extractArkRunDeclarationsFromSource,
40
+ extractArkRunKernelCallsFromSource,
41
+ extractArkRunManagedNewsFromSource,
42
+ } from './ark-run-facts.mjs';
38
43
  import {
39
44
  collectGovernedFiles,
40
45
  isGovernableSourceFile,
@@ -998,6 +1003,12 @@ export function resolveCandidateFacts({
998
1003
  const safetyUses = [];
999
1004
  /** ADR 0013 class-shape facts for ArkRules structure sensors. */
1000
1005
  const classShapes = [];
1006
+ /** ADR 0022 / RN03 — syntax evidence only; sensors emit in RN04. */
1007
+ const arkRunKernelCalls = [];
1008
+ const arkRunManagedNews = [];
1009
+ const arkRunCompositionRootHits = [];
1010
+ const arkRunDeclarations = [];
1011
+ const compositionRootPatterns = [...(config.arkRun?.compositionRoots ?? [])];
1001
1012
 
1002
1013
  for (const candidate of candidateFiles) {
1003
1014
  const sourceFile = ts.createSourceFile(
@@ -1087,6 +1098,56 @@ export function resolveCandidateFacts({
1087
1098
  } catch {
1088
1099
  // Never fail the resolver for shape extraction; sensors stay silent on this file.
1089
1100
  }
1101
+ try {
1102
+ arkRunKernelCalls.push(
1103
+ ...extractArkRunKernelCallsFromSource(candidate.path, candidate.content)
1104
+ );
1105
+ } catch {
1106
+ // Never fail the resolver for ArkRun call extraction.
1107
+ }
1108
+ try {
1109
+ arkRunDeclarations.push(
1110
+ ...extractArkRunDeclarationsFromSource(candidate.path, candidate.content)
1111
+ );
1112
+ } catch {
1113
+ // Never fail the resolver for ArkRun declaration extraction.
1114
+ }
1115
+ }
1116
+ }
1117
+
1118
+ const admittedTypeNames = new Set(classShapes.map((shape) => shape.className));
1119
+ for (const candidate of candidateFiles) {
1120
+ if (!/\.(tsx?|mts|cts)$/i.test(candidate.path)) continue;
1121
+ try {
1122
+ arkRunManagedNews.push(
1123
+ ...extractArkRunManagedNewsFromSource(
1124
+ candidate.path,
1125
+ candidate.content,
1126
+ admittedTypeNames
1127
+ )
1128
+ );
1129
+ } catch {
1130
+ // Never fail the resolver for managed-new extraction.
1131
+ }
1132
+ }
1133
+
1134
+ if (compositionRootPatterns.length > 0) {
1135
+ const factoryFiles = new Set(
1136
+ arkRunKernelCalls.filter((call) => call.kind === 'factory').map((call) => call.file)
1137
+ );
1138
+ for (const candidate of candidateFiles) {
1139
+ for (const pattern of compositionRootPatterns) {
1140
+ try {
1141
+ if (!globToRegExp(pattern).test(candidate.path)) continue;
1142
+ } catch {
1143
+ continue;
1144
+ }
1145
+ arkRunCompositionRootHits.push({
1146
+ file: candidate.path,
1147
+ matchedRoot: pattern,
1148
+ hasKernelFactory: factoryFiles.has(candidate.path),
1149
+ });
1150
+ }
1090
1151
  }
1091
1152
  }
1092
1153
 
@@ -1152,7 +1213,7 @@ export function resolveCandidateFacts({
1152
1213
 
1153
1214
  const projectPackageName = readPackageName(canonicalRoot, observeInput);
1154
1215
  return createTrustedResolvedCandidateFacts({
1155
- schemaVersion: '1.1',
1216
+ schemaVersion: '1.2',
1156
1217
  completeness: completenessReasons.length === 0 ? 'complete' : 'partial',
1157
1218
  completenessReasons,
1158
1219
  resolverIdentity: RESOLVED_FACTS_RESOLVER_IDENTITY,
@@ -1169,5 +1230,9 @@ export function resolveCandidateFacts({
1169
1230
  intentReferences,
1170
1231
  safetyUses,
1171
1232
  classShapes,
1233
+ arkRunKernelCalls,
1234
+ arkRunManagedNews,
1235
+ arkRunCompositionRootHits,
1236
+ arkRunDeclarations,
1172
1237
  });
1173
1238
  }
@@ -7,6 +7,11 @@
7
7
  import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
8
8
  import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
9
9
  import { loadInvariantCoverageInputs } from './invariant-coverage-io.mjs';
10
+ import {
11
+ EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
12
+ composeMergePlanesHonesty,
13
+ demoteExtraPlaneTeethUnderClassificationFloor,
14
+ } from './extra-merge-teeth.mjs';
10
15
 
11
16
  /**
12
17
  * Cap long catalogs in doctor JSON (and HTML, which consumes the same summary).
@@ -16,12 +21,11 @@ const COVERED_SAMPLE_MAX = 24;
16
21
  const STRUCTURE_CATALOG_MAX = 40;
17
22
  const UNCOVERED_CATALOG_MAX = 30;
18
23
 
19
- /** Minimum governed % before enforced ArkRules may arm extra merge teeth (P1M / FG-EXTRATEETH). */
20
- export const EXTRA_MERGE_TEETH_GOVERNED_FLOOR = 50;
24
+ export { EXTRA_MERGE_TEETH_GOVERNED_FLOOR };
21
25
 
22
26
  /**
23
27
  * P1M / extraMergeTeeth: under the classification floor, demote enforced ArkRules
24
- * structure/invariant findings so merge matches doctor stamp (layer graph only).
28
+ * and ArkRun findings so merge matches doctor stamp (layer graph only).
25
29
  * Unknown classification (null/null) → do not demote (contract-only callers).
26
30
  *
27
31
  * @param {object[]} violations
@@ -29,28 +33,7 @@ export const EXTRA_MERGE_TEETH_GOVERNED_FLOOR = 50;
29
33
  * @returns {object[]}
30
34
  */
31
35
  export function demoteArkRuleTeethUnderClassificationFloor(violations, classification = {}) {
32
- if (!Array.isArray(violations)) return violations;
33
- const governed =
34
- typeof classification.governedPercent === 'number' ? classification.governedPercent : null;
35
- const populated =
36
- typeof classification.populatedLayerCount === 'number'
37
- ? classification.populatedLayerCount
38
- : null;
39
- if (governed == null && populated == null) return violations;
40
- const allowsTeeth =
41
- (governed ?? 0) >= EXTRA_MERGE_TEETH_GOVERNED_FLOOR && (populated ?? 0) >= 1;
42
- if (allowsTeeth) return violations;
43
- for (const v of violations) {
44
- const isArkRule =
45
- v?.arkruleId != null ||
46
- (typeof v?.ruleId === 'string' &&
47
- (v.ruleId.startsWith('ARKRULE') || v.ruleId.startsWith('arkrule')));
48
- if (isArkRule && v.failsStrict !== false) {
49
- v.failsStrict = false;
50
- if (v.severity === 'error') v.severity = 'warning';
51
- }
52
- }
53
- return violations;
36
+ return demoteExtraPlaneTeethUnderClassificationFloor(violations, classification);
54
37
  }
55
38
 
56
39
  /**
@@ -63,6 +46,18 @@ export function demoteArkRuleTeethUnderClassificationFloor(violations, classific
63
46
  * classifiedFiles?: number | null,
64
47
  * }} [classification] layer-plane coverage (when known)
65
48
  */
49
+ function arkRunMergeInput(config, residualCount = 0) {
50
+ const extra = config?.arkRun;
51
+ if (!extra || typeof extra !== 'object') {
52
+ return { present: false, mode: null, residualCount: 0 };
53
+ }
54
+ return {
55
+ present: true,
56
+ mode: extra.mode === 'enforced' || extra.mode === 'advisory' ? extra.mode : null,
57
+ residualCount: Number(residualCount) || 0,
58
+ };
59
+ }
60
+
66
61
  export function summarizeRulesUnderContract(root, config, facts, classification) {
67
62
  if (!config?.arkRules || Object.keys(config.arkRules).length === 0) {
68
63
  return {
@@ -71,6 +66,11 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
71
66
  invariants: 0,
72
67
  coveredInvariants: 0,
73
68
  uncoveredInvariants: 0,
69
+ mergePlanes: composeMergePlanesHonesty({
70
+ classification,
71
+ arkRules: { active: false },
72
+ arkRun: arkRunMergeInput(config),
73
+ }),
74
74
  notAScore: true,
75
75
  note: 'No arkRules map — intra-layer ArkRules are opt-in.',
76
76
  };
@@ -162,73 +162,21 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
162
162
  const invariantAdvisory = invariants - invariantEnforced;
163
163
  const coveredInvariants = coverage.coverage.filter((c) => c.covered).length;
164
164
  const uncoveredInvariants = coverage.coverage.filter((c) => !c.covered).length;
165
- const hasEnforcedTeeth = structureEnforced > 0 || invariantEnforced > 0;
166
- // P1M-EXTRATEETH-EMPTY-GRAPH / FG-EXTRATEETH-EMPTY-CLASSIFICATION:
167
- // Do not arm structure/invariant merge teeth when the layer plane is empty or
168
- // barely classified (e.g. 0% governed). Classification unknown → allow teeth
169
- // (contract-only callers / unit tests without coverage).
170
- const governedPercent =
171
- classification && typeof classification.governedPercent === 'number'
172
- ? classification.governedPercent
173
- : null;
174
- const populatedLayerCount =
175
- classification && typeof classification.populatedLayerCount === 'number'
176
- ? classification.populatedLayerCount
177
- : classification && typeof classification.classifiedFiles === 'number'
178
- ? classification.classifiedFiles > 0
179
- ? 1
180
- : 0
181
- : null;
182
- const classificationKnown = governedPercent != null || populatedLayerCount != null;
183
- const classificationAllowsTeeth = !classificationKnown
184
- ? true
185
- : (governedPercent ?? 0) >= EXTRA_MERGE_TEETH_GOVERNED_FLOOR &&
186
- (populatedLayerCount ?? 0) >= 1;
187
- const extraMergeTeeth = hasEnforcedTeeth && classificationAllowsTeeth;
188
- const teethDeferredForClassification =
189
- hasEnforcedTeeth && classificationKnown && !classificationAllowsTeeth;
190
- // P1-M — which plane can fail merge (layers vs enforced structure vs invariants).
191
- const mergePlanes = {
192
- layers: {
193
- role: 'inter-layer-edges',
194
- alwaysOnGate: true,
195
- note: 'Import/export layer graph — the default merge plane. Absent arkRules changes nothing here.',
196
- },
197
- structureSensors: {
198
- role: 'intra-layer-heuristics',
199
- total: structureRules,
200
- enforced: structureEnforced,
201
- advisory: structureAdvisory,
202
- note: 'Structure sensors are heuristics (prefer false negatives). Only mode:enforced fails merge; noisy sensors stay advisory by default. Advisory-only packs never add merge teeth (FG-ARKRULES-ADVISORY-ONLY).',
203
- },
204
- invariants: {
205
- role: 'catalog-plus-coverage',
206
- total: invariants,
207
- enforced: invariantEnforced,
208
- advisory: invariantAdvisory,
165
+ const mergePlanes = composeMergePlanesHonesty({
166
+ classification,
167
+ arkRules: {
168
+ active: true,
169
+ structureEnforced,
170
+ structureTotal: structureRules,
171
+ structureAdvisory,
172
+ invariantEnforced,
173
+ invariantTotal: invariants,
174
+ invariantAdvisory,
209
175
  covered: coveredInvariants,
210
176
  uncovered: uncoveredInvariants,
211
- note: 'Invariants are catalog + coverage evidence, not a business runtime. Enforced + proven-uncovered fails merge; absence of enforced rules adds no extra teeth.',
212
177
  },
213
- dualPlaneStamp:
214
- 'Structure = heuristics; invariants = catalog+coverage evidence (not business runtime). The two planes never merge into one architecture score. Advisory ArkRules ≠ merge teeth.',
215
- extraMergeTeeth,
216
- ...(classificationKnown
217
- ? {
218
- classificationGate: {
219
- governedPercent: governedPercent ?? null,
220
- populatedLayerCount: populatedLayerCount ?? null,
221
- floorPercent: EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
222
- allowsTeeth: classificationAllowsTeeth,
223
- },
224
- }
225
- : {}),
226
- failMergeWhen: extraMergeTeeth
227
- ? 'Layer graph failures plus enforced structure/invariant findings (advisory sensors never fail merge alone).'
228
- : teethDeferredForClassification
229
- ? `Layer graph only — enforced ArkRules structure/invariant findings are demoted under the teeth floor (need ≥${EXTRA_MERGE_TEETH_GOVERNED_FLOOR}% governed and ≥1 populated layer); they do not merge-block until classification is honest.`
230
- : 'Layer graph only — no enforced ArkRules structure/invariant teeth on this tree. Advisory packs do not arm merge teeth.',
231
- };
178
+ arkRun: arkRunMergeInput(config),
179
+ });
232
180
 
233
181
  return {
234
182
  active: true,
@@ -0,0 +1,126 @@
1
+ /**
2
+ * HS post-write skill catalog: adapters, home skip/prune, Claude/Grok home skip.
3
+ * Kept out of install-migrate so that file stays inside its module budget.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { arkCommand } from '../ark-shared.mjs';
8
+ import { installRequestedAgentHomes } from './agent-homes.mjs';
9
+ import { codexSkillsDir } from './codex-home.mjs';
10
+ import {
11
+ canonicalSkillPath,
12
+ linkSkillHostAdapters,
13
+ pruneHomeArkSkillDuplicates,
14
+ skillTemplateNames,
15
+ } from './skill-install.mjs';
16
+ import { installSkillCatalog, skillInstallLine } from './skill-write.mjs';
17
+
18
+ function skillName(skill) {
19
+ return Array.isArray(skill) ? skill[0] : skill?.name || skill;
20
+ }
21
+
22
+ export function projectCatalogReady(root, skillNames) {
23
+ return skillNames.some((name) => fs.existsSync(path.join(root, canonicalSkillPath(name))));
24
+ }
25
+
26
+ export function applySkillCatalogFollowup({
27
+ root,
28
+ tools,
29
+ skills,
30
+ version,
31
+ args,
32
+ }) {
33
+ const skillNames = skills.map(skillName);
34
+ if (!args.compact && skillNames.length > 0) {
35
+ const adapterResults = linkSkillHostAdapters(root, tools, skillNames, Boolean(args.force));
36
+ for (const row of adapterResults) {
37
+ if (row.status === 'linked' || row.status === 'copied') {
38
+ console.log(
39
+ ` ${row.status.padEnd(7)} ${row.relativePath} (adapter → .agents/skills/${row.name})`
40
+ );
41
+ }
42
+ }
43
+ }
44
+ if (args.pruneHomeDuplicates) {
45
+ const pruned = pruneHomeArkSkillDuplicates(
46
+ root,
47
+ skillNames.length ? skillNames : skillTemplateNames()
48
+ );
49
+ if (!pruned.ok) {
50
+ console.log(' skip --prune-home-duplicates (no project .agents/skills catalog yet)');
51
+ } else if (pruned.removed.length === 0) {
52
+ console.log(' skip --prune-home-duplicates (no home ark-* copies)');
53
+ } else {
54
+ console.log(` pruned ${pruned.removed.length} home ark-* path(s) (project catalog is enough)`);
55
+ }
56
+ }
57
+
58
+ const homeResults = [];
59
+ if (args.codexHome) {
60
+ const dir = codexSkillsDir();
61
+ console.log('');
62
+ console.log(
63
+ `Codex home skills (scope=home-shared; source=${version ? `arkgate@${version}` : 'arkgate@unknown'}; target=${dir}/<name>/SKILL.md):`
64
+ );
65
+ console.log(
66
+ ' Compatibility: monotonic downgrade protection requires every shared-catalog writer ' +
67
+ 'to use ArkGate 4.2.0+; pre-4.2 --codex-home ignores this catalog. Upgrade legacy repos first.'
68
+ );
69
+ try {
70
+ fs.mkdirSync(dir, { recursive: true });
71
+ } catch (error) {
72
+ console.error(` FAILED to create ${dir} (${error.message})`);
73
+ homeResults.push({ relativePath: dir, status: 'failed' });
74
+ }
75
+ if (homeResults.length === 0) {
76
+ const hasCatalog = skills.some((skill) =>
77
+ fs.existsSync(path.join(root, '.agents', 'skills', skillName(skill), 'SKILL.md'))
78
+ );
79
+ if (hasCatalog) {
80
+ console.log(
81
+ ' Skip home write — project .agents/skills is the catalog. Codex lists user+repo; a home copy duplicates every /ark-*.'
82
+ );
83
+ console.log(
84
+ ` Remove leftover home copies: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --prune-home-duplicates')}`
85
+ );
86
+ } else {
87
+ for (const result of installSkillCatalog({
88
+ directory: dir,
89
+ skills,
90
+ packageVersion: version,
91
+ force: args.force,
92
+ scope: 'home',
93
+ })) {
94
+ console.log(skillInstallLine(result));
95
+ homeResults.push(result);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ const catalogReady = projectCatalogReady(root, skillNames);
102
+ if ((args.claudeHome || args.grokHome || args.agentHomes) && catalogReady) {
103
+ if (!args.json) {
104
+ console.log('');
105
+ console.log(
106
+ 'Skip Claude/Grok home skill write — project .agents/skills + adapters are the catalog. Home ark-* copies override or duplicate.'
107
+ );
108
+ console.log(
109
+ ` Remove leftover home copies: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --prune-home-duplicates')}`
110
+ );
111
+ }
112
+ } else {
113
+ installRequestedAgentHomes({
114
+ root,
115
+ skills,
116
+ version,
117
+ force: args.force,
118
+ claudeHome: args.claudeHome,
119
+ grokHome: args.grokHome,
120
+ agentHomes: args.agentHomes,
121
+ json: args.json,
122
+ });
123
+ }
124
+
125
+ return { skillNames, homeResults };
126
+ }