arkgate 3.6.1 → 3.8.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 (83) hide show
  1. package/CHANGELOG.md +122 -1145
  2. package/README.md +59 -19
  3. package/bin/ark-check-runtime.mjs +1598 -0
  4. package/bin/ark-check.mjs +32 -1565
  5. package/bin/ark-layer-match.mjs +2 -1
  6. package/bin/ark-mcp-runtime.mjs +1976 -0
  7. package/bin/ark-mcp.mjs +84 -1495
  8. package/bin/ark-shared.mjs +34 -38
  9. package/bin/ark.mjs +33 -66
  10. package/bin/lib/adapter-contract.mjs +161 -9
  11. package/bin/lib/agent-gates.mjs +1 -0
  12. package/bin/lib/analysis-completeness.mjs +28 -0
  13. package/bin/lib/analysis-engine.mjs +8 -8
  14. package/bin/lib/analysis-policy.mjs +27 -0
  15. package/bin/lib/architecture-scan.mjs +70 -304
  16. package/bin/lib/auto-patch.mjs +76 -8
  17. package/bin/lib/ci-and-commands.mjs +1 -1
  18. package/bin/lib/codex-home.mjs +43 -16
  19. package/bin/lib/design-delta.mjs +4 -0
  20. package/bin/lib/design-smells.mjs +67 -14
  21. package/bin/lib/doctor-advisories.mjs +23 -7
  22. package/bin/lib/doctor-plan.mjs +44 -47
  23. package/bin/lib/enforcement-state.mjs +2 -0
  24. package/bin/lib/github-enforcement.mjs +443 -0
  25. package/bin/lib/hook-templates.mjs +12 -148
  26. package/bin/lib/html-report-advisories.mjs +59 -0
  27. package/bin/lib/html-report-depth.mjs +9 -0
  28. package/bin/lib/html-report.mjs +5 -5
  29. package/bin/lib/install-migrate.mjs +83 -79
  30. package/bin/lib/managed-upgrade.mjs +622 -0
  31. package/bin/lib/mcp-adoption.mjs +3 -1
  32. package/bin/lib/parse-health.mjs +75 -0
  33. package/bin/lib/port-proof.mjs +2 -2
  34. package/bin/lib/prepare-change.mjs +68 -38
  35. package/bin/lib/prepare-write.mjs +7 -1
  36. package/bin/lib/reshape-decisions.mjs +284 -0
  37. package/bin/lib/resident-doctor-client.mjs +55 -0
  38. package/bin/lib/resident-hook.mjs +247 -0
  39. package/bin/lib/resolved-candidate-facts.mjs +1160 -0
  40. package/bin/lib/scan-files.mjs +19 -6
  41. package/bin/lib/snippet-analysis.mjs +119 -0
  42. package/bin/lib/source-policy.mjs +24 -0
  43. package/bin/lib/typescript-host.mjs +15 -18
  44. package/bin/lib/unavailable-analysis.mjs +76 -0
  45. package/bin/lib/upgrade-command.mjs +115 -0
  46. package/bin/lib/weakest-link.mjs +21 -179
  47. package/bin/lib/write-path-capabilities.mjs +167 -16
  48. package/bin/lib/write-path-detect.mjs +3 -2
  49. package/dist/eslint/index.cjs +3 -3
  50. package/dist/eslint/index.d.ts +3 -0
  51. package/dist/eslint/index.js +3 -3
  52. package/dist/index.cjs +7 -7
  53. package/dist/index.d.ts +1073 -141
  54. package/dist/index.js +7 -7
  55. package/docs/agent-guide.md +127 -52
  56. package/docs/ai-gates.md +100 -18
  57. package/docs/configuration.md +6 -0
  58. package/docs/demos/01-write-gate-self-correction.md +2 -2
  59. package/docs/enthusiast/README.md +10 -10
  60. package/docs/enthusiast/how-to-gallery-starter.md +2 -2
  61. package/docs/enthusiast/reference-commands.md +18 -1
  62. package/docs/enthusiast/tutorial-first-project.md +2 -2
  63. package/docs/package-surface.md +101 -14
  64. package/docs/typescript-support.md +118 -37
  65. package/package.json +33 -4
  66. package/schemas/ark.analysis-result.schema.json +159 -2
  67. package/schemas/ark.design-delta.schema.json +1 -0
  68. package/schemas/ark.enforcement-state.schema.json +84 -0
  69. package/schemas/ark.resolved-candidate-facts.schema.json +1 -0
  70. package/server.json +2 -2
  71. package/templates/skills/ark-autopilot.md +12 -0
  72. package/templates/skills/ark-explore.md +12 -5
  73. package/templates/skills/ark-fix.md +12 -2
  74. package/templates/skills/ark-loop.md +14 -1
  75. package/templates/skills/ark-runtime.md +15 -8
  76. package/templates/skills/ark-upgrade.md +122 -182
  77. package/bin/lib/ai-velocity.mjs +0 -293
  78. package/bin/lib/graph-cycles.mjs +0 -6
  79. package/bin/lib/safety-diagnostics.mjs +0 -284
  80. package/bin/lib/ts-resolve.mjs +0 -227
  81. package/dist/configTypes-DAPvBqK6.d.cts +0 -61
  82. package/dist/eslint/index.d.cts +0 -146
  83. package/dist/index.d.cts +0 -986
