arkgate 3.8.3 → 3.9.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.
@@ -177,13 +177,90 @@ function isResolvedAnalysisInput(relativePath, args, compilerInputs = new Set())
177
177
  return /(?:^|\/)configs?\/[^/]+\.jsonc?$/i.test(relative);
178
178
  }
179
179
 
180
+ /**
181
+ * Map Google Antigravity write tools (PascalCase args) onto Claude Write/Edit/MultiEdit.
182
+ * @returns {{ toolName: string, toolInput: object }|null}
183
+ */
184
+ function mapAntigravityToolCall(toolCall) {
185
+ if (!toolCall || typeof toolCall !== 'object') return null;
186
+ const name = toolCall.name ?? '';
187
+ const args = toolCall.args && typeof toolCall.args === 'object' ? toolCall.args : {};
188
+ const filePath = args.TargetFile ?? args.targetFile ?? args.file_path ?? args.path;
189
+ if (name === 'write_to_file') {
190
+ return {
191
+ toolName: 'Write',
192
+ toolInput: {
193
+ file_path: filePath,
194
+ content: args.CodeContent ?? args.codeContent ?? args.content ?? '',
195
+ },
196
+ operation: 'write_to_file',
197
+ };
198
+ }
199
+ if (name === 'replace_file_content') {
200
+ return {
201
+ toolName: 'Edit',
202
+ toolInput: {
203
+ file_path: filePath,
204
+ old_string: args.TargetContent ?? args.targetContent ?? args.old_string ?? '',
205
+ new_string: args.ReplacementContent ?? args.replacementContent ?? args.new_string ?? '',
206
+ replace_all: Boolean(args.AllowMultiple ?? args.allowMultiple),
207
+ },
208
+ operation: 'replace_file_content',
209
+ };
210
+ }
211
+ if (name === 'multi_replace_file_content') {
212
+ const chunks = Array.isArray(args.ReplacementChunks)
213
+ ? args.ReplacementChunks
214
+ : Array.isArray(args.replacementChunks)
215
+ ? args.replacementChunks
216
+ : [];
217
+ return {
218
+ toolName: 'MultiEdit',
219
+ toolInput: {
220
+ file_path: filePath,
221
+ edits: chunks.map((chunk) => ({
222
+ old_string: chunk?.TargetContent ?? chunk?.targetContent ?? chunk?.old_string ?? '',
223
+ new_string:
224
+ chunk?.ReplacementContent ?? chunk?.replacementContent ?? chunk?.new_string ?? '',
225
+ replace_all: Boolean(chunk?.AllowMultiple ?? chunk?.allowMultiple),
226
+ })),
227
+ },
228
+ operation: 'multi_replace_file_content',
229
+ };
230
+ }
231
+ return {
232
+ toolName: name,
233
+ toolInput: { ...args, file_path: filePath },
234
+ operation: name,
235
+ };
236
+ }
237
+
180
238
  /**
181
239
  * Normalize agent PreToolUse payloads.
182
240
  * Claude Code: { tool_name, tool_input: { file_path, content | old_string/new_string } }
183
241
  * Grok Build: { toolName, toolInput: { file_path, content | old_string/new_string } }
184
242
  * (aliases Write/Edit/MultiEdit → write/search_replace; matcher keeps both)
243
+ * Antigravity: { toolCall: { name, args: { TargetFile, CodeContent, … } } }
185
244
  */
186
245
  function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_HOOK_EVENT)) {
246
+ const antigravityStyle =
247
+ payload != null && typeof payload === 'object' && 'toolCall' in payload;
248
+ if (antigravityStyle) {
249
+ const mapped = mapAntigravityToolCall(payload.toolCall);
250
+ const filePath =
251
+ mapped?.toolInput?.file_path ??
252
+ mapped?.toolInput?.filePath ??
253
+ mapped?.toolInput?.path ??
254
+ mapped?.toolInput?.target_file;
255
+ return {
256
+ toolName: mapped?.toolName ?? '',
257
+ toolInput: { ...(mapped?.toolInput ?? {}), file_path: filePath },
258
+ grokStyle: true, // decision JSON on stdout (deny)
259
+ antigravityStyle: true,
260
+ operation: mapped?.operation ?? mapped?.toolName ?? null,
261
+ };
262
+ }
263
+
187
264
  const rawName = payload?.tool_name ?? payload?.toolName ?? '';
188
265
  const toolInput = payload?.tool_input ?? payload?.toolInput ?? {};
189
266
  const nameMap = {
@@ -194,6 +271,9 @@ function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_
194
271
  MultiEdit: 'MultiEdit',
195
272
  ApplyPatch: 'ApplyPatch',
196
273
  apply_patch: 'ApplyPatch',
274
+ write_to_file: 'Write',
275
+ replace_file_content: 'Edit',
276
+ multi_replace_file_content: 'MultiEdit',
197
277
  };
198
278
  const toolName = nameMap[rawName] ?? rawName;
199
279
  const filePath =
@@ -205,6 +285,8 @@ function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_
205
285
  grokStyle:
206
286
  grokHookEvent ||
207
287
  (payload != null && typeof payload === 'object' && 'toolName' in payload),
288
+ antigravityStyle: false,
289
+ operation: null,
208
290
  };
