arkgate 4.1.0 → 4.2.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 (63) hide show
  1. package/CHANGELOG.md +124 -2
  2. package/README.md +24 -14
  3. package/bin/ark-check-runtime.mjs +16 -5
  4. package/bin/ark-mcp-runtime.mjs +766 -64
  5. package/bin/lib/agent-gates.mjs +1 -0
  6. package/bin/lib/ark-gitignore.mjs +88 -0
  7. package/bin/lib/ci-and-commands.mjs +33 -8
  8. package/bin/lib/codex-home.mjs +90 -8
  9. package/bin/lib/design-smells.mjs +71 -9
  10. package/bin/lib/doctor-plan.mjs +47 -39
  11. package/bin/lib/effective-contract-load.mjs +73 -9
  12. package/bin/lib/enforcement-honesty.mjs +78 -22
  13. package/bin/lib/enforcement-state.mjs +1 -1
  14. package/bin/lib/gate-files.mjs +441 -9
  15. package/bin/lib/github-enforcement.mjs +168 -7
  16. package/bin/lib/hook-templates.mjs +12 -11
  17. package/bin/lib/host-support-matrix.mjs +91 -17
  18. package/bin/lib/html-report-depth.mjs +13 -2
  19. package/bin/lib/html-report-evolution.mjs +114 -0
  20. package/bin/lib/html-report.mjs +18 -97
  21. package/bin/lib/import-resolve.mjs +33 -11
  22. package/bin/lib/install-activation.mjs +87 -0
  23. package/bin/lib/install-migrate.mjs +66 -50
  24. package/bin/lib/managed-upgrade.mjs +10 -41
  25. package/bin/lib/mcp-adoption.mjs +15 -5
  26. package/bin/lib/pilot-loop.mjs +25 -8
  27. package/bin/lib/project-identity.mjs +103 -0
  28. package/bin/lib/report-snapshot-context.mjs +28 -0
  29. package/bin/lib/resident-hook.mjs +33 -9
  30. package/bin/lib/rules-inventory.mjs +100 -8
  31. package/bin/lib/skill-install.mjs +272 -22
  32. package/bin/lib/skill-write.mjs +899 -0
  33. package/bin/lib/start-preview.mjs +84 -1
  34. package/bin/lib/upgrade-command.mjs +2 -5
  35. package/bin/lib/write-path-detect.mjs +2 -2
  36. package/dist/index.cjs +13 -13
  37. package/dist/index.d.ts +194 -2
  38. package/dist/index.js +13 -13
  39. package/docs/README.md +6 -4
  40. package/docs/agent-guide.md +115 -17
  41. package/docs/ai-gates.md +133 -25
  42. package/docs/assets/ark-write-gate.svg +2 -2
  43. package/docs/develop.md +16 -6
  44. package/docs/enthusiast/how-to-agent-gates.md +6 -0
  45. package/docs/package-surface.md +16 -9
  46. package/docs/product-voice.md +22 -4
  47. package/docs/use.md +3 -1
  48. package/package.json +3 -1
  49. package/schemas/ark.project-identity.schema.json +116 -0
  50. package/server.json +2 -2
  51. package/templates/skills/ark-adopt.md +9 -0
  52. package/templates/skills/ark-architect.md +12 -2
  53. package/templates/skills/ark-autopilot.md +9 -0
  54. package/templates/skills/ark-contract.md +11 -1
  55. package/templates/skills/ark-coverage.md +9 -0
  56. package/templates/skills/ark-explain.md +13 -1
  57. package/templates/skills/ark-explore.md +9 -0
  58. package/templates/skills/ark-fix.md +10 -1
  59. package/templates/skills/ark-loop.md +11 -2
  60. package/templates/skills/ark-place.md +17 -6
  61. package/templates/skills/ark-runtime.md +8 -0
  62. package/templates/skills/ark-think.md +14 -2
  63. package/templates/skills/ark-upgrade.md +9 -0
@@ -51,7 +51,14 @@ function shellSegments(text) {
51
51
  index++;
52
52
  continue;
53
53
  }