@@ -152,6 +152,46 @@ function ambientStateHtml(state) {
152
152
  </section>`;
153
153
  }
154
154
 
155
+ function reshapeDecisionsHtml(memory) {
156
+ if (!memory) return '';
157
+ const lifecycle = memory.lifecycle ?? {};
158
+ const rows = [];
159
+ if (memory.decisionFile?.invalid) {
160
+ rows.push(
161
+ `<p><span class="tag warn">invalid</span> ${esc(memory.decisionFile.path)} is ignored; no reshape decision suppresses a pilot.</p>`
162
+ );
163
+ }
164
+ for (const decision of memory.current ?? []) {
165
+ const tag = decision.verdict === 'accepted' ? 'ok' : 'warn';
166
+ const review = decision.reviewBy ? ` · review-by ${esc(decision.reviewBy)}` : '';
167
+ rows.push(
168
+ `<p><span class="tag ${tag}">${esc(decision.verdict)}</span> <b>${esc(decision.concept)}</b>${review} — ${esc(decision.reason)}${decision.suppressesPilot ? ' Pilot pressure suppressed; mirror facts remain visible.' : ' Pilot remains available.'}</p>`
169
+ );
170
+ }
171
+ if ((memory.currentCount ?? 0) > (memory.current?.length ?? 0)) {
172
+ rows.push(`<p class="muted">…(+${memory.currentCount - memory.current.length} more current decision(s))</p>`);
173
+ }
174
+ if ((lifecycle.expiredCount ?? 0) > 0) {
175
+ rows.push(`<p><span class="tag warn">expired</span> ${lifecycle.expiredCount} decision(s) no longer apply; pilot pressure is active again.</p>`);
176
+ }
177
+ if ((lifecycle.malformedCount ?? 0) > 0) {
178
+ rows.push(`<p><span class="tag warn">malformed</span> ${lifecycle.malformedCount} review-by date(s) are invalid; those decisions are ignored.</p>`);
179
+ }
180
+ if ((lifecycle.staleCount ?? 0) > 0) {
181
+ const stale = (lifecycle.stale ?? [])
182
+ .slice(0, 4)
183
+ .map((decision) => `<code>${esc(decision.concept)}</code>`)
184
+ .join(' · ');
185
+ const more = lifecycle.staleCount > 4 ? ` …(+${lifecycle.staleCount - 4} more)` : '';
186
+ rows.push(`<p><span class="tag warn">stale</span> ${lifecycle.staleCount} decision(s) have a changed anchor set and no longer apply: ${stale}${more}</p>`);
187
+ }
188
+ if ((lifecycle.undated ?? 0) > 0) {
189
+ rows.push(`<p class="muted">${lifecycle.undated} current decision(s) have no review-by date.</p>`);
190
+ }
191
+ if (rows.length === 0) return '';
192
+ return `<div data-advisory="reshapeDecisions"><h3>Reshape decisions <span class="muted">(explicit; pilot pressure only)</span></h3>${rows.join('\n')}</div>`;
193
+ }
194
+
155
195
  function physicalCohesionHtml(pc) {
156
196
  if (!pc) return '';
157
197
  const findings = Array.isArray(pc.findings) ? pc.findings : [];
@@ -173,10 +213,28 @@ function physicalCohesionHtml(pc) {
173
213
  <section data-advisory="physicalCohesion">
174
214
  <h2>Physical cohesion <span class="muted">(advisory — facts, not a score; the verdict is unchanged)</span></h2>
175
215
  ${body}
216
+ ${reshapeDecisionsHtml(pc.reshapeDecisions)}
176
217
  ${pilot}
177
218
  </section>`;
178
219
  }