209
291
  }
210
292
 
@@ -409,8 +491,14 @@ function processHookOutput() {
409
491
  };
410
492
  }
411
493
 
494
+ /** Antigravity PreToolUse requires stdout `decision` on every response (allow included). */
495
+ function emitAntigravityAllow(output, antigravityStyle) {
496
+ if (!antigravityStyle) return;
497
+ output.stdout(`${JSON.stringify({ decision: 'allow' })}\n`);
498
+ }
499
+
412
500
  function runHookPayload(payload, gate, config, args, ts, attemptContext, output = processHookOutput()) {
413
- const { toolName, toolInput, grokStyle } = normalizeHookPayload(
501
+ const { toolName, toolInput, grokStyle, antigravityStyle, operation } = normalizeHookPayload(
414
502
  payload,
415
503
  attemptContext?.grokHookEvent ?? Boolean(process.env.GROK_HOOK_EVENT)
416
504
  );
@@ -419,7 +507,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
419
507
  const parsedPatch = codexPatchWrites(patch, args.root);
420
508
  // Codex ApplyPatch is only preflighted when Ark can reconstruct every file operation.
421
509
  // An incomplete reconstruction must not be mislabeled as atomic or hard enforcement.
422
- if (!parsedPatch.complete) return;
510
+ if (!parsedPatch.complete) {
511
+ emitAntigravityAllow(output, antigravityStyle);
512
+ return;
513
+ }
423
514
  const patchWrites = parsedPatch.writes;
424
515
  const sourceWrites = patchWrites.filter((change) =>
425
516
  isGovernableSourceFile(path.basename(String(change.path)))
@@ -482,7 +573,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
482
573
  .join(', ')}; source-only virtual preflight cannot model those contents.`
483
574
  );
484
575
  }
485
- if (changes.length === 0) return;
576
+ if (changes.length === 0) {
577
+ emitAntigravityAllow(output, antigravityStyle);
578
+ return;
579
+ }
486
580
  result = prepareChangeFromRoot({
487
581
  root: args.root,
488
582
  config,
@@ -537,7 +631,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
537
631
  const designDelta = args.failOnNewSmells
538
632
  ? evaluateWriteDesignDelta({ root: args.root, config, changes, ts })
539
633
  : null;
540
- if (result.valid && (designDelta?.valid ?? true)) return;
634
+ if (result.valid && (designDelta?.valid ?? true)) {
635
+ emitAntigravityAllow(output, antigravityStyle);
636
+ return;
637
+ }
541
638
  const message = [
542
639
  `Ark architecture gate blocked this complete ${toolName} (${changes.length} governed file(s)):`,
543
640
  ...result.diagnostics.map(
@@ -567,16 +664,27 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
567
664
  return;
568
665
  }
569
666
  const filePath = toolInput.file_path;
570
- if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) return;
667
+ if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) {
668
+ // Non-file tools: fail-open. Antigravity still needs an explicit allow decision.
669
+ emitAntigravityAllow(output, antigravityStyle);
670
+ return;
671
+ }
571
672
  if (typeof filePath !== 'string' || !SOURCE_FILE.test(filePath) || filePath.endsWith('.d.ts')) {
673
+ emitAntigravityAllow(output, antigravityStyle);
572
674
  return;
573
675
  }
574
676
  const rel = path.relative(args.root, path.resolve(filePath));
575
677
  const segments = rel.split(path.sep);
576
- if (segments[0] === '..' || segments.includes('node_modules')) return;
678
+ if (segments[0] === '..' || segments.includes('node_modules')) {
679
+ emitAntigravityAllow(output, antigravityStyle);
680
+ return;
681
+ }
577
682
 
578
683
  const source = proposedSource(toolName, toolInput);
579
- if (typeof source !== 'string') return;
684
+ if (typeof source !== 'string') {
685
+ emitAntigravityAllow(output, antigravityStyle);
686
+ return;
687
+ }
580
688
 
581
689
  const layer = inferLayer(filePath, config, args.root);
582
690
  const validateOnce = (src) =>
@@ -611,7 +719,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
611
719
  ts,
612
720
  })
613
721
  : null;
614
- if (result.valid && (designDelta?.valid ?? true)) return;
722
+ if (result.valid && (designDelta?.valid ?? true)) {
723
+ emitAntigravityAllow(output, antigravityStyle);
724
+ return;
725
+ }
615
726
 
616
727
  // Ratchet semantics (same philosophy as ark-check --baseline): an edit is blocked only
617
728
  // when it ADDS violations relative to the file's current on-disk state. Otherwise a
@@ -639,7 +750,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
639
750
  existingCounts.set(key, remaining - 1);
640
751
  return false;
641
752
  });
642
- if (newViolations.length === 0 && (designDelta?.valid ?? true)) return;
753
+ if (newViolations.length === 0 && (designDelta?.valid ?? true)) {
754
+ emitAntigravityAllow(output, antigravityStyle);
755
+ return;
756
+ }
643
757
  const combinedViolations = [...newViolations, ...designDeltaViolations(designDelta)];
644
758
  const adapterResult = createAdapterResult({
645
759
  valid: false,
@@ -698,9 +812,21 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
698
812
  filePath: normalizedRel,
699
813
  enforcement: hookEnforcement(
700
814
  args.root,
701
- attemptContext?.host ?? (grokStyle ? 'grok' : 'claude'),
815
+ attemptContext?.host ??
816
+ (antigravityStyle ? 'antigravity' : grokStyle ? 'grok' : 'claude'),
702
817
  attemptContext?.operation ??
703
- (grokStyle ? (toolName === 'Edit' ? 'search_replace' : 'write') : toolName),
818
+ operation ??
819
+ (antigravityStyle
820
+ ? toolName === 'Edit'
821
+ ? 'replace_file_content'
822
+ : toolName === 'MultiEdit'
823
+ ? 'multi_replace_file_content'
824
+ : 'write_to_file'
825
+ : grokStyle
826
+ ? toolName === 'Edit'
827
+ ? 'search_replace'
828
+ : 'write'
829
+ : toolName),
704
830
  toolName === 'Write' || Boolean(attemptContext?.completePatch)
705
831
  ),
706
832
  ...(layer ? { layer } : {}),
@@ -23,10 +23,14 @@ export {
23
23
 
24
24
  export {
25
25
  PREFERRED_MCP_BIN,
26
+ antigravityHooks,
26
27
  claudeSettings,
27
28
  codexProjectConfig,
28
29
  grokHooks,
29
30
  grokProjectConfig,
31
+ mergeAntigravityArkHook,
32
+ mergeOpencodeArkMcp,
33
+ opencodeProjectConfig,
30
34
  } from './hook-templates.mjs';
31
35
 
32
36
  export { detectWritePathCapabilities } from './write-path-detect.mjs';
@@ -128,16 +128,13 @@ export function agentInstructions(root) {
128
128
 
129
129
  ## Default agent flow (if unsure, do only this)
130
130
 
131
- 1. If \`ark.config.json\` is missing: run \`${startCmd}\` once.
132
- 2. For adoption / cleanup / “make architecture sound”: run the **\`/ark-autopilot\`** skill
133
- (explore first → dual plan: remediation + pattern bets → safe fixes gates). Day-zero
134
- origin is frozen by \`ark start\`/\`ark init\` (or autopilot if missing) **before** agent docs.
135
- Do **not** invent a second architecture curriculum outside the routing table below — when a
136
- trigger matches, use that skill; when unsure, stay on autopilot.
137
- 3. Status anytime: \`${doctorCmd}\` (status light + next action — not a mode picker).
138
- 4. After ordinary feature edits: run \`${checkCmd}\`. On violations → **\`/ark-fix\`** (or
139
- \`/ark-place\` for new files, \`/ark-contract\` only if the contract itself is wrong).
131
+ 1. Status anytime: \`${doctorCmd}\` — **control plane** (one status light, one next action; not a mode picker).
132
+ 2. If \`ark.config.json\` is missing: run \`${startCmd}\` once (preview), then \`${startCmd} --apply\`.
133
+ 3. Guided end-to-end work (“make architecture sound”): **\`/ark-autopilot\`** — explore → dual plan A (edges) + B (shape)mechanical-safe fixes; B only with user OK. Day-zero origin is frozen by \`ark start\`/\`ark init\` (or autopilot if missing) **before** agent docs.
134
+ 4. After ordinary feature edits: run \`${checkCmd}\`. On violations → **\`/ark-fix\`** (or \`/ark-place\` for new files, \`/ark-contract\` only if the contract itself is wrong).
140
135
 
136
+ Do **not** skill-shop the full table for routine work. When unsure, do doctor top action #1 only
137
+ (re-run doctor after). Do **not** jump to \`/ark-autopilot\` unless #1 or a STOP handoff names it.
141
138
  Skills are **dual-engine**: deterministic CLI sensors + exploratory read of *this* repo — not JSON-only wrappers.
142
139
  When a skill says **STOP — do not continue this skill as complete**, stop and invoke the named handoff skill.
143
140
 
@@ -151,14 +148,16 @@ scouts (disjoint path scopes) and merge in the parent. If the host does **not**,
151
148
  **fall back to sequential** — one cluster/step at a time. Never parallel-write the same
152
149
  files; never weaken the gate via subagents.
153
150
 
154
- ## Skill routing (triggers → skill)
151
+ ## Skill routing (expert depth — triggers → skill)
155
152
 
156
- Do **not** run overlapping skills for the same job. Pick **one** primary skill from the table.
153
+ **Escapes, not a second curriculum.** Do **not** run overlapping skills for the same job.
154
+ Pick **one** primary skill. Prefer doctor top action #1 when unsure.
157
155
 
158
156
  | When | Invoke | Not this |
159
157
  |------|--------|----------|
160
- | Unsure / make architecture sound (apply path) | **/ark-autopilot** (default) | explore-only, coverage-only |
161
- | **Messy / spaghetti / design-weak after green / clarify for AI** | **Single path:** \`/ark-explore\` shape-focus → dual-plan B, then \`/ark-autopilot\` only to apply B with OK | coverage, think, loop-as-done, skill-shopping |
158
+ | Unsure what to do next | **Doctor top action #1** (\`${doctorCmd}\`), then re-run doctor | skill-shopping, defaulting to autopilot |
159
+ | Make architecture sound (guided apply path) | **/ark-autopilot** | explore-only, coverage-only |
160
+ | **Messy / spaghetti / design-weak after green / Shape residual** | **Single path:** \`/ark-explore\` shape-focus → dual-plan B, then \`/ark-autopilot\` only to apply B with OK | coverage, think, loop-as-done, skill-shopping |
162
161
  | Map / residual / dual-plan seed only (no apply, already know you want recon) | \`/ark-explore\` | coverage (fitness only) |
163
162
  | Greenfield shape / empty tree | \`/ark-architect\` | adopt |
164
163
  | Brownfield / wrong contract / false-green | \`/ark-adopt\` then \`/ark-contract\` if globs wrong | architect |
@@ -209,20 +208,28 @@ export function compactAgentInstructions(root, host = null) {
209
208
  'ark-check',
210
209
  `--install-agent-gates --skills-only --tools ${selectedHost === 'none' ? '<host>' : selectedHost}`
211
210
  );
211
+ // Progressive disclosure: primary path only. Full /ark-* catalog is expert depth
212
+ // (install via --skills-only). See docs/product-voice.md.
212
213
  return `# Ark Enforcement
213
214
 
214
215
  <!-- arkgate:compact-router host=${selectedHost} -->
215
216
  ## Compact router
216
217
 
217
- This project uses the ArkGate package and its \`ark\` MCP resources as its one
218
- agent router. Before editing TypeScript or JavaScript, read \`ark://manifest\`
219
- when available; use \`ark_place\` for new files and \`validate_code\` after edits.
220
- If MCP is unavailable, inspect \`ark.config.json\` and run \`${checkCmd}\`.
218
+ **Primary path (do this):**
221
219
 
222
- For architecture status, run \`${doctorCmd}\`. The selected host is
223
- \`${selectedHost}\`; its host registration and CI gate are installed alongside
224
- this file. Full \`/ark-*\` guided workflows are optional and can be added later
225
- with \`${installSkills}\`.
220
+ 1. Status anytime: \`${doctorCmd}\` one status light, one next action (control plane).
221
+ 2. Day to day: read \`ark://manifest\` when MCP is available; place new files with \`ark_place\`; validate after edits; run \`${checkCmd}\`. On a gate deny, fix the architecture do not weaken the contract.
222
+ 3. If MCP is unavailable: inspect \`ark.config.json\` and run \`${checkCmd}\`.
223
+
224
+ The selected host is \`${selectedHost}\`. Host registration and CI are installed with this file.
225
+ This compact router is enough for normal feature work.
226
+
227
+ ## Expert depth (optional)
228
+
229
+ Full \`/ark-*\` skills (including guided end-to-end \`/ark-autopilot\`) are **not** the default
230
+ curriculum. Install them only when doctor top action #1 or a STOP handoff names a skill:
231
+
232
+ \`${installSkills}\`
226
233
  `;
227
234
  }
228
235
 
@@ -36,6 +36,7 @@ import {
36
36
  mergePostGreenTopActions,
37
37
  isDoctorHealthyNothingToDo,
38
38
  } from './post-green-path.mjs';
39
+ import { doctorWritePathHonestyMessage } from './host-support-matrix.mjs';
39
40
  import {
40
41
  computePureLayerOptInNudge,
41
42
  loadGoldenPattern,
@@ -578,14 +579,20 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
578
579
  console.log('');
579
580
  console.log(color.bold('Operating mode'));
580
581
  // Modes are detected states, not user-picked settings. Plain-language "what you do next".
581
- const modeMark = mode === 'enforce' ? ok : mode === 'adapt' ? warn : warn;
582
+ // Never paint green (ok) under design residual edges clean design done (product-voice).
583
+ const modeMark =
584
+ mode === 'enforce' && !designFitness.designWeak
585
+ ? ok
586
+ : warn;
587
+ // Status lights are detected states, not user-picked settings (see docs/product-voice.md).
588
+ // modeTitle alone names the light — bodies must not re-prefix Suggest/Adapt/Enforce.
582
589
  const modeHelp = {
583
590
  suggest:
584
- 'Setup Ark proposes a starting architecture shape. You do not pick this mode; it means the tree is thin or new. Next: accept the shape (ark start / ark init) and add real layers as you grow.',
591
+ 'thin or new tree; the contract is not yet the control plane. You do not pick this light. Next: ark start (preview), then ark start --apply; re-check with --doctor.',
585
592
  adapt:
586
- 'Align — contract and folders still disagree, or coverage is weak / debt is open. You do not pick this mode. Next: classify ungoverned dirs (/ark-contract, /ark-adopt), run the plan (/ark-autopilot or /ark-loop). Gates do not fully protect you yet.',
593
+ 'contract and tree still disagree, or debt is open. Write path does not fully protect you yet. You do not pick this light. Next: do doctor top action #1 (often /ark-adopt, /ark-contract, or /ark-autopilot).',
587
594
  enforce:
588
- 'Guard — contract coverage is honest and checked edges are clean. You do not pick this mode; you arrived here. Next: keep the host-appropriate write path and CI check on; only NEW violations should fail.',
595
+ 'honest coverage and clean checked edges. You arrived here; you never turn Enforce on. Next: keep the host write path and CI check; only NEW violations should fail.',
589
596
  };
590
597
  const modeTitle =
591
598
  mode === 'enforce' && designFitness.designWeak
@@ -595,7 +602,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
595
602
  modeMark,
596
603
  `${modeTitle} — ${
597
604
  designFitness.designWeak
598
- ? 'Guard on edges is honest, but design smells remain (Shape residual). You do not pick this mode. Next: single path — /ark-explore shape-focus → dual-plan B, then /ark-autopilot only to apply B with your OK. Never empty plan A = done.'
605
+ ? 'checked edges are honest; design smells remain. Green is not elegant design. You do not pick this light. Next: one Shape door — /ark-explore shape-focus → dual-plan B; apply B only with /ark-autopilot and your OK. Empty plan A is not done.'
599
606
  : modeHelp[mode]
600
607
  }`
601
608
  );
@@ -692,34 +699,25 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
692
699
  if (showNewHere) {
693
700
  console.log('');
694
701
  console.log(color.bold('New here?'));
702
+ // Suggest residual: start → doctor only (not a competing recommend/architect curriculum).
703
+ line(ok, `Primary path: ${arkCommand(root, 'ark', 'start')} (preview) → ${arkCommand(root, 'ark', 'start --apply')} → re-run --doctor`);
695
704
  if (recommendation) {
696
- line(warn, `Suggested application shape: ${recommendation.archetype} — ${recommendation.label} (preset ${recommendation.preset})`);
697
- if (recommendation.galleryStarter) {
698
- line(ok, `Gallery starter: ${recommendation.galleryStarter}`);
699
- }
705
+ line(warn, `Sensor shape hint (not a second curriculum): ${recommendation.archetype} — ${recommendation.label} (preset ${recommendation.preset})`);
706
+ if (recommendation.galleryStarter) line(ok, `Gallery starter (optional): ${recommendation.galleryStarter}`);
700
707
  if (recommendation.policyPack) {
701
- line(ok, `Policy pack: ${arkCommand(root, 'ark-check', `--apply-policy-pack ${recommendation.policyPack}`)}`);
708
+ line(ok, `Policy pack (optional expert): ${arkCommand(root, 'ark-check', `--apply-policy-pack ${recommendation.policyPack}`)}`);
702
709
  }
703
710
  if (recommendation.signals?.nestFramework) {
704
- line(
705
- ok,
706
- 'Nest modular monolith → prefer hexagonal (or ddd-bounded-contexts if you have src/contexts/*)'
707
- );
711
+ line(ok, 'Nest modular monolith → prefer hexagonal (or ddd-bounded-contexts if you have src/contexts/*)');
708
712
  }
709
713
  if (recommendation.signals?.monorepoTooling?.length) {
710
- line(
711
- ok,
712
- `Monorepo tooling (${recommendation.signals.monorepoTooling.join(', ')}) → preset monorepo (apps/packages/libs)`
713
- );
714
+ line(ok, `Monorepo tooling (${recommendation.signals.monorepoTooling.join(', ')}) → preset monorepo (apps/packages/libs)`);
714
715
  }
715
716
  } else {
716
- line(warn, 'Low governed coverage or fresh config — pick an application shape before adding code.');
717
- }
718
- line(ok, `See the plan: ${arkCommand(root, 'ark-check', '--recommend')}`);
719
- if (recommendation?.archetype) {
720
- line(ok, `Quick setup: ${arkCommand(root, 'ark', `init --archetype ${recommendation.archetype} --yes`)}`);
717
+ line(warn, 'Low governed coverage or fresh config — finish start, then re-run doctor before adding layers of code.');
721
718
  }
722
- actions.unshift('run ark-check --recommend or /ark-architect to choose your application shape');
719
+ line(ok, `Optional sensor detail: ${arkCommand(root, 'ark-check', '--recommend')}`);
720
+ actions.unshift('finish ark start (preview + --apply), then re-run --doctor');
723
721
  }
724
722
 
725
723
  console.log('');
@@ -731,8 +729,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
731
729
  warn,
732
730
  'No active violations — coverage is still thin, so green is not yet honest enforcement'
733
731
  );