54
- if (char === ';' || char === '|' || char === '\n') {
54
+ if (
55
+ char === '&' &&
56
+ (input[index - 1] === '>' || input[index - 1] === '<' || input[index + 1] === '>')
57
+ ) {
58
+ current += char;
59
+ continue;
60
+ }
61
+ if (char === ';' || char === '|' || char === '&' || char === '\n') {
55
62
  push(char);
56
63
  continue;
57
64
  }
@@ -70,12 +77,18 @@ function analyzeCommands(commands, script = '') {
70
77
  .filter((segment) => DIRECT_ARK.test(executableText(segment.text)))
71
78
  .map((segment) => ({
72
79
  text: segment.text,
73
- enforcing: segment.terminator !== '||' && segment.terminator !== '|',
80
+ enforcing:
81
+ segment.terminator !== '||' &&
82
+ segment.terminator !== '|' &&
83
+ segment.terminator !== '&',
74
84
  }));
75
85
  const found = [];
76
86
  for (const segment of shellSegments(commands)) {
77
87
  const executable = executableText(segment.text);
78
- const outerEnforcing = segment.terminator !== '||' && segment.terminator !== '|';
88
+ const outerEnforcing =
89
+ segment.terminator !== '||' &&
90
+ segment.terminator !== '|' &&
91
+ segment.terminator !== '&';
79
92
  if (DIRECT_ARK.test(executable)) {
80
93
  found.push({ text: segment.text, enforcing: outerEnforcing });
81
94
  } else if (CHECK_SCRIPT.test(executable)) {
@@ -362,12 +375,132 @@ function parseJson(result) {
362
375
  }
363
376
  }
364
377
 
378
+ /**
379
+ * Classify GitHub provider API failures (EH06).
380
+ * Plan/tier claims require **explicit upgrade/plan language** — bare HTTP 403
381
+ * (token/SSO/scope) stays generic `provider-enforcement-unverified` so we never
382
+ * overclaim "proven not required" / Free-plan walls.
383
+ *
384
+ * @param {string} errorText combined stderr/stdout from gh api
385
+ * @param {{ classicAvailable?: boolean, rulesAvailable?: boolean }} [opts]
386
+ * @returns {'provider-policy-unavailable-plan'|'provider-enforcement-unverified'|'ok'}
387
+ */
388
+ export function classifyGithubProviderFailure(errorText, opts = {}) {
389
+ const text = String(errorText || '');
390
+ const lower = text.toLowerCase();
391
+ // Explicit plan/tier walls only — not every 403 (token/SSO/scope stay unverified).
392
+ const planRestricted =
393
+ /upgrade to github (pro|team|enterprise)/i.test(lower) ||
394
+ /not available (on|for) (your|this) (current )?plan/i.test(lower) ||
395
+ /requires a paid github/i.test(lower) ||
396
+ /github pro.*branch protection|branch protection.*github pro/i.test(lower) ||
397
+ /only available (with|on) github (pro|team|enterprise)/i.test(lower) ||
398
+ /this feature is not available (on|for) (free|your plan)/i.test(lower);
399
+ if (planRestricted) return 'provider-policy-unavailable-plan';
400
+ if (opts.classicAvailable && opts.rulesAvailable) return 'ok';
401
+ return 'provider-enforcement-unverified';
402
+ }
403
+
404
+ /**
405
+ * True when a workflow/job title is an Ark architecture check (not lint-only / spark-ci / dark-theme).
406
+ * Prefer exact product tokens; "architecture" only with gate/check phrasing.
407
+ *
408
+ * @param {{ name?: string, workflowName?: string, displayTitle?: string }} run
409
+ * @returns {boolean}
410
+ */
411
+ export function isArkishCiRun(run) {
412
+ const blob = `${run?.name || ''} ${run?.workflowName || ''} ${run?.displayTitle || ''}`.toLowerCase();
413
+ if (!blob.trim()) return false;
414
+ // Product tokens with word boundaries (avoid spark, dark, lark false positives).
415
+ if (/\barkgate(?:-check)?\b/.test(blob)) return true;
416
+ if (/\bark-check\b/.test(blob)) return true;
417
+ if (/\bark\s+architecture\b/.test(blob)) return true;
418
+ if (/\barchitecture\s+(?:gate|check)\b/.test(blob)) return true;
419
+ if (/\b(?:arkgate|ark)\s+architecture\s+gate\b/.test(blob)) return true;
420
+ // Standalone workflow names generated by Ark (`name: Ark architecture gate`)
421
+ if (/\bark architecture gate\b/.test(blob)) return true;
422
+ return false;
423
+ }
424
+
425
+ /**
426
+ * Observe recent GitHub Actions success for Ark architecture checks (EH06).
427
+ * Independent of branch-protection / required-status policy APIs.
428
+ * Never falls back to non-Ark green jobs (lint/test).
429
+ *
430
+ * @param {{ cwd?: string, env?: NodeJS.ProcessEnv, repo?: string, limit?: number }} [opts]
431
+ * @returns {{ runtimeObserved: boolean, latestCiRun: string|null, reason: string, runs?: unknown[] }}
432
+ */
433
+ export function reportGithubCiRuntime(opts = {}) {
434
+ const cwd = opts.cwd ?? process.cwd();
435
+ const env = opts.env ?? process.env;
436
+ const limit = Number.isFinite(Number(opts.limit)) ? Math.max(1, Number(opts.limit)) : 30;
437
+ if (spawnSync('gh', ['--version'], { encoding: 'utf8', env }).status !== 0) {
438
+ return { runtimeObserved: false, latestCiRun: null, reason: 'gh-cli-unavailable' };
439
+ }
440
+ const args = [
441
+ 'run', 'list',
442
+ '--limit', String(limit),
443
+ '--json', 'name,conclusion,status,workflowName,displayTitle,event',
444
+ ];
445
+ if (opts.repo) args.push('--repo', opts.repo);
446
+ const result = spawnSync('gh', args, { cwd, encoding: 'utf8', env });
447
+ if (result.status !== 0) {
448
+ const err = `${result.stderr || ''}${result.stdout || ''}`.slice(0, 400);
449
+ return {
450
+ runtimeObserved: false,
451
+ latestCiRun: null,
452
+ reason: classifyGithubProviderFailure(err) === 'provider-policy-unavailable-plan'
453
+ ? 'provider-policy-unavailable-plan'
454
+ : 'ci-runtime-unverified',
455
+ };
456
+ }
457
+ let runs = [];
458
+ try {
459
+ runs = JSON.parse(result.stdout || '[]');
460
+ } catch {
461
+ return { runtimeObserved: false, latestCiRun: null, reason: 'ci-runtime-unverified' };
462
+ }
463
+ if (!Array.isArray(runs)) {
464
+ return { runtimeObserved: false, latestCiRun: null, reason: 'ci-runtime-unverified' };
465
+ }
466
+ const relevant = runs.filter(isArkishCiRun);
467
+ if (relevant.length === 0) {
468
+ return {
469
+ runtimeObserved: false,
470
+ latestCiRun: null,
471
+ reason: runs.length === 0 ? 'ci-runtime-empty' : 'ci-runtime-no-ark-runs',
472
+ runs: runs.slice(0, 5),
473
+ };
474
+ }
475
+ const success = relevant.find(
476
+ (run) => String(run?.conclusion || '').toLowerCase() === 'success'
477
+ );
478
+ if (success) {
479
+ return {
480
+ runtimeObserved: true,
481
+ latestCiRun: 'success',
482
+ reason: 'ok',
483
+ runs: relevant.slice(0, 5),
484
+ };
485
+ }
486
+ const latest = relevant[0];
487
+ const latestConclusion = latest
488
+ ? String(latest.conclusion || latest.status || 'unknown').toLowerCase()
489
+ : null;
490
+ return {
491
+ runtimeObserved: false,
492
+ latestCiRun: latestConclusion,
493
+ reason: 'ci-runtime-no-success',
494
+ runs: relevant.slice(0, 5),
495
+ };
496
+ }
497
+
365
498
  /** Query classic branch protection and all active branch rules before reporting absence. */
366
499
  export function reportGithubBranchProtection(opts = {}) {
367
500
  const cwd = opts.cwd ?? process.cwd();
368
501
  const env = opts.env ?? process.env;
369
502
  if (spawnSync('gh', ['--version'], { encoding: 'utf8', env }).status !== 0) {
370
- return { available: false, reason: 'gh-cli-unavailable' };
503
+ return { available: false, reason: 'gh-cli-unavailable', runtimeObserved: false, latestCiRun: null };
371
504
  }
372
505
  let repo = opts.repo;
373
506
  let branch = opts.branch;
@@ -375,7 +508,7 @@ export function reportGithubBranchProtection(opts = {}) {
375
508
  const args = ['repo', 'view', ...(repo ? [repo] : []), '--json', 'nameWithOwner,defaultBranchRef'];
376
509
  const metadata = parseJson(spawnSync('gh', args, { cwd, encoding: 'utf8', env }));
377
510
  if (!metadata?.nameWithOwner || !metadata?.defaultBranchRef?.name) {
378
- return { available: false, reason: 'gh-repo-unavailable' };
511
+ return { available: false, reason: 'gh-repo-unavailable', runtimeObserved: false, latestCiRun: null };
379
512
  }
380
513
  repo ??= metadata.nameWithOwner;
381
514
  branch ??= metadata.defaultBranchRef.name;
@@ -426,9 +559,32 @@ export function reportGithubBranchProtection(opts = {}) {
426
559
  const all = [...new Set([...contexts, ...checks.map((check) => check.context), ...statusRules.map((check) => check.context)])];
427
560
  const error = `${classicResult.stderr || ''}${rulesResult.stderr || ''}`.slice(0, 400);
428
561
 
562
+ let reason = available ? 'ok' : 'provider-enforcement-unverified';
563
+ if (!available) {
564
+ // Plan-restriction only when neither classic nor ruleset evidence could be read.
565
+ // Partial success (one source OK, other 403) stays generic unverified — not a plan claim.
566
+ if (!classicAvailable && !rulesAvailable) {
567
+ const classicFail =
568
+ classicResult.status !== 0 ? String(classicResult.stderr || classicResult.stdout || '') : '';
569
+ const rulesFail =
570
+ rulesResult.status !== 0 ? String(rulesResult.stderr || rulesResult.stdout || '') : '';
571
+ reason = classifyGithubProviderFailure(`${classicFail}\n${rulesFail}\n${error}`, {
572
+ classicAvailable,
573
+ rulesAvailable,
574
+ });
575
+ }
576
+ }
577
+
578
+ // EH06: CI runtime observation is independent of branch-protection availability.
579
+ const ciRuntime = opts.includeCiRuntime === false
580
+ ? { runtimeObserved: false, latestCiRun: null, reason: 'skipped' }
581
+ : reportGithubCiRuntime({ cwd, env, repo });
582
+
429
583
  return {
430
584
  available,
431
- reason: available ? 'ok' : 'provider-enforcement-unverified',
585
+ reason,
586
+ // Alias for consumers that look for the short code
587
+ policyReason: reason === 'provider-policy-unavailable-plan' ? 'unavailable-plan' : reason,
432
588
  repo,
433
589
  branch,
434
590
  requiredStatusChecks: all,
@@ -438,6 +594,11 @@ export function reportGithubBranchProtection(opts = {}) {
438
594
  enforcesAdmins: classicAvailable ? Boolean(classic.enforcesAdmins) : null,
439
595
  arkCheckRequired,
440
596
  arkCheckSourceBound,
441
- raw: { classic, rules, ...(error ? { error } : {}) },
597
+ // hard merge remains false when status is not proven required
598
+ hard: arkCheckRequired === true ? undefined : false,
599
+ runtimeObserved: ciRuntime.runtimeObserved === true,
600
+ latestCiRun: ciRuntime.latestCiRun,
601
+ ciRuntimeReason: ciRuntime.reason,
602
+ raw: { classic, rules, ...(error ? { error } : {}), ciRuntime },
442
603
  };
443
604
  }
@@ -1,19 +1,20 @@
1
1
  // Generated from hook-templates.source.mjs — run npm run generate:packaged-tooling.
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
- # Restart Codex after changes; MCP servers are loaded when the project session starts.
2
+ import{execCommandParts as i,execRunner as s}from"../ark-shared.mjs";const c="arkgate-mcp";function l(o){const r=s(o);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${c} --session-context --root . --root-env 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 . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
3
+ `}function p(o){const r=s(o);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root . --root-env CODEX_PROJECT_DIR --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 . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
4
+ `}function f(o){const{command:r,args:e}=i(o,c,["--root",".","--config","ark.config.json"]),t=a=>a.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),n=e.map(a=>`"${t(a)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
5
+ # CONFIGURED ON DISK \u2014 RUNTIME NOT VERIFIED.
6
+ # Restart Codex, then call ark_identity with expectedRoot before trusting MCP verdicts.
6
7
  [mcp_servers.ark]
7
8
  command = "${t(r)}"
8
9
  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
+ `}function g(o){const{command:r,args:e}=i(o,c,["--root",".","--config","ark.config.json"]),t=e.map(n=>`"${n.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
10
11
  # Restart Grok (or /mcps \u2192 refresh) after changes. Also loads repo-root .mcp.json.
11
12
  [mcp_servers.ark]
12
13
  command = "${r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
13
14
  args = [${t}]
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};
15
+ `}function u(o){const r=s(o);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --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 . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
16
+ `}function k(o){const r=s(o);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 . --config ark.config.json`}]}]}},null,2)}
17
+ `}function d(o){const{command:r,args:e}=i(o,c,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...e],enabled:!0}}},null,2)}
18
+ `}function h(o,r){let e,t;try{e=o&&o.trim()?JSON.parse(o):{},t=JSON.parse(r)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e)||!t||typeof t!="object"||Array.isArray(t))return null;const n=t["ark-write-gate"];if(!n||typeof n!="object")return null;const a={...e,"ark-write-gate":n};return`${JSON.stringify(a,null,2)}
19
+ `}function y(o,r){let e,t;try{e=o&&o.trim()?JSON.parse(o):{},t=JSON.parse(r)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;const n={...e};!n.$schema&&t.$schema&&(n.$schema=t.$schema);const a=e.mcp&&typeof e.mcp=="object"&&!Array.isArray(e.mcp)?{...e.mcp}:{};return a.ark=t.mcp.ark,n.mcp=a,`${JSON.stringify(n,null,2)}
20
+ `}export{c as PREFERRED_MCP_BIN,k as antigravityHooks,l as claudeSettings,p as codexHooks,f as codexProjectConfig,u as grokHooks,g as grokProjectConfig,h as mergeAntigravityArkHook,y as mergeOpencodeArkMcp,d as opencodeProjectConfig};
@@ -6,7 +6,21 @@
6
6
  * reported separately by write-path-capabilities.mjs.
7
7
  */
8
8
 
9
- function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, repairPayload) {
9
+ /**
10
+ * @param {string} label
11
+ * @param {string|null} hookPath
12
+ * @param {string|null} hookSurface
13
+ * @param {string[]} hookOperations
14
+ * @param {boolean} hardWrite
15
+ * @param {boolean} repairPayload reinjection guaranteed under hard boundary (historical key)
16
+ * @param {{ repairEnvelopeEmitted?: boolean, operationCoverage?: Record<string, boolean> }} [extras]
17
+ */
18
+ function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, repairPayload, extras = {}) {
19
+ // EH07: repair envelope emission ≠ reinjection guarantee.
20
+ // Codex hooks may emit --hook-repair JSON while reinjection stays host-dependent / not hard.
21
+ const repairEnvelopeEmitted =
22
+ extras.repairEnvelopeEmitted === true || repairPayload === true;
23
+ const repairReinjectionGuaranteed = hardWrite === true && repairPayload === true;
10
24
  return Object.freeze({
11
25
  label,
12
26
  hookPath,
@@ -16,8 +30,16 @@ function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, re
16
30
  'hard-write': hardWrite,
17
31
  'advisory-write': true,
18
32
  'merge-gate': true,
33
+ // Historical key: true only when hard reinjection path is package-supported.
19
34
  'repair-payload': repairPayload,
35
+ 'repair-envelope-emitted': repairEnvelopeEmitted,
36
+ 'repair-reinjection-guaranteed': repairReinjectionGuaranteed,
20
37
  }),
38
+ // EH07 minimum ops matrix (hard=false for soft hosts on every listed op).
39
+ operationCoverage: Object.freeze(
40
+ extras.operationCoverage ||
41
+ Object.fromEntries(hookOperations.map((op) => [op, hardWrite === true]))
42
+ ),
21
43
  });