179
220
 
221
+ function parseHealthHtml(health) {
222
+ if (!health) return '';
223
+ const files = Array.isArray(health.files) ? health.files : [];
224
+ const body = health.available === false
225
+ ? '<p class="muted">Parse health was not available for this rendering — no clean claim is made.</p>'
226
+ : health.affectedFiles === 0
227
+ ? `<p class="muted">No parse diagnostics found across ${health.scannedFiles ?? 0} governed file(s) scanned.</p>`
228
+ : `<p><span class="tag warn">${health.affectedFiles} affected</span> ${health.diagnosticCount} parse diagnostic(s) across ${health.scannedFiles} governed file(s).</p>` +
229
+ `<ul>${files.map((f) => `<li><code>${esc(f.file)}</code> — ${f.diagnosticCount} parse diagnostic(s)</li>`).join('')}</ul>` +
230
+ (health.truncated > 0 ? `<p class="muted">…(+${health.truncated} more affected file(s); doctor list capped)</p>` : '');
231
+ return `
232
+ <section data-advisory="parseHealth">
233
+ <h2>Parse health <span class="muted">(completeness evidence — affected syntax makes analysis partial)</span></h2>
234
+ ${body}
235
+ </section>`;
236
+ }
237
+
180
238
  /**
181
239
  * Render every doctor advisory as report sections. Keys must cover everything
182
240
  * `computeDoctorAdvisories` returns — the parity guard enforces it.
@@ -189,6 +247,7 @@ export function renderAdvisorySections(advisories, escape) {
189
247
  contractHealthHtml(advisories.contractHealth),
190
248
  ambientStateHtml(advisories.ambientState),
191
249
  physicalCohesionHtml(advisories.physicalCohesion),
250
+ parseHealthHtml(advisories.parseHealth),
192
251
  ]
193
252
  .filter(Boolean)
194
253
  .join('\n');
@@ -106,6 +106,14 @@ export function renderWritePathAdoptionBlock(writePath) {
106
106
  const gapNote = writePath.gap
107
107
  ? ` Gap: <b>${esc(writePath.gap.id)}</b> — ${esc(writePath.gap.message || '')}`
108
108
  : '';
109
+ const state = writePath.enforcementState;
110
+ const value = (entry) => entry === true ? 'yes' : entry === false ? 'no' : String(entry);
111
+ const boundary = (label, entry) => entry
112
+ ? `<li><b>${esc(label)}</b> — supported ${esc(value(entry.supported))}; analyzed ${esc(value(entry.analyzed))}; configured ${esc(value(entry.configured))}; installed ${esc(value(entry.installed))}; runtime observed ${esc(value(entry.runtimeObserved))}; operation ${esc(entry.operation ?? 'none')}; operation covered ${esc(value(entry.operationCoverage))}; active ${esc(value(entry.active))}; bypassable ${esc(value(entry.bypassable))}; required ${esc(value(entry.required))}; hard ${esc(value(entry.hard))}</li>`
113
+ : '';
114
+ const stateHtml = state
115
+ ? `<ul class="senior-list" style="margin:.45rem 0 0">${boundary('Local write', state.localWrite)}${boundary('Advisory MCP', state.advisoryMcp)}${boundary('CI merge', state.ciMerge)}</ul>`
116
+ : '';
109
117
  // invLine / gapNote already include escaped user content; only plain strings go through esc().
110
118
  return `<div class="write-path-block" title="${esc(writePathModeHint(mode))}">
111
119
  <p class="dim" style="margin:.65rem 0 .2rem;font-size:.84rem">
@@ -117,6 +125,7 @@ export function renderWritePathAdoptionBlock(writePath) {
117
125
  <p class="kpi-hint" style="max-width:none;margin:0">
118
126
  ${esc(writePathModeHint(mode))} ${invLine}${esc(unknownNote)}${gapNote}
119
127
  </p>
128
+ ${stateHtml}
120
129
  </div>`;
121
130
  }
122
131
 
@@ -51,9 +51,9 @@ export function detectEnforcement(root) {
51
51
  '.grok/hooks/ark-write-gate.json') ||
52
52
  null;
53
53
  return [
54
- { name: 'Write gate', where: writeGateFile, what: 'blocks a bad edit as you type (PreToolUse hook / MCP)' },
54
+ { name: 'Write gate', where: writeGateFile, what: 'configured local gate; active blocking depends on observed host/runtime evidence' },
55
55
  { name: 'ESLint', where: eslintFile || null, what: 'flags violations in your editor' },
56
- { name: 'CI check', where: workflowsMentionArk(), what: 'blocks the merge if the architecture breaks' },
56
+ { name: 'CI check', where: workflowsMentionArk(), what: 'runs Ark; merge blocking requires provider-confirmed required status' },
57
57
  { name: 'Baseline', where: has('.ark-baseline.json') ? '.ark-baseline.json' : null, what: 'old violations frozen; new ones fail' },
58
58
  ].map((e) => ({ ...e, on: !!e.where }));
59
59
  }
@@ -1061,8 +1061,8 @@ export function renderHtmlReport({
1061
1061
  )}
1062
1062
  ${metricKpi(
1063
1063
  `${gatesOn}/${enforcement.length}`,
1064
- 'Gates live',
1065
- 'Write hook, CI workflow, ESLint plugin, and baseline file — how many enforcement points are actually present.'
1064
+ 'Gates configured',
1065
+ 'Write hook, CI workflow, ESLint plugin, and baseline file — how many enforcement surfaces are present, not proof that each is active or required.'
1066
1066
  )}
1067
1067
  ${metricKpi(
1068
1068
  `${violations.length}${suppressed ? ` · ${suppressed}Δ` : ''}`,
@@ -1222,7 +1222,7 @@ export function renderHtmlReport({
1222
1222
  ['Type-only violations', originSnapshot.typeOnlyViolations, currentSnapshot.typeOnlyViolations, ''],
1223
1223
  ['Layers', originSnapshot.layerCount, currentSnapshot.layerCount, ''],
1224
1224
  ['Deny rules', originSnapshot.denyRules, currentSnapshot.denyRules, ''],
1225
- ['Gates live', originSnapshot.gatesOn, currentSnapshot.gatesOn, ''],
1225
+ ['Gates configured', originSnapshot.gatesOn, currentSnapshot.gatesOn, ''],
1226
1226
  ];
1227
1227
  const originDate = (originSnapshot.generatedAt || '').slice(0, 10) || 'origin';
1228
1228
  const nowDate = (currentSnapshot.generatedAt || '').slice(0, 10) || 'now';
@@ -125,6 +125,81 @@ export function warnLockfileConflict(root) {
125
125
  );
126
126
  }
127
127
 
128
+ /**
129
+ * Canonical, side-effect-free catalog of project assets emitted by the gate installer.
130
+ * Upgrade consumes this same allowlist; a manifest can record identities but can never
131
+ * introduce a new write path.
132
+ */
133
+ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsOnly = false }) {
134
+ const selectedTools = tools instanceof Set ? tools : new Set(tools ?? []);
135
+ const assets = [];
136
+ const add = (relativePath, content, kind = 'gate', scope = 'whole-file') => {
137
+ assets.push({
138
+ relativePath,
139
+ content,
140
+ kind,
141
+ scope,
142
+ templateId: `${kind}:${relativePath}`,
143
+ });
144
+ };
145
+
146
+ if (!skillsOnly) {
147
+ const compactHost = compact ? [...selectedTools][0] ?? null : null;
148
+ add(
149
+ 'AGENTS.md',
150
+ compact ? compactAgentInstructions(root, compactHost) : agentInstructions(root)
151
+ );
152
+ if (!compact || !compactHost || compactHost === 'claude') add('.mcp.json', mcpJson(root));
153
+ const deploy = detectDeployPathQuality(root);
154
+ add(
155
+ '.github/workflows/ark-check.yml',
156
+ githubWorkflow(packageManager(root), detectCiNode(root), {
157
+ hasLintScript: deploy.hasLintScript,
158
+ hasTypecheckScript: deploy.hasTypecheckScript,
159
+ })
160
+ );
161
+ if (selectedTools.has('cursor')) {
162
+ add('.cursor/mcp.json', mcpJson(root));
163
+ if (!compact) add('.cursor/rules/ark.mdc', cursorRule(root));
164
+ }
165
+ if (selectedTools.has('claude')) add('.claude/settings.json', claudeSettings(root));
166
+ if (selectedTools.has('codex')) {
167
+ add('.codex/hooks.json', codexHooks(root));
168
+ add('.codex/config.toml', codexProjectConfig(root), 'gate', 'toml-section');
169
+ if (!compact) add('docs/ark-codex-config.toml', codexTomlSnippet(root));
170
+ }
171
+ if (selectedTools.has('grok')) {
172
+ add('.grok/config.toml', grokProjectConfig(root));
173
+ add('.grok/hooks/ark-write-gate.json', grokHooks(root));
174
+ }
175
+ if (selectedTools.has('windsurf')) add('.windsurf/rules/ark.md', instructionRule(root));
176
+ if (selectedTools.has('cline')) add('.clinerules/ark.md', instructionRule(root));
177
+ if (selectedTools.has('copilot')) {
178
+ add('.github/copilot-instructions.md', instructionRule(root));
179
+ }
180
+ if (selectedTools.has('kiro')) add('.kiro/steering/ark.md', instructionRule(root));
181
+ if (selectedTools.has('roo')) add('.roo/rules/ark.md', instructionRule(root));
182
+ if (selectedTools.has('continue')) add('.continue/rules/ark.md', instructionRule(root));
183
+ if (selectedTools.has('gemini')) add('GEMINI.md', instructionRule(root));
184
+ }
185
+
186
+ const version = arkPackageVersion();
187
+ const skills = skillTemplates().map(([name, content]) => [name, stampSkill(content, version)]);
188
+ const skillPaths = new Set();
189
+ if (!compact) {
190
+ for (const tool of selectedTools) {
191
+ const target = SKILL_TOOL_TARGETS[tool];
192
+ if (!target) continue;
193
+ for (const [name, content] of skills) {
194
+ const relativePath = target(name);
195
+ skillPaths.add(relativePath);
196
+ add(relativePath, content, 'skill');
197
+ }
198
+ }
199
+ }
200
+ return { assets, skills, skillPaths, version };
201
+ }
202
+
128
203
  // --migrate-commands: rewrite ONLY the Ark command runner in existing gate files to the