732
+ } else if (designFitness.designWeak) {
733
+ line(warn, 'None on checked edges — edges match the contract; design residual remains (ENFORCE · design-weak). Not healthy finished.');
734
734
  } else {
735
- line(ok, 'None — the code matches the contract');
735
+ line(ok, 'None — the code matches the contract on checked edges');
736
736
  }
737
737
  } else {
738
738
  const typeNote = summary.typeOnlyCount > 0 ? ` (${summary.valueCount} value · ${summary.typeOnlyCount} type-only)` : '';
@@ -770,6 +770,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
770
770
  line(' ', `Active host: ${writePath.activeHost}`);
771
771
  line(' ', `Supported profile: ${writePath.supportSummary}`);
772
772
  line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`);
773
+ const honestyLine = doctorWritePathHonestyMessage(writePath.activeHost, capabilities['hard-write']);
774
+ if (honestyLine) line(warn, honestyLine);
773
775
  if (writePath.sessionNote) {
774
776
  line(warn, writePath.sessionNote);
775
777
  }
@@ -932,18 +934,25 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
932
934
  const uniqueActions = mergePostGreenTopActions(actions, postGreenPath);
933
935
  if (isDoctorHealthyNothingToDo(designFitness, uniqueActions)) {
934
936
  console.log(color.green('✔ Healthy — nothing to do.'));
937
+ console.log(color.dim(' Contract edges and design residual are clear. Keep write path + CI.'));
935
938
  } else {
936
939
  if (designFitness.designWeak && uniqueActions.length === 0 && postGreenPath) {
937
940
  uniqueActions.push(postGreenPath.action);
938
941
  }
939
- console.log(color.bold(`Top actions (${uniqueActions.length}):`));
940
- uniqueActions.forEach((action, index) => console.log(` ${index + 1}. ${action}`));
942
+ console.log(color.bold(`Primary next action`));
943
+ console.log(` 1. ${uniqueActions[0]}`);
944
+ if (uniqueActions.length > 1) {
945
+ console.log(color.bold(`Also (${uniqueActions.length - 1}):`));
946
+ uniqueActions.slice(1).forEach((action, index) => console.log(` ${index + 2}. ${action}`));
947
+ }
941
948
  if (postGreenPath) {
942
949
  console.log(
943
950
  color.dim(
944
- ' (post-green path is primary when ENFORCE · design-weak — do not skill-shop explore vs coverage vs think)'
951
+ ' Shape residual is the primary door under ENFORCE · design-weak — do not skill-shop explore vs coverage vs think.'
945
952
  )
946
953
  );
954
+ } else {
955
+ console.log(color.dim(' Doctor is the control plane: do #1 first, then re-run --doctor.'));
947
956
  }
948
957
  }
949
958
  }
@@ -1,15 +1,19 @@
1
1
  // Generated from hook-templates.source.mjs — run npm run generate:packaged-tooling.
2
- import{execCommandParts as i,execRunner as a}from"../ark-shared.mjs";const r="arkgate-mcp";function g(e){const o=a(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${o} ${r} --session-context --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${o} ${r} --hook --hook-repair --fail-on-new-smells --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`}]}]}},null,2)}
3
- `}function l(e){const o=a(e),n="${CODEX_PROJECT_DIR:-${PWD:-.}}";return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${o} ${r} --session-context --root "${n}" --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${o} ${r} --hook --hook-repair --fail-on-new-smells --root "${n}" --config ark.config.json`}]}]}},null,2)}
4
- `}function p(e){const{command:o,args:n}=i(e,r,["--root",".","--config","ark.config.json"]),t=c=>c.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),s=n.map(c=>`"${t(c)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
2
+ import{execCommandParts as i,execRunner as s}from"../ark-shared.mjs";const c="arkgate-mcp";function l(e){const r=s(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${c} --session-context --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`}]}]}},null,2)}
3
+ `}function p(e){const r=s(e),o="${CODEX_PROJECT_DIR:-${PWD:-.}}";return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root "${o}" --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "${o}" --config ark.config.json`}]}]}},null,2)}
4
+ `}function g(e){const{command:r,args:o}=i(e,c,["--root",".","--config","ark.config.json"]),t=a=>a.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),n=o.map(a=>`"${t(a)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
5
5
  # Restart Codex after changes; MCP servers are loaded when the project session starts.
6
6
  [mcp_servers.ark]
7
- command = "${t(o)}"
8
- args = [${s}]
9
- `}function k(e){const{command:o,args:n}=i(e,r,["--root",".","--config","ark.config.json"]),t=n.map(s=>`"${s.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
7
+ command = "${t(r)}"
8
+ args = [${n}]
9
+ `}function f(e){const{command:r,args:o}=i(e,c,["--root",".","--config","ark.config.json"]),t=o.map(n=>`"${n.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
10
10
  # Restart Grok (or /mcps \u2192 refresh) after changes. Also loads repo-root .mcp.json.
11
11
  [mcp_servers.ark]
12
- command = "${o.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
12
+ command = "${r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
13
13
  args = [${t}]
14
- `}function d(e){const o=a(e),n="${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}";return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${o} ${r} --session-context --root "${n}" --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${o} ${r} --hook --hook-repair --fail-on-new-smells --root "${n}" --config ark.config.json`}]}]}},null,2)}
15
- `}export{r as PREFERRED_MCP_BIN,g as claudeSettings,l as codexHooks,p as codexProjectConfig,d as grokHooks,k as grokProjectConfig};
14
+ `}function u(e){const r=s(e),o="${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}";return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root "${o}" --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "${o}" --config ark.config.json`}]}]}},null,2)}
15
+ `}function k(e){const r=s(e);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "\${PWD:-.}" --config ark.config.json`}]}]}},null,2)}
16
+ `}function d(e){const{command:r,args:o}=i(e,c,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...o],enabled:!0}}},null,2)}
17
+ `}function $(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!t||typeof t!="object"||Array.isArray(t))return null;const n=t["ark-write-gate"];if(!n||typeof n!="object")return null;const a={...o,"ark-write-gate":n};return`${JSON.stringify(a,null,2)}
18
+ `}function h(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o))return null;const n={...o};!n.$schema&&t.$schema&&(n.$schema=t.$schema);const a=o.mcp&&typeof o.mcp=="object"&&!Array.isArray(o.mcp)?{...o.mcp}:{};return a.ark=t.mcp.ark,n.mcp=a,`${JSON.stringify(n,null,2)}
19
+ `}export{c as PREFERRED_MCP_BIN,k as antigravityHooks,l as claudeSettings,p as codexHooks,g as codexProjectConfig,u as grokHooks,f as grokProjectConfig,$ as mergeAntigravityArkHook,h as mergeOpencodeArkMcp,d as opencodeProjectConfig};
@@ -38,6 +38,16 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
38
38
  true,
