Sign In

arkgate

Package Overview
Dependencies
Maintainers
1
Versions
52
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

arkgate - npm Package Compare versions

Comparing version
4.1.0
to
4.1.1
+88
bin/lib/ark-gitignore.mjs
/**
* Ark report / local-state .gitignore coverage (EH03).
* Extracted from html-report.mjs to keep that module under its LOC budget.
*/
/**
* Whether .gitignore already covers Ark local state / reports.
* Exact-line equality is insufficient: `.ark/*`, `/.ark/*`, and `.ark/reports/`
* (and common gitignore variants) already cover report output.
*
* @param {string} text
* @returns {boolean}
*/
export function gitignoreCoversArkState(text) {
const lines = String(text || '')
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith('#'));
/** Patterns that cover the whole `.ark` tree or its report subtree. */
const cover = new Set([
'.ark',
'.ark/',
'/.ark',
'/.ark/',
'**/.ark',
'**/.ark/',
'.ark/*',
'/.ark/*',
'**/.ark/*',
'.ark/**',
'/.ark/**',
'**/.ark/**',
'.ark/reports',
'.ark/reports/',
'/.ark/reports',
'/.ark/reports/',
'.ark/reports/*',
'/.ark/reports/*',
'**/.ark/reports',
'**/.ark/reports/',
'**/.ark/reports/*',
'.ark/reports/**',
'/.ark/reports/**',
]);
for (const line of lines) {
if (line.startsWith('!')) continue;
if (cover.has(line)) return true;
if (cover.has(`${line}/`) || cover.has(line.replace(/\/$/, ''))) return true;
}
return false;
}
/**
* Whether .gitignore already has a `!` exception under `.ark` (narrower policy).
* Appending a broad `.ark/` after such a policy defeats tracked golden-pattern exceptions.
*
* @param {string} text
* @returns {boolean}
*/
export function gitignoreHasArkNegationException(text) {
return String(text || '')
.split(/\r?\n/)
.map((line) => line.trim())
.some((line) => line.startsWith('!') && /(^|\/)\.ark(\/|$|\b)/.test(line.slice(1)));
}
/**
* Decide whether report archiving should append a .gitignore rule (EH03).
* - Already covered → do not mutate.
* - Has `!` exceptions under `.ark` without full cover → append **narrow**
* `.ark/reports/` only (never broad `.ark/`, which would defeat exceptions).
* - Otherwise → append `.ark/`.
*
* @param {string} text
* @returns {{ append: boolean, rule: string|null, reason: string }}
*/
export function arkGitignoreAppendDecision(text) {
if (gitignoreCoversArkState(text)) {
return { append: false, rule: null, reason: 'already-covered' };
}
if (gitignoreHasArkNegationException(text)) {
// Narrow reports-only ignore keeps !/.ark/golden-pattern.json (etc.) working.
return { append: true, rule: '.ark/reports/', reason: 'append-narrow-reports' };
}
return { append: true, rule: '.ark/', reason: 'append-broad' };
}
+17
-1