129
204
  // project's package manager (no --force clobber). Closes the upgrade gap where a repo that
130
205
  // adopted before the package-manager-aware templates keeps a stale `npx`.
@@ -220,7 +295,6 @@ export function runInstallAgentGates(args) {
220
295
  process.exitCode = 2;
221
296
  return;
222
297
  }
223
- const pm = packageManager(root);
224
298
  const hasCheckScript = hasCheckArchitectureScript(root);
225
299
  const { tools, source } = args.compact && args.tools == null
226
300
  ? { tools: new Set(), source: 'compact-none' }
@@ -239,7 +313,6 @@ export function runInstallAgentGates(args) {
239
313
  ? 'no active host detected'
240
314
  : 'default set — no agent config dirs found';
241
315
  console.log(`Agent gates for: ${[...tools].sort().join(', ')} (${toolSource})`);
242
- const templates = [];
243
316
  // --skills-only refreshes just the canonical /ark-* skills, which are safe to
244
317
  // overwrite (they track the package). The gate/instruction files (AGENTS.md,
245
318
  // settings.json, CI workflow, rules) are the ones users customize, so a plain
@@ -253,84 +326,15 @@ export function runInstallAgentGates(args) {
253
326
  `Added package.json script "typecheck": "${typecheckBootstrap.script}" (tsconfig present; local/CI parity).`
254
327
  );
255
328
  }