39
39
  true
40
40
  ),
41
+ // Google Antigravity: official PreToolUse deny is a hard block. Claim hard only when
42
+ // installed + trusted and the listed write tools are covered by the adapter.
43
+ antigravity: hostProfile(
44
+ 'Google Antigravity',
45
+ '.agents/hooks.json',
46
+ 'PreToolUse `write_to_file` / `replace_file_content` / `multi_replace_file_content`',
47
+ ['write_to_file', 'replace_file_content', 'multi_replace_file_content'],
48
+ true,
49
+ true
50
+ ),
41
51
  cursor: hostProfile('Cursor', null, null, [], false, false),
42
52
  codex: hostProfile(
43
53
  'OpenAI Codex',
@@ -47,6 +57,16 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
47
57
  false,
48
58
  false
49
59
  ),
60
+ // OpenCode: first-class MCP + permissions; plugin tool.execute.before is incomplete
61
+ // (subagent holes). Never claim hard write.
62
+ opencode: hostProfile(
63
+ 'OpenCode',
64
+ null,
65
+ 'Advisory MCP + optional experimental plugin (`tool.execute.before`); not a hard boundary',
66
+ [],
67
+ false,
68
+ false
69
+ ),
50
70
  });
51
71
 
52
72
  export const HOST_SUPPORT_HOSTS = Object.freeze(Object.keys(HOST_SUPPORT_MATRIX));
