arkgate 4.5.6 → 4.6.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 (47) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +16 -14
  3. package/bin/ark-check-runtime.mjs +15 -5
  4. package/bin/ark-mcp-runtime.mjs +82 -61
  5. package/bin/ark.mjs +2 -2
  6. package/bin/lib/agent-gates.mjs +2 -0
  7. package/bin/lib/agent-homes.mjs +296 -0
  8. package/bin/lib/agent-projection.mjs +1 -1
  9. package/bin/lib/ci-and-commands.mjs +9 -8
  10. package/bin/lib/design-smells.mjs +3 -7
  11. package/bin/lib/doctor-plan.mjs +36 -12
  12. package/bin/lib/gate-files.mjs +1 -1
  13. package/bin/lib/golden-pattern.mjs +1 -1
  14. package/bin/lib/hook-templates.mjs +13 -11
  15. package/bin/lib/host-support-matrix.mjs +32 -12
  16. package/bin/lib/html-report-depth.mjs +7 -8
  17. package/bin/lib/html-report.mjs +2 -1
  18. package/bin/lib/install-migrate.mjs +36 -0
  19. package/bin/lib/managed-upgrade.mjs +6 -1
  20. package/bin/lib/mcp-adoption.mjs +6 -1
  21. package/bin/lib/mcp-process-package.mjs +95 -0
  22. package/bin/lib/post-green-path.mjs +3 -2
  23. package/bin/lib/product-copy.mjs +32 -0
  24. package/bin/lib/skill-write.mjs +1 -1
  25. package/bin/lib/start-preview.mjs +5 -1
  26. package/bin/lib/upgrade-whats-new.mjs +16 -0
  27. package/bin/lib/write-path-capabilities.mjs +62 -1
  28. package/dist/index.cjs +19 -19
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +22 -22
  31. package/docs/README.md +3 -2
  32. package/docs/agent-guide.md +18 -13
  33. package/docs/ai-gates.md +43 -14
  34. package/docs/develop.md +3 -3
  35. package/docs/enthusiast/how-to-agent-gates.md +4 -3
  36. package/docs/package-surface.md +3 -3
  37. package/docs/product-voice.md +80 -74
  38. package/docs/use.md +7 -5
  39. package/package.json +2 -2
  40. package/server.json +3 -3
  41. package/templates/agent-skills/README.md +1 -1
  42. package/templates/agent-skills/ark-autopilot/SKILL.md +1 -1
  43. package/templates/agent-skills/ark-explore/SKILL.md +5 -5
  44. package/templates/agent-skills/ark-upgrade/SKILL.md +2 -1
  45. package/templates/skills/ark-autopilot.md +1 -1
  46. package/templates/skills/ark-explore.md +5 -5
  47. package/templates/skills/ark-upgrade.md +2 -1
@@ -70,9 +70,27 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
70
70
  true,
71
71
  true
72
72
  ),