@@ -576,4 +576,20 @@ /**

ARK_POLICY_BASE_REF: \${{ github.event.pull_request.base.sha || github.event.before }}
run: ${pm.run} --fail-on-new-smells --base-ref "\${{ github.event.pull_request.base.sha || github.event.before }}"
run: |
set -euo pipefail
# EH04: first push uses all-zero github.event.before — skip delta smells, keep full merge gate.
BASE_REF="\${ARK_POLICY_BASE_REF:-}"
if [[ "\$BASE_REF" =~ ^0{40,64}$ ]]; then
BASE_REF=""
fi
if [ -n "\$BASE_REF" ] && ! git cat-file -e "\${BASE_REF}^{commit}" 2>/dev/null; then
git fetch --no-tags --depth=1 origin "\$BASE_REF" 2>/dev/null || true
fi
if [ -n "\$BASE_REF" ] && git cat-file -e "\${BASE_REF}^{commit}" 2>/dev/null; then
export ARK_POLICY_BASE_REF="\$BASE_REF"
${pm.run} --fail-on-new-smells --base-ref "\$BASE_REF"
else
export ARK_POLICY_BASE_REF=""
${pm.run}
fi
`;
}
+11
-3

@@ -836,3 +836,3 @@ /** Coverage, plan, and doctor CLI surfaces (roadmap #11). */

);
if (productHonesty.unfinished && Array.isArray(productHonesty.reasonIds) && productHonesty.reasonIds.length > 0) {
if (Array.isArray(productHonesty.reasonIds) && productHonesty.reasonIds.length > 0) {
line(' ', color.dim(`signals: ${productHonesty.reasonIds.join(', ')} (notAScore)`));

@@ -1049,5 +1049,13 @@ }

for (const row of enforcementDoctorLines(enforcement)) line(row.level === 'ok' ? ok : row.level === 'bad' ? bad : warn, row.text);
// EH07 Repair: use support/matrix caps (inventory omits envelope-emitted on Codex).
const supportCaps = writePath.support?.capabilities || {};
const repairReinjection = supportCaps['repair-reinjection-guaranteed'] === true;
const repairEnvelope = supportCaps['repair-envelope-emitted'] === true || supportCaps['repair-payload'] === true;
line(
capabilities['repair-payload'] ? ok : warn,
`Repair payload at hard boundary: ${capabilities['repair-payload'] ? 'yes' : 'no'}`
repairReinjection ? ok : warn,
repairReinjection
? 'Repair: envelope + reinjection guaranteed on hard path when installed + trusted'
: repairEnvelope
? 'Repair: envelope may emit (`--hook-repair`); reinjection not guaranteed (advisory host)'
: 'Repair: no hard-boundary payload'
);

@@ -1054,0 +1062,0 @@ if (writePath.gap) {

@@ -210,3 +210,4 @@ /**

hardWriteUnverified: hardCapable && !effectiveHard,
hardMergeBoundary: 'required-ci-status (arkgate-check --strict-merge)',
hardMergeBoundary:
'required-github-status-context (CLI: arkgate-check --strict-merge / ark-check --strict-merge)',
packageInstalled,

@@ -371,6 +372,11 @@ packagePinAbsent: pinAbsentForUser,

// EH05: soft-write-host is a permanent host posture residual — keep in evidence,
// do NOT alone force architecture "Not finished". Reclassified out of contract debt.
if (write?.softWriteHost) {
reasons.push({
id: 'soft-write-host',
message: write.message || 'Local write is advisory; required CI status is the hard merge boundary.',
bucket: 'environment',
message:
write.message ||
'Local write is advisory; hard merge boundary = a required GitHub status context running arkgate-check --strict-merge (alias ark-check --strict-merge).',
});

@@ -404,3 +410,9 @@ }

const unfinished = reasons.length > 0;
// EH05: environment residual deny-list (future reason ids stay architecture debt by default).
const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host']);
const environmentResiduals = reasons.filter((r) => ENVIRONMENT_REASON_IDS.has(r.id));
const architectureReasons = reasons.filter((r) => !ENVIRONMENT_REASON_IDS.has(r.id));
// unfinished = any non-environment residual (deny-list env, not allowlist architecture)
const unfinished = architectureReasons.length > 0;
const wholeTreeGoverned = wholeTreeGovernedEarly;

@@ -413,27 +425,52 @@ const coverageIncomplete =

const softWriteOnly =
!unfinished && environmentResiduals.some((r) => r.id === 'soft-write-host');
const hostLabel = (() => {
const h = typeof write?.activeHost === 'string' ? write.activeHost.trim().toLowerCase() : '';
if (h === 'codex') return 'Codex';
if (h === 'cursor') return 'Cursor';
if (h === 'opencode') return 'OpenCode';
if (h) return h;
return 'this host';
})();
const primary =
reasons.find((r) => r.id === 'active-blocking-violations') ||
reasons.find((r) => r.id === 'mode-adapt-with-debt') ||
reasons.find((r) => r.id === 'mode-suggest-with-debt') ||
reasons.find((r) => r.id === 'design-weak') ||
reasons.find((r) => r.id === 'design-smells-open-edges') ||
reasons.find((r) => r.id === 'coverage-weak-or-empty') ||
reasons.find((r) => r.id === 'dirty-freeze') ||
reasons.find((r) => r.id === 'package-version-dual-truth') ||
reasons.find((r) => r.id === 'package-pin-absent') ||
reasons.find((r) => r.id === 'baseline-missing-with-debt') ||
reasons.find((r) => r.id === 'residual-pilot') ||
reasons[0];
architectureReasons.find((r) => r.id === 'active-blocking-violations') ||
architectureReasons.find((r) => r.id === 'mode-adapt-with-debt') ||
architectureReasons.find((r) => r.id === 'mode-suggest-with-debt') ||
architectureReasons.find((r) => r.id === 'design-weak') ||
architectureReasons.find((r) => r.id === 'design-smells-open-edges') ||
architectureReasons.find((r) => r.id === 'coverage-weak-or-empty') ||
architectureReasons.find((r) => r.id === 'dirty-freeze') ||
architectureReasons.find((r) => r.id === 'package-version-dual-truth') ||
architectureReasons.find((r) => r.id === 'package-pin-absent') ||
architectureReasons.find((r) => r.id === 'baseline-missing-with-debt') ||
architectureReasons.find((r) => r.id === 'residual-pilot') ||
architectureReasons[0] ||
environmentResiduals[0];
const primaryMessage = unfinished
? primary?.message ||
'Not finished: residual honesty signals remain (violations, mode, coverage, freeze, design, package pin, or pilots).'
: wholeTreeGoverned
? 'No residual honesty blockers on this slice — still not a numeric architecture score; re-doctor after material change.'
: 'No residual honesty blockers flagged — green is only as wide as the governed slice.';
let primaryMessage;
if (unfinished) {
primaryMessage =
primary?.message ||
'Not finished: residual honesty signals remain (violations, mode, coverage, freeze, design, package pin, or pilots).';
} else if (softWriteOnly) {
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).`;
} else if (wholeTreeGoverned) {
primaryMessage =
'No residual honesty blockers on this slice — still not a numeric architecture score; re-doctor after material change.';
} else {
primaryMessage =
'No residual honesty blockers flagged — green is only as wide as the governed slice.';
}
// P0B-HEADLINE: dual-truth / pin-only unfinished must not claim "not whole-tree"
// when the governed tree is already 100%.
// EH05: soft-write alone → composite readiness headline, never global "Not finished".
let headline;
if (!unfinished) {
if (!unfinished && softWriteOnly) {
headline = wholeTreeGoverned
? `Architecture contract ready; ${hostLabel} local writes are advisory`
: `Contract residual clear; ${hostLabel} local writes are advisory`;
} else if (!unfinished) {
headline = 'Honesty clear on residual signals';

@@ -447,2 +484,3 @@ } else if (coverageIncomplete) {

// Prefer caller next action; dual-truth / pin-absent get install/pin path when empty.
// Soft-write-only must not leave a failure headline with null next action (EH05).
let primaryNextAction = input.primaryNextAction || null;

@@ -457,4 +495,16 @@ if (!primaryNextAction && dualTruth) {

'Add arkgate to package.json and install so CI/npx resolve this CLI (PACKAGE_PIN_ABSENT)';
} else if (!primaryNextAction && softWriteOnly) {
primaryNextAction =
'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.';
}
const contractReadiness = unfinished ? 'not-ready' : wholeTreeGoverned ? 'ready' : 'partial';
const localWriteBoundary = write?.softWriteHost
? 'advisory'
: write?.hardWriteActive
? 'hard'
: write?.hardWriteSupported
? 'unverified'
: 'unknown';
return {

@@ -471,4 +521,10 @@ finished: !unfinished && wholeTreeGoverned && !designWeak && activeBlocking === 0,

notAScore: true,
// Full evidence including soft-write-host (reclassified, not silenced)
reasonIds: reasons.map((r) => r.id),
reasons,
architectureReasonIds: architectureReasons.map((r) => r.id),
environmentResidualIds: environmentResiduals.map((r) => r.id),
environmentResiduals,
contractReadiness,
localWriteBoundary,
primaryMessage,

@@ -475,0 +531,0 @@ primaryNextAction,

// Generated from enforcement-state.source.mjs — run npm run generate:packaged-tooling.
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};
import m 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(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 M(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:v,bypassable:u,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:v,bypassable:u,required:c,hard:t,evidence:[...M(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:v},{field:"bypassable",source:l.bypassable,value:u},{field:"required",source:l.required,value:c},{field:"hard",source:l.hard,value:t}]}}function O(a,e){const r=S(a),o=!!e.support?.capabilities?.["hard-write"],i=!!e.support?.capabilities?.["advisory-write"],s=e.capabilityEvidence["hard-write"],v=e.capabilityEvidence["advisory-write"],u=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&&v.length>0&&r.installed?n:!1,y=!!(e.ci?.failClosed&&u.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:v,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?u:[],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 R(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,v=!!(a.enforcementState.ciMerge.configured&&a.enforcementState.ciMerge.installed),u=s===!0?v:s===!1?!1:v?n:!1,c=u===!0?e.arkCheckSourceBound===!1?!0:n:s===!1?!0:v?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=u===!0&&c===!1&&p===!0,f=$(a.enforcementState.ciMerge,["active","runtimeObserved","operationCoverage","bypassable","required","hard"],l,{active:u,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 x(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{O as buildEnforcementState,x as enforcementDoctorLines,S as packageInstallation,R as withCiProviderEvidence};

@@ -365,2 +365,122 @@ /** Exact local workflow evidence plus GitHub classic-protection/ruleset correlation. */

/**
* Classify GitHub provider API failures (EH06).
* Plan/tier claims require **explicit upgrade/plan language** — bare HTTP 403
* (token/SSO/scope) stays generic `provider-enforcement-unverified` so we never
* overclaim "proven not required" / Free-plan walls.
*
* @param {string} errorText combined stderr/stdout from gh api
* @param {{ classicAvailable?: boolean, rulesAvailable?: boolean }} [opts]
* @returns {'provider-policy-unavailable-plan'|'provider-enforcement-unverified'|'ok'}
*/
export function classifyGithubProviderFailure(errorText, opts = {}) {
const text = String(errorText || '');
const lower = text.toLowerCase();
// Explicit plan/tier walls only — not every 403 (token/SSO/scope stay unverified).
const planRestricted =
/upgrade to github (pro|team|enterprise)/i.test(lower) ||
/not available (on|for) (your|this) (current )?plan/i.test(lower) ||
/requires a paid github/i.test(lower) ||
/github pro.*branch protection|branch protection.*github pro/i.test(lower) ||
/only available (with|on) github (pro|team|enterprise)/i.test(lower) ||
/this feature is not available (on|for) (free|your plan)/i.test(lower);
if (planRestricted) return 'provider-policy-unavailable-plan';
if (opts.classicAvailable && opts.rulesAvailable) return 'ok';
return 'provider-enforcement-unverified';
}
/**
* True when a workflow/job title is an Ark architecture check (not lint-only / spark-ci / dark-theme).
* Prefer exact product tokens; "architecture" only with gate/check phrasing.
*
* @param {{ name?: string, workflowName?: string, displayTitle?: string }} run
* @returns {boolean}
*/
export function isArkishCiRun(run) {
const blob = `${run?.name || ''} ${run?.workflowName || ''} ${run?.displayTitle || ''}`.toLowerCase();
if (!blob.trim()) return false;
// Product tokens with word boundaries (avoid spark, dark, lark false positives).
if (/\barkgate(?:-check)?\b/.test(blob)) return true;
if (/\bark-check\b/.test(blob)) return true;
if (/\bark\s+architecture\b/.test(blob)) return true;
if (/\barchitecture\s+(?:gate|check)\b/.test(blob)) return true;
if (/\b(?:arkgate|ark)\s+architecture\s+gate\b/.test(blob)) return true;
// Standalone workflow names generated by Ark (`name: Ark architecture gate`)
if (/\bark architecture gate\b/.test(blob)) return true;
return false;
}
/**
* Observe recent GitHub Actions success for Ark architecture checks (EH06).
* Independent of branch-protection / required-status policy APIs.
* Never falls back to non-Ark green jobs (lint/test).
*
* @param {{ cwd?: string, env?: NodeJS.ProcessEnv, repo?: string, limit?: number }} [opts]
* @returns {{ runtimeObserved: boolean, latestCiRun: string|null, reason: string, runs?: unknown[] }}
*/
export function reportGithubCiRuntime(opts = {}) {
const cwd = opts.cwd ?? process.cwd();
const env = opts.env ?? process.env;
const limit = Number.isFinite(Number(opts.limit)) ? Math.max(1, Number(opts.limit)) : 30;
if (spawnSync('gh', ['--version'], { encoding: 'utf8', env }).status !== 0) {
return { runtimeObserved: false, latestCiRun: null, reason: 'gh-cli-unavailable' };
}
const args = [
'run', 'list',
'--limit', String(limit),
'--json', 'name,conclusion,status,workflowName,displayTitle,event',
];
if (opts.repo) args.push('--repo', opts.repo);
const result = spawnSync('gh', args, { cwd, encoding: 'utf8', env });
if (result.status !== 0) {
const err = `${result.stderr || ''}${result.stdout || ''}`.slice(0, 400);
return {
runtimeObserved: false,
latestCiRun: null,
reason: classifyGithubProviderFailure(err) === 'provider-policy-unavailable-plan'
? 'provider-policy-unavailable-plan'
: 'ci-runtime-unverified',
};
}
let runs = [];
try {
runs = JSON.parse(result.stdout || '[]');
} catch {
return { runtimeObserved: false, latestCiRun: null, reason: 'ci-runtime-unverified' };
}
if (!Array.isArray(runs)) {
return { runtimeObserved: false, latestCiRun: null, reason: 'ci-runtime-unverified' };
}
const relevant = runs.filter(isArkishCiRun);
if (relevant.length === 0) {
return {
runtimeObserved: false,
latestCiRun: null,
reason: runs.length === 0 ? 'ci-runtime-empty' : 'ci-runtime-no-ark-runs',
runs: runs.slice(0, 5),
};
}
const success = relevant.find(
(run) => String(run?.conclusion || '').toLowerCase() === 'success'
);
if (success) {
return {
runtimeObserved: true,
latestCiRun: 'success',
reason: 'ok',
runs: relevant.slice(0, 5),
};
}
const latest = relevant[0];
const latestConclusion = latest
? String(latest.conclusion || latest.status || 'unknown').toLowerCase()
: null;
return {
runtimeObserved: false,
latestCiRun: latestConclusion,
reason: 'ci-runtime-no-success',
runs: relevant.slice(0, 5),
};
}
/** Query classic branch protection and all active branch rules before reporting absence. */

@@ -371,3 +491,3 @@ export function reportGithubBranchProtection(opts = {}) {

if (spawnSync('gh', ['--version'], { encoding: 'utf8', env }).status !== 0) {
return { available: false, reason: 'gh-cli-unavailable' };
return { available: false, reason: 'gh-cli-unavailable', runtimeObserved: false, latestCiRun: null };
}

@@ -380,3 +500,3 @@ let repo = opts.repo;

if (!metadata?.nameWithOwner || !metadata?.defaultBranchRef?.name) {
return { available: false, reason: 'gh-repo-unavailable' };
return { available: false, reason: 'gh-repo-unavailable', runtimeObserved: false, latestCiRun: null };
}

@@ -432,5 +552,28 @@ repo ??= metadata.nameWithOwner;

let reason = available ? 'ok' : 'provider-enforcement-unverified';
if (!available) {
// Plan-restriction only when neither classic nor ruleset evidence could be read.
// Partial success (one source OK, other 403) stays generic unverified — not a plan claim.
if (!classicAvailable && !rulesAvailable) {
const classicFail =
classicResult.status !== 0 ? String(classicResult.stderr || classicResult.stdout || '') : '';
const rulesFail =
rulesResult.status !== 0 ? String(rulesResult.stderr || rulesResult.stdout || '') : '';
reason = classifyGithubProviderFailure(`${classicFail}\n${rulesFail}\n${error}`, {
classicAvailable,
rulesAvailable,
});
}
}
// EH06: CI runtime observation is independent of branch-protection availability.
const ciRuntime = opts.includeCiRuntime === false
? { runtimeObserved: false, latestCiRun: null, reason: 'skipped' }
: reportGithubCiRuntime({ cwd, env, repo });
return {
available,
reason: available ? 'ok' : 'provider-enforcement-unverified',
reason,
// Alias for consumers that look for the short code
policyReason: reason === 'provider-policy-unavailable-plan' ? 'unavailable-plan' : reason,
repo,

@@ -445,4 +588,9 @@ branch,

arkCheckSourceBound,
raw: { classic, rules, ...(error ? { error } : {}) },
// hard merge remains false when status is not proven required
hard: arkCheckRequired === true ? undefined : false,
runtimeObserved: ciRuntime.runtimeObserved === true,
latestCiRun: ciRuntime.latestCiRun,
ciRuntimeReason: ciRuntime.reason,
raw: { classic, rules, ...(error ? { error } : {}), ciRuntime },
};
}

@@ -9,3 +9,17 @@ /**

function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, repairPayload) {
/**
* @param {string} label
* @param {string|null} hookPath
* @param {string|null} hookSurface
* @param {string[]} hookOperations
* @param {boolean} hardWrite
* @param {boolean} repairPayload reinjection guaranteed under hard boundary (historical key)
* @param {{ repairEnvelopeEmitted?: boolean, operationCoverage?: Record<string, boolean> }} [extras]
*/
function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, repairPayload, extras = {}) {
// EH07: repair envelope emission ≠ reinjection guarantee.
// Codex hooks may emit --hook-repair JSON while reinjection stays host-dependent / not hard.
const repairEnvelopeEmitted =
extras.repairEnvelopeEmitted === true || repairPayload === true;
const repairReinjectionGuaranteed = hardWrite === true && repairPayload === true;
return Object.freeze({

@@ -20,4 +34,12 @@ label,

'merge-gate': true,
// Historical key: true only when hard reinjection path is package-supported.
'repair-payload': repairPayload,
'repair-envelope-emitted': repairEnvelopeEmitted,
'repair-reinjection-guaranteed': repairReinjectionGuaranteed,
}),
// EH07 minimum ops matrix (hard=false for soft hosts on every listed op).
operationCoverage: Object.freeze(
extras.operationCoverage ||
Object.fromEntries(hookOperations.map((op) => [op, hardWrite === true]))
),
});