@@ -70,18 +90,58 @@ export function renderHostSupportMatrixMarkdown() {
70
90
  const rows = HOST_SUPPORT_HOSTS.map((host) => {
71
91
  const profile = HOST_SUPPORT_MATRIX[host];
72
92
  const capabilities = profile.capabilities;
73
- const local = capabilities['hard-write']
74
- ? `Hard block for ${profile.hookSurface}`
75
- : 'No hard hook; MCP/rules are advisory';
93
+ // Fail-closed honesty: Cursor/Codex/OpenCode never claim hard write; CI is required-status.
94
+ // hookSurface already includes "PreToolUse …" — do not prefix PreToolUse again.
95
+ let local;
96
+ if (capabilities['hard-write']) {
97
+ local = `**Hard** block for listed ops (${profile.hookSurface}) when installed + trusted`;
98
+ } else if (host === 'codex') {
99
+ local =
100
+ '**Advisory / best-effort** at write (not equivalent to Claude/Grok hard block)';
101
+ } else if (host === 'opencode') {
102
+ local =
103
+ '**Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary)';
104
+ } else {
105
+ local = '**Advisory only** at write (no hard hook)';
106
+ }
76
107
  const repair = capabilities['repair-payload']
77
108
  ? 'Emitted on hook deny; host must re-inject'
78
109
  : 'No hard-boundary payload';
79
- return `| ${profile.label} | ${local} | Advisory; the agent must call it | Available \`arkgate-check --strict-merge\` check | ${repair} |`;
110
+ const merge = capabilities['hard-write']
111
+ ? '**Required status** = hard merge boundary (`arkgate-check --strict-merge`)'
112
+ : '**Required status** = hard merge boundary (same CI)';
113
+ return `| ${profile.label} | ${local} | Advisory; the agent must call it | ${merge} | ${repair} |`;
80
114
  }).join('\n');
81
115
 
82
116
  return `| Host | Local write boundary | MCP validation | CI / merge path | Repair payload |
83
117
  |------|----------------------|----------------|-----------------|----------------|
84
118
  ${rows}
85
119
 
120
+ **Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
121
+ merge check — not “CI file present.” Cursor/Codex/OpenCode never get a fake hard write claim.
122
+
86
123
  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 payloads never write code silently: the host must re-inject the candidate and ArkGate revalidates it. Run \`arkgate-check --doctor\` for the evidence actually detected in the current repository.`;
87
124
  }