73
- cursor: hostProfile('Cursor', null, null, [], false, false, {
74
- operationCoverage: { shell: false, 'pre-commit': false },
75
- }),
73
+ // Cursor: official preToolUse deny / exit 2 is a hard block for matched tools when
74
+ // `.cursor/hooks.json` is installed + trusted. Claim hard only for Write|StrReplace;
75
+ // Shell/Tab/human edits still rely on CI. Repair envelope may emit; Write updated_input
76
+ // reinjection is not guaranteed on Cursor (agent_message + retry is the supported path).
77
+ cursor: hostProfile(
78
+ 'Cursor',
79
+ '.cursor/hooks.json',
80
+ 'preToolUse `Write` / `StrReplace`',
81
+ ['Write', 'StrReplace'],
82
+ true,
83
+ false,
84
+ {
85
+ repairEnvelopeEmitted: true,
86
+ operationCoverage: {
87
+ Write: true,
88
+ StrReplace: true,
89
+ shell: false,
90
+ 'pre-commit': false,
91
+ },
92
+ }
93
+ ),
76
94
  codex: hostProfile(
77
95
  'OpenAI Codex',
78
96
  '.codex/hooks.json',
@@ -134,14 +152,15 @@ export function renderHostSupportMatrixMarkdown() {
134
152
  const rows = HOST_SUPPORT_HOSTS.map((host) => {
135
153
  const profile = HOST_SUPPORT_MATRIX[host];
136
154
  const capabilities = profile.capabilities;
137
- // Fail-closed honesty: Cursor/Codex/OpenCode never claim hard write; CI is required-status.
138
- // hookSurface already includes "PreToolUse …" do not prefix PreToolUse again.
155
+ // Fail-closed honesty: Codex/OpenCode never claim hard write; CI is required-status.
156
+ // Cursor claims hard only for listed preToolUse ops when hooks are installed + trusted.
157
+ // hookSurface already includes "PreToolUse/preToolUse …" — do not prefix again.
139
158
  let local;
140
159
  if (capabilities['hard-write']) {
141
160
  local = `**Hard** block for listed ops (${profile.hookSurface}) when installed + trusted`;
142
161
  } else if (host === 'codex') {
143
162
  local =
144
- '**Advisory / best-effort** at write (not equivalent to Claude/Grok hard block)';
163
+ '**Advisory / best-effort** at write (not equivalent to Claude/Grok/Cursor hard block)';
145
164
  } else if (host === 'opencode') {
146
165
  local =
147
166
  '**Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary)';
@@ -169,7 +188,8 @@ ${rows}
169
188
 
170
189
  **Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
171
190
  GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
172
- Cursor/Codex/OpenCode never get a fake hard write claim.
191
+ Codex/OpenCode never get a fake hard write claim. Cursor hard write covers only listed
192
+ \`preToolUse\` ops when \`.cursor/hooks.json\` is installed and trusted — Shell/Tab/human edits still rely on CI.
173
193
 
174
194
  This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run \`arkgate-check --doctor\` (or \`ark-check --doctor\`) for the evidence actually detected in the current repository.`;
175
195
  }
@@ -203,19 +223,19 @@ export function doctorWritePathHonestyMessage(activeHost, hardWriteActive) {
203
223
  // EH07: distinguish CLI command (arkgate-check / ark-check) from the GitHub required status context name.
204
224
  const mergeBoundary =
205
225
  'Required CI hard merge boundary = a required GitHub status context that runs arkgate-check --strict-merge (alias ark-check --strict-merge)';
206
- if (host === 'cursor') {
207
- return `Cursor: write path is advisory (MCP/rules; no hard PreToolUse). ${mergeBoundary}.`;
226
+ if (host === 'cursor' && !hardWriteActive) {
227
+ return `Cursor: pre-write block is supported for Write/StrReplace when .cursor/hooks.json is installed + trusted; without runtime-observed hook evidence, the block is unverified. ${mergeBoundary}.`;
208
228
  }
209
229
  if (host === 'codex') {
210
- return `Codex: write path is advisory / best-effort at write (not Claude/Grok hard). ${mergeBoundary}.`;
230
+ return `Codex: edits are warning only (not blocked) at write time. ${mergeBoundary}.`;
211
231
  }
212
232
  if (host === 'opencode') {
213
- return `OpenCode: write path is advisory / best-effort (MCP + optional plugin; not Claude/Grok/Antigravity hard). ${mergeBoundary}.`;
233
+ return `OpenCode: edits are warning only (not blocked). ${mergeBoundary}.`;
214
234
  }
215
235
  if ((host === 'claude' || host === 'grok' || host === 'antigravity') && !hardWriteActive) {
216
236
  const label =
217
237
  host === 'claude' ? 'Claude' : host === 'grok' ? 'Grok' : 'Antigravity';
218
- return `${label}: hard PreToolUse is supported for listed ops when installed + trusted; without runtime-observed hook evidence, hard is unverified. ${mergeBoundary}.`;
238
+ return `${label}: pre-write block is supported for listed ops when installed + trusted; without runtime-observed hook evidence, the block is unverified. ${mergeBoundary}.`;
219
239
  }
220
240
  return null;
221
241
  }
@@ -24,6 +24,7 @@ import { describePackageVersionDualTruth } from './field-install.mjs';
24
24
  import { buildDoctorImprovementCompass } from './improvement-compass-doctor.mjs';
25
25
  import { buildDeepModuleCoachAdvisory } from './deep-module-coach.mjs';
26
26
  import { computePhysicalCohesion } from './physical-cohesion.mjs';
27
+ import { POST_GREEN_LEDE, operatingModeTitle } from './product-copy.mjs';
27
28
 
28
29
  function esc(value) {
29
30
  return String(value)
@@ -383,13 +384,11 @@ export function renderDesignDepthStrip(depth = {}) {
383
384
 
384
385
  const mode = String(depth.mode || '').toLowerCase();
385
386
  const title = designWeak
386
- ? mode === 'enforce'
387
- ? 'ENFORCE · design-weak'
388
- : `${(mode || 'edges').toUpperCase()} · design-weak`
389
- : 'Design smells (edges still open)';
387
+ ? operatingModeTitle(mode || 'enforce', true)
388
+ : 'Design smells (imports still open)';
390
389
  const lede = designWeak
391
- ? 'Contract edges are clean, but lived design residual remains. This does not fail PASS — it blocks “healthy finished” until Shape work lands.'
392
- : 'Design smells exist alongside open edge debt. Fix edges first; treat smells as Shape residual after green.';
390
+ ? POST_GREEN_LEDE
391
+ : 'Design smells exist alongside open import-rule debt. Fix imports first; treat smells as leftover design work after green.';
393
392
 
394
393
  const smellItems = smells
395
394
  .slice(0, 6)
@@ -411,7 +410,7 @@ export function renderDesignDepthStrip(depth = {}) {
411
410
  ? `<div class="pilot-card">
412
411
  <h3 style="margin-top:.85rem">Next pilot (one at a time)</h3>
413
412
  <p class="dim" style="margin:.15rem 0 .4rem;font-size:.86rem">
414
- Judgment only — never mechanical-safe · never multi-pilot batch
413
+ Judgment only — never auto-applied · never multi-pilot batch
415
414
  </p>
416
415
  <ul class="senior-list">
417
416
  <li><b>Smell</b> · <code>${esc(pilot.smellId || pilot.id || '—')}</code></li>
@@ -489,7 +488,7 @@ export function renderDesignCleanNote(depth = {}) {
489
488
  return `<div class="section card design-strip is-clean" id="design-depth">
490
489
  <div class="design-head">
491
490
  <span class="badge design-ok" title="No deterministic design smells with clean edges">Design depth · OK</span>
492
- <span class="dim" style="font-size:.86rem">No design-weak residual detected</span>
491
+ <span class="dim" style="font-size:.86rem">No leftover design work detected</span>
493
492
  </div>
494
493
  <p class="dim" style="margin:.45rem 0 0;font-size:.88rem">
495
494
  Edges and deterministic design sensors agree. Keep placing new code on the golden path;
@@ -11,6 +11,7 @@ import {
11
11
  resolveOperatingMode,
12
12
  } from '../ark-shared.mjs';
13
13
  import { collectAdoptionGaps, arkCheckCommand } from './agent-gates.mjs';
14
+ import { LEFTOVER_DESIGN_BADGE } from './product-copy.mjs';
14
15
  import { CORE_LAYER_NAMES } from './core-layers.mjs';
15
16
  import {
16
17
  renderBaselineSignalLegend,
@@ -525,7 +526,7 @@ export function renderHtmlReport({
525
526
  const designSmells = Array.isArray(depth.designSmells) ? depth.designSmells : [];
526
527
  const designWeakBadge =
527
528
  designFitness?.designWeak === true
528
- ? ` <span class="badge design" title="Edges can be green while lived design residual remains (Shape). Not a FAIL.">design-weak</span>`
529
+ ? ` <span class="badge design" title="Import rules can be green while leftover design work remains. Not a FAIL.">${LEFTOVER_DESIGN_BADGE}</span>`
529
530
  : '';
530
531
  const designStripHtml =
531
532
  renderDesignDepthStrip({
@@ -25,9 +25,11 @@ import {
25
25
  claudeSettings,
26
26
  codexHooks,
27
27
  codexProjectConfig,
28
+ cursorHooks,
28
29
  grokHooks,
29
30
  grokProjectConfig,
30
31
  mergeAntigravityArkHook,
32
+ mergeCursorArkHook,
31
33
  mergeOpencodeArkMcp,
32
34
  opencodeProjectConfig,
33
35
  } from './hook-templates.mjs';
@@ -78,6 +80,7 @@ import {
78
80
  RUNNER_BEFORE_ARK,
79
81
  } from './mcp-adoption.mjs';
80
82
  import { inspectCodexInstallActivation, printCodexActivationHandoff, reportPartialInstall } from './install-activation.mjs';
83
+ import { installRequestedAgentHomes } from './agent-homes.mjs';
81
84
  import {
82
85
  hasHardWriteHook,
83
86
  validateHardWriteRequest,
@@ -194,6 +197,9 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
194
197
  );
195
198
  if (selectedTools.has('cursor')) {
196
199
  add('.cursor/mcp.json', mcpJson(root));
200
+ // whole-file for managed upgrade/manifest (json-merge is install-time only via
201
+ // mergeCursorArkHook below — same pattern as Antigravity hooks).
202
+ add('.cursor/hooks.json', cursorHooks(root));
197
203
  if (!compact) add('.cursor/rules/ark.mdc', cursorRule(root));
198
204
  }
199
205
  if (selectedTools.has('claude')) add('.claude/settings.json', claudeSettings(root));
@@ -516,6 +522,25 @@ export function runInstallAgentGates(args) {
516
522
  if (merged === existing) return { relativePath, status: 'skipped' };
517
523
  return writeTemplate(root, relativePath, merged, true);
518
524
  }
525
+ if (relativePath === '.cursor/hooks.json') {
526
+ const fullPath = path.join(root, relativePath);
527
+ let existing = '';
528
+ try {
529
+ existing = fs.readFileSync(fullPath, 'utf8');
530
+ } catch {
531
+ // Missing hooks file → write generated Cursor preToolUse gate.
532
+ }
533
+ if (!existing) {
534
+ return writeTemplate(root, relativePath, content, true);
535
+ }
536
+ const merged = mergeCursorArkHook(existing, content);
537
+ if (merged == null) {
538
+ return { relativePath, status: 'skipped-non-ark' };
539
+ }
540
+ if (merged === existing) return { relativePath, status: 'skipped' };
541
+ // Upsert Ark preToolUse without requiring --force; never wipe sibling hooks.
542
+ return writeTemplate(root, relativePath, merged, true);
543
+ }
519
544
  if (relativePath === '.agents/hooks.json') {
520
545
  const fullPath = path.join(root, relativePath);
521
546
  let existing = '';
@@ -623,6 +648,17 @@ export function runInstallAgentGates(args) {
623
648
  }
624
649
  }
625
650
 
651
+ installRequestedAgentHomes({
652
+ root,
653
+ skills,
654
+ version,
655
+ force: args.force,
656
+ claudeHome: args.claudeHome,
657
+ grokHome: args.grokHome,
658
+ agentHomes: args.agentHomes,
659
+ json: args.json,
660
+ });
661
+
626
662
  // Optional legacy/home fallback. Normal Codex installs use the project-scoped
627
663
  // .codex/config.toml above, avoiding cross-project primary binding conflicts.
628
664
  //
@@ -126,7 +126,12 @@ function hasArkText(root, relativePath) {
126
126
 
127
127
  const HOST_SIGNALS = {
128
128
  claude: ['.claude/settings.json', '.claude/skills/ark-upgrade/SKILL.md'],
129
- cursor: ['.cursor/mcp.json', '.cursor/rules/ark.mdc', '.cursor/commands/ark-upgrade.md'],
129
+ cursor: [
130
+ '.cursor/mcp.json',
131
+ '.cursor/hooks.json',
132
+ '.cursor/rules/ark.mdc',
133
+ '.cursor/commands/ark-upgrade.md',
134
+ ],
130
135
  codex: ['.codex/hooks.json', '.codex/config.toml', '.agents/skills/ark-upgrade/SKILL.md'],
131
136
  grok: ['.grok/config.toml', '.grok/hooks/ark-write-gate.json', '.grok/skills/ark-upgrade/SKILL.md'],
132
137
  antigravity: ['.agents/hooks.json', '.agents/skills/ark-upgrade/SKILL.md'],
@@ -27,7 +27,12 @@ export const COMMAND_GATE_TEXT_FILES = [
27
27
  '.grok/hooks/ark-write-gate.json', '.grok/config.toml', '.codex/config.toml',
28
28
  '.agents/hooks.json',
29
29
  ];
30
- export const COMMAND_GATE_JSON_FILES = ['.mcp.json', '.cursor/mcp.json', 'opencode.json'];
30
+ export const COMMAND_GATE_JSON_FILES = [
31
+ '.mcp.json',
32
+ '.cursor/mcp.json',
33
+ '.cursor/hooks.json',
34
+ 'opencode.json',
35
+ ];
31
36
  // Primary CLI names (product) + one-major aliases. migrate-commands must strip ALL of these
32
37
  // before re-emitting a single preferred bin — otherwise a partial rename leaves
33
38
  // args: ["ark-mcp", "arkgate-mcp", ...] which breaks stdio MCP hosts.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * FX06 — MCP process package vs project install honesty.
3
+ *
4
+ * Pure-ish helpers so unit tests need no MCP server. Production MCP runtime
5
+ * calls buildProcessPackageHonesty with the process version + project root.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { createRequire } from 'node:module';
10
+
11
+ /**
12
+ * Resolve installed arkgate version for a project root (shallow node_modules,
13
+ * then Node module resolution for hoisted monorepos).
14
+ * @param {string} root
15
+ * @returns {string|null}
16
+ */
17
+ export function readProjectInstalledArkgateVersion(root) {
18
+ const resolvedRoot = path.resolve(root);
19
+ try {
20
+ const shallow = path.join(resolvedRoot, 'node_modules', 'arkgate', 'package.json');
21
+ if (fs.existsSync(shallow)) {
22
+ const v = JSON.parse(fs.readFileSync(shallow, 'utf8')).version;
23
+ return typeof v === 'string' && v.trim() ? v.trim() : null;
24
+ }
25
+ } catch {
26
+ /* fall through */
27
+ }
28
+ try {
29
+ const requireFromProject = createRequire(path.join(resolvedRoot, 'package.json'));
30
+ const pkgJson = requireFromProject.resolve('arkgate/package.json');
31
+ const v = JSON.parse(fs.readFileSync(pkgJson, 'utf8')).version;
32
+ return typeof v === 'string' && v.trim() ? v.trim() : null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * @param {{
40
+ * processVersion?: string|null,
41
+ * projectInstalledVersion?: string|null,
42
+ * root?: string,
43
+ * }} input
44
+ * @returns {{
45
+ * schemaVersion: '1.0',
46
+ * notAScore: true,
47
+ * processArkgateVersion: string|null,
48
+ * projectInstalledVersion: string|null,
49
+ * processPackageMismatch: boolean,
50
+ * processStale: boolean,
51
+ * nextAction: string,
52
+ * }}
53
+ */
54
+ export function buildProcessPackageHonesty(input = {}) {
55
+ const processVersion =
56
+ typeof input.processVersion === 'string' && input.processVersion.trim()
57
+ ? input.processVersion.trim()
58
+ : null;
59
+ let projectInstalledVersion =
60
+ input.projectInstalledVersion !== undefined
61
+ ? input.projectInstalledVersion
62
+ : null;
63
+ if (
64
+ projectInstalledVersion === null &&
65
+ input.projectInstalledVersion === undefined &&
66
+ typeof input.root === 'string' &&
67
+ input.root
68
+ ) {
69
+ projectInstalledVersion = readProjectInstalledArkgateVersion(input.root);
70
+ }
71
+ if (typeof projectInstalledVersion === 'string') {
72
+ projectInstalledVersion = projectInstalledVersion.trim() || null;
73
+ } else if (projectInstalledVersion !== null) {
74
+ projectInstalledVersion = null;
75
+ }
76
+
77
+ const mismatch =
78
+ processVersion != null &&
79
+ projectInstalledVersion != null &&
80
+ processVersion !== projectInstalledVersion;
81
+
82
+ return {
83
+ schemaVersion: '1.0',
84
+ notAScore: true,
85
+ processArkgateVersion: processVersion,
86
+ projectInstalledVersion,
87
+ processPackageMismatch: mismatch,
88
+ processStale: mismatch,
89
+ nextAction: mismatch
90
+ ? 'Restart or retarget the Ark MCP server so process arkgateVersion matches the project install. Prefer project-local CLI (`npx arkgate` / `npx arkgate-check`) until identity is matched and versions align. Multi-checkout users: one expectedRoot per project; never reuse another checkout’s projectId.'
91
+ : projectInstalledVersion == null
92
+ ? 'Project has no resolvable node_modules/arkgate; install the package or use CLI from a project that pins arkgate.'
93
+ : 'Process package version matches project install for this MCP root.',
94
+ };
95
+ }
@@ -6,6 +6,8 @@
6
6
  * Plan B stays never mechanical-safe.
7
7
  */
8
8
 
9
+ import { POST_GREEN_HUMAN } from './product-copy.mjs';
10
+
9
11
  /** Stable product id for JSON / tests. */
10
12
  export const POST_GREEN_PATH_ID = 'clarify-for-ai';
11
13
 
@@ -16,8 +18,7 @@ export const POST_GREEN_PRIMARY_SKILL = '/ark-explore';
16
18
  * Canonical human / agent next-action string (single door).
17
19
  * Chained: explore shape-focus then autopilot only to apply B with user OK.
18
20
  */
19
- export const POST_GREEN_PRIMARY_ACTION =
20
- 'Shape residual (design-weak): edges are clean, design is not finished. Map with /ark-explore shape-focus → dual-plan B; apply B only via /ark-autopilot with your OK. Empty plan A is not done; pattern bets are never mechanical-safe.';
21
+ export const POST_GREEN_PRIMARY_ACTION = POST_GREEN_HUMAN;
21
22
 
22
23
  /** Short label for tables / metrics. */
23
24
  export const POST_GREEN_PRIMARY_SHORT =
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Human-facing product copy (4.6). JSON field names stay stable; this module
3
+ * owns labels people read in doctor, HTML, and compact router.
4
+ *
5
+ * Brands kept: ArkGate, ArkRules. Dialect (design-weak, hard write, …) maps to
6
+ * common software words. See docs/product-voice.md.
7
+ */
8
+
9
+ /** Status-light leftover-design qualifier (was “design-weak”). */
10
+ export const LEFTOVER_DESIGN_LABEL = 'leftover design work';
11
+
12
+ /**
13
+ * Operating-mode title for humans (and doctor JSON `designFitness.label` prefix).
14
+ * @param {string|null|undefined} mode suggest|adapt|enforce
15
+ * @param {boolean} leftoverDesign
16
+ */
17
+ export function operatingModeTitle(mode, leftoverDesign) {
18
+ const light = String(mode || 'enforce').toUpperCase();
19
+ return leftoverDesign ? `${light} · ${LEFTOVER_DESIGN_LABEL}` : light;
20
+ }
21
+
22
+ /** Short HTML/doctor badge text. */
23
+ export const LEFTOVER_DESIGN_BADGE = LEFTOVER_DESIGN_LABEL;
24
+
25
+ export const POST_GREEN_HUMAN =
26
+ 'Imports check out, but the design is still messy. Map leftover work with /ark-explore shape-focus, then apply one small refactor via /ark-autopilot with your OK. A clean import check is not done; pattern bets are never auto-applied.';
27
+
28
+ export const POST_GREEN_LEDE =
29
+ 'Import rules are clean, but leftover design work remains. That does not fail the check — it only means “done” is still wrong until you tidy shape.';
30
+
31
+ export const HARD_WRITE_HUMAN = 'pre-write block';
32
+ export const ADVISORY_WRITE_HUMAN = 'warning only (not blocked)';
@@ -16,7 +16,7 @@ export const HOME_SKILL_PENDING_CATALOG = '.arkgate-catalog.pending.json';
16
16
  const HOME_SKILL_LOCK = '.arkgate-install.lock';
17
17
  const HOME_SKILL_CATALOG_SCHEMA = '1.0';
18
18
  const HOME_SKILL_LOCK_STALE_MS = 5 * 60 * 1000;
19
- const HOME_SKILL_LOCK_ATTEMPTS = 100;
19
+ const HOME_SKILL_LOCK_ATTEMPTS = 200;
20
20
  const HOME_SKILL_LOCK_RETRY_MS = 25;
21
21
  const UUID_PATTERN =
22
22
  /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
@@ -10,6 +10,7 @@ import {
10
10
  claudeSettings,
11
11
  codexHooks,
12
12
  codexProjectConfig,
13
+ cursorHooks,
13
14
  grokHooks,
14
15
  grokProjectConfig,
15
16
  opencodeProjectConfig,
@@ -30,7 +31,10 @@ const COMPACT_HOST_TEMPLATES = {
30
31
  ['.agents/hooks.json', antigravityHooks(root)],
31
32
  ['.mcp.json', mcpJson(root)],
32
33
  ],
33
- cursor: (root) => [['.cursor/mcp.json', mcpJson(root)]],
34
+ cursor: (root) => [
35
+ ['.cursor/mcp.json', mcpJson(root)],
36
+ ['.cursor/hooks.json', cursorHooks(root)],
37
+ ],
34
38
  codex: (root) => [
35
39
  ['.codex/hooks.json', codexHooks(root)],
36
40
  ['.codex/config.toml', codexProjectConfig(root)],
@@ -33,6 +33,22 @@ export function buildUpgradeWhatsNewSuggestions() {
33
33
  neverGateInput: true,
34
34
  title: 'Suggested improvements — try or inspect after this package',
35
35
  items: [
36
+ {
37
+ id: 'plain-language-doctor',
38
+ title: 'Doctor in plain language',
39
+ try: 'npx arkgate-check --doctor',
40
+ inspect: 'Status light + leftover design work (not a score)',
41
+ why:
42
+ 'Doctor and the HTML report use common words (import rules, leftover design work, pre-write block). ArkGate and ArkRules stay as product names.',
43
+ },
44
+ {
45
+ id: 'shared-agent-homes',
46
+ title: 'Shared agent skills (home)',
47
+ try: 'npx arkgate-check --install-agent-gates --skills-only --agent-homes --force',
48
+ inspect: 'doctor.agentHomeGaps (Claude/Grok ~/.*/skills when those catalogs exist)',
49
+ why:
50
+ 'Project skills follow this pin. Shared homes stay on the newest ArkGate on the machine (additive; never downgrade). Orphan 2.x global skills stop coaching the wrong version.',
51
+ },
36
52
  {
37
53
  id: 'deep-module-coach',
38
54
  title: 'Deep-module coach (hot paths + deepening)',
@@ -216,6 +216,9 @@ function requiredWriteOperations(relativePath) {
216
216
  if (relativePath === '.agents/hooks.json' || relativePath.startsWith('.agents/')) {
217
217
  return ['write_to_file', 'replace_file_content', 'multi_replace_file_content'];
218
218
  }
219
+ if (relativePath === '.cursor/hooks.json' || relativePath.startsWith('.cursor/hooks')) {
220
+ return ['Write', 'StrReplace'];
221
+ }
219
222
  return ['Write', 'Edit', 'MultiEdit'];
220
223
  }
221
224
 
@@ -284,6 +287,51 @@ function collectPreToolUseGroups(parsed) {
284
287
  return [];
285
288
  }
286
289
 
290
+ /**
291
+ * Cursor hooks.json (version 1): { hooks: { preToolUse: [{ command, matcher, failClosed }] } }
292
+ * Flat command entries — not Claude's nested `{ hooks: [{ type, command }] }` groups.
293
+ */
294
+ function cursorHookEvidence(root) {
295
+ const relativePath = '.cursor/hooks.json';
296
+ const text = readText(path.join(root, relativePath));
297
+ let hooks = [];
298
+ try {
299
+ const parsed = JSON.parse(text);
300
+ const entries = Array.isArray(parsed?.hooks?.preToolUse) ? parsed.hooks.preToolUse : [];
301
+ hooks = entries
302
+ .filter((entry) => entry && typeof entry === 'object' && (!entry.type || entry.type === 'command'))
303
+ .map((entry) => ({
304
+ hook: entry,
305
+ invocation: commandArkMcpInvocation(entry?.command),
306
+ operations: matcherOperations(relativePath, entry?.matcher),
307
+ }))
308
+ .filter((entry) => entry.invocation);
309
+ } catch {
310
+ hooks = [];
311
+ }
312
+ const hardHooks = hooks.filter((entry) => entry.invocation.binArgs.includes('--hook'));
313
+ const required = requiredWriteOperations(relativePath);
314
+ const hard = required.every((operation) =>
315
+ hardHooks.some((entry) => entry.operations.includes(operation))
316
+ );
317
+ const repair =
318
+ hard &&
319
+ required.every((operation) =>
320
+ hardHooks.some(
321
+ ({ hook, invocation, operations }) =>
322
+ operations.includes(operation) &&
323
+ (invocation.binArgs.includes('--hook-repair') ||
324
+ /^(?:1|true|yes|on)$/i.test(
325
+ String(hook.env?.ARK_HOOK_REPAIR ?? invocation.environment.ARK_HOOK_REPAIR ?? '')
326
+ ))
327
+ )
328
+ );
329
+ return {
330
+ hard: hard ? [relativePath] : [],
331
+ repair: repair ? [relativePath] : [],
332
+ };
333
+ }
334
+
287
335
  function hookEvidence(root, relativePath) {
288
336
  const text = readText(path.join(root, relativePath));
289
337
  let hooks = [];
@@ -396,6 +444,7 @@ export function detectWritePathInventory(root) {
396
444
  const claudeHook = hookEvidence(root, '.claude/settings.json');
397
445
  const grokHook = hookEvidence(root, '.grok/hooks/ark-write-gate.json');
398
446
  const antigravityHook = hookEvidence(root, '.agents/hooks.json');
447
+ const cursorHook = cursorHookEvidence(root);
399
448
  const hosts = {
400
449
  claude: hostRecord(
401
450
  claudeHook.hard,
@@ -416,7 +465,19 @@ export function detectWritePathInventory(root) {
416
465
  antigravityHook.repair,
417
466
  merge
418
467
  ),
419
- cursor: hostRecord([], mcpEvidence(root, '.cursor/mcp.json'), [], merge),
468
+ cursor: hostRecord(
469
+ cursorHook.hard,
470
+ [
471
+ ...mcpEvidence(root, '.cursor/mcp.json'),
472
+ // Shared project MCP is also a valid advisory surface for Cursor sessions.
473
+ ...mcpEvidence(root, '.mcp.json'),
474
+ ],
475
+ // EH07: hooks may emit --hook-repair envelopes, but Cursor Write updated_input
476
+ // reinjection is not package-guaranteed — keep inventory repair-payload false
477
+ // (envelope honesty lives on support.repair-envelope-emitted).
478
+ [],
479
+ merge
480
+ ),
420
481
  // Codex 0.123+ emits PreToolUse for the native apply_patch handler, but some
421
482
  // Code Mode hosts execute deferred nested writes without dispatching that
422
483
  // project hook. Keep the installed hook as best-effort protection; do not