arkgate 3.8.2 → 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 } : {}),
@@ -618,11 +618,98 @@ export function execCommandParts(root, bin, binArgs = []) {
618
618
  return { command: 'npx', args: [bin, ...binArgs] };
619
619
  }
620
620
 
621
+ /**
622
+ * True when this directory is a pnpm workspace root (needs `pnpm add -w` for root deps).
623
+ * Nested packages under the workspace are not roots.
624
+ */
625
+ export function isPnpmWorkspaceRoot(root) {
626
+ return fs.existsSync(path.join(root, 'pnpm-workspace.yaml'));
627
+ }
628
+
629
+ /**
630
+ * True when package.json declares npm/yarn workspaces (yarn classic needs `-W` at root).
631
+ */
632
+ export function isNpmYarnWorkspaceRoot(root) {
633
+ const pkg = readPackageJson(root);
634
+ if (!pkg) return false;
635
+ const ws = pkg.workspaces;
636
+ return Array.isArray(ws) || (ws && typeof ws === 'object' && Array.isArray(ws.packages));
637
+ }
638
+
639
+ /**
640
+ * Normalize a version/range/spec into an installable package argument for arkgate.
641
+ * Accepts `latest`, `^3.8.2`, `arkgate@latest`, or a full package name.
642
+ */
643
+ export function normalizeArkgateInstallSpec(versionSpec) {
644
+ const raw = typeof versionSpec === 'string' && versionSpec.trim() ? versionSpec.trim() : 'latest';
645
+ if (raw.startsWith('arkgate@') || raw === 'arkgate') return raw === 'arkgate' ? 'arkgate@latest' : raw;
646
+ if (raw.includes('/') || raw.startsWith('file:') || raw.startsWith('link:')) return raw;
647
+ return `arkgate@${raw}`;
648
+ }
649
+
650
+ /**
651
+ * Package-manager argv to add a dev dependency (e.g. arkgate@latest).
652
+ * pnpm workspace roots get `-w`; yarn classic workspaces get `-W`.
653
+ *
654
+ * @param {string} root
655
+ * @param {string} [versionSpec] package name or name@version (default arkgate@latest)
656
+ * @returns {[string, string[]]}
657
+ */
658
+ export function packageInstallArgv(root, versionSpec = 'latest') {
659
+ const pkgSpec = normalizeArkgateInstallSpec(versionSpec);
660
+ const pm = detectPackageManager(root);
661
+ if (pm === 'pnpm') {
662
+ const args = ['add', '-D', pkgSpec];
663
+ if (isPnpmWorkspaceRoot(root)) args.push('-w');
664
+ return ['pnpm', args];
665
+ }
666
+ if (pm === 'yarn') {
667
+ const args = ['add', '-D', pkgSpec];
668
+ if (isNpmYarnWorkspaceRoot(root)) args.push('-W');
669
+ return ['yarn', args];
670
+ }
671
+ return ['npm', ['install', '-D', pkgSpec]];
672
+ }
673
+
674
+ /**
675
+ * Whether an install of arkgate@latest can be skipped because node_modules already
676
+ * resolves the same version as this CLI package.
677
+ *
678
+ * @param {string} root
679
+ * @param {string} [cliVersion] this binary's package version
680
+ * @returns {{ skip: boolean, installedVersion: string|null, reason: string }}
681
+ */
682
+ export function shouldSkipArkgateInstall(root, cliVersion) {
683
+ const pkgPath = path.join(root, 'node_modules', 'arkgate', 'package.json');
684
+ if (!fs.existsSync(pkgPath)) {
685
+ return { skip: false, installedVersion: null, reason: 'not-installed' };
686
+ }
687
+ let installedVersion = null;
688
+ try {
689
+ installedVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? null;
690
+ } catch {
691
+ return { skip: false, installedVersion: null, reason: 'unreadable' };
692
+ }
693
+ if (
694
+ typeof cliVersion === 'string' &&
695
+ cliVersion &&
696
+ installedVersion &&
697
+ installedVersion === cliVersion
698
+ ) {
699
+ return { skip: true, installedVersion, reason: 'already-current' };
700
+ }
701
+ return { skip: false, installedVersion, reason: 'version-differs' };
702
+ }
703
+
621
704
  /** Package-manager aware "install a dev dependency" hint (e.g. for a missing typescript). */
622
705
  export function installDevHint(root, pkg) {
623
706
  const pm = detectPackageManager(root);
624
- if (pm === 'pnpm') return `pnpm add -D ${pkg}`;
625
- if (pm === 'yarn') return `yarn add -D ${pkg}`;
707
+ if (pm === 'pnpm') {
708
+ return isPnpmWorkspaceRoot(root) ? `pnpm add -D ${pkg} -w` : `pnpm add -D ${pkg}`;
709
+ }
710
+ if (pm === 'yarn') {
711
+ return isNpmYarnWorkspaceRoot(root) ? `yarn add -D ${pkg} -W` : `yarn add -D ${pkg}`;
712
+ }
626
713
  return `npm install -D ${pkg}`;
627
714
  }
628
715
 
package/bin/ark.mjs CHANGED
@@ -14,8 +14,10 @@ import {
14
14
  INIT_WIZARD_CHOICES,
15
15
  isValidArchetypeId,
16
16
  mapWizardChoiceToArchetype,
17
+ packageInstallArgv,
17
18
  resolveArchetypePreset,
18
19
  resolveOperatingMode,
20
+ shouldSkipArkgateInstall,
19
21
  } from './ark-shared.mjs';