@@ -53,3 +75,5 @@ }

),
cursor: hostProfile('Cursor', null, null, [], false, false),
cursor: hostProfile('Cursor', null, null, [], false, false, {
operationCoverage: { shell: false, 'pre-commit': false },
}),
codex: hostProfile(

@@ -61,3 +85,12 @@ 'OpenAI Codex',

false,
false
false,
{
// Install writes --hook-repair; envelope can be emitted; reinjection is not guaranteed.
repairEnvelopeEmitted: true,
operationCoverage: {
apply_patch: false,
shell: false,
'pre-commit': false,
},
}
),

@@ -72,3 +105,6 @@ // OpenCode: first-class MCP + permissions; plugin tool.execute.before is incomplete

false,
false
false,
{
operationCoverage: { shell: false, 'pre-commit': false },
}
),

@@ -90,3 +126,11 @@ });

: 'no hard local write boundary';
const repair = capabilities['repair-payload'] ? 'repair payload' : 'no hard-boundary repair';
// EH07 three-way repair story: reinjection guaranteed / envelope-only / none.
let repair;
if (capabilities['repair-reinjection-guaranteed']) {
repair = 'repair reinjection (hard path)';
} else if (capabilities['repair-envelope-emitted']) {
repair = 'repair envelope may emit (reinjection not guaranteed)';
} else {
repair = 'no hard-boundary repair';
}
return `${write} + advisory MCP + CI check + ${repair}`;