256
- // Base gates: tool-agnostic contract + CI backstop, always written.
257
- const compactHost = args.compact ? [...tools][0] ?? null : null;
258
- templates.push([
259
- 'AGENTS.md',
260
- args.compact ? compactAgentInstructions(root, compactHost) : agentInstructions(root),
261
- ]);
262
- if (!args.compact || !compactHost || compactHost === 'claude') {
263
- templates.push(['.mcp.json', mcpJson(root)]);
264
- }
265
- templates.push([
266
- '.github/workflows/ark-check.yml',
267
- (() => {
268
- const deploy = detectDeployPathQuality(root);
269
- return githubWorkflow(pm, detectCiNode(root), {
270
- hasLintScript: deploy.hasLintScript,
271
- hasTypecheckScript: deploy.hasTypecheckScript,
272
- });
273
- })(),
274
- ]);
275
- if (tools.has('cursor')) {
276
- templates.push(['.cursor/mcp.json', mcpJson(root)]);
277
- if (!args.compact) templates.push(['.cursor/rules/ark.mdc', cursorRule(root)]);
278
- }
279
- if (tools.has('claude')) {
280
- templates.push(['.claude/settings.json', claudeSettings(root)]);
281
- }
282
- if (tools.has('codex')) {
283
- templates.push(['.codex/hooks.json', codexHooks(root)]);
284
- templates.push(['.codex/config.toml', codexProjectConfig(root)]);
285
- if (!args.compact) templates.push(['docs/ark-codex-config.toml', codexTomlSnippet(root)]);
286
- }
287
- if (tools.has('grok')) {
288
- templates.push(['.grok/config.toml', grokProjectConfig(root)]);
289
- templates.push(['.grok/hooks/ark-write-gate.json', grokHooks(root)]);
290
- }
291
- // Instruction-tier hosts: one shared rule text, host-specific path.
292
- if (tools.has('windsurf')) {
293
- templates.push(['.windsurf/rules/ark.md', instructionRule(root)]);
294
- }
295
- if (tools.has('cline')) {
296
- templates.push(['.clinerules/ark.md', instructionRule(root)]);
297
- }
298
- if (tools.has('copilot')) {
299
- templates.push(['.github/copilot-instructions.md', instructionRule(root)]);
300
- }
301
- if (tools.has('kiro')) {
302
- templates.push(['.kiro/steering/ark.md', instructionRule(root)]);
303
- }
304
- if (tools.has('roo')) {
305
- templates.push(['.roo/rules/ark.md', instructionRule(root)]);
306
- }
307
- if (tools.has('continue')) {
308
- templates.push(['.continue/rules/ark.md', instructionRule(root)]);
309
- }
310
- // Gemini CLI reads GEMINI.md as its primary project context (it also reads
311
- // AGENTS.md, but GEMINI.md wins when both are present), so the rule lives there.
312
- if (tools.has('gemini')) {
313
- templates.push(['GEMINI.md', instructionRule(root)]);
314
- }
315
- }
316
- // /ark-* skills for every detected tool that supports project-level commands.
317
- // Stamp each with the shipping version so a later ark-check can flag skills
318
- // left behind by an older Ark (see detectSkillGaps) without nagging about
319
- // user edits to the body.
320
- const version = arkPackageVersion();
321
- const skills = skillTemplates().map(([name, content]) => [name, stampSkill(content, version)]);
322
- const skillPaths = new Set();
323
- if (!args.compact) {
324
- for (const tool of tools) {
325
- const target = SKILL_TOOL_TARGETS[tool];
326
- if (!target) continue;
327
- for (const [name, content] of skills) {
328
- const relativePath = target(name);
329
- skillPaths.add(relativePath);
330
- templates.push([relativePath, content]);
331
- }
332
- }
333
329
  }
330
+ const catalog = buildManagedAssetCatalog({
331
+ root,
332
+ tools,
333
+ compact: args.compact,
334
+ skillsOnly: args.skillsOnly,
335
+ });
336
+ const { skills, skillPaths, version } = catalog;
337
+ const templates = catalog.assets.map(({ relativePath, content }) => [relativePath, content]);
334
338
 
335
339
  // A compact router can be moved back from an explicit host removal. Delete the
336
340
  // generic MCP file only when it exactly matches Ark's generated artifact.