22
44
  }
23
45
 
@@ -48,14 +70,25 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
48
70
  true,
49
71
  true
50
72
  ),
51
- cursor: hostProfile('Cursor', null, null, [], false, false),
73
+ cursor: hostProfile('Cursor', null, null, [], false, false, {
74
+ operationCoverage: { shell: false, 'pre-commit': false },
75
+ }),
52
76
  codex: hostProfile(
53
77
  'OpenAI Codex',
54
78
  '.codex/hooks.json',
55
79
  'Best-effort PreToolUse `apply_patch`; Code Mode hosts may bypass the event',
56
80
  ['apply_patch'],
57
81
  false,
58
- false
82
+ false,
83
+ {
84
+ // Install writes --hook-repair; envelope can be emitted; reinjection is not guaranteed.
85
+ repairEnvelopeEmitted: true,
86
+ operationCoverage: {
87
+ apply_patch: false,
88
+ shell: false,
89
+ 'pre-commit': false,
90
+ },
91
+ }
59
92
  ),
60
93
  // OpenCode: first-class MCP + permissions; plugin tool.execute.before is incomplete
61
94
  // (subagent holes). Never claim hard write.
@@ -65,7 +98,10 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
65
98
  'Advisory MCP + optional experimental plugin (`tool.execute.before`); not a hard boundary',
