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
@@ -10,6 +10,35 @@ import {
10
10
  loadArkRulesContract,
11
11
  } from './arkrules-contract.mjs';
12
12
 
13
+ function normalizeProjectRelativePath(value) {
14
+ const normalized = value.replace(/\\/g, '/');
15
+ if (
16
+ !normalized ||
17
+ normalized.startsWith('/') ||
18
+ /^[A-Za-z]:/.test(normalized) ||
19
+ normalized.includes('\0')
20
+ ) {
21
+ return undefined;
22
+ }
23
+ const segments = [];
24
+ for (const segment of normalized.split('/')) {
25
+ if (!segment || segment === '.') continue;
26
+ if (segment === '..') return undefined;
27
+ segments.push(segment);
28
+ }
29
+ return segments.length > 0 ? segments.join('/') : undefined;
30
+ }
31
+
32
+ function isWithinRoot(root, candidate) {
33
+ const relative = path.relative(root, candidate);
34
+ return (
35
+ relative === '' ||
36
+ (!relative.startsWith(`..${path.sep}`) &&
37
+ relative !== '..' &&
38
+ !path.isAbsolute(relative))
39
+ );
40
+ }
41
+
13
42
  /**
14
43
  * @param {string} root
15
44
  * @param {Record<string, unknown>} config loaded ark.config.json object
@@ -29,6 +58,7 @@ export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
29
58
  const warnings = [];
30
59
  const parts = [];
31
60
  const referenced = new Set();
61
+ const canonicalRoot = fs.realpathSync(root);
32
62
 
33
63
  for (const layer of Object.keys(refs).sort()) {
34
64
  const relRaw = refs[layer];
@@ -37,10 +67,12 @@ export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
37
67
  errors.push({ path: pathKey, message: 'must be a non-empty relative path string' });
38
68
  continue;
39
69
  }
40
- if (relRaw.startsWith('/') || /^[A-Za-z]:[\\/]/.test(relRaw)) {
70
+ const rel = normalizeProjectRelativePath(relRaw);
71
+ if (!rel) {
41
72
  errors.push({
42
73
  path: pathKey,
43
- message: 'must be a project-relative path (absolute paths are not allowed)',
74
+ message:
75
+ 'must be a project-relative path without absolute roots or parent-directory traversal',
44
76
  });
45
77
  continue;
46
78
  }
@@ -52,17 +84,42 @@ export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
52
84
  continue;
53
85
  }
54
86
 
55
- const rel = relRaw.replace(/\\/g, '/').replace(/^\.\//, '');
56
87
  referenced.add(rel);
57
- const absolute = path.resolve(root, rel);
58
- opts.observeInput?.(absolute, 'arkrules');
59
- if (!fs.existsSync(absolute)) {
88
+ const lexicalTarget = path.resolve(canonicalRoot, ...rel.split('/'));
89
+ if (!isWithinRoot(canonicalRoot, lexicalTarget)) {
90
+ errors.push({
91
+ path: pathKey,
92
+ message: `referenced ArkRules path ${JSON.stringify(rel)} resolves outside the project root`,
93
+ });
94
+ continue;
95
+ }
96
+ if (!fs.existsSync(lexicalTarget)) {
60
97
  errors.push({
61
98
  path: pathKey,
62
99
  message: `referenced ArkRules file ${JSON.stringify(rel)} is missing`,
63
100
  });
64
101
  continue;
65
102
  }
103
+ let absolute;
104
+ try {
105
+ absolute = fs.realpathSync(lexicalTarget);
106
+ } catch (error) {
107
+ errors.push({
108
+ path: pathKey,
109
+ message: `referenced ArkRules file ${JSON.stringify(rel)} could not be resolved: ${
110
+ error instanceof Error ? error.message : String(error)
111
+ }`,
112
+ });
113
+ continue;
114
+ }
115
+ if (!isWithinRoot(canonicalRoot, absolute)) {
116
+ errors.push({
117
+ path: pathKey,
118
+ message: `referenced ArkRules path ${JSON.stringify(rel)} resolves outside the project root`,
119
+ });
120
+ continue;
121
+ }
122
+ opts.observeInput?.(absolute, 'arkrules');
66
123
  let content;
67
124
  try {
68
125
  content = fs.readFileSync(absolute, 'utf8');
@@ -90,9 +147,16 @@ export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
90
147
  }
91
148
 
92
149
  // Drift: unreferenced files under arkrules/
93
- const arkrulesDir = path.join(root, 'arkrules');
94
- if (fs.existsSync(arkrulesDir) && fs.statSync(arkrulesDir).isDirectory()) {
95
- for (const name of fs.readdirSync(arkrulesDir).sort()) {
150
+ const arkrulesDir = path.join(canonicalRoot, 'arkrules');
151
+ const resolvedArkRulesDir = fs.existsSync(arkrulesDir)
152
+ ? fs.realpathSync(arkrulesDir)
153
+ : undefined;
154
+ if (
155
+ resolvedArkRulesDir &&
156
+ isWithinRoot(canonicalRoot, resolvedArkRulesDir) &&
157
+ fs.statSync(resolvedArkRulesDir).isDirectory()
158
+ ) {
159
+ for (const name of fs.readdirSync(resolvedArkRulesDir).sort()) {
96
160
  if (!name.endsWith('.json')) continue;
97
161
  const rel = `arkrules/${name}`;
98
162
  if (!referenced.has(rel)) {
@@ -207,7 +207,8 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
207
207
  hardWriteSupported: hardCapable,
208
208
  hardWriteActive: effectiveHard,
209
209
  hardWriteUnverified: hardCapable && !effectiveHard,
210
- hardMergeBoundary: 'required-ci-status (arkgate-check --strict-merge)',
210
+ hardMergeBoundary:
211
+ 'required-github-status-context (CLI: arkgate-check --strict-merge / ark-check --strict-merge)',
211
212
  packageInstalled,
212
213
  packagePinAbsent: pinAbsentForUser,
213
214
  ...(pinCode ? { packagePinCode: pinCode } : {}),
@@ -367,10 +368,15 @@ export function buildProductHonesty(input = {}) {
367
368
  });
368
369
  }
369
370
 
371
+ // EH05: soft-write-host is a permanent host posture residual — keep in evidence,
372
+ // do NOT alone force architecture "Not finished". Reclassified out of contract debt.
370
373
  if (write?.softWriteHost) {
371
374
  reasons.push({
372
375
  id: 'soft-write-host',
373
- message: write.message || 'Local write is advisory; required CI status is the hard merge boundary.',
376
+ bucket: 'environment',
377
+ message:
378
+ write.message ||
379
+ 'Local write is advisory; hard merge boundary = a required GitHub status context running arkgate-check --strict-merge (alias ark-check --strict-merge).',
374
380
  });
375
381
  }
376
382
 
@@ -399,7 +405,13 @@ export function buildProductHonesty(input = {}) {
399
405
  // Informational only when no enforced arkrule plane — does not alone make unfinished.
400
406
  }
401
407
 
402
- const unfinished = reasons.length > 0;
408
+ // EH05: environment residual deny-list (future reason ids stay architecture debt by default).
409
+ const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host']);
410
+
411
+ const environmentResiduals = reasons.filter((r) => ENVIRONMENT_REASON_IDS.has(r.id));
412
+ const architectureReasons = reasons.filter((r) => !ENVIRONMENT_REASON_IDS.has(r.id));
413
+ // unfinished = any non-environment residual (deny-list env, not allowlist architecture)
414
+ const unfinished = architectureReasons.length > 0;
403
415
  const wholeTreeGoverned = wholeTreeGovernedEarly;
404
416
  const coverageIncomplete =
405
417
  cov?.status === 'empty-scope' ||
@@ -407,31 +419,56 @@ export function buildProductHonesty(input = {}) {
407
419
  cov?.greenIsNotEnforcement === true ||
408
420
  !wholeTreeGoverned;
409
421
 
422
+ const softWriteOnly =
423
+ !unfinished && environmentResiduals.some((r) => r.id === 'soft-write-host');
424
+ const hostLabel = (() => {
425
+ const h = typeof write?.activeHost === 'string' ? write.activeHost.trim().toLowerCase() : '';
426
+ if (h === 'codex') return 'Codex';
427
+ if (h === 'cursor') return 'Cursor';
428
+ if (h === 'opencode') return 'OpenCode';
429
+ if (h) return h;
430
+ return 'this host';
431
+ })();
432
+
410
433
  const primary =
411
- reasons.find((r) => r.id === 'active-blocking-violations') ||
412
- reasons.find((r) => r.id === 'mode-adapt-with-debt') ||
413
- reasons.find((r) => r.id === 'mode-suggest-with-debt') ||
414
- reasons.find((r) => r.id === 'design-weak') ||
415
- reasons.find((r) => r.id === 'design-smells-open-edges') ||
416
- reasons.find((r) => r.id === 'coverage-weak-or-empty') ||
417
- reasons.find((r) => r.id === 'dirty-freeze') ||
418
- reasons.find((r) => r.id === 'package-version-dual-truth') ||
419
- reasons.find((r) => r.id === 'package-pin-absent') ||
420
- reasons.find((r) => r.id === 'baseline-missing-with-debt') ||
421
- reasons.find((r) => r.id === 'residual-pilot') ||
422
- reasons[0];
434
+ architectureReasons.find((r) => r.id === 'active-blocking-violations') ||
435
+ architectureReasons.find((r) => r.id === 'mode-adapt-with-debt') ||
436
+ architectureReasons.find((r) => r.id === 'mode-suggest-with-debt') ||
437
+ architectureReasons.find((r) => r.id === 'design-weak') ||
438
+ architectureReasons.find((r) => r.id === 'design-smells-open-edges') ||
439
+ architectureReasons.find((r) => r.id === 'coverage-weak-or-empty') ||
440
+ architectureReasons.find((r) => r.id === 'dirty-freeze') ||
441
+ architectureReasons.find((r) => r.id === 'package-version-dual-truth') ||
442
+ architectureReasons.find((r) => r.id === 'package-pin-absent') ||
443
+ architectureReasons.find((r) => r.id === 'baseline-missing-with-debt') ||
444
+ architectureReasons.find((r) => r.id === 'residual-pilot') ||
445
+ architectureReasons[0] ||
446
+ environmentResiduals[0];
423
447
 
424
- const primaryMessage = unfinished
425
- ? primary?.message ||
426
- 'Not finished: residual honesty signals remain (violations, mode, coverage, freeze, design, package pin, or pilots).'
427
- : wholeTreeGoverned
428
- ? 'No residual honesty blockers on this slice still not a numeric architecture score; re-doctor after material change.'
429
- : 'No residual honesty blockers flagged — green is only as wide as the governed slice.';
448
+ let primaryMessage;
449
+ if (unfinished) {
450
+ primaryMessage =
451
+ primary?.message ||
452
+ 'Not finished: residual honesty signals remain (violations, mode, coverage, freeze, design, package pin, or pilots).';
453
+ } else if (softWriteOnly) {
454
+ primaryMessage = `${hostLabel} local writes stay advisory/bypassable; architecture contract on this slice is ready. Hard merge boundary is a required GitHub status context running arkgate-check --strict-merge (alias ark-check --strict-merge).`;
455
+ } else if (wholeTreeGoverned) {
456
+ primaryMessage =
457
+ 'No residual honesty blockers on this slice — still not a numeric architecture score; re-doctor after material change.';
458
+ } else {
459
+ primaryMessage =
460
+ 'No residual honesty blockers flagged — green is only as wide as the governed slice.';
461
+ }
430
462
 
431
463
  // P0B-HEADLINE: dual-truth / pin-only unfinished must not claim "not whole-tree"
432
464
  // when the governed tree is already 100%.
465
+ // EH05: soft-write alone → composite readiness headline, never global "Not finished".
433
466
  let headline;
434
- if (!unfinished) {
467
+ if (!unfinished && softWriteOnly) {
468
+ headline = wholeTreeGoverned
469
+ ? `Architecture contract ready; ${hostLabel} local writes are advisory`
470
+ : `Contract residual clear; ${hostLabel} local writes are advisory`;
471
+ } else if (!unfinished) {
435
472
  headline = 'Honesty clear on residual signals';
436
473
  } else if (coverageIncomplete) {
437
474
  headline = 'Not finished / not whole-tree guarantee';
@@ -440,6 +477,7 @@ export function buildProductHonesty(input = {}) {
440
477
  }
441
478
 
442
479
  // Prefer caller next action; dual-truth / pin-absent get install/pin path when empty.
480
+ // Soft-write-only must not leave a failure headline with null next action (EH05).
443
481
  let primaryNextAction = input.primaryNextAction || null;
444
482
  if (!primaryNextAction && dualTruth) {
445
483
  const ver = input.packageVersionTruth?.cliVersion;
@@ -449,8 +487,20 @@ export function buildProductHonesty(input = {}) {
449
487
  } else if (!primaryNextAction && pinAbsent) {
450
488
  primaryNextAction =
451
489
  'Add arkgate to package.json and install so CI/npx resolve this CLI (PACKAGE_PIN_ABSENT)';
490
+ } else if (!primaryNextAction && softWriteOnly) {
491
+ primaryNextAction =
492
+ 'Confirm the GitHub required status context name runs arkgate-check --strict-merge (or ark-check --strict-merge). Soft-write hosts stay advisory at local write; the required status is the hard merge boundary.';
452
493
  }
453
494
 
495
+ const contractReadiness = unfinished ? 'not-ready' : wholeTreeGoverned ? 'ready' : 'partial';
496
+ const localWriteBoundary = write?.softWriteHost
497
+ ? 'advisory'
498
+ : write?.hardWriteActive
499
+ ? 'hard'
500
+ : write?.hardWriteSupported
501
+ ? 'unverified'
502
+ : 'unknown';
503
+
454
504
  return {
455
505
  finished: !unfinished && wholeTreeGoverned && !designWeak && activeBlocking === 0,
456
506
  elegant: !designWeak && !base?.dirtyBaselineRisk && !designSmellsOpenEdges && activeBlocking === 0,
@@ -462,8 +512,14 @@ export function buildProductHonesty(input = {}) {
462
512
  activeBlocking === 0,
463
513
  unfinished,
464
514
  notAScore: true,
515
+ // Full evidence including soft-write-host (reclassified, not silenced)
465
516
  reasonIds: reasons.map((r) => r.id),
466
517
  reasons,
518
+ architectureReasonIds: architectureReasons.map((r) => r.id),
519
+ environmentResidualIds: environmentResiduals.map((r) => r.id),
520
+ environmentResiduals,
521
+ contractReadiness,
522
+ localWriteBoundary,
467
523
  primaryMessage,
468
524
  primaryNextAction,
469
525
  headline,
@@ -1,2 +1,2 @@
1
1
  // Generated from enforcement-state.source.mjs — run npm run generate:packaged-tooling.
2
- import b from"node:fs";import{createRequire as q}from"node:module";import h from"node:path";const n="unverified";function S(a){const e=h.join(a,"package.json");try{if(JSON.parse(b.readFileSync(e,"utf8"))?.name==="arkgate"&&b.statSync(h.join(a,"bin","ark-check.mjs"),{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"package.json + bin/ark-check.mjs (self-host)",selfHost:!0}}catch{}try{const r=q(e).resolve("arkgate/package.json"),o=h.dirname(r),t=JSON.parse(b.readFileSync(r,"utf8")),l=h.join(o,"bin","ark-check.mjs");if(t?.name==="arkgate"&&b.statSync(l,{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"arkgate/package.json via project resolver",selfHost:!1}}catch{}return{installed:!1,source:"arkgate/package.json unresolved from project",selfHost:!1}}function M(a){return a.length>0?a:["filesystem scan (no matching configuration)"]}function y({supported:a,configuredPaths:e,installed:r,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,sources:s}){const p=e.length>0,v=!!r.installed;return{supported:a,analyzed:!0,configured:p,installed:v,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,evidence:[...M(e).map(g=>({field:"configured",source:g,value:p})),{field:"installed",source:r.source,value:v},{field:"active",source:s.active,value:o},{field:"runtimeObserved",source:s.runtimeObserved,value:t},{field:"operationCoverage",source:s.operationCoverage,value:d},{field:"bypassable",source:s.bypassable,value:u},{field:"required",source:s.required,value:c},{field:"hard",source:s.hard,value:i}]}}function O(a,e){const r=S(a),o=!!e.support?.capabilities?.["hard-write"],t=!!e.support?.capabilities?.["advisory-write"],l=e.capabilityEvidence["hard-write"],d=e.capabilityEvidence["advisory-write"],u=e.capabilityEvidence["merge-gate"],c=e.enforcementLadder.localWrite,i=typeof c.operationCovered=="boolean",s=i?c.operationCovered:n,p=i&&s===!0,v=!!(o&&r.installed&&p&&c.hard===!0),g=i?p&&r.installed:o&&l.length>0&&r.installed?n:!1,k=t&&d.length>0&&r.installed?n:!1,f=!!(e.ci?.failClosed&&u.length>0),C=f&&r.installed?n:!1;return{schemaVersion:"1.1",activeHost:e.activeHost,localWrite:y({supported:o,configuredPaths:l,installed:r,active:g,runtimeObserved:i,operation:i?c.operation??null:null,operationCoverage:s,bypassable:v?!1:o&&!i?n:!0,required:n,hard:v,sources:{active:i?"observed PreToolUse attempt":"runtime observation unavailable",runtimeObserved:i?"fresh PreToolUse invocation":"runtime observation unavailable",operationCoverage:i?"active-host operation matcher":"operation not observed",bypassable:v?"observed hard write boundary":"host runtime bypass evidence unavailable",required:"local host policy unavailable",hard:v?"fresh covered active-host invocation":"hardness not proven for this invocation"}}),advisoryMcp:y({supported:t,configuredPaths:d,installed:r,active:k,runtimeObserved:!1,operation:null,operationCoverage:n,bypassable:!0,required:n,hard:!1,sources:{active:"MCP runtime observation unavailable",runtimeObserved:"doctor did not observe an MCP tool invocation",operationCoverage:"advisory MCP is caller-invoked",bypassable:"advisory MCP does not intercept every write",required:"local host policy unavailable",hard:"MCP presence is advisory and never proves a hard boundary"}}),ciMerge:y({supported:!0,configuredPaths:f?u:[],installed:r,active:C,runtimeObserved:!1,operation:"merge",operationCoverage:f?n:!1,bypassable:f?n:!0,required:n,hard:!1,sources:{active:"CI run and provider enforcement not observed",runtimeObserved:"provider evidence unavailable",operationCoverage:"required-status operation coverage unavailable",bypassable:"branch-protection evidence unavailable",required:"branch-protection evidence unavailable",hard:"merge hardness requires fresh provider evidence"}})}}function w(a,e,r,o){return{...a,...o,evidence:[...a.evidence.filter(t=>!e.includes(t.field)),...e.map(t=>({field:t,source:r,value:o[t]}))]}}function x(a,e){if(!e?.available)return a;const r=typeof e.arkCheckRequired=="boolean"?e.arkCheckRequired:n,o=!!(a.enforcementState.ciMerge.configured&&a.enforcementState.ciMerge.installed),t=r===!0?o:r===!1?!1:o?n:!1,l=t===!0?e.arkCheckSourceBound===!1?!0:n:r===!1?!0:o?n:!0,d=`GitHub branch protection (${e.repo??"repository"}:${e.branch??"default"})`,u=!0,c=r,i=t===!0&&l===!1&&c===!0,s=w(a.enforcementState.ciMerge,["active","runtimeObserved","operationCoverage","bypassable","required","hard"],d,{active:t,runtimeObserved:u,operationCoverage:c,bypassable:l,required:r,hard:i});return{...a,enforcementState:{...a.enforcementState,ciMerge:s},enforcementLadder:{...a.enforcementLadder,ciMerge:{...a.enforcementLadder.ciMerge,requiredStatus:r}}}}function m(a,e){const r=o=>o===!0?"yes":o===!1?"no":String(o);return`${a} \u2014 supported: ${r(e.supported)} \xB7 analyzed: ${r(e.analyzed)} \xB7 configured: ${r(e.configured)} \xB7 installed: ${r(e.installed)} \xB7 runtime observed: ${r(e.runtimeObserved)} \xB7 operation: ${e.operation??"none"} \xB7 operation covered: ${r(e.operationCoverage)} \xB7 active: ${r(e.active)} \xB7 bypassable: ${r(e.bypassable)} \xB7 required: ${r(e.required)} \xB7 hard: ${r(e.hard)}`}function P(a){const e=[{level:a.localWrite.active===!0?"ok":"warn",text:m("Local write",a.localWrite)},{level:"warn",text:m("Advisory MCP",a.advisoryMcp)},{level:a.ciMerge.required===!0?"ok":"warn",text:m("CI merge",a.ciMerge)}];return a.localWrite.active===n&&a.localWrite.hard===!1&&e.push({level:"bad",text:"RED FLAG: local hook assets exist, but this active-host operation was not observed at runtime; hard blocking is unverified."}),a.activeHost==="unknown"&&e.push({level:"warn",text:"Active host unknown for this invocation \u2014 enforcementState is session projection only. See writePath.inventory for on-disk host hooks; hard write is never claimed without runtime proof."}),e}export{O as buildEnforcementState,P as enforcementDoctorLines,S as packageInstallation,x as withCiProviderEvidence};
2
+ import m from"node:fs";import{createRequire as q}from"node:module";import h from"node:path";const n="unverified";function O({configuredOnDisk:a=!1,restartRequired:e=a}={}){return{configuredOnDisk:!!a,restartRequired:!!e,runtimeObserved:!1,identityMatch:n,active:!1}}function M(a){const e=h.join(a,"package.json");try{if(JSON.parse(m.readFileSync(e,"utf8"))?.name==="arkgate"&&m.statSync(h.join(a,"bin","ark-check.mjs"),{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"package.json + bin/ark-check.mjs (self-host)",selfHost:!0}}catch{}try{const r=q(e).resolve("arkgate/package.json"),o=h.dirname(r),i=JSON.parse(m.readFileSync(r,"utf8")),s=h.join(o,"bin","ark-check.mjs");if(i?.name==="arkgate"&&m.statSync(s,{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"arkgate/package.json via project resolver",selfHost:!1}}catch{}return{installed:!1,source:"arkgate/package.json unresolved from project",selfHost:!1}}function S(a){return a.length>0?a:["filesystem scan (no matching configuration)"]}function g({supported:a,configuredPaths:e,installed:r,active:o,runtimeObserved:i,operation:s,operationCoverage:u,bypassable:v,required:c,hard:t,sources:l}){const p=e.length>0,d=!!r.installed;return{supported:a,analyzed:!0,configured:p,installed:d,active:o,runtimeObserved:i,operation:s,operationCoverage:u,bypassable:v,required:c,hard:t,evidence:[...S(e).map(f=>({field:"configured",source:f,value:p})),{field:"installed",source:r.source,value:d},{field:"active",source:l.active,value:o},{field:"runtimeObserved",source:l.runtimeObserved,value:i},{field:"operationCoverage",source:l.operationCoverage,value:u},{field:"bypassable",source:l.bypassable,value:v},{field:"required",source:l.required,value:c},{field:"hard",source:l.hard,value:t}]}}function E(a,e){const r=M(a),o=!!e.support?.capabilities?.["hard-write"],i=!!e.support?.capabilities?.["advisory-write"],s=e.capabilityEvidence["hard-write"],u=e.capabilityEvidence["advisory-write"],v=e.capabilityEvidence["merge-gate"],c=e.enforcementLadder.localWrite,t=typeof c.operationCovered=="boolean",l=t?c.operationCovered:n,p=t&&l===!0,d=!!(o&&r.installed&&p&&c.hard===!0),f=t?p&&r.installed:o&&s.length>0&&r.installed?n:!1,b=i&&u.length>0&&r.installed?n:!1,y=!!(e.ci?.failClosed&&v.length>0),C=y&&r.installed?n:!1;return{schemaVersion:"1.1",activeHost:e.activeHost,localWrite:g({supported:o,configuredPaths:s,installed:r,active:f,runtimeObserved:t,operation:t?c.operation??null:null,operationCoverage:l,bypassable:d?!1:o&&!t?n:!0,required:n,hard:d,sources:{active:t?"observed PreToolUse attempt":"runtime observation unavailable",runtimeObserved:t?"fresh PreToolUse invocation":"runtime observation unavailable",operationCoverage:t?"active-host operation matcher":"operation not observed",bypassable:d?"observed hard write boundary":"host runtime bypass evidence unavailable",required:"local host policy unavailable",hard:d?"fresh covered active-host invocation":"hardness not proven for this invocation"}}),advisoryMcp:g({supported:i,configuredPaths:u,installed:r,active:b,runtimeObserved:!1,operation:null,operationCoverage:n,bypassable:!0,required:n,hard:!1,sources:{active:"MCP runtime observation unavailable",runtimeObserved:"doctor did not observe an MCP tool invocation",operationCoverage:"advisory MCP is caller-invoked",bypassable:"advisory MCP does not intercept every write",required:"local host policy unavailable",hard:"MCP presence is advisory and never proves a hard boundary"}}),ciMerge:g({supported:!0,configuredPaths:y?v:[],installed:r,active:C,runtimeObserved:!1,operation:"merge",operationCoverage:y?n:!1,bypassable:y?n:!0,required:n,hard:!1,sources:{active:"CI run and provider enforcement not observed",runtimeObserved:"provider evidence unavailable",operationCoverage:"required-status operation coverage unavailable",bypassable:"branch-protection evidence unavailable",required:"branch-protection evidence unavailable",hard:"merge hardness requires fresh provider evidence"}})}}function $(a,e,r,o){return{...a,...o,evidence:[...a.evidence.filter(i=>!e.includes(i.field)),...e.map(i=>({field:i,source:r,value:o[i]}))]}}function x(a,e){if(!e)return a;const r=e.reason==="provider-policy-unavailable-plan"||e.policyReason==="unavailable-plan",o=e.available===!0,i=!o&&!r&&(e.reason==="provider-enforcement-unverified"||e.reason==="gh-cli-unavailable"||e.reason==="gh-repo-unavailable"||!!e.reason);if(!o&&e.runtimeObserved!==!0&&!r&&!i)return a;const s=o?typeof e.arkCheckRequired=="boolean"?e.arkCheckRequired:n:r?!1:n,u=!!(a.enforcementState.ciMerge.configured&&a.enforcementState.ciMerge.installed),v=s===!0?u:s===!1?!1:u?n:!1,c=v===!0?e.arkCheckSourceBound===!1?!0:n:s===!1?!0:u?n:!0,t=e.runtimeObserved===!0,l=o?`GitHub branch protection (${e.repo??"repository"}:${e.branch??"default"})`:r?`GitHub provider policy unavailable (plan) (${e.repo??"repository"}:${e.branch??"default"})`:`GitHub CI runtime (${e.repo??"repository"})`,p=s,d=v===!0&&c===!1&&p===!0,f=$(a.enforcementState.ciMerge,["active","runtimeObserved","operationCoverage","bypassable","required","hard"],l,{active:v,runtimeObserved:t,operationCoverage:p,bypassable:c,required:s,hard:d}),b={...a,enforcementState:{...a.enforcementState,ciMerge:f},enforcementLadder:{...a.enforcementLadder,ciMerge:{...a.enforcementLadder.ciMerge,requiredStatus:s,...e.latestCiRun?{latestCiRun:e.latestCiRun}:{},...r?{providerPolicy:"unavailable-plan"}:{}}}};return(r||e.reason)&&(b.providerEnforcement={available:o,reason:e.reason||(r?"provider-policy-unavailable-plan":"provider-enforcement-unverified"),policyReason:e.policyReason||(r?"unavailable-plan":null),runtimeObserved:t,latestCiRun:e.latestCiRun??null,hard:d===!0}),b}function k(a,e){const r=o=>o===!0?"yes":o===!1?"no":String(o);return`${a} \u2014 supported: ${r(e.supported)} \xB7 analyzed: ${r(e.analyzed)} \xB7 configured: ${r(e.configured)} \xB7 installed: ${r(e.installed)} \xB7 runtime observed: ${r(e.runtimeObserved)} \xB7 operation: ${e.operation??"none"} \xB7 operation covered: ${r(e.operationCoverage)} \xB7 active: ${r(e.active)} \xB7 bypassable: ${r(e.bypassable)} \xB7 required: ${r(e.required)} \xB7 hard: ${r(e.hard)}`}function B(a){const e=[{level:a.localWrite.active===!0?"ok":"warn",text:k("Local write",a.localWrite)},{level:"warn",text:k("Advisory MCP",a.advisoryMcp)},{level:a.ciMerge.required===!0?"ok":"warn",text:k("CI merge",a.ciMerge)}];return a.localWrite.active===n&&a.localWrite.hard===!1&&e.push({level:"bad",text:"RED FLAG: local hook assets exist, but this active-host operation was not observed at runtime; hard blocking is unverified."}),a.activeHost==="unknown"&&e.push({level:"warn",text:"Active host unknown for this invocation \u2014 enforcementState is session projection only. See writePath.inventory for on-disk host hooks; hard write is never claimed without runtime proof."}),e}export{E as buildEnforcementState,O as codexRuntimeActivation,B as enforcementDoctorLines,M as packageInstallation,x as withCiProviderEvidence};