@@ -113,8 +157,14 @@ }

}
const repair = capabilities['repair-payload']
? 'Emitted on hook deny; host must re-inject'
: 'No hard-boundary payload';
const merge = capabilities['hard-write']
? '**Required status** = hard merge boundary (`arkgate-check --strict-merge`)'
: '**Required status** = hard merge boundary (same CI)';
// EH07: distinguish envelope emission vs reinjection guarantee in the repair column.
let repair;
if (capabilities['repair-reinjection-guaranteed']) {
repair = 'Emitted on hook deny; host must re-inject (hard path when installed + trusted)';
} else if (capabilities['repair-envelope-emitted']) {
repair = 'Envelope may emit (`--hook-repair`); reinjection **not** guaranteed (advisory host)';
} else {
repair = 'No hard-boundary payload';
}
// EH07: name the CLI explicitly; required status is a GitHub status context name, not the CLI alone.
const merge =
'**Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`)';
return `| ${profile.label} | ${local} | Advisory; the agent must call it | ${merge} | ${repair} |`;

@@ -128,8 +178,29 @@ }).join('\n');

**Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
merge check — not “CI file present.” Cursor/Codex/OpenCode never get a fake hard write claim.
GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
Cursor/Codex/OpenCode never get a fake hard write claim.
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.`;
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.`;
}
/**
* EH07 doctor/JSON host capability split for repair envelope vs reinjection.
* @param {string|null|undefined} host
*/
export function hostRepairCapabilities(host) {
const profile = getHostSupportProfile(host);
if (!profile) {
return {
repairEnvelopeEmitted: false,
repairReinjectionGuaranteed: false,
operationCoverage: {},
};
}
return {
repairEnvelopeEmitted: profile.capabilities['repair-envelope-emitted'] === true,
repairReinjectionGuaranteed: profile.capabilities['repair-reinjection-guaranteed'] === true,
operationCoverage: { ...(profile.operationCoverage || {}) },
};
}
/**
* Doctor human one-liner for active-host write honesty (fail-closed).

@@ -140,10 +211,13 @@ * @returns {string|null}

const host = typeof activeHost === 'string' ? activeHost.trim().toLowerCase() : '';
// EH07: distinguish CLI command (arkgate-check / ark-check) from the GitHub required status context name.
const mergeBoundary =
'Required CI hard merge boundary = a required GitHub status context that runs arkgate-check --strict-merge (alias ark-check --strict-merge)';
if (host === 'cursor') {
return 'Cursor: write path is advisory (MCP/rules; no hard PreToolUse). Required CI status (arkgate-check --strict-merge) is the hard merge boundary.';
return `Cursor: write path is advisory (MCP/rules; no hard PreToolUse). ${mergeBoundary}.`;
}
if (host === 'codex') {
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.';
return `Codex: write path is advisory / best-effort at write (not Claude/Grok hard). ${mergeBoundary}.`;
}
if (host === 'opencode') {
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.';
return `OpenCode: write path is advisory / best-effort (MCP + optional plugin; not Claude/Grok/Antigravity hard). ${mergeBoundary}.`;
}

@@ -153,5 +227,5 @@ if ((host === 'claude' || host === 'grok' || host === 'antigravity') && !hardWriteActive) {

host === 'claude' ? 'Claude' : host === 'grok' ? 'Grok' : 'Antigravity';
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.`;
return `${label}: hard PreToolUse is supported for listed ops when installed + trusted; without runtime-observed hook evidence, hard is unverified. ${mergeBoundary}.`;
}
return null;
}

@@ -291,2 +291,6 @@ /**

const unfinished = productHonesty.unfinished === true;
const envResiduals = Array.isArray(productHonesty.environmentResidualIds)
? productHonesty.environmentResidualIds
: [];
const envOnly = !unfinished && envResiduals.length > 0;
const headline = productHonesty.headline || (unfinished ? 'Not finished' : 'Honesty clear');

@@ -302,3 +306,5 @@ const primary = productHonesty.primaryMessage || '';

? 'Residual honesty signals remain — not a whole-tree guarantee and not a score.'
: 'No residual honesty blockers on this slice — still not a numeric architecture score.';
: envOnly
? 'Architecture residual clear; host/environment residual remains (advisory write) — not a score.'
: 'No residual honesty blockers on this slice — still not a numeric architecture score.';
} else if (p.toLowerCase().startsWith(h.toLowerCase())) {

@@ -324,6 +330,11 @@ const stripped = p.slice(h.length).replace(/^[\s—–:-]+/, '').trim();

: '';
const subtitle = unfinished
? 'architecture residual'
: envOnly
? 'environment residual (advisory write)'
: 'no residual honesty blockers';
return `<div class="section card design-strip ${unfinished ? 'is-weak' : 'is-clean'}" id="product-honesty" data-product-honesty="1">
<div class="design-head">
<span class="badge design" title="Product honesty — not a score">${esc(headline)}</span>
<span class="dim" style="font-size:.86rem">${unfinished ? 'residual honesty signals' : 'no residual honesty blockers'}</span>
<span class="dim" style="font-size:.86rem">${esc(subtitle)}</span>
</div>

@@ -330,0 +341,0 @@ <p style="margin:.45rem 0 0">${esc(body)}</p>

@@ -120,3 +120,3 @@ /**

'not equivalent to Claude/Grok/Antigravity PreToolUse hard-write). ' +
'The hard merge backstop is CI --strict-merge plus a required status check.'
'The hard merge backstop is a required GitHub status context running --strict-merge.'
: `Active host ${activeHost} has advisory prepare-write/autoPatch tools, ` +

@@ -131,3 +131,3 @@ 'but no hard write boundary; CI can report failure, while merge blocking requires provider policy.';

activeHost === 'codex' || activeHost === 'opencode'
? 'Keep CI on --strict-merge and require the ark-check status on the default branch; ' +
? 'Keep a required GitHub status context on arkgate-check --strict-merge (alias ark-check); ' +
`refresh ${activeHost} MCP/skills with ${arkCommand(root, 'ark-check', `--install-agent-gates --tools ${activeHost}`)}`

@@ -134,0 +134,0 @@ : arkCommand(root, 'ark-check', `--install-agent-gates --tools ${tools}`),

@@ -43,5 +43,7 @@ # ArkGate — Agent Integration Guide

installed and trusted. Cursor/Codex/OpenCode remain **advisory at write**. For every host, the
repository-wide hard boundary is a **required** CI status (`arkgate-check --strict-merge`) —
never claim Cursor/Codex/OpenCode hard write. See [ai-gates.md](ai-gates.md) and the README host
matrix.
repository-wide hard boundary is a **required GitHub status context** that runs
`arkgate-check --strict-merge` (alias `ark-check --strict-merge`) — the CLI name is not the
status context name. Never claim Cursor/Codex/OpenCode hard write. Soft-write alone does not mean
the project is unfinished; doctor keeps it as an environment residual. See [ai-gates.md](ai-gates.md)
and the README host matrix.

@@ -48,0 +50,0 @@ ## Architecture playbook and `ark-check --recommend`

@@ -310,4 +310,6 @@ # Gating AI Agents with ArkGate

The generated hook includes `--hook-repair`, so a rejected patch carries the same structured
repair envelope as Claude and Grok. Codex still needs hook trust enabled for the project.
The generated hook includes `--hook-repair`, so a rejected patch **may emit** a structured
repair envelope (same JSON shape as Claude/Grok). **Reinjection is not guaranteed** on Codex —
local write stays advisory/bypassable; the host must re-apply any fix, and required CI remains
the hard merge boundary. Codex still needs hook trust enabled for the project.

@@ -391,3 +393,3 @@ Modern Codex resolves MCP servers from the active project's `.codex/config.toml`. Ark writes

write boundary and is **not** equivalent to Claude/Grok PreToolUse hard-write + repair.
The hard merge backstop is CI `--strict-merge` (or `--strict`) plus a required status check.
The hard merge backstop is CI `--strict-merge` (or `--strict`) as a **required GitHub status context** (not “workflow file present”).
- CI workflows that run ark-check without the fail-closed profile (or with only

@@ -596,10 +598,31 @@ `--strict-config`) surface gap `enforcement-ci-not-fail-closed`.

```yaml
- run: npx ark-check --root . --config ark.config.json --strict-merge --fail-on-new-smells --base-ref "${{ github.event.pull_request.base.sha || github.event.before }}"
# EH04: first push may have all-zero github.event.before — only pass --base-ref when resolvable.
- name: Ark architecture check
env:
ARK_POLICY_BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before }}
run: |
set -euo pipefail
BASE_REF="${ARK_POLICY_BASE_REF:-}"
if [[ "$BASE_REF" =~ ^0{40,64}$ ]]; then BASE_REF=""; fi
if [ -n "$BASE_REF" ] && git cat-file -e "${BASE_REF}^{commit}" 2>/dev/null; then
export ARK_POLICY_BASE_REF="$BASE_REF"
npx ark-check --root . --config ark.config.json --strict-merge \
--fail-on-new-smells --base-ref "$BASE_REF"
else
export ARK_POLICY_BASE_REF=""
npx ark-check --root . --config ark.config.json --strict-merge
fi
```
This explicit brownfield ratchet records schema `1.0` identities, touched paths, and stable
evidence; missing base exits `2`. Its first semantic smell is `domain-logic-in-ui`; residual,
path-only moves, and unrelated work stay green. Generated Claude/Grok hooks share the delta and
golden-pattern repair hint. MCP exposes the result but stays advisory.
evidence; missing base with `--fail-on-new-smells` exits `2`, so the generated workflow skips the
delta when the SHA is all-zero or unresolvable while keeping the full merge gate. Its first
semantic smell is `domain-logic-in-ui`; residual, path-only moves, and unrelated work stay green.
Generated Claude/Grok hooks share the delta and golden-pattern repair hint. MCP exposes the result
but stays advisory.
**CLI vs required status:** `arkgate-check --strict-merge` / `ark-check --strict-merge` is the
**command**. The hard merge boundary is making that job a **required GitHub status context** —
not “workflow file present.”
Or use the repository's composite Action at a pinned release or commit:

@@ -606,0 +629,0 @@

@@ -17,3 +17,5 @@ # Develop with ArkGate

Make the architecture check a **required** merge status (GitHub/GitLab/etc.):
Make the architecture check a **required** merge **status context** (GitHub/GitLab/etc.). The CLI
command is `arkgate-check --strict-merge` / `ark-check --strict-merge` — the hard boundary is
requiring that job’s status, not merely adding a workflow file:

@@ -25,3 +27,5 @@ ```yaml

`--strict-merge` (or compatibility `--strict`) is the repository-wide hard boundary for every agent host.
Generated workflows also gate `--fail-on-new-smells --base-ref` so a first push with an all-zero
`github.event.before` still runs the full merge gate without a broken delta (see
[ai-gates.md](ai-gates.md#ci-backstop)).

@@ -36,5 +40,5 @@ ---

|------|-------------|-----|-------|
| Claude · Grok · Antigravity | Hard PreToolUse when installed + trusted | Advisory | Required status |
| Codex · OpenCode | Best-effort / advisory | Advisory | Required status |
| Cursor | Advisory only | Advisory | Required status |
| Claude · Grok · Antigravity | Hard PreToolUse when installed + trusted | Advisory | Required status context |
| Codex · OpenCode | Best-effort / advisory | Advisory | Required status context |
| Cursor | Advisory only | Advisory | Required status context |

@@ -52,4 +56,10 @@ Full matrix and install commands: [ai-gates.md](ai-gates.md) · canonical table in [README](../README.md#host-enforcement-support).

Doctor reports what is actually installed and observed (`writePath` / enforcement state). Installed files alone do not imply `hard:true` without runtime evidence where the product requires it.
Doctor reports what is actually installed and observed (`writePath` / enforcement state). Installed
files alone do not imply `hard:true` without runtime evidence where the product requires it.
**Evidence split (Phase EH):** soft-write hosts keep `soft-write-host` in evidence without forcing
global **Not finished** when the contract is ready. With `ARK_DOCTOR_GITHUB=1`, successful CI runs
can show `runtimeObserved: true` even when branch-protection policy is plan-unavailable
(`unavailable-plan` on GitHub Free private); `hard: false` until the status is required.
---

@@ -56,0 +66,0 @@

@@ -40,2 +40,3 @@ # ArkGate package surface policy

| **Package pin dual-truth (4.0)** | doctor JSON `packageVersionTruth`; upgrade JSON/human note when pin behind CLI | Additive, advisory. Surfaces after `upgrade --no-install` when managed CLI is ahead of package.json. |
| **Product honesty readiness split (4.1.1 EH)** | doctor JSON `productHonesty` | Additive. `unfinished` / `headline` / `primaryNextAction` / `reasonIds` remain; EH adds `contractReadiness` (`ready`\|`partial`\|`not-ready`), `localWriteBoundary` (`advisory`\|`hard`\|`unverified`\|`unknown`), `architectureReasonIds`, `environmentResidualIds` / `environmentResiduals`. Soft-write hosts stay in evidence without alone forcing global **Not finished**. `notAScore: true` always. |
| **Policy transition analysis (T01, 3.1.0)** | `analyzePolicyDelta(...)`; MCP `ark_policy_delta`; CLI `--policy-base` / `--policy-base-ref` / `--policy-ack`; check JSON `policyDelta` | Additive schema `1.0`. Classifications and finding ids are deterministic. Weakening/judgment requires an acknowledgement bound to both policy hashes and the exact blocking finding set. |

@@ -193,5 +194,6 @@ | **Atomic change preflight (T02, 3.1.0)** | `preflightChange(...)`; CLI `ark preflight --changes <file> --json`; MCP `ark_prepare_change` | Additive schema `1.0`. One complete governed production-source `{path,content}` / `{path,delete:true}` batch; read-only; returns operation, content/tree/policy/compiler fingerprints and stable graph findings. MCP availability alone is advisory. |

Ship notes for a version live under [releases/](https://github.com/pedroknigge/arkgate/tree/main/docs/releases)
(current published: [4.0.1.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.0.1.md);
next prepared: [4.1.0.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.1.0.md);
previous: [4.0.0.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.0.0.md)).
(current published: [4.1.0.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.1.0.md);
prepared next: [4.1.1.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.1.1.md);
previous: [4.0.1.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.0.1.md),
[4.0.0.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.0.0.md)).
Publish path: signed annotated tag → GitHub Release → `publish-npm.yml` (see [CONTRIBUTING.md](https://github.com/pedroknigge/arkgate/blob/main/CONTRIBUTING.md)).

@@ -76,3 +76,5 @@ # ArkGate product voice

| **advisory write** | MCP/rules coach only (Cursor/Codex at write time) — not a hard block |
| **required CI** | Merge hard boundary when the repository makes `arkgate-check` a required status |
| **required CI / status context** | Merge hard boundary when the repository makes the Ark job a **required GitHub status context** (CLI: `arkgate-check --strict-merge` / `ark-check --strict-merge`) |
| **contract ready** | Project/layers/ArkRules honesty residual clear — not the same as “hard local write” |
| **environment residual** | Permanent host/provider posture (e.g. soft-write Codex) kept in evidence without forcing global **Not finished** |

@@ -86,7 +88,9 @@ ## Do (product copy)

| Label expert skills as escapes | “Install skill pack only when doctor or a STOP handoff names a skill.” |
| State host write honesty | “Cursor/Codex: advisory write. Required CI is the hard merge boundary.” |
| State host write honesty | “Cursor/Codex: advisory write. Required GitHub status context is the hard merge boundary.” |
| Soft-write ≠ unfinished project | “Architecture contract ready; Codex local writes are advisory.” Keep `soft-write-host` in evidence; reserve **Not finished** for contract/project debt. |
| Keep Suggest on start → doctor | New-here primary is finish `start`, not a competing recommend/architect curriculum |
| Qualify edge-clean under design-weak | “None on checked edges … design residual remains. Not healthy finished.” |
| Prefer fail-closed over fake hard | Incomplete analysis, unobserved hooks, and soft MCP never paint as hard green |
| Honesty clear ≠ architecture healthy | `productHonesty.finished` means residual honesty sensors are clear — not a green graph score. Open blocking violations, ADAPT/SUGGEST with debt, dual-truth pin, or design residual keep `unfinished: true`. |
| Honesty clear ≠ architecture healthy | `productHonesty.finished` means residual **architecture** honesty sensors are clear — not a green graph score. Open blocking violations, ADAPT/SUGGEST with debt, dual-truth pin, or design residual keep `unfinished: true`. Permanent soft-write alone does **not**. |
| Separate CI runtime from provider policy | Successful CI run ≠ required status; GitHub Free plan 403 → `unavailable-plan`, not “CI never ran.” |

@@ -105,2 +109,4 @@ ## Avoid

| “Honesty clear” as “architecture finished” | Honesty clear only means residual honesty sensors are quiet; graph/mode debt is separate |
| “Not finished” solely because host is Codex/Cursor | Soft-write is environment residual; do not paint a green whole-tree project as unfinished architecture |
| Conflating CLI name with required status | `ark-check` is the command; the hard boundary is the GitHub required **status context** |
| “ArkRules prove business correctness” | They enforce *declared* structure/coverage evidence, not arbitrary logic or full semantic proof |

@@ -107,0 +113,0 @@ | “Structure enforced = Domain extraction done” | Structure sensors are **heuristics**; extraction is judgment (`/ark-fix` / pilot) |

@@ -55,3 +55,3 @@ # ArkGate documentation

| Release notes (by version) | [releases/](releases/) · [CHANGELOG.md](../CHANGELOG.md) |
| Epic plans (seeded + shipped) | [plans/](plans/) |
| Epic plans (seeded + shipped) | [plans/](plans/) · latest: [enforcement-evidence-and-docs-truth](plans/enforcement-evidence-and-docs-truth/README.md) (Phase EH shipped; 4.1.1 prepared) |
| Claims audit | [audit/claims-matrix.md](audit/claims-matrix.md) |

@@ -61,4 +61,4 @@ | Field adoption kit (scaffolding, not closed) | [field/](field/) |

Current published: [releases/4.0.1.md](releases/4.0.1.md) (`arkgate@4.0.1` on npm `latest`).
Next prepared: [releases/4.1.0.md](releases/4.1.0.md) (`arkgate@4.1.0` — field product phases + CI PR slim; not published until registry).
Current published: [releases/4.1.0.md](releases/4.1.0.md) (`arkgate@4.1.0` on npm `latest`).
Prepared next: [releases/4.1.1.md](releases/4.1.1.md) (Phase EH — **Status: prepared**, not on npm until publish).
Previous major: [releases/4.0.0.md](releases/4.0.0.md) (`arkgate@4.0.0`).

@@ -73,3 +73,3 @@ Config: [configuration.md](configuration.md) · Agent skills dual-plane: [agent-guide.md](agent-guide.md).

2. **One primary flow** — `start` → doctor → optional guided work.
3. **Honest hardness** — host write guarantees differ; CI required status is the shared merge boundary.
3. **Honest hardness** — host write guarantees differ; a **required GitHub status context** running the merge CLI is the shared hard boundary.
4. **History is not the product** — version archaeology lives under `releases/` and `plans/`, not the front door.

@@ -36,5 +36,7 @@ # Use ArkGate

| While the AI writes | Host write gate or advisory MCP (depends on host) |
| Before merge | `arkgate-check` — make it a **required** CI status |
| Before merge | Make the Ark job a **required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) |
| Anytime | Doctor: Suggest / Adapt / Enforce (+ design-weak if residual) |
**Codex / Cursor / OpenCode:** local write stays advisory forever — that is not unfinished architecture. Doctor may say **contract ready** while still reminding you that local writes are advisory; **Not finished** is reserved for real project/contract debt.
ArkGate is **not** a web framework, ORM, or app runtime. It is architecture enforcement + co-pilot for AI TypeScript.

@@ -41,0 +43,0 @@

{
"name": "arkgate",
"version": "4.1.0",
"version": "4.1.1",
"description": "ArkGate — architecture co-pilot for AI TypeScript (write gate, CI gate, plan/loop; optional ArkRules)",

@@ -5,0 +5,0 @@ "type": "module",

+12
-13

@@ -19,5 +19,4 @@ <div align="center">

> **ArkGate 4.0.1** is on npm `latest` (stale global CLI upgrade guard + ArkRules HTML/docs honesty).
> **Next prepared:** [4.1.0](docs/releases/4.1.0.md) (field product train + CI PR slim — not published until registry).
> [4.0.1 notes](docs/releases/4.0.1.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
> **ArkGate 4.1.0** is on npm `latest`. Tree prepares **4.1.1** (Phase EH honesty + CI/report fixes) — [4.1.1 notes](docs/releases/4.1.1.md) (**prepared**, not on npm until publish).
> [4.1.0 notes](docs/releases/4.1.0.md) · [4.0.1](docs/releases/4.0.1.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)

@@ -103,13 +102,14 @@ ---

|------|----------------------|----------------|-----------------|----------------|
| Claude Code | **Hard** block for listed ops (PreToolUse `Write` / `Edit` / `MultiEdit`) when installed + trusted | Advisory; the agent must call it | **Required status** = hard merge boundary (`arkgate-check --strict-merge`) | Emitted on hook deny; host must re-inject |
| Grok Build | **Hard** block for listed ops (PreToolUse `write` / `search_replace` (plus aliases)) when installed + trusted | Advisory; the agent must call it | **Required status** = hard merge boundary (`arkgate-check --strict-merge`) | Emitted on hook deny; host must re-inject |
| Google Antigravity | **Hard** block for listed ops (PreToolUse `write_to_file` / `replace_file_content` / `multi_replace_file_content`) when installed + trusted | Advisory; the agent must call it | **Required status** = hard merge boundary (`arkgate-check --strict-merge`) | Emitted on hook deny; host must re-inject |
| Cursor | **Advisory only** at write (no hard hook) | Advisory; the agent must call it | **Required status** = hard merge boundary (same CI) | No hard-boundary payload |
| OpenAI Codex | **Advisory / best-effort** at write (not equivalent to Claude/Grok hard block) | Advisory; the agent must call it | **Required status** = hard merge boundary (same CI) | No hard-boundary payload |
| OpenCode | **Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary) | Advisory; the agent must call it | **Required status** = hard merge boundary (same CI) | No hard-boundary payload |
| Claude Code | **Hard** block for listed ops (PreToolUse `Write` / `Edit` / `MultiEdit`) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
| Grok Build | **Hard** block for listed ops (PreToolUse `write` / `search_replace` (plus aliases)) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
| Google Antigravity | **Hard** block for listed ops (PreToolUse `write_to_file` / `replace_file_content` / `multi_replace_file_content`) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
| Cursor | **Advisory only** at write (no hard hook) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | No hard-boundary payload |
| OpenAI Codex | **Advisory / best-effort** at write (not equivalent to Claude/Grok hard block) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Envelope may emit (`--hook-repair`); reinjection **not** guaranteed (advisory host) |
| OpenCode | **Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | No hard-boundary payload |
**Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
merge check — not “CI file present.” Cursor/Codex/OpenCode never get a fake hard write claim.
GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
Cursor/Codex/OpenCode never get a fake hard write claim.
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.
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.
<!-- arkgate-host-support:end -->

@@ -191,4 +191,3 @@

| Security | [SECURITY.md](SECURITY.md) |
| Current published (4.0.1) | [docs/releases/4.0.1.md](docs/releases/4.0.1.md) · [CHANGELOG](CHANGELOG.md) |
| Next prepared (4.1.0) | [docs/releases/4.1.0.md](docs/releases/4.1.0.md) — not on npm until publish |
| Current published (4.1.0) | [docs/releases/4.1.0.md](docs/releases/4.1.0.md) · [CHANGELOG](CHANGELOG.md) |
| Previous (4.0.0) | [docs/releases/4.0.0.md](docs/releases/4.0.0.md) |

@@ -195,0 +194,0 @@ | Previous (3.9.2) | [docs/releases/3.9.2.md](docs/releases/3.9.2.md) |

@@ -9,3 +9,3 @@ {

},
"version": "4.1.0",
"version": "4.1.1",
"packages": [

@@ -15,3 +15,3 @@ {

"identifier": "arkgate",
"version": "4.1.0",
"version": "4.1.1",
"runtimeHint": "npx",

@@ -18,0 +18,0 @@ "transport": {

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display