66
99
  [],
67
100
  false,
68
- false
101
+ false,
102
+ {
103
+ operationCoverage: { shell: false, 'pre-commit': false },
104
+ }
69
105
  ),
70
106
  });
71
107
 
@@ -82,7 +118,15 @@ export function formatHostSupportSummary(profile) {
82
118
  const write = capabilities['hard-write']
83
119
  ? 'hard local write boundary'
84
120
  : 'no hard local write boundary';
85
- const repair = capabilities['repair-payload'] ? 'repair payload' : 'no hard-boundary repair';
121
+ // EH07 three-way repair story: reinjection guaranteed / envelope-only / none.
122
+ let repair;
123
+ if (capabilities['repair-reinjection-guaranteed']) {
124
+ repair = 'repair reinjection (hard path)';
125
+ } else if (capabilities['repair-envelope-emitted']) {
126
+ repair = 'repair envelope may emit (reinjection not guaranteed)';
127
+ } else {
128
+ repair = 'no hard-boundary repair';
129
+ }
86
130
  return `${write} + advisory MCP + CI check + ${repair}`;
87
131
  }
88
132
 
@@ -104,12 +148,18 @@ export function renderHostSupportMatrixMarkdown() {
104
148
  } else {
105
149
  local = '**Advisory only** at write (no hard hook)';
106
150
  }