20
22
  import { pinArkgateDevDependency, FALSE_GREEN_GAP_ID } from './lib/field-install.mjs';
21
23
  import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
@@ -155,9 +157,9 @@ Options:
155
157
  (Also the implicit default when stdin/stdout are not a TTY — agents never hang on prompts.)
156
158
  --force Allow generated files to overwrite existing files.
157
159
  --no-strict Skip the final strict ark-check run.
158
- --install Add arkgate to package.json explicitly before applying a start plan.
160
+ --install Pin and install arkgate as a project devDependency (default for start).
159
161
  --no-install Skip adding/installing arkgate as a project devDependency (start/upgrade).
160
- --apply Apply a start plan; for upgrade, update/repreview or apply with --no-install.
162
+ --apply Apply a start plan; for upgrade, update/repreview or apply managed bytes.
161
163
  --accept-conflicts
162
164
  Allow upgrade to recreate deleted managed assets or replace recorded conflicts.
163
165
  --plan-digest Digest emitted by an upgrade preview; required to apply managed bytes.
@@ -189,20 +191,7 @@ function cliVersion() {
189
191
  }
190
192
  }
191
193
 
192
- // The package-manager command that adds arkgate as a dev dependency.
193
- // Prefer an explicit version/range when pin already chose one (avoid pin=^2.9.0 then
194
- // `npm i arkgate@latest` rewriting package.json to a different range).
195
- function packageInstallArgv(root, versionSpec) {
196
- const range =
197
- typeof versionSpec === 'string' && versionSpec.trim()
198
- ? versionSpec.trim()
199
- : 'latest';
200
- const spec = range.startsWith('arkgate@') ? range : `arkgate@${range}`;
201
- const pm = detectPackageManager(root);
202
- if (pm === 'pnpm') return ['pnpm', ['add', '-D', spec]];
203
- if (pm === 'yarn') return ['yarn', ['add', '-D', spec]];
204
- return ['npm', ['install', '-D', spec]];
205
- }
194
+ // packageInstallArgv is imported from ark-shared (workspace-aware -w / -W).
206
195
 
207
196
  function runCommand(command, commandArgs, cwd) {
208
197
  const result = spawnSync(command, commandArgs, { cwd, stdio: 'inherit', encoding: 'utf8' });
@@ -438,6 +427,32 @@ async function start(args) {
438
427
  if (!args.apply) return 0;
439
428
  applyStartPreview(args.root, preview);
440
429
  if (!args.json) console.log(`Applied ${preview.changes.length} previewed mutation(s).`);
430
+ // After applying exact preview bytes, install the pinned package when requested
431
+ // (preview itself never runs the package manager — field: start left pin without node_modules).
432
+ if (
433
+ args.install &&
434
+ !args.skipPackageManager &&
435
+ fs.existsSync(path.join(args.root, 'package.json'))
436
+ ) {
437
+ const skip = shouldSkipArkgateInstall(args.root, cliVersion());
438
+ if (!skip.skip) {
439
+ const [command, commandArgs] = packageInstallArgv(args.root, `^${cliVersion()}`);
440
+ if (!args.json) console.log(`Installing package: ${command} ${commandArgs.join(' ')}`);
441
+ // Keep stdout clean for --json consumers (package managers are chatty on stdout).
442
+ const status = args.json
443
+ ? (spawnSync(command, commandArgs, {
444
+ cwd: args.root,
445
+ stdio: ['ignore', 'pipe', 'pipe'],
446
+ encoding: 'utf8',
447
+ }).status ?? 1)
448
+ : runCommand(command, commandArgs, args.root);
449
+ if (status !== 0 && !args.json) {
450
+ console.log(
451
+ `Package manager exited ${status}. package.json is pinned; run the install command when online.`
452
+ );
453
+ }
454
+ }
455
+ }
441
456
  return 0;
442
457
  }
443
458
  const root = args.root;
@@ -492,7 +507,8 @@ async function start(args) {
492
507
  }
493
508
 
494
509
  // 2b) Pin arkgate as a project devDependency so CI/npx do not depend on a stale global.
495
- if (args.installExplicit && args.install && fs.existsSync(path.join(root, 'package.json'))) {
510
+ // Default install=true; only --no-install skips. (installExplicit tracks user override for copy.)
511
+ if (args.install && fs.existsSync(path.join(root, 'package.json'))) {
496
512
  const { pinned, installStatus } = ensureProjectArkgateDependency(root, {
497
513
  install: true,
498
514
  runPackageManager: !args.skipPackageManager,
@@ -507,7 +523,7 @@ async function start(args) {
507
523
  } else if (pinned.reason === 'already-present') {
508
524
  console.log(` arkgate already in package.json (${pinned.version}).`);
509
525
  }
510
- } else if (args.installExplicit && args.install === false) {
526
+ } else if (!args.install) {
511
527
  console.log(' Skipping arkgate package pin (--no-install).');
512
528
  }
513
529
 
@@ -789,7 +805,13 @@ async function main() {
789
805
 
790
806
  if (args.command === 'upgrade' || args.command === 'update') {
791
807
  try {
792
- return runUpgradeCommand(args, { arkCheck, packageInstallArgv, runArkCheck });
808
+ return runUpgradeCommand(args, {
809
+ arkCheck,
810
+ packageInstallArgv,
811
+ runArkCheck,
812
+ cliVersion: cliVersion(),
813
+ shouldSkipArkgateInstall,
814
+ });
793
815
  } catch (error) {
794
816
  console.error(error instanceof Error ? error.message : String(error));
795
817
  return 2;
@@ -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