125
+
126
+ /**
127
+ * Doctor human one-liner for active-host write honesty (fail-closed).
128
+ * @returns {string|null}
129
+ */
130
+ export function doctorWritePathHonestyMessage(activeHost, hardWriteActive) {
131
+ const host = typeof activeHost === 'string' ? activeHost.trim().toLowerCase() : '';
132
+ if (host === 'cursor') {
133
+ return 'Cursor: write path is advisory (MCP/rules; no hard PreToolUse). Required CI status (arkgate-check --strict-merge) is the hard merge boundary.';
134
+ }
135
+ if (host === 'codex') {
136
+ return 'Codex: write path is advisory / best-effort at write (not Claude/Grok hard). Required CI status (arkgate-check --strict-merge) is the hard merge boundary.';
137
+ }
138
+ if (host === 'opencode') {
139
+ return 'OpenCode: write path is advisory / best-effort (MCP + optional plugin; not Claude/Grok/Antigravity hard). Required CI status (arkgate-check --strict-merge) is the hard merge boundary.';
140
+ }
141
+ if ((host === 'claude' || host === 'grok' || host === 'antigravity') && !hardWriteActive) {
142
+ const label =
143
+ host === 'claude' ? 'Claude' : host === 'grok' ? 'Grok' : 'Antigravity';
144
+ return `${label}: hard PreToolUse is supported for listed ops when installed + trusted; without runtime-observed hook evidence, hard is unverified. Required CI remains the merge hard boundary.`;
145
+ }
146
+ return null;
147
+ }