107
- const repair = capabilities['repair-payload']
108
- ? 'Emitted on hook deny; host must re-inject'
109
- : 'No hard-boundary payload';
110
- const merge = capabilities['hard-write']
111
- ? '**Required status** = hard merge boundary (`arkgate-check --strict-merge`)'
112
- : '**Required status** = hard merge boundary (same CI)';
151
+ // EH07: distinguish envelope emission vs reinjection guarantee in the repair column.
152
+ let repair;
153
+ if (capabilities['repair-reinjection-guaranteed']) {
154
+ repair = 'Emitted on hook deny; host must re-inject (hard path when installed + trusted)';
155
+ } else if (capabilities['repair-envelope-emitted']) {
156
+ repair = 'Envelope may emit (`--hook-repair`); reinjection **not** guaranteed (advisory host)';
157
+ } else {
158
+ repair = 'No hard-boundary payload';
159
+ }
160
+ // EH07: name the CLI explicitly; required status is a GitHub status context name, not the CLI alone.
161
+ const merge =
162
+ '**Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`)';
113
163
  return `| ${profile.label} | ${local} | Advisory; the agent must call it | ${merge} | ${repair} |`;
114
164
  }).join('\n');
115
165
 
@@ -118,9 +168,30 @@ export function renderHostSupportMatrixMarkdown() {
118
168
  ${rows}
119
169
 
120
170
  **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.
171
+ 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.
173
+
174
+ 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
+ }
122
176
 
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.`;
177
+ /**
178
+ * EH07 doctor/JSON host capability split for repair envelope vs reinjection.
179
+ * @param {string|null|undefined} host
180
+ */
181
+ export function hostRepairCapabilities(host) {
182
+ const profile = getHostSupportProfile(host);
183
+ if (!profile) {
184
+ return {
185
+ repairEnvelopeEmitted: false,
186
+ repairReinjectionGuaranteed: false,
187
+ operationCoverage: {},
188
+ };
189
+ }
190
+ return {
191
+ repairEnvelopeEmitted: profile.capabilities['repair-envelope-emitted'] === true,
192
+ repairReinjectionGuaranteed: profile.capabilities['repair-reinjection-guaranteed'] === true,
193
+ operationCoverage: { ...(profile.operationCoverage || {}) },
194
+ };
124
195
  }
125
196
 
126
197
  /**
@@ -129,19 +200,22 @@ This table describes the supported profile **after its files are installed and t
129
200
  */
130
201
  export function doctorWritePathHonestyMessage(activeHost, hardWriteActive) {
131
202
  const host = typeof activeHost === 'string' ? activeHost.trim().toLowerCase() : '';
203
+ // EH07: distinguish CLI command (arkgate-check / ark-check) from the GitHub required status context name.
204
+ const mergeBoundary =
205
+ 'Required CI hard merge boundary = a required GitHub status context that runs arkgate-check --strict-merge (alias ark-check --strict-merge)';
132
206
  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.';
207
+ return `Cursor: write path is advisory (MCP/rules; no hard PreToolUse). ${mergeBoundary}.`;
134
208
  }
135
209
  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.';
210
+ return `Codex: write path is advisory / best-effort at write (not Claude/Grok hard). ${mergeBoundary}.`;
137
211
  }
138
212
  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.';
213
+ return `OpenCode: write path is advisory / best-effort (MCP + optional plugin; not Claude/Grok/Antigravity hard). ${mergeBoundary}.`;
140
214
  }
141
215
  if ((host === 'claude' || host === 'grok' || host === 'antigravity') && !hardWriteActive) {
142
216
  const label =
143
217
  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.`;
218
+ return `${label}: hard PreToolUse is supported for listed ops when installed + trusted; without runtime-observed hook evidence, hard is unverified. ${mergeBoundary}.`;
145
219
  }
146
220
  return null;
147
221
  }
@@ -288,6 +288,10 @@ function baselineLegendBody(signal) {
288
288
  export function renderProductHonestyCard(productHonesty, mergePlanes = null) {
289
289
  if (!productHonesty || typeof productHonesty !== 'object') return '';
290
290
  const unfinished = productHonesty.unfinished === true;
291
+ const envResiduals = Array.isArray(productHonesty.environmentResidualIds)
292
+ ? productHonesty.environmentResidualIds
293
+ : [];
294
+ const envOnly = !unfinished && envResiduals.length > 0;
291
295
  const headline = productHonesty.headline || (unfinished ? 'Not finished' : 'Honesty clear');
292
296
  const primary = productHonesty.primaryMessage || '';
293
297
  // Avoid repeating the same status label in title and body (past-issue pattern).
@@ -298,7 +302,9 @@ export function renderProductHonestyCard(productHonesty, mergePlanes = null) {
298
302
  if (p === h) {
299
303
  body = unfinished
300
304
  ? 'Residual honesty signals remain — not a whole-tree guarantee and not a score.'
301
- : 'No residual honesty blockers on this slice — still not a numeric architecture score.';
305
+ : envOnly
306
+ ? 'Architecture residual clear; host/environment residual remains (advisory write) — not a score.'
307
+ : 'No residual honesty blockers on this slice — still not a numeric architecture score.';
302
308
  } else if (p.toLowerCase().startsWith(h.toLowerCase())) {
303
309
  const stripped = p.slice(h.length).replace(/^[\s—–:-]+/, '').trim();
304
310
  body = stripped || p;
@@ -319,10 +325,15 @@ export function renderProductHonestyCard(productHonesty, mergePlanes = null) {
319
325
  mergePlanes?.dualPlaneStamp
320
326
  ? `<p class="dim" style="margin:.25rem 0 0;font-size:.84rem">${esc(mergePlanes.dualPlaneStamp)}</p>`
321
327
  : '';
328
+ const subtitle = unfinished
329
+ ? 'architecture residual'
330
+ : envOnly
331
+ ? 'environment residual (advisory write)'
332
+ : 'no residual honesty blockers';
322
333
  return `<div class="section card design-strip ${unfinished ? 'is-weak' : 'is-clean'}" id="product-honesty" data-product-honesty="1">
323
334
  <div class="design-head">
324
335
  <span class="badge design" title="Product honesty — not a score">${esc(headline)}</span>
325
- <span class="dim" style="font-size:.86rem">${unfinished ? 'residual honesty signals' : 'no residual honesty blockers'}</span>
336
+ <span class="dim" style="font-size:.86rem">${esc(subtitle)}</span>
326
337
  </div>
327
338
  <p style="margin:.45rem 0 0">${esc(body)}</p>
328
339
  ${reasonHtml}
@@ -0,0 +1,114 @@
1
+ export function renderEvolutionSection({
2
+ originSnapshot,
3
+ currentSnapshot,
4
+ originJustCreated,
5
+ esc,
6
+ formatDelta,
7
+ historyMax,
8
+ }) {
9
+ if (!currentSnapshot) return '';
10
+ if (originJustCreated || !originSnapshot) {
11
+ return `<div class="section card evolve">
12
+ <h2>Origin baseline captured</h2>
13
+ <p class="dim" style="margin:.2rem 0 0;font-size:.9rem">
14
+ This is the <b>first</b> architecture snapshot for this project
15
+ (<code>.ark/reports/origin.json</code> + <code>origin.html</code>).
16
+ Future reports will show deltas against this starting point so you can prove evolution.
17
+ </p>
18
+ </div>`;
19
+ }
20
+ const scoreComparable =
21
+ typeof originSnapshot.arkVersion === 'string' &&
22
+ originSnapshot.arkVersion.length > 0 &&
23
+ originSnapshot.arkVersion === currentSnapshot.arkVersion;
24
+ const rows = [
25
+ ['Ark score', originSnapshot.score, currentSnapshot.score, '', scoreComparable],
26
+ ['Governed %', originSnapshot.governedPercent, currentSnapshot.governedPercent, 'pp', true],
27
+ ['Files in scope', originSnapshot.totalFiles, currentSnapshot.totalFiles, '', true],
28
+ ['Classified files', originSnapshot.classifiedFiles, currentSnapshot.classifiedFiles, '', true],
29
+ ['Active violations', originSnapshot.activeViolations, currentSnapshot.activeViolations, '', true],
30
+ ['Value violations', originSnapshot.valueViolations, currentSnapshot.valueViolations, '', true],
31
+ ['Type-only violations', originSnapshot.typeOnlyViolations, currentSnapshot.typeOnlyViolations, '', true],
32
+ ['Layers', originSnapshot.layerCount, currentSnapshot.layerCount, '', true],
33
+ ['Deny rules', originSnapshot.denyRules, currentSnapshot.denyRules, '', true],
34
+ ['Gates configured', originSnapshot.gatesOn, currentSnapshot.gatesOn, '', true],
35
+ ];
36
+ const originDate = (originSnapshot.generatedAt || '').slice(0, 10) || 'origin';
37
+ const nowDate = (currentSnapshot.generatedAt || '').slice(0, 10) || 'now';
38
+ const tr = rows
39
+ .map(([label, from, to, unit, comparable]) => {
40
+ const d =
41
+ comparable && typeof from === 'number' && typeof to === 'number'
42
+ ? to - from
43
+ : null;
44
+ const good =
45
+ label.includes('violation') || label.includes('Violation')
46
+ ? d != null && d <= 0
47
+ : label.includes('Governed') ||
48
+ label.includes('score') ||
49
+ label.includes('Classified') ||
50
+ label.includes('Gates')
51
+ ? d != null && d >= 0
52
+ : null;
53
+ const cls =
54
+ d == null || d === 0 ? 'flat' : good === true ? 'up' : good === false ? 'down' : 'flat';
55
+ const delta =
56
+ d == null
57
+ ? '—'
58
+ : unit === 'pp'
59
+ ? formatDelta(Math.round(d * 10) / 10, { suffix: ' pp' })
60
+ : formatDelta(d);
61
+ return `<tr>
62
+ <td>${esc(label)}</td>
63
+ <td class="num">${from ?? '—'}</td>
64
+ <td class="num">${to ?? '—'}</td>
65
+ <td class="num delta ${cls}">${esc(delta)}</td>
66
+ </tr>`;
67
+ })
68
+ .join('\n');
69
+ const originLayers = originSnapshot.layerFiles || {};
70
+ const currentLayers = currentSnapshot.layerFiles || {};
71
+ const layerKeys = [
72
+ ...new Set([...Object.keys(originLayers), ...Object.keys(currentLayers)]),
73
+ ].sort();
74
+ const layerTr = layerKeys
75
+ .map((name) => {
76
+ const from = originLayers[name] || 0;
77
+ const to = currentLayers[name] || 0;
78
+ const d = to - from;
79
+ const cls = d === 0 ? 'flat' : d > 0 ? 'up' : 'down';
80
+ return `<tr>
81
+ <td class="ln">${esc(name)}</td>
82
+ <td class="num">${from}</td>
83
+ <td class="num">${to}</td>
84
+ <td class="num delta ${cls}">${esc(formatDelta(d))}</td>
85
+ </tr>`;
86
+ })
87
+ .join('\n');
88
+ const scoreNote = scoreComparable
89
+ ? ''
90
+ : `<p class="dim" style="margin:-.35rem 0 .75rem;font-size:.88rem">
91
+ Ark score is not comparable across Ark versions
92
+ (<code>${esc(originSnapshot.arkVersion ?? 'unknown')}</code> →
93
+ <code>${esc(currentSnapshot.arkVersion ?? 'unknown')}</code>); its Δ is shown as —.
94
+ Raw coverage, files, violations, layers, rules, and gate metrics remain visible.
95
+ </p>`;
96
+ return `<div class="section card evolve">
97
+ <h2>Evolution vs origin</h2>
98
+ <p class="dim" style="margin:.15rem 0 .75rem;font-size:.88rem">
99
+ Origin snapshot <code>${esc(originDate)}</code> → this report <code>${esc(nowDate)}</code>
100
+ · frozen at <code>.ark/reports/origin.*</code> · reopen origin HTML anytime for the starting picture.
101
+ </p>
102
+ ${scoreNote}
103
+ <table class="layers">
104
+ <tr><th>Metric</th><th>Origin</th><th>Now</th><th>Δ</th></tr>
105
+ ${tr}
106
+ </table>
107
+ <h3>Files per layer</h3>
108
+ <table class="layers">
109
+ <tr><th>Layer</th><th>Origin</th><th>Now</th><th>Δ</th></tr>
110
+ ${layerTr || '<tr><td colspan="4" class="dim">No layer file data in snapshots.</td></tr>'}
111
+ </table>
112
+ <p class="legend">Green Δ = improvement for that metric (↑ coverage/score/gates, ↓ violations). Score Δ is comparable only within the same Ark version. History JSON under <code>.ark/reports/history/</code> (last ${historyMax}).</p>
113
+ </div>`;
114
+ }