| /** | ||
| * ark-check CLI flag parsing. | ||
| */ | ||
| import path from 'node:path'; | ||
| import { discoverLocalBaseRef, normalizePolicyBaseRef } from './policy-delta-io.mjs'; | ||
| export function resolveDesignDeltaBaseRef(root, explicit, env = process.env) { | ||
| const flag = typeof explicit === 'string' ? explicit.trim() : ''; | ||
| if (flag) return flag; | ||
| const envRef = normalizePolicyBaseRef(env.ARK_POLICY_BASE_REF); | ||
| if (envRef) return envRef; | ||
| const githubBase = typeof env.GITHUB_BASE_REF === 'string' ? env.GITHUB_BASE_REF.trim() : ''; | ||
| if (githubBase) return `origin/${githubBase}`; | ||
| return discoverLocalBaseRef(root) || undefined; | ||
| } | ||
| export function parseArgs(argv) { | ||
| const args = { | ||
| root: process.cwd(), | ||
| config: 'ark.config.json', | ||
| manifest: undefined, | ||
| printConfig: undefined, | ||
| tsconfig: undefined, | ||
| json: false, | ||
| strictConfig: false, | ||
| strictMerge: false, | ||
| requireGates: false, | ||
| requireWriteHook: undefined, | ||
| init: false, | ||
| installAgentGates: false, | ||
| compact: false, | ||
| tools: undefined, | ||
| force: false, | ||
| skillsOnly: false, | ||
| baseline: undefined, | ||
| policyBase: undefined, | ||
| policyBaseRef: undefined, | ||
| policyAck: undefined, failOnNewSmells: false, baseRef: undefined, | ||
| contractSession: false, | ||
| contractDiff: false, | ||
| changed: false, | ||
| against: undefined, | ||
| base: undefined, | ||
| persona: undefined, | ||
| author: undefined, | ||
| failUngoverned: false, | ||
| updateBaseline: false, | ||
| noCache: false, | ||
| resident: false, | ||
| coverage: false, | ||
| migrateCommands: false, | ||
| doctor: false, | ||
| plan: false, | ||
| recommend: false, | ||
| writePlan: false, | ||
| listPolicyPacks: false, | ||
| applyPolicyPack: undefined, | ||
| watch: false, | ||
| beginner: false, | ||
| openReport: false, | ||
| noOpenReport: false, | ||
| version: false, | ||
| help: false, | ||
| all: false, | ||
| followConfigRoot: false, | ||
| }; | ||
| const requireValue = (flag, index) => { | ||
| const value = argv[index + 1]; | ||
| if (value === undefined || value.startsWith('-')) { | ||
| throw new Error(`Missing value for ${flag}. Run arkgate-check --help for usage.`); | ||
| } | ||
| return value; | ||
| }; | ||
| for (let i = 2; i < argv.length; i += 1) { | ||
| const arg = argv[i]; | ||
| if (arg === '--json') args.json = true; | ||
| else if (arg === '--strict' || arg === '--strict-merge') { | ||
| args.strictConfig = true; | ||
| args.requireGates = true; | ||
| args.strictMerge = true; | ||
| } | ||
| else if (arg === '--strict-config') args.strictConfig = true; | ||
| else if (arg === '--require-gates') { | ||
| args.requireGates = true; | ||
| args.strictConfig = true; | ||
| } | ||
| else if (arg === '--require-write-hook') { | ||
| args.requireWriteHook = requireValue(arg, i++).trim().toLowerCase(); | ||
| } | ||
| else if (arg === '--init') args.init = true; | ||
| else if (arg === '--preset') args.preset = requireValue(arg, i++); | ||
| else if (arg === '--install-agent-gates') args.installAgentGates = true; | ||
| else if (arg === '--compact') args.compact = true; | ||
| else if (arg === '--tools') { | ||
| // Consume the next arg only when it isn't another flag (same rule as --baseline), | ||
| // so `--tools --force` can't silently eat --force as a "tool name". | ||
| const next = argv[i + 1]; | ||
| if (next !== undefined && !next.startsWith('-')) { | ||
| i += 1; | ||
| args.tools = next | ||
| .split(',') | ||
| .map((tool) => tool.trim().toLowerCase()) | ||
| .filter(Boolean); | ||
| } else { | ||
| args.tools = []; // flag without a value — rejected in runInstallAgentGates | ||
| } | ||
| } | ||
| else if (arg === '--force') args.force = true; | ||
| else if (arg === '--follow-config-root') args.followConfigRoot = true; | ||
| else if (arg === '--skills-only') args.skillsOnly = true; | ||
| else if (arg === '--coverage') args.coverage = true; | ||
| else if (arg === '--doctor') args.doctor = true; | ||
| else if (arg === '--plan') args.plan = true; | ||
| else if (arg === '--rules-inventory') args.rulesInventory = true; | ||
| else if (arg === '--recommend') args.recommend = true; | ||
| else if (arg === '--write-plan') args.writePlan = true; | ||
| else if (arg === '--list-policy-packs') args.listPolicyPacks = true; | ||
| else if (arg === '--apply-policy-pack') args.applyPolicyPack = requireValue(arg, i++); | ||
| else if (arg === '--suggest-include') args.suggestInclude = true; | ||
| else if (arg === '--adopt-contract') args.adoptContract = true; | ||
| else if (arg === '--migrate-contract') args.migrateContract = true; | ||
| else if (arg === '--ratchet-cores') args.ratchetCores = true; | ||
| else if (arg === '--write') args.write = true; | ||
| else if (arg === '--watch') args.watch = true; | ||
| else if (arg === '--beginner') args.beginner = true; | ||
| else if (arg === '--codex-home') args.codexHome = true; | ||
| else if (arg === '--claude-home') args.claudeHome = true; | ||
| else if (arg === '--grok-home') args.grokHome = true; | ||
| else if (arg === '--agent-homes') { | ||
| args.agentHomes = true; | ||
| args.codexHome = true; | ||
| args.claudeHome = true; | ||
| args.grokHome = true; | ||
| } | ||
| else if (arg === '--migrate-commands') args.migrateCommands = true; | ||
| else if (arg === '--no-cache') args.noCache = true; | ||
| else if (arg === '--resident') args.resident = true; | ||
| else if (arg === '--report') { | ||
| const next = argv[i + 1]; | ||
| args.report = next && !next.startsWith('-') ? argv[++i] : 'ark-report.html'; | ||
| } | ||
| else if (arg === '--reset-origin') args.resetOrigin = true; | ||
| else if (arg === '--no-archive') args.noArchive = true; | ||
| else if (arg === '--open') args.openReport = true; | ||
| else if (arg === '--no-open') args.noOpenReport = true; | ||
| else if (arg === '--baseline' || arg === '--update-baseline') { | ||
| if (arg === '--update-baseline') args.updateBaseline = true; | ||
| // optional path value: consume the next arg only when it isn't another flag | ||
| const next = argv[i + 1]; | ||
| args.baseline = next && !next.startsWith('-') ? argv[++i] : '.ark-baseline.json'; | ||
| } | ||
| else if (arg === '--policy-base') args.policyBase = requireValue(arg, i++); | ||
| else if (arg === '--policy-base-ref') args.policyBaseRef = requireValue(arg, i++); | ||
| else if (arg === '--policy-ack') args.policyAck = requireValue(arg, i++); else if (arg === '--fail-on-new-smells') args.failOnNewSmells = true; else if (arg === '--base-ref') args.baseRef = requireValue(arg, i++); | ||
| else if (arg === '--contract-session') args.contractSession = true; | ||
| else if (arg === '--contract-diff') args.contractDiff = true; | ||
| else if (arg === '--changed') args.changed = true; | ||
| else if (arg === '--against') args.against = requireValue(arg, i++); | ||
| else if (arg === '--base') args.base = requireValue(arg, i++); | ||
| else if (arg === '--persona') args.persona = requireValue(arg, i++); | ||
| else if (arg === '--author') args.author = requireValue(arg, i++); | ||
| else if (arg === '--root') args.root = path.resolve(requireValue(arg, i++)); | ||
| else if (arg === '--config') args.config = requireValue(arg, i++); | ||
| else if (arg === '--manifest') args.manifest = requireValue(arg, i++); | ||
| else if (arg === '--print-config') args.printConfig = requireValue(arg, i++); | ||
| else if (arg === '--tsconfig') args.tsconfig = requireValue(arg, i++); | ||
| else if (arg === '--help' || arg === '-h') args.help = true; | ||
| else if (arg === '--all') args.all = true; | ||
| else if (arg === '--version' || arg === '-V') args.version = true; | ||
| else throw new Error(`Unknown argument: ${arg}. Run arkgate-check --help for usage.`); | ||
| } | ||
| return args; | ||
| } |
| /** | ||
| * Convention-based ark.config detection used by --init. | ||
| */ | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
| import { | ||
| DEFAULT_DOMAIN_FORBIDDEN_GLOBALS, | ||
| DEFAULT_INTENT_PREFIXES, | ||
| DEFAULT_LAYER_DIRECTORIES, | ||
| DEFAULT_RULES, | ||
| } from '../ark-shared.mjs'; | ||
| import { normalize, walk } from './scan-files.mjs'; | ||
| import { suggestLayerForDir } from './suggestions.mjs'; | ||
| /** | ||
| * Infer an ark.config.json from the directories that actually exist in the project, | ||
| * using the same layer→directory conventions as the eleven-layer template. A directory | ||
| * only counts when it contains at least one source file, so an empty scaffold dir can't | ||
| * produce a layer whose pattern matches nothing (which --strict-config would fail). | ||
| */ | ||
| export function detectConfig(root) { | ||
| const srcDir = fs.existsSync(path.join(root, 'src')) ? 'src' : '.'; | ||
| const layers = []; | ||
| for (const entry of DEFAULT_INTENT_PREFIXES) { | ||
| const directories = (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? []).filter( | ||
| (directory) => walk(path.join(root, srcDir, directory), [], { root }).length > 0 | ||
| ); | ||
| if (directories.length === 0) continue; | ||
| layers.push({ | ||
| name: entry.layer, | ||
| patterns: directories.map((directory) => `${normalize(path.join(srcDir, directory))}/**`), | ||
| intentPrefixes: entry.prefixes, | ||
| ...(entry.layer === 'DomainModel' | ||
| ? { forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS } | ||
| : {}), | ||
| }); | ||
| } | ||
| const names = new Set(layers.map((layer) => layer.name)); | ||
| const rules = DEFAULT_RULES.filter((rule) => names.has(rule.from) && names.has(rule.to)); | ||
| return { srcDir, config: { include: [srcDir], layers, rules } }; | ||
| } | ||
| /** Top-level directories under srcDir not covered by any detected layer pattern. */ | ||
| export function uncoveredDirectories(root, srcDir, layers) { | ||
| const base = path.join(root, srcDir); | ||
| if (!fs.existsSync(base)) return []; | ||
| return fs | ||
| .readdirSync(base, { withFileTypes: true }) | ||
| .filter( | ||
| (entry) => | ||
| entry.isDirectory() && | ||
| entry.name !== 'node_modules' && | ||
| entry.name !== 'dist' && | ||
| !entry.name.startsWith('.') | ||
| ) | ||
| .map((entry) => entry.name) | ||
| .filter((name) => { | ||
| const prefix = `${normalize(path.join(srcDir, name))}/`; | ||
| return !layers.some((layer) => | ||
| layer.patterns.some((pattern) => pattern.startsWith(prefix)) | ||
| ); | ||
| }); | ||
| } | ||
| export function proposeForUncovered(root, srcDir, layers) { | ||
| const proposals = []; | ||
| for (const top of uncoveredDirectories(root, srcDir, layers)) { | ||
| const direct = suggestLayerForDir(top); | ||
| if (direct) { | ||
| proposals.push({ dir: `${srcDir}/${top}`, ...direct }); | ||
| continue; | ||
| } | ||
| let children = []; | ||
| try { | ||
| children = fs | ||
| .readdirSync(path.join(root, srcDir, top), { withFileTypes: true }) | ||
| .filter((e) => e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.')) | ||
| .map((e) => e.name); | ||
| } catch { | ||
| /* not a readable directory — treat as unrecognized below */ | ||
| } | ||
| if (children.length > 0) { | ||
| // Descend: propose per child so a mixed `lib/` yields lib/repositories → Persistence | ||
| // AND flags lib/db as unrecognized, instead of dropping the parts Ark can't place. | ||
| for (const child of children) { | ||
| const hit = suggestLayerForDir(child); | ||
| proposals.push( | ||
| hit | ||
| ? { dir: `${srcDir}/${top}/${child}`, ...hit } | ||
| : { dir: `${srcDir}/${top}/${child}`, unrecognized: true } | ||
| ); | ||
| } | ||
| } else { | ||
| proposals.push({ dir: `${srcDir}/${top}`, unrecognized: true }); | ||
| } | ||
| } | ||
| return proposals; | ||
| } |
| /** | ||
| * ark-check --watch loop (polling fallback when fs.watch is unavailable). | ||
| */ | ||
| import { spawnSync } from 'node:child_process'; | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
| function watchFingerprint(target) { | ||
| const pending = [target]; | ||
| const entries = []; | ||
| while (pending.length > 0) { | ||
| const current = pending.pop(); | ||
| let stat; | ||
| try { | ||
| stat = fs.statSync(current); | ||
| } catch { | ||
| continue; | ||
| } | ||
| entries.push(`${current}:${stat.mtimeMs}:${stat.size}`); | ||
| if (!stat.isDirectory()) continue; | ||
| try { | ||
| for (const name of fs.readdirSync(current)) pending.push(path.join(current, name)); | ||
| } catch { | ||
| // A concurrent delete is represented by the next fingerprint. | ||
| } | ||
| } | ||
| return entries.sort().join('|'); | ||
| } | ||
| function watchByPolling(target, onChange) { | ||
| let previous = watchFingerprint(target); | ||
| setInterval(() => { | ||
| const current = watchFingerprint(target); | ||
| if (current === previous) return; | ||
| previous = current; | ||
| onChange(); | ||
| }, 250); | ||
| } | ||
| export async function runWatchMode(args, { cliPath, loadConfig, dim }) { | ||
| const argv = process.argv.slice(2).filter((token) => token !== '--watch'); | ||
| let debounce; | ||
| const rerun = () => { | ||
| clearTimeout(debounce); | ||
| debounce = setTimeout(() => { | ||
| const result = spawnSync(process.execPath, [cliPath, ...argv], { | ||
| cwd: args.root, | ||
| stdio: 'inherit', | ||
| env: process.env, | ||
| }); | ||
| process.exitCode = result.status ?? 1; | ||
| }, 300); | ||
| }; | ||
| let config; | ||
| try { | ||
| config = loadConfig(args.root, args.config); | ||
| } catch (error) { | ||
| console.error(error instanceof Error ? error.message : String(error)); | ||
| process.exitCode = 2; | ||
| return; | ||
| } | ||
| for (const entry of config.include ?? []) { | ||
| const target = path.join(args.root, entry); | ||
| if (!fs.existsSync(target)) continue; | ||
| try { | ||
| const watcher = fs.watch(target, { recursive: true }, rerun); | ||
| watcher.on('error', () => { | ||
| watcher.close(); | ||
| watchByPolling(target, rerun); | ||
| }); | ||
| } catch { | ||
| watchByPolling(target, rerun); | ||
| } | ||
| } | ||
| console.log(dim('Watching governed paths for changes… (Ctrl+C to stop)')); | ||
| await new Promise(() => {}); | ||
| } |
| /** | ||
| * Human doctor screens (AL06): compact first screen vs Details encyclopedia. | ||
| * Independently invocable. JSON / compass / coach stay in runDoctor. | ||
| */ | ||
| import path from 'node:path'; | ||
| import { arkCommand } from '../ark-shared.mjs'; | ||
| import { operatingModeTitle } from './product-copy.mjs'; | ||
| import { isDoctorHealthyNothingToDo } from './post-green-path.mjs'; | ||
| import { printParseHealthSection } from './parse-health.mjs'; | ||
| import { printDoctorAdvisories } from './doctor-advisories.mjs'; | ||
| import { designDeltaDoctorLines } from './design-delta.mjs'; | ||
| import { enforcementDoctorLines } from './enforcement-state.mjs'; | ||
| import { analysisIncompleteStatement } from './analysis-completeness.mjs'; | ||
| import { skillGapsForActiveHost, detectCodexHomeGap, codexConcernIsActive } from './agent-gates.mjs'; | ||
| import { agentHomeConcernIsActive } from './agent-homes.mjs'; | ||
| function lineWith(ok, warn, bad, color) { | ||
| return (mark, text) => console.log(` ${mark} ${text}`); | ||
| } | ||
| function marks(color) { | ||
| return { | ||
| ok: color.green('✓'), | ||
| warn: color.yellow('!'), | ||
| bad: color.red('✗'), | ||
| }; | ||
| } | ||
| /** | ||
| * Compact first doctor screen. Always ends with `More: --doctor --all`. | ||
| * Includes thin-coverage and incomplete-analysis honesty. Never prints Details. | ||
| */ | ||
| export function printDoctorCompactHuman(view) { | ||
| const color = view.color; | ||
| const { ok, warn, bad } = marks(color); | ||
| const line = lineWith(ok, warn, bad, color); | ||
| const { | ||
| root, | ||
| analysisComplete, | ||
| completeness, | ||
| doctorAdvisories, | ||
| operatingMode, | ||
| designFitness, | ||
| adopted, | ||
| stewardUnfinished, | ||
| emptyScope, | ||
| uniqueActions, | ||
| ciMergeBoundary, | ||
| cov, | ||
| writePath, | ||
| writePathHonesty, | ||
| gatesMissing, | ||
| violations, | ||
| } = view; | ||
| console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`)); | ||
| if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness)); | ||
| printParseHealthSection(doctorAdvisories.parseHealth, { color, warn, line }); | ||
| const mode = operatingMode; | ||
| console.log(''); | ||
| console.log(color.bold('Operating mode')); | ||
| const modeMark = | ||
| mode === 'enforce' && | ||
| !designFitness.designWeak && | ||
| adopted !== 'not-adopted' && | ||
| !stewardUnfinished | ||
| ? ok | ||
| : warn; | ||
| const modeHelp = { | ||
| suggest: 'thin or new tree. Next: ark start --apply, then doctor.', | ||
| adapt: 'config and tree still disagree. Next: do #1.', | ||
| enforce: | ||
| adopted === 'not-adopted' | ||
| ? 'import rules check out; merge boundary not adopted.' | ||
| : 'import rules check out. Keep host + CI.', | ||
| }; | ||
| const modeTitle = operatingModeTitle(mode, designFitness.designWeak, stewardUnfinished); | ||
| line( | ||
| modeMark, | ||
| `${modeTitle} — ${ | ||
| designFitness.designWeak | ||
| ? 'import rules check out; leftover design work remains.' | ||
| : modeHelp[mode] | ||
| }` | ||
| ); | ||
| if (emptyScope) { | ||
| line( | ||
| bad, | ||
| 'Empty scope: include paths match 0 source files — a green check is meaningless until include/layers match the tree (monorepo → apps/packages, or /ark-adopt).' | ||
| ); | ||
| } | ||
| console.log(''); | ||
| if (ciMergeBoundary?.ci?.state) { | ||
| line( | ||
| ciMergeBoundary.ci.state === 'required' ? ok : warn, | ||
| `CI merge: ${ciMergeBoundary.ci.state}` | ||
| ); | ||
| } | ||
| if (adopted === 'advisory-only-acked') { | ||
| line(warn, 'Adoption: advisory-only ack — not a required GitHub status.'); | ||
| } | ||
| if (isDoctorHealthyNothingToDo(designFitness, uniqueActions, adopted)) { | ||
| console.log(color.green('✔ Healthy — nothing to do.')); | ||
| console.log(color.dim(' Keep write path + CI.')); | ||
| } else { | ||
| console.log(color.bold('Primary next action')); | ||
| console.log(` 1. ${uniqueActions[0]}`); | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Coverage')); | ||
| const govMark = | ||
| emptyScope || cov.governed.percent < 50 | ||
| ? bad | ||
| : cov.governed.percent >= 80 | ||
| ? ok | ||
| : warn; | ||
| line(govMark, `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`); | ||
| const hostRed = | ||
| gatesMissing.length > 0 || | ||
| Boolean(writePath.gap) || | ||
| writePathHonesty?.softWriteHost === true; | ||
| if (hostRed) { | ||
| console.log(''); | ||
| console.log(color.bold('Host / CI')); | ||
| if (writePath.activeHost) line(' ', `Active host: ${writePath.activeHost}`); | ||
| if (gatesMissing.length > 0) line(bad, `Missing gates: ${gatesMissing.join(', ')}`); | ||
| else if (writePath.gap || writePathHonesty?.softWriteHost) { | ||
| line(warn, 'Local writes are advisory; required CI is the merge boundary.'); | ||
| } | ||
| } | ||
| const nudge = doctorAdvisories.stewardNudge; | ||
| if ((nudge?.needsStewards || nudge?.drift || nudge?.emptyStewardsPastGrace) && nudge.ask) { | ||
| console.log(''); | ||
| console.log(color.bold('Stewards')); | ||
| line(warn, nudge.ask); | ||
| } | ||
| if (violations.length === 0) { | ||
| if (!analysisComplete) { | ||
| console.log(''); | ||
| line( | ||
| warn, | ||
| 'No reported violations — contract compliance is not verified until analysis is complete' | ||
| ); | ||
| } else if (emptyScope || cov.governed.percent < 50) { | ||
| console.log(''); | ||
| line( | ||
| warn, | ||
| 'No active violations — coverage is still thin, so green is not yet honest enforcement' | ||
| ); | ||
| } | ||
| } | ||
| console.log(''); | ||
| console.log(color.dim('More: --doctor --all')); | ||
| } | ||
| /** | ||
| * Details encyclopedia. Only invoked for `--doctor --all` / `all: true`. | ||
| */ | ||
| export function printDoctorDetailsHuman(view) { | ||
| const color = view.color; | ||
| const { ok, warn, bad } = marks(color); | ||
| const line = lineWith(ok, warn, bad, color); | ||
| const { | ||
| root, | ||
| analysisComplete, | ||
| doctorAdvisories, | ||
| operatingMode, | ||
| designFitness, | ||
| adopted, | ||
| stewardUnfinished, | ||
| emptyScope, | ||
| options, | ||
| cov, | ||
| writePath, | ||
| writePathHonesty, | ||
| gatesMissing, | ||
| violations, | ||
| coverageHonesty, | ||
| packageVersionTruth, | ||
| designSmells, | ||
| pilotLoop, | ||
| goldenPattern, | ||
| pureLayerOptIn, | ||
| summary, | ||
| suppressed, | ||
| activeCount, | ||
| skillGaps, | ||
| agentHomeGaps, | ||
| baseline, | ||
| baselineHonesty, | ||
| staleBaseline, | ||
| staleRunners, | ||
| adoption, | ||
| } = view; | ||
| const modeTitle = operatingModeTitle(operatingMode, designFitness.designWeak, stewardUnfinished); | ||
| console.log(''); | ||
| console.log(color.dim('---')); | ||
| console.log(color.bold('Details')); | ||
| if (coverageHonesty.greenIsNotEnforcement) { | ||
| line(coverageHonesty.worseThanNoGate ? bad : warn, coverageHonesty.message); | ||
| } | ||
| if (cov.suggestions.length > 0) { | ||
| line(warn, `${cov.suggestions.length} ungoverned director(y/ies) — proposals: ${arkCommand(root, 'ark-check', '--coverage')}`); | ||
| } | ||
| if (cov.emptyLayers.length > 0) line(warn, `Empty layers (pattern matches nothing): ${cov.emptyLayers.join(', ')}`); | ||
| if (cov.layersWithoutRules.length > 0) line(warn, `Layers with no rule edge: ${cov.layersWithoutRules.join(', ')}`); | ||
| if (cov.dualMembership?.count > 0) { | ||
| line( | ||
| warn, | ||
| `Dual-match: ${cov.dualMembership.count} file(s) match multiple layers — ${cov.dualMembership.note ?? 'review overlapping globs'}` | ||
| ); | ||
| } | ||
| if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) line(ok, 'Every layer classifies files; no empty layers'); | ||
| if (packageVersionTruth?.dualTruth) { | ||
| console.log(''); | ||
| console.log(color.bold('Package pin (dual-truth)')); | ||
| line(warn, packageVersionTruth.note); | ||
| } else if (packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT') { | ||
| console.log(''); | ||
| console.log(color.bold('Package pin')); | ||
| line(warn, packageVersionTruth.note); | ||
| } | ||
| if (options.configWalkedUp && options.configRoot) { | ||
| line( | ||
| ok, | ||
| `Config walk-up: using monorepo root ${options.configRoot} (ark.config.json not in cwd package)` | ||
| ); | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Design fitness')); | ||
| if (designSmells.length === 0) { | ||
| line(analysisComplete ? ok : warn, designFitness.label); | ||
| } else { | ||
| line(designFitness.designWeak ? warn : warn, designFitness.label); | ||
| for (const smell of designSmells.slice(0, 5)) { | ||
| const outcome = smell.outcome || smell.message; | ||
| line(' ', `[${smell.id}] ${outcome}`); | ||
| if (smell.outcome && smell.message && smell.message !== smell.outcome) { | ||
| line(' ', color.dim(`detail: ${smell.message}`)); | ||
| } | ||
| if (smell.evidence?.length) { | ||
| line(' ', color.dim(`evidence: ${smell.evidence.slice(0, 4).join(', ')}`)); | ||
| } | ||
| } | ||
| if (pilotLoop?.active && pilotLoop.nextPilot) { | ||
| const np = pilotLoop.nextPilot; | ||
| line( | ||
| warn, | ||
| `Next pilot (one at a time): ${np.pilotTarget || np.pilot} [${np.smellId}] → re-doctor after change` | ||
| ); | ||
| line(' ', color.dim(`success: ${np.successSignal}`)); | ||
| line(' ', color.dim('never multi-pilot batch; pattern bets are never auto-applied')); | ||
| } | ||
| } | ||
| if (options.designDelta) { | ||
| console.log(''); | ||
| console.log(color.bold('Design delta (opt-in)')); | ||
| for (const row of designDeltaDoctorLines(options.designDelta)) | ||
| line(row.level === 'bad' ? bad : row.level === 'ok' ? ok : ' ', row.level === 'dim' ? color.dim(row.text) : row.text); | ||
| } | ||
| if (goldenPattern.present) { | ||
| console.log(''); | ||
| console.log(color.bold('Golden pattern (new code)')); | ||
| line( | ||
| ok, | ||
| `"${goldenPattern.name}" — ${goldenPattern.norm}` + | ||
| (goldenPattern.newCodeHome ? ` Prefer: ${goldenPattern.newCodeHome}.` : '') + | ||
| ' Advisory only — does not clear leftover design work or replace the gate.' | ||
| ); | ||
| } else if (goldenPattern.invalid) { | ||
| console.log(''); | ||
| console.log(color.bold('Golden pattern (new code)')); | ||
| line( | ||
| warn, | ||
| `${goldenPattern.path} is present but invalid (${goldenPattern.error || 'invalid'}). ` + | ||
| 'Fix or remove it — absence is fine; a bad file is not guidance.' | ||
| ); | ||
| } | ||
| if (pureLayerOptIn) { | ||
| line(' ', color.dim(pureLayerOptIn.message)); | ||
| } | ||
| printDoctorAdvisories(doctorAdvisories, { line, warn, color }); | ||
| console.log(''); | ||
| console.log(color.bold('Violations')); | ||
| if (violations.length === 0) { | ||
| if (!analysisComplete) line(warn, 'No reported violations — contract compliance is not verified until analysis is complete'); | ||
| else if (emptyScope || cov.governed.percent < 50) { | ||
| line( | ||
| warn, | ||
| 'No active violations — coverage is still thin, so green is not yet honest enforcement' | ||
| ); | ||
| } else if (designFitness.designWeak) { | ||
| line(warn, `None on checked imports — import rules match the config; leftover design work remains (${modeTitle}). Not healthy finished.`); | ||
| } else { | ||
| line(ok, 'None — the code matches the contract on checked edges'); | ||
| } | ||
| } else { | ||
| const typeNote = summary.typeOnlyCount > 0 ? ` (${summary.valueCount} value · ${summary.typeOnlyCount} type-only)` : ''; | ||
| const supNote = suppressed > 0 ? `, ${suppressed} frozen` : ''; | ||
| line( | ||
| activeCount > 0 ? warn : ok, | ||
| `${violations.length} total${typeNote}${supNote}${activeCount > 0 ? ` — ${activeCount} NOT baselined` : ''}` | ||
| ); | ||
| for (const edge of summary.edges.slice(0, 3)) line(' ', color.dim(`${edge.count} ${edge.edge}`)); | ||
| if (summary.concentrated) { | ||
| line(warn, color.dim(`${Math.round(summary.dominantShare * 100)}% on one edge (${summary.dominant}) — likely a contract fix, not debt`)); | ||
| } | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Write path (agent)')); | ||
| const capabilities = writePath.capabilities; | ||
| const writePathLabels = { | ||
| repair: 'repair-capable — hard block + machine-readable autoPatch / ARK_REPAIR_JSON', | ||
| 'reject-only': 'reject-only — hard block with prose; no repair payload', | ||
| 'mcp-only': 'MCP tools only — prepare-write/autoPatch available; no PreToolUse hook', | ||
| none: 'no write gate hook and no Ark MCP', | ||
| }; | ||
| const wpMark = | ||
| capabilities['hard-write'] | ||
| ? ok | ||
| : capabilities['advisory-write'] || capabilities['merge-gate'] | ||
| ? warn | ||
| : bad; | ||
| line(' ', `Active host: ${writePath.activeHost}`); | ||
| line(' ', `Supported profile: ${writePath.supportSummary}`); | ||
| line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`); | ||
| if (writePathHonesty.message) line(warn, writePathHonesty.message); | ||
| if (writePath.sessionNote) { | ||
| line(warn, writePath.sessionNote); | ||
| } | ||
| const enforcement = writePath.enforcementState; | ||
| for (const row of enforcementDoctorLines(enforcement)) line(row.level === 'ok' ? ok : row.level === 'bad' ? bad : warn, row.text); | ||
| const supportCaps = writePath.support?.capabilities || {}; | ||
| const repairReinjection = supportCaps['repair-reinjection-guaranteed'] === true; | ||
| const repairEnvelope = supportCaps['repair-envelope-emitted'] === true || supportCaps['repair-payload'] === true; | ||
| line( | ||
| 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' | ||
| ); | ||
| if (writePath.gap) { | ||
| line(writePath.gap.severity === 'warn' ? warn : warn, writePath.gap.message); | ||
| if (writePath.gap.fix) { | ||
| line(' ', color.dim(`Fix: ${writePath.gap.fix}`)); | ||
| } | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Gates & skills')); | ||
| if (gatesMissing.length === 0) line(ok, 'Shared gate artifacts found on disk (AGENTS.md, .mcp.json, CI); runtime activation is reported separately'); | ||
| else { | ||
| line(bad, `Missing gates: ${gatesMissing.join(', ')}`); | ||
| } | ||
| const humanSkillGaps = skillGapsForActiveHost(skillGaps); | ||
| const legacyCodex = humanSkillGaps.some((g) => g.tool === 'codex' && g.legacyPromptsOnly); | ||
| const codexLegacySafeDelete = humanSkillGaps.some( | ||
| (g) => g.tool === 'codex' && g.legacyAdvisory && g.catalogComplete | ||
| ); | ||
| const remainingGaps = humanSkillGaps.filter( | ||
| (g) => !(g.tool === 'codex' && (g.legacyPromptsOnly || g.legacyAdvisory)) | ||
| ); | ||
| const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0); | ||
| const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0); | ||
| if (remMiss + remStale === 0 && !legacyCodex) line(ok, '/ark-* skills current for detected tools'); | ||
| if (legacyCodex) { | ||
| line(warn, 'Codex: legacy flat .codex/prompts only (not a loadable skill catalog)'); | ||
| } | ||
| if (codexLegacySafeDelete) { | ||
| line( | ||
| ' ', | ||
| color.dim( | ||
| 'Codex catalog complete — leftover .codex/prompts/ark-*.md are safe to delete (not loadable; not required).' | ||
| ) | ||
| ); | ||
| } | ||
| if (remMiss + remStale > 0) { | ||
| line( | ||
| warn, | ||
| `${remMiss} missing / ${remStale} content-behind-package /ark-* skill(s) for ${remainingGaps.map((g) => g.tool).join(', ')}` | ||
| ); | ||
| } | ||
| const codexHomeGap = detectCodexHomeGap(root); | ||
| if (codexHomeGap) { | ||
| const parts = [ | ||
| codexHomeGap.legacyPromptsOnly ? 'legacy-prompts-only' : null, | ||
| codexHomeGap.missing > 0 ? `${codexHomeGap.missing} missing` : null, | ||
| codexHomeGap.stale > 0 ? `${codexHomeGap.stale} content-behind-package` : null, codexHomeGap.catalogStateReason, | ||
| ].filter(Boolean); | ||
| const deferred = !codexConcernIsActive(); | ||
| if (deferred) { | ||
| line(color.dim('·'), color.dim(`Codex home skills ${parts.join(', ')} (deferred — not on Codex session)`)); | ||
| } else { | ||
| line(warn, `Codex home skills ${parts.join(', ')}`); | ||
| } | ||
| } | ||
| for (const gap of agentHomeGaps) { | ||
| const parts = [ | ||
| gap.missing > 0 ? `${gap.missing} missing` : null, | ||
| gap.stale > 0 ? `${gap.stale} content-behind-package` : null, | ||
| gap.catalogStateReason, | ||
| ].filter(Boolean); | ||
| const deferred = !agentHomeConcernIsActive(gap.host); | ||
| const gapSummary = `${gap.label} shared agent skills ${parts.join(', ')}`; | ||
| if (deferred) { | ||
| line(color.dim('·'), color.dim(`${gapSummary} (deferred — not this session)`)); | ||
| } else { | ||
| line(warn, gapSummary); | ||
| } | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Baseline')); | ||
| if (!baseline.exists) { | ||
| line(!analysisComplete || violations.length > 0 ? warn : ok, !analysisComplete ? 'No baseline — current violations were not fully evaluated' : violations.length > 0 ? 'No baseline — adopting a dirty repo? freeze with --update-baseline' : 'No baseline (nothing to freeze)'); | ||
| } else { | ||
| const baseMark = !analysisComplete || baselineHonesty.dirtyBaselineRisk ? warn : ok; | ||
| line(baseMark, `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`); | ||
| if (analysisComplete && baselineHonesty.dirtyBaselineRisk) { | ||
| line(warn, baselineHonesty.message); | ||
| } | ||
| if (analysisComplete && staleBaseline > 0) { | ||
| line(warn, `${staleBaseline} stale entr(y/ies) no longer occur — tighten with --update-baseline`); | ||
| } | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Command runners')); | ||
| if (staleRunners.length === 0) line(ok, 'Emitted commands match the package manager'); | ||
| else { | ||
| line(warn, `Stale runner in ${staleRunners.join(', ')}`); | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Adoption (separate from fitness score)')); | ||
| if (adoption.gaps.length === 0 && !adoption.layerBalance) { | ||
| line( | ||
| ok, | ||
| 'Hosts, MCP argv, core optionality, origin report, baseline policy, and deploy-path lint/types look complete' | ||
| ); | ||
| } else { | ||
| for (const gap of adoption.gaps) { | ||
| const mark = gap.deferred | ||
| ? color.dim('·') | ||
| : gap.severity === 'warn' | ||
| ? warn | ||
| : gap.severity === 'info' | ||
| ? warn | ||
| : bad; | ||
| line(mark, gap.message); | ||
| if (gap.fix) { | ||
| line(' ', color.dim(gap.deferred ? `When using Codex: ${gap.fix}` : `Fix: ${gap.fix}`)); | ||
| } | ||
| } | ||
| if (adoption.layerBalance) { | ||
| line(warn, color.dim(adoption.layerBalance.educational)); | ||
| } | ||
| } | ||
| if (adoption.baseline) { | ||
| line( | ||
| ' ', | ||
| color.dim( | ||
| `Baseline policy: ${adoption.baseline.signal}` + | ||
| (adoption.baseline.primaryPathUsesBaseline | ||
| ? ' · primary path uses --baseline' | ||
| : ' · primary path does not use --baseline') | ||
| ) | ||
| ); | ||
| } | ||
| if (adoption.originReport.present) { | ||
| line(ok, 'Origin architecture snapshot present (.ark/reports/origin.json)'); | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Safety / bypass resistance')); | ||
| const safety = options.safety; | ||
| if (!safety) { | ||
| line(warn, 'Safety diagnostics unavailable'); | ||
| } else { | ||
| const rows = [ | ||
| ['Non-literal dynamic dependencies', safety.nonLiteralDynamicImports], | ||
| ['@ts-ignore / @ts-nocheck', safety.tsSuppressions], | ||
| ['Explicit any casts', safety.anyCasts], | ||
| ['InMemory stores in production source', safety.inMemoryProductionStores], | ||
| ['Rules with peerIsolation: false', safety.disabledPeerIsolationRules], | ||
| ]; | ||
| for (const [label, entries] of rows) { | ||
| line(entries.length === 0 ? ok : warn, `${label}: ${entries.length}`); | ||
| } | ||
| } | ||
| } |
| /** | ||
| * Host PreToolUse payload mapping (Claude/Grok/Cursor/Antigravity/Codex). | ||
| */ | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
| /** | ||
| * Map Google Antigravity write tools (PascalCase args) onto Claude Write/Edit/MultiEdit. | ||
| * @returns {{ toolName: string, toolInput: object }|null} | ||
| */ | ||
| export function mapAntigravityToolCall(toolCall) { | ||
| if (!toolCall || typeof toolCall !== 'object') return null; | ||
| const name = toolCall.name ?? ''; | ||
| const args = toolCall.args && typeof toolCall.args === 'object' ? toolCall.args : {}; | ||
| const filePath = args.TargetFile ?? args.targetFile ?? args.file_path ?? args.path; | ||
| if (name === 'write_to_file') { | ||
| return { | ||
| toolName: 'Write', | ||
| toolInput: { | ||
| file_path: filePath, | ||
| content: args.CodeContent ?? args.codeContent ?? args.content ?? '', | ||
| }, | ||
| operation: 'write_to_file', | ||
| }; | ||
| } | ||
| if (name === 'replace_file_content') { | ||
| return { | ||
| toolName: 'Edit', | ||
| toolInput: { | ||
| file_path: filePath, | ||
| old_string: args.TargetContent ?? args.targetContent ?? args.old_string ?? '', | ||
| new_string: args.ReplacementContent ?? args.replacementContent ?? args.new_string ?? '', | ||
| replace_all: Boolean(args.AllowMultiple ?? args.allowMultiple), | ||
| }, | ||
| operation: 'replace_file_content', | ||
| }; | ||
| } | ||
| if (name === 'multi_replace_file_content') { | ||
| const chunks = Array.isArray(args.ReplacementChunks) | ||
| ? args.ReplacementChunks | ||
| : Array.isArray(args.replacementChunks) | ||
| ? args.replacementChunks | ||
| : []; | ||
| return { | ||
| toolName: 'MultiEdit', | ||
| toolInput: { | ||
| file_path: filePath, | ||
| edits: chunks.map((chunk) => ({ | ||
| old_string: chunk?.TargetContent ?? chunk?.targetContent ?? chunk?.old_string ?? '', | ||
| new_string: | ||
| chunk?.ReplacementContent ?? chunk?.replacementContent ?? chunk?.new_string ?? '', | ||
| replace_all: Boolean(chunk?.AllowMultiple ?? chunk?.allowMultiple), | ||
| })), | ||
| }, | ||
| operation: 'multi_replace_file_content', | ||
| }; | ||
| } | ||
| return { | ||
| toolName: name, | ||
| toolInput: { ...args, file_path: filePath }, | ||
| operation: name, | ||
| }; | ||
| } | ||
| /** | ||
| * Normalize agent PreToolUse payloads. | ||
| * Claude Code: { tool_name, tool_input: { file_path, content | old_string/new_string } } | ||
| * Grok Build: { toolName, toolInput: { file_path, content | old_string/new_string } } | ||
| * (aliases Write/Edit/MultiEdit → write/search_replace; matcher keeps both) | ||
| * Antigravity: { toolCall: { name, args: { TargetFile, CodeContent, … } } } | ||
| * Cursor: { tool_name, tool_input, hook_event_name?, workspace_roots? } | ||
| * Write uses `contents`; StrReplace maps to Edit (path/old_string/new_string). | ||
| * Codex: { tool_name: "apply_patch", tool_input: { command: "*** Begin Patch..." } } | ||
| */ | ||
| export function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_HOOK_EVENT)) { | ||
| const antigravityStyle = | ||
| payload != null && typeof payload === 'object' && 'toolCall' in payload; | ||
| if (antigravityStyle) { | ||
| const mapped = mapAntigravityToolCall(payload.toolCall); | ||
| const filePath = | ||
| mapped?.toolInput?.file_path ?? | ||
| mapped?.toolInput?.filePath ?? | ||
| mapped?.toolInput?.path ?? | ||
| mapped?.toolInput?.target_file; | ||
| return { | ||
| toolName: mapped?.toolName ?? '', | ||
| toolInput: { ...(mapped?.toolInput ?? {}), file_path: filePath }, | ||
| grokStyle: true, // decision JSON on stdout (deny) | ||
| antigravityStyle: true, | ||
| cursorStyle: false, | ||
| operation: mapped?.operation ?? mapped?.toolName ?? null, | ||
| }; | ||
| } | ||
| const rawName = payload?.tool_name ?? payload?.toolName ?? ''; | ||
| const toolInputRaw = payload?.tool_input ?? payload?.toolInput ?? {}; | ||
| const toolInput = | ||
| toolInputRaw && typeof toolInputRaw === 'object' ? { ...toolInputRaw } : {}; | ||
| // Cursor Write uses `contents`; Claude/Grok use `content`. | ||
| if (toolInput.content == null && typeof toolInput.contents === 'string') { | ||
| toolInput.content = toolInput.contents; | ||
| } | ||
| const nameMap = { | ||
| Write: 'Write', | ||
| write: 'Write', | ||
| Edit: 'Edit', | ||
| search_replace: 'Edit', | ||
| StrReplace: 'Edit', | ||
| MultiEdit: 'MultiEdit', | ||
| ApplyPatch: 'ApplyPatch', | ||
| apply_patch: 'ApplyPatch', | ||
| write_to_file: 'Write', | ||
| replace_file_content: 'Edit', | ||
| multi_replace_file_content: 'MultiEdit', | ||
| }; | ||
| const toolName = nameMap[rawName] ?? rawName; | ||
| const filePath = | ||
| toolInput.file_path ?? toolInput.filePath ?? toolInput.path ?? toolInput.target_file; | ||
| const cursorStyle = | ||
| Boolean(process.env.CURSOR_PROJECT_DIR) || | ||
| Boolean(process.env.CURSOR_VERSION) || | ||
| (payload != null && | ||
| typeof payload === 'object' && | ||
| (payload.hook_event_name === 'preToolUse' || | ||
| Array.isArray(payload.workspace_roots) || | ||
| rawName === 'StrReplace' || | ||
| (rawName === 'Write' && typeof toolInputRaw?.contents === 'string'))); | ||
| return { | ||
| toolName, | ||
| toolInput: { ...toolInput, file_path: filePath }, | ||
| // Grok-style camelCase (or GROK_HOOK_EVENT) → also emit deny JSON on stdout. | ||
| grokStyle: | ||
| grokHookEvent || | ||
| (payload != null && typeof payload === 'object' && 'toolName' in payload), | ||
| antigravityStyle: false, | ||
| cursorStyle, | ||
| operation: rawName === 'StrReplace' ? 'StrReplace' : null, | ||
| }; | ||
| } | ||
| export function applyCodexUpdatePatch(current, lines) { | ||
| let source = current.split('\n'); | ||
| let cursor = 0; | ||
| const hunks = []; | ||
| let hunk = null; | ||
| for (const line of lines) { | ||
| if (line.startsWith('@@')) { | ||
| if (hunk) hunks.push(hunk); | ||
| hunk = { anchor: line.slice(2).trim(), entries: [] }; | ||
| } else if (/^[ +\-]/.test(line)) { | ||
| if (!hunk) return null; | ||
| hunk.entries.push(line); | ||
| } | ||
| } | ||
| if (hunk) hunks.push(hunk); | ||
| for (const { anchor, entries } of hunks) { | ||
| if (anchor) { | ||
| const anchorAt = source.findIndex((line, index) => index >= cursor && line === anchor); | ||
| if (anchorAt < 0) return null; | ||
| cursor = anchorAt + 1; | ||
| } | ||
| const oldLines = entries.filter((line) => !line.startsWith('+')).map((line) => line.slice(1)); | ||
| const newLines = entries.filter((line) => !line.startsWith('-')).map((line) => line.slice(1)); | ||
| let found = -1; | ||
| for (let at = cursor; at <= source.length - oldLines.length; at += 1) { | ||
| if (oldLines.every((line, index) => source[at + index] === line)) { | ||
| found = at; | ||
| break; | ||
| } | ||
| } | ||
| if (found < 0) return null; | ||
| source.splice(found, oldLines.length, ...newLines); | ||
| cursor = found + newLines.length; | ||
| } | ||
| return source.join('\n'); | ||
| } | ||
| export function codexPatchWrites(patch, root) { | ||
| if (typeof patch !== 'string') { | ||
| return { writes: [], complete: false }; | ||
| } | ||
| const lines = patch.split('\n'); | ||
| const begin = lines.indexOf('*** Begin Patch'); | ||
| const end = lines.indexOf('*** End Patch', begin + 1); | ||
| if (begin < 0 || end <= begin) return { writes: [], complete: false }; | ||
| const writes = []; | ||
| const seenPaths = new Set(); | ||
| let complete = [ | ||
| ...lines.slice(0, begin), | ||
| ...lines.slice(end + 1), | ||
| ].every((line) => line.trim() === ''); | ||
| let sawFileDirective = false; | ||
| for (let index = begin + 1; index < end; index += 1) { | ||
| const match = lines[index].match(/^\*\*\* (Add|Update|Delete) File: (.+)$/); | ||
| if (!match) { | ||
| if (lines[index].trim() !== '') complete = false; | ||
| continue; | ||
| } | ||
| sawFileDirective = true; | ||
| const [, action, relativePath] = match; | ||
| const body = []; | ||
| for (index += 1; index < end && !lines[index].startsWith('*** '); index += 1) { | ||
| body.push(lines[index]); | ||
| } | ||
| index -= 1; | ||
| const filePath = path.resolve(root, relativePath); | ||
| const rel = path.relative(root, filePath); | ||
| if ( | ||
| seenPaths.has(filePath) || | ||
| rel.startsWith(`..${path.sep}`) || | ||
| rel === '..' || | ||
| path.isAbsolute(rel) | ||
| ) { | ||
| complete = false; | ||
| continue; | ||
| } | ||
| seenPaths.add(filePath); | ||
| const canonicalRelativePath = rel.split(path.sep).join('/'); | ||
| if (action === 'Delete') { | ||
| if (body.some((line) => line.trim() !== '') || !fs.existsSync(filePath)) { | ||
| complete = false; | ||
| continue; | ||
| } | ||
| writes.push({ path: canonicalRelativePath, filePath, delete: true }); | ||
| continue; | ||
| } | ||
| let content; | ||
| if (action === 'Add') { | ||
| if ( | ||
| body.length === 0 || | ||
| fs.existsSync(filePath) || | ||
| body.some((line) => !line.startsWith('+')) | ||
| ) { | ||
| complete = false; | ||
| continue; | ||
| } | ||
| content = body.filter((line) => line.startsWith('+')).map((line) => line.slice(1)).join('\n'); | ||
| if (body.some((line) => line.startsWith('+'))) content += '\n'; | ||
| } else { | ||
| if ( | ||
| !body.some((line) => line.startsWith('@@')) || | ||
| body.some((line) => !line.startsWith('@@') && !/^[ +\-]/.test(line)) | ||
| ) { | ||
| complete = false; | ||
| continue; | ||
| } | ||
| let current; | ||
| try { | ||
| current = fs.readFileSync(filePath, 'utf8'); | ||
| } catch { | ||
| complete = false; | ||
| continue; | ||
| } | ||
| content = applyCodexUpdatePatch(current, body); | ||
| if (content === null) complete = false; | ||
| } | ||
| if (typeof content === 'string') { | ||
| writes.push({ path: canonicalRelativePath, filePath, content }); | ||
| } | ||
| } | ||
| return { writes, complete: complete && sawFileDirective }; | ||
| } | ||
| /** | ||
| * Compute the file content a Write/Edit/MultiEdit is about to produce. Edits are applied | ||
| * to the CURRENT on-disk file so the gate judges the real post-edit state, not the edit | ||
| * snippet out of context. Replacement uses a function argument so `$&`-style sequences in | ||
| * generated code are inserted literally, never interpreted as replacement patterns. | ||
| */ | ||
| export function proposedSource(toolName, toolInput) { | ||
| if (toolName === 'Write') return toolInput.content ?? toolInput.contents; | ||
| let text = ''; | ||
| try { | ||
| text = fs.readFileSync(toolInput.file_path, 'utf8'); | ||
| } catch { | ||
| // New file created via Edit: fall through with an empty base. | ||
| } | ||
| const edits = toolName === 'MultiEdit' ? toolInput.edits ?? [] : [toolInput]; | ||
| for (const edit of edits) { | ||
| const from = edit.old_string ?? ''; | ||
| const to = edit.new_string ?? ''; | ||
| if (from === '') { | ||
| text = to; | ||
| } else if (edit.replace_all) { | ||
| text = text.split(from).join(to); | ||
| } else { | ||
| text = text.replace(from, () => to); | ||
| } | ||
| } | ||
| return text; | ||
| } | ||
| /** Antigravity PreToolUse requires stdout `decision` on every response (allow included). */ | ||
| export function emitAntigravityAllow(output, antigravityStyle) { | ||
| if (!antigravityStyle) return; | ||
| output.stdout(`${JSON.stringify({ decision: 'allow' })}\n`); | ||
| } | ||
| /** Cursor preToolUse accepts explicit allow; exit 0 alone also works. */ | ||
| export function emitCursorAllow(output, cursorStyle) { | ||
| if (!cursorStyle) return; | ||
| output.stdout(`${JSON.stringify({ permission: 'allow' })}\n`); | ||
| } | ||
| export function emitHostAllow(output, { antigravityStyle, cursorStyle }) { | ||
| emitAntigravityAllow(output, antigravityStyle); | ||
| emitCursorAllow(output, cursorStyle); | ||
| } | ||
| /** | ||
| * Socket-style write-gate deny: two lines first. Pass/fail, no score. | ||
| * Rule id stays on a following line, not the first sentence. | ||
| */ | ||
| export function formatWriteGateDeny({ file, reason, ruleId, nextAction, extraLines = [] }) { | ||
| const target = file || 'this write'; | ||
| const why = String(reason || 'this change breaks the architecture layers').replace(/\s+/g, ' ').trim(); | ||
| const next = | ||
| nextAction && /place|move|import|port/i.test(nextAction) | ||
| ? nextAction | ||
| : 'Move the import or run /ark-place. Do not weaken ark.config.json.'; | ||
| const lines = [`blocked ${target} — ${why}`, `Next: ${next}`]; | ||
| if (ruleId) lines.push(`[${ruleId}]`); | ||
| for (const extra of extraLines) { | ||
| if (extra) lines.push(extra); | ||
| } | ||
| return lines.join('\n'); | ||
| } |
| /** | ||
| * Package-manager detection and emitted install/run command shapes. | ||
| */ | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
| /** The three package managers Ark emits commands for. */ | ||
| const LOCKFILES = { pnpm: 'pnpm-lock.yaml', yarn: 'yarn.lock', npm: 'package-lock.json' }; | ||
| function readPackageJson(root) { | ||
| const file = path.join(root, 'package.json'); | ||
| if (!fs.existsSync(file)) return null; | ||
| try { | ||
| return JSON.parse(fs.readFileSync(file, 'utf8')); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * The Corepack `packageManager` field (and the newer `devEngines.packageManager`) is the | ||
| * project's OWN authoritative statement of its package manager. When present it wins over any | ||
| * lockfile guess. Returns 'pnpm' | 'yarn' | 'npm' | undefined. | ||
| */ | ||
| function declaredPackageManager(root) { | ||
| let pkg; | ||
| try { | ||
| pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| const raw = | ||
| (typeof pkg.packageManager === 'string' ? pkg.packageManager.split('@')[0] : undefined) ?? | ||
| (typeof pkg.devEngines?.packageManager?.name === 'string' | ||
| ? pkg.devEngines.packageManager.name | ||
| : undefined); | ||
| const name = raw?.trim().toLowerCase(); | ||
| return name === 'pnpm' || name === 'yarn' || name === 'npm' ? name : undefined; | ||
| } | ||
| /** Lockfiles present in the project root, in { pnpm, yarn, npm } key order. */ | ||
| export function presentLockfiles(root) { | ||
| return Object.entries(LOCKFILES) | ||
| .filter(([, file]) => fs.existsSync(path.join(root, file))) | ||
| .map(([pm]) => pm); | ||
| } | ||
| /** | ||
| * Detect the project's package manager: 'pnpm' | 'yarn' | 'npm'. | ||
| * | ||
| * Priority: (1) the `packageManager` / `devEngines` field (the project's own declaration); | ||
| * (2) a single lockfile; (3) on CONFLICT (more than one lockfile and no declaration) prefer | ||
| * npm whenever a package-lock.json is present. Rationale: `npx` runs fine inside a pnpm/yarn | ||
| * repo, but `pnpm exec` / `yarn` in an npm repo BREAKS (frozen-lockfile / no-TTY / a spurious | ||
| * pnpm-lock). So a stray pnpm-lock.yaml left in an npm project must NOT hijack it into pnpm — | ||
| * package-lock.json wins the tie, and the field is the escape hatch for a genuine pnpm repo | ||
| * that still carries a package-lock.json. Falls back to npm when nothing is detectable. | ||
| */ | ||
| export function detectPackageManager(root) { | ||
| const declared = declaredPackageManager(root); | ||
| if (declared) return declared; | ||
| const locks = presentLockfiles(root); | ||
| if (locks.length <= 1) return locks[0] ?? 'npm'; | ||
| if (locks.includes('npm')) return 'npm'; | ||
| return locks[0]; // pnpm over yarn when only those two collide | ||
| } | ||
| // pnpm 10+ `pnpm exec` runs a deps-status pre-check that fails with ERR_PNPM_IGNORED_BUILDS | ||
| // when the repo has un-approved native build scripts (sharp, esbuild, tailwind oxide, …) — | ||
| // the common state of real pnpm apps. Skip that gate so Ark's emitted commands still run. | ||
| const PNPM_EXEC = 'pnpm --config.verify-deps-before-run=false exec'; | ||
| const RUNNER_BY_PM = { pnpm: PNPM_EXEC, yarn: 'yarn', npm: 'npx' }; | ||
| /** | ||
| * The command prefix that runs an INSTALLED package binary, matched to the project's | ||
| * package manager. `npx` is used for npm and as the safe fallback. | ||
| * | ||
| * This is the single source of truth that makes every command Ark EMITS — the AGENTS.md | ||
| * contract, .mcp.json, the Claude/Codex hooks, the check:architecture script, the | ||
| * SessionStart summary and every console hint — respect a pnpm-only or yarn repo instead | ||
| * of hardcoding `npx`. (A "pnpm only, never npx" repo treats an emitted `npx` as a policy | ||
| * violation.) `packageManager()` in ark-check.mjs builds the CI-workflow variant on the | ||
| * same detection. | ||
| */ | ||
| export function execRunner(root) { | ||
| return RUNNER_BY_PM[detectPackageManager(root)]; | ||
| } | ||
| /** Full runnable command string for an installed Ark binary, package-manager aware. */ | ||
| export function arkCommand(root, bin, argsStr = '') { | ||
| return `${execRunner(root)} ${bin}${argsStr ? ` ${argsStr}` : ''}`; | ||
| } | ||
| /** | ||
| * Split { command, args } form for JSON/TOML configs (.mcp.json, config.toml) that spawn | ||
| * the binary directly. `pnpm exec ark-mcp` becomes command "pnpm" + args ["exec","ark-mcp",…] | ||
| * so the runner is a real argv[0], not a space-joined string a shell would mis-split. | ||
| */ | ||
| export function execCommandParts(root, bin, binArgs = []) { | ||
| const runner = execRunner(root); | ||
| if (runner === PNPM_EXEC || runner.startsWith('pnpm ')) { | ||
| return { | ||
| command: 'pnpm', | ||
| args: ['--config.verify-deps-before-run=false', 'exec', bin, ...binArgs], | ||
| }; | ||
| } | ||
| if (runner === 'yarn') return { command: 'yarn', args: [bin, ...binArgs] }; | ||
| return { command: 'npx', args: [bin, ...binArgs] }; | ||
| } | ||
| /** | ||
| * True when this directory is a pnpm workspace root (needs `pnpm add -w` for root deps). | ||
| * Nested packages under the workspace are not roots. | ||
| */ | ||
| export function isPnpmWorkspaceRoot(root) { | ||
| return fs.existsSync(path.join(root, 'pnpm-workspace.yaml')); | ||
| } | ||
| /** | ||
| * True when package.json declares npm/yarn workspaces (yarn classic needs `-W` at root). | ||
| */ | ||
| export function isNpmYarnWorkspaceRoot(root) { | ||
| const pkg = readPackageJson(root); | ||
| if (!pkg) return false; | ||
| const ws = pkg.workspaces; | ||
| return Array.isArray(ws) || (ws && typeof ws === 'object' && Array.isArray(ws.packages)); | ||
| } | ||
| /** | ||
| * Normalize a version/range/spec into an installable package argument for arkgate. | ||
| * Accepts `latest`, `^3.8.2`, `arkgate@latest`, or a full package name. | ||
| */ | ||
| export function normalizeArkgateInstallSpec(versionSpec) { | ||
| const raw = typeof versionSpec === 'string' && versionSpec.trim() ? versionSpec.trim() : 'latest'; | ||
| if (raw.startsWith('arkgate@') || raw === 'arkgate') return raw === 'arkgate' ? 'arkgate@latest' : raw; | ||
| if (raw.includes('/') || raw.startsWith('file:') || raw.startsWith('link:')) return raw; | ||
| return `arkgate@${raw}`; | ||
| } | ||
| /** | ||
| * Package-manager argv to add a dev dependency (e.g. arkgate@latest). | ||
| * pnpm workspace roots get `-w`; yarn classic workspaces get `-W`. | ||
| * | ||
| * @param {string} root | ||
| * @param {string} [versionSpec] package name or name@version (default arkgate@latest) | ||
| * @returns {[string, string[]]} | ||
| */ | ||
| export function packageInstallArgv(root, versionSpec = 'latest') { | ||
| const pkgSpec = normalizeArkgateInstallSpec(versionSpec); | ||
| const pm = detectPackageManager(root); | ||
| if (pm === 'pnpm') { | ||
| const args = ['add', '-D', pkgSpec]; | ||
| if (isPnpmWorkspaceRoot(root)) args.push('-w'); | ||
| return ['pnpm', args]; | ||
| } | ||
| if (pm === 'yarn') { | ||
| const args = ['add', '-D', pkgSpec]; | ||
| if (isNpmYarnWorkspaceRoot(root)) args.push('-W'); | ||
| return ['yarn', args]; | ||
| } | ||
| return ['npm', ['install', '-D', pkgSpec]]; | ||
| } | ||
| /** Package-manager aware "install a dev dependency" hint (e.g. for a missing typescript). */ | ||
| export function installDevHint(root, pkg) { | ||
| const pm = detectPackageManager(root); | ||
| if (pm === 'pnpm') { | ||
| return isPnpmWorkspaceRoot(root) ? `pnpm add -D ${pkg} -w` : `pnpm add -D ${pkg}`; | ||
| } | ||
| if (pm === 'yarn') { | ||
| return isNpmYarnWorkspaceRoot(root) ? `yarn add -D ${pkg} -W` : `yarn add -D ${pkg}`; | ||
| } | ||
| return `npm install -D ${pkg}`; | ||
| } |
@@ -25,2 +25,4 @@ /** | ||
| export const HOT_PATH_MIN_HITS = 3; | ||
| /** Kill hung git instead of stalling CI. */ | ||
| export const SPAWN_TIMEOUT_MS = 8000; | ||
@@ -44,2 +46,3 @@ /** | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| timeout: SPAWN_TIMEOUT_MS, | ||
| }); | ||
@@ -46,0 +49,0 @@ |
+42
-429
@@ -15,5 +15,6 @@ /** Coverage, plan, and doctor CLI surfaces (roadmap #11). */ | ||
| import { describePackageVersionDualTruth } from './field-install.mjs'; | ||
| import { detectAgentHomeGaps, agentHomeConcernIsActive } from './agent-homes.mjs'; | ||
| import { operatingModeTitle } from './product-copy.mjs'; | ||
| import { detectAgentHomeGaps } from './agent-homes.mjs'; | ||
| import { collectDoctorNextActions } from './doctor-next-actions.mjs'; | ||
| import { printDoctorCompactHuman, printDoctorDetailsHuman } from './doctor-human.mjs'; | ||
| export { printDoctorCompactHuman, printDoctorDetailsHuman }; | ||
| export { summarizeRulesUnderContract }; | ||
@@ -34,3 +35,2 @@ | ||
| staleRunnerGateFiles, | ||
| skillGapsForActiveHost, | ||
| } from './agent-gates.mjs'; | ||
@@ -52,3 +52,2 @@ import { | ||
| buildPostGreenNextAction, | ||
| isDoctorHealthyNothingToDo, | ||
| DESIGN_WEAK_HONESTY_FLAGS, | ||
@@ -66,6 +65,3 @@ } from './post-green-path.mjs'; | ||
| import { summarizePilotLoop } from './pilot-loop.mjs'; | ||
| import { computeDoctorAdvisories, printDoctorAdvisories } from './doctor-advisories.mjs'; | ||
| import { printParseHealthSection } from './parse-health.mjs'; | ||
| import { designDeltaDoctorLines } from './design-delta.mjs'; | ||
| import { enforcementDoctorLines } from './enforcement-state.mjs'; | ||
| import { computeDoctorAdvisories } from './doctor-advisories.mjs'; | ||
| import { ANALYSIS_COMPLETENESS, analysisIncompleteStatement, normalizeAnalysisCompleteness } from './analysis-completeness.mjs'; | ||
@@ -866,48 +862,3 @@ import { buildDoctorImprovementCompass } from './improvement-compass-doctor.mjs'; | ||
| const ok = color.green('✓'); | ||
| const warn = color.yellow('!'); | ||
| const bad = color.red('✗'); | ||
| const line = (mark, text) => console.log(` ${mark} ${text}`); | ||
| console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`)); | ||
| if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness)); | ||
| printParseHealthSection(doctorAdvisories.parseHealth, { color, warn, line }); | ||
| const emptyScope = emptyScopeEarly; | ||
| const mode = operatingMode; | ||
| console.log(''); | ||
| console.log(color.bold('Operating mode')); | ||
| // Modes are detected states, not user-picked settings. Plain-language "what you do next". | ||
| // Never paint green (ok) under design residual — edges clean ≠ design done (product-voice). | ||
| const modeMark = | ||
| mode === 'enforce' && | ||
| !designFitness.designWeak && | ||
| adopted !== 'not-adopted' && | ||
| !stewardUnfinished | ||
| ? ok | ||
| : warn; | ||
| // modeTitle alone names the light — bodies must not re-prefix Suggest/Adapt/Enforce. | ||
| const modeHelp = { | ||
| suggest: 'thin or new tree. Next: ark start --apply, then doctor.', | ||
| adapt: 'config and tree still disagree. Next: do #1.', | ||
| enforce: | ||
| adopted === 'not-adopted' | ||
| ? 'import rules check out; merge boundary not adopted.' | ||
| : 'import rules check out. Keep host + CI.', | ||
| }; | ||
| const modeTitle = operatingModeTitle(mode, designFitness.designWeak, stewardUnfinished); | ||
| line( | ||
| modeMark, | ||
| `${modeTitle} — ${ | ||
| designFitness.designWeak | ||
| ? 'import rules check out; leftover design work remains.' | ||
| : modeHelp[mode] | ||
| }` | ||
| ); | ||
| if (emptyScope) { | ||
| line( | ||
| bad, | ||
| 'Empty scope: include paths match 0 source files — a green check is meaningless until include/layers match the tree (monorepo → apps/packages, or /ark-adopt).' | ||
| ); | ||
| } | ||
| const safetyHasEntries = Boolean( | ||
@@ -951,379 +902,41 @@ options.safety && | ||
| }); | ||
| console.log(''); | ||
| if (ciMergeBoundary?.ci?.state) { | ||
| line( | ||
| ciMergeBoundary.ci.state === 'required' ? ok : warn, | ||
| `CI merge: ${ciMergeBoundary.ci.state}` | ||
| ); | ||
| } | ||
| if (adopted === 'advisory-only-acked') { | ||
| line(warn, 'Adoption: advisory-only ack — not a required GitHub status.'); | ||
| } | ||
| if (isDoctorHealthyNothingToDo(designFitness, uniqueActions, adopted)) { | ||
| console.log(color.green('✔ Healthy — nothing to do.')); | ||
| console.log(color.dim(' Keep write path + CI.')); | ||
| } else { | ||
| console.log(color.bold('Primary next action')); | ||
| console.log(` 1. ${uniqueActions[0]}`); | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Coverage')); | ||
| const govMark = | ||
| emptyScope || cov.governed.percent < 50 | ||
| ? bad | ||
| : cov.governed.percent >= 80 | ||
| ? ok | ||
| : warn; | ||
| line(govMark, `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`); | ||
| const hostRed = | ||
| gatesMissing.length > 0 || | ||
| Boolean(writePath.gap) || | ||
| writePathHonesty?.softWriteHost === true; | ||
| if (hostRed) { | ||
| console.log(''); | ||
| console.log(color.bold('Host / CI')); | ||
| if (writePath.activeHost) line(' ', `Active host: ${writePath.activeHost}`); | ||
| if (gatesMissing.length > 0) line(bad, `Missing gates: ${gatesMissing.join(', ')}`); | ||
| else if (writePath.gap || writePathHonesty?.softWriteHost) { | ||
| line(warn, 'Local writes are advisory; required CI is the merge boundary.'); | ||
| } | ||
| } | ||
| const nudge = doctorAdvisories.stewardNudge; | ||
| if ((nudge?.needsStewards || nudge?.drift || nudge?.emptyStewardsPastGrace) && nudge.ask) { | ||
| console.log(''); | ||
| console.log(color.bold('Stewards')); | ||
| line(warn, nudge.ask); | ||
| } | ||
| if (violations.length === 0) { | ||
| if (!analysisComplete) { | ||
| console.log(''); | ||
| line( | ||
| warn, | ||
| 'No reported violations — contract compliance is not verified until analysis is complete' | ||
| ); | ||
| } else if (emptyScope || cov.governed.percent < 50) { | ||
| console.log(''); | ||
| line( | ||
| warn, | ||
| 'No active violations — coverage is still thin, so green is not yet honest enforcement' | ||
| ); | ||
| } | ||
| } | ||
| if (!options.all) { | ||
| console.log(''); | ||
| console.log(color.dim('More: --doctor --all')); | ||
| return; | ||
| } | ||
| console.log(''); | ||
| console.log(color.dim('---')); | ||
| console.log(color.bold('Details')); | ||
| if (coverageHonesty.greenIsNotEnforcement) { | ||
| line(coverageHonesty.worseThanNoGate ? bad : warn, coverageHonesty.message); | ||
| } | ||
| if (cov.suggestions.length > 0) { | ||
| line(warn, `${cov.suggestions.length} ungoverned director(y/ies) — proposals: ${arkCommand(root, 'ark-check', '--coverage')}`); | ||
| } | ||
| if (cov.emptyLayers.length > 0) line(warn, `Empty layers (pattern matches nothing): ${cov.emptyLayers.join(', ')}`); | ||
| if (cov.layersWithoutRules.length > 0) line(warn, `Layers with no rule edge: ${cov.layersWithoutRules.join(', ')}`); | ||
| if (cov.dualMembership?.count > 0) { | ||
| line( | ||
| warn, | ||
| `Dual-match: ${cov.dualMembership.count} file(s) match multiple layers — ${cov.dualMembership.note ?? 'review overlapping globs'}` | ||
| ); | ||
| } | ||
| if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) line(ok, 'Every layer classifies files; no empty layers'); | ||
| if (packageVersionTruth?.dualTruth) { | ||
| console.log(''); | ||
| console.log(color.bold('Package pin (dual-truth)')); | ||
| line(warn, packageVersionTruth.note); | ||
| } else if (packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT') { | ||
| console.log(''); | ||
| console.log(color.bold('Package pin')); | ||
| line(warn, packageVersionTruth.note); | ||
| } | ||
| if (options.configWalkedUp && options.configRoot) { | ||
| line( | ||
| ok, | ||
| `Config walk-up: using monorepo root ${options.configRoot} (ark.config.json not in cwd package)` | ||
| ); | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Design fitness')); | ||
| if (designSmells.length === 0) { | ||
| line(analysisComplete ? ok : warn, designFitness.label); | ||
| } else { | ||
| line(designFitness.designWeak ? warn : warn, designFitness.label); | ||
| for (const smell of designSmells.slice(0, 5)) { | ||
| const outcome = smell.outcome || smell.message; | ||
| line(' ', `[${smell.id}] ${outcome}`); | ||
| if (smell.outcome && smell.message && smell.message !== smell.outcome) { | ||
| line(' ', color.dim(`detail: ${smell.message}`)); | ||
| } | ||
| if (smell.evidence?.length) { | ||
| line(' ', color.dim(`evidence: ${smell.evidence.slice(0, 4).join(', ')}`)); | ||
| } | ||
| } | ||
| if (pilotLoop?.active && pilotLoop.nextPilot) { | ||
| const np = pilotLoop.nextPilot; | ||
| line( | ||
| warn, | ||
| `Next pilot (one at a time): ${np.pilotTarget || np.pilot} [${np.smellId}] → re-doctor after change` | ||
| ); | ||
| line(' ', color.dim(`success: ${np.successSignal}`)); | ||
| line(' ', color.dim('never multi-pilot batch; pattern bets are never auto-applied')); | ||
| } | ||
| } | ||
| if (options.designDelta) { | ||
| console.log(''); | ||
| console.log(color.bold('Design delta (opt-in)')); | ||
| for (const row of designDeltaDoctorLines(options.designDelta)) | ||
| line(row.level === 'bad' ? bad : row.level === 'ok' ? ok : ' ', row.level === 'dim' ? color.dim(row.text) : row.text); | ||
| } | ||
| if (goldenPattern.present) { | ||
| console.log(''); | ||
| console.log(color.bold('Golden pattern (new code)')); | ||
| line( | ||
| ok, | ||
| `"${goldenPattern.name}" — ${goldenPattern.norm}` + | ||
| (goldenPattern.newCodeHome ? ` Prefer: ${goldenPattern.newCodeHome}.` : '') + | ||
| ' Advisory only — does not clear leftover design work or replace the gate.' | ||
| ); | ||
| } else if (goldenPattern.invalid) { | ||
| console.log(''); | ||
| console.log(color.bold('Golden pattern (new code)')); | ||
| line( | ||
| warn, | ||
| `${goldenPattern.path} is present but invalid (${goldenPattern.error || 'invalid'}). ` + | ||
| 'Fix or remove it — absence is fine; a bad file is not guidance.' | ||
| ); | ||
| } | ||
| if (pureLayerOptIn) { | ||
| line(' ', color.dim(pureLayerOptIn.message)); | ||
| } | ||
| printDoctorAdvisories(doctorAdvisories, { line, warn, color }); | ||
| console.log(''); | ||
| console.log(color.bold('Violations')); | ||
| if (violations.length === 0) { | ||
| if (!analysisComplete) line(warn, 'No reported violations — contract compliance is not verified until analysis is complete'); | ||
| else if (emptyScope || cov.governed.percent < 50) { | ||
| line( | ||
| warn, | ||
| 'No active violations — coverage is still thin, so green is not yet honest enforcement' | ||
| ); | ||
| } else if (designFitness.designWeak) { | ||
| line(warn, `None on checked imports — import rules match the config; leftover design work remains (${modeTitle}). Not healthy finished.`); | ||
| } else { | ||
| line(ok, 'None — the code matches the contract on checked edges'); | ||
| } | ||
| } else { | ||
| const typeNote = summary.typeOnlyCount > 0 ? ` (${summary.valueCount} value · ${summary.typeOnlyCount} type-only)` : ''; | ||
| const supNote = suppressed > 0 ? `, ${suppressed} frozen` : ''; | ||
| line( | ||
| activeCount > 0 ? warn : ok, | ||
| `${violations.length} total${typeNote}${supNote}${activeCount > 0 ? ` — ${activeCount} NOT baselined` : ''}` | ||
| ); | ||
| for (const edge of summary.edges.slice(0, 3)) line(' ', color.dim(`${edge.count} ${edge.edge}`)); | ||
| if (summary.concentrated) { | ||
| line(warn, color.dim(`${Math.round(summary.dominantShare * 100)}% on one edge (${summary.dominant}) — likely a contract fix, not debt`)); | ||
| } | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Write path (agent)')); | ||
| const capabilities = writePath.capabilities; | ||
| const writePathLabels = { | ||
| repair: 'repair-capable — hard block + machine-readable autoPatch / ARK_REPAIR_JSON', | ||
| 'reject-only': 'reject-only — hard block with prose; no repair payload', | ||
| 'mcp-only': 'MCP tools only — prepare-write/autoPatch available; no PreToolUse hook', | ||
| none: 'no write gate hook and no Ark MCP', | ||
| const humanView = { | ||
| root, | ||
| analysisComplete, | ||
| completeness, | ||
| doctorAdvisories, | ||
| operatingMode, | ||
| designFitness, | ||
| adopted, | ||
| stewardUnfinished, | ||
| emptyScope, | ||
| options, | ||
| uniqueActions, | ||
| ciMergeBoundary, | ||
| cov, | ||
| writePath, | ||
| writePathHonesty, | ||
| gatesMissing, | ||
| violations, | ||
| coverageHonesty, | ||
| packageVersionTruth, | ||
| designSmells, | ||
| pilotLoop, | ||
| goldenPattern, | ||
| pureLayerOptIn, | ||
| summary, | ||
| suppressed, | ||
| activeCount, | ||
| skillGaps, | ||
| agentHomeGaps, | ||
| baseline, | ||
| baselineHonesty, | ||
| staleBaseline, | ||
| staleRunners, | ||
| adoption, | ||
| color, | ||
| }; | ||
| const wpMark = | ||
| capabilities['hard-write'] | ||
| ? ok | ||
| : capabilities['advisory-write'] || capabilities['merge-gate'] | ||
| ? warn | ||
| : bad; | ||
| line(' ', `Active host: ${writePath.activeHost}`); | ||
| line(' ', `Supported profile: ${writePath.supportSummary}`); | ||
| line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`); | ||
| if (writePathHonesty.message) line(warn, writePathHonesty.message); | ||
| if (writePath.sessionNote) { | ||
| line(warn, writePath.sessionNote); | ||
| } | ||
| const enforcement = writePath.enforcementState; | ||
| for (const row of enforcementDoctorLines(enforcement)) line(row.level === 'ok' ? ok : row.level === 'bad' ? bad : warn, row.text); | ||
| const supportCaps = writePath.support?.capabilities || {}; | ||
| const repairReinjection = supportCaps['repair-reinjection-guaranteed'] === true; | ||
| const repairEnvelope = supportCaps['repair-envelope-emitted'] === true || supportCaps['repair-payload'] === true; | ||
| line( | ||
| 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' | ||
| ); | ||
| if (writePath.gap) { | ||
| line(writePath.gap.severity === 'warn' ? warn : warn, writePath.gap.message); | ||
| if (writePath.gap.fix) { | ||
| line(' ', color.dim(`Fix: ${writePath.gap.fix}`)); | ||
| } | ||
| } | ||
| printDoctorCompactHuman(humanView); | ||
| if (options.all) printDoctorDetailsHuman(humanView); | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Gates & skills')); | ||
| if (gatesMissing.length === 0) line(ok, 'Shared gate artifacts found on disk (AGENTS.md, .mcp.json, CI); runtime activation is reported separately'); | ||
| else { | ||
| line(bad, `Missing gates: ${gatesMissing.join(', ')}`); | ||
| } | ||
| const humanSkillGaps = skillGapsForActiveHost(skillGaps); | ||
| const legacyCodex = humanSkillGaps.some((g) => g.tool === 'codex' && g.legacyPromptsOnly); | ||
| const codexLegacySafeDelete = humanSkillGaps.some( | ||
| (g) => g.tool === 'codex' && g.legacyAdvisory && g.catalogComplete | ||
| ); | ||
| const remainingGaps = humanSkillGaps.filter( | ||
| (g) => !(g.tool === 'codex' && (g.legacyPromptsOnly || g.legacyAdvisory)) | ||
| ); | ||
| const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0); | ||
| const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0); | ||
| if (remMiss + remStale === 0 && !legacyCodex) line(ok, '/ark-* skills current for detected tools'); | ||
| if (legacyCodex) { | ||
| line(warn, 'Codex: legacy flat .codex/prompts only (not a loadable skill catalog)'); | ||
| } | ||
| if (codexLegacySafeDelete) { | ||
| line( | ||
| ' ', | ||
| color.dim( | ||
| 'Codex catalog complete — leftover .codex/prompts/ark-*.md are safe to delete (not loadable; not required).' | ||
| ) | ||
| ); | ||
| } | ||
| if (remMiss + remStale > 0) { | ||
| line( | ||
| warn, | ||
| `${remMiss} missing / ${remStale} content-behind-package /ark-* skill(s) for ${remainingGaps.map((g) => g.tool).join(', ')}` | ||
| ); | ||
| } | ||
| const codexHomeGap = detectCodexHomeGap(root); | ||
| if (codexHomeGap) { | ||
| const parts = [ | ||
| codexHomeGap.legacyPromptsOnly ? 'legacy-prompts-only' : null, | ||
| codexHomeGap.missing > 0 ? `${codexHomeGap.missing} missing` : null, | ||
| codexHomeGap.stale > 0 ? `${codexHomeGap.stale} content-behind-package` : null, codexHomeGap.catalogStateReason, | ||
| ].filter(Boolean); | ||
| const deferred = !codexConcernIsActive(); | ||
| if (deferred) { | ||
| line(color.dim('·'), color.dim(`Codex home skills ${parts.join(', ')} (deferred — not on Codex session)`)); | ||
| } else { | ||
| line(warn, `Codex home skills ${parts.join(', ')}`); | ||
| } | ||
| } | ||
| for (const gap of agentHomeGaps) { | ||
| const parts = [ | ||
| gap.missing > 0 ? `${gap.missing} missing` : null, | ||
| gap.stale > 0 ? `${gap.stale} content-behind-package` : null, | ||
| gap.catalogStateReason, | ||
| ].filter(Boolean); | ||
| const deferred = !agentHomeConcernIsActive(gap.host); | ||
| const summary = `${gap.label} shared agent skills ${parts.join(', ')}`; | ||
| if (deferred) { | ||
| line(color.dim('·'), color.dim(`${summary} (deferred — not this session)`)); | ||
| } else { | ||
| line(warn, summary); | ||
| } | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Baseline')); | ||
| if (!baseline.exists) { | ||
| line(!analysisComplete || violations.length > 0 ? warn : ok, !analysisComplete ? 'No baseline — current violations were not fully evaluated' : violations.length > 0 ? 'No baseline — adopting a dirty repo? freeze with --update-baseline' : 'No baseline (nothing to freeze)'); | ||
| } else { | ||
| const baseMark = !analysisComplete || baselineHonesty.dirtyBaselineRisk ? warn : ok; | ||
| line(baseMark, `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`); | ||
| if (analysisComplete && baselineHonesty.dirtyBaselineRisk) { | ||
| line(warn, baselineHonesty.message); | ||
| } | ||
| if (analysisComplete && staleBaseline > 0) { | ||
| line(warn, `${staleBaseline} stale entr(y/ies) no longer occur — tighten with --update-baseline`); | ||
| } | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Command runners')); | ||
| if (staleRunners.length === 0) line(ok, 'Emitted commands match the package manager'); | ||
| else { | ||
| line(warn, `Stale runner in ${staleRunners.join(', ')}`); | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Adoption (separate from fitness score)')); | ||
| if (adoption.gaps.length === 0 && !adoption.layerBalance) { | ||
| line( | ||
| ok, | ||
| 'Hosts, MCP argv, core optionality, origin report, baseline policy, and deploy-path lint/types look complete' | ||
| ); | ||
| } else { | ||
| for (const gap of adoption.gaps) { | ||
| const mark = gap.deferred | ||
| ? color.dim('·') | ||
| : gap.severity === 'warn' | ||
| ? warn | ||
| : gap.severity === 'info' | ||
| ? warn | ||
| : bad; | ||
| line(mark, gap.message); | ||
| if (gap.fix) { | ||
| line(' ', color.dim(gap.deferred ? `When using Codex: ${gap.fix}` : `Fix: ${gap.fix}`)); | ||
| } | ||
| } | ||
| if (adoption.layerBalance) { | ||
| line(warn, color.dim(adoption.layerBalance.educational)); | ||
| } | ||
| } | ||
| if (adoption.baseline) { | ||
| line( | ||
| ' ', | ||
| color.dim( | ||
| `Baseline policy: ${adoption.baseline.signal}` + | ||
| (adoption.baseline.primaryPathUsesBaseline | ||
| ? ' · primary path uses --baseline' | ||
| : ' · primary path does not use --baseline') | ||
| ) | ||
| ); | ||
| } | ||
| if (adoption.originReport.present) { | ||
| line(ok, 'Origin architecture snapshot present (.ark/reports/origin.json)'); | ||
| } | ||
| console.log(''); | ||
| console.log(color.bold('Safety / bypass resistance')); | ||
| const safety = options.safety; | ||
| if (!safety) { | ||
| line(warn, 'Safety diagnostics unavailable'); | ||
| } else { | ||
| const rows = [ | ||
| ['Non-literal dynamic dependencies', safety.nonLiteralDynamicImports], | ||
| ['@ts-ignore / @ts-nocheck', safety.tsSuppressions], | ||
| ['Explicit any casts', safety.anyCasts], | ||
| ['InMemory stores in production source', safety.inMemoryProductionStores], | ||
| ['Rules with peerIsolation: false', safety.disabledPeerIsolationRules], | ||
| ]; | ||
| for (const [label, entries] of rows) { | ||
| line(entries.length === 0 ? ok : warn, `${label}: ${entries.length}`); | ||
| } | ||
| } | ||
| } |
@@ -6,2 +6,15 @@ /** Exact local workflow evidence plus GitHub classic-protection/ruleset correlation. */ | ||
| /** Kill hung gh instead of stalling CI. */ | ||
| export const SPAWN_TIMEOUT_MS = 8000; | ||
| function runGh(args, opts = {}) { | ||
| const { run, ...rest } = opts; | ||
| const spawn = typeof run === 'function' ? run : spawnSync; | ||
| return spawn('gh', args, { | ||
| encoding: 'utf8', | ||
| ...rest, | ||
| timeout: SPAWN_TIMEOUT_MS, | ||
| }); | ||
| } | ||
| const IF_LINE = /^[ \t]*(?:-\s+)?(?:"if"|'if'|if):\s*(.*?)\s*(?:#.*)?$/i; | ||
@@ -438,3 +451,3 @@ const CONTINUE_LINE = /^[ \t]*(?:-\s+)?(?:"continue-on-error"|'continue-on-error'|continue-on-error):\s*(.*?)\s*(?:#.*)?$/i; | ||
| const limit = Number.isFinite(Number(opts.limit)) ? Math.max(1, Number(opts.limit)) : 30; | ||
| if (spawnSync('gh', ['--version'], { encoding: 'utf8', env }).status !== 0) { | ||
| if (runGh(['--version'], { env, run: opts.run }).status !== 0) { | ||
| return { runtimeObserved: false, latestCiRun: null, reason: 'gh-cli-unavailable' }; | ||
@@ -448,3 +461,3 @@ } | ||
| if (opts.repo) args.push('--repo', opts.repo); | ||
| const result = spawnSync('gh', args, { cwd, encoding: 'utf8', env }); | ||
| const result = runGh(args, { cwd, env, run: opts.run }); | ||
| if (result.status !== 0) { | ||
@@ -505,3 +518,3 @@ const err = `${result.stderr || ''}${result.stdout || ''}`.slice(0, 400); | ||
| const env = opts.env ?? process.env; | ||
| if (spawnSync('gh', ['--version'], { encoding: 'utf8', env }).status !== 0) { | ||
| if (runGh(['--version'], { env, run: opts.run }).status !== 0) { | ||
| return { available: false, reason: 'gh-cli-unavailable', runtimeObserved: false, latestCiRun: null }; | ||
@@ -513,3 +526,3 @@ } | ||
| const args = ['repo', 'view', ...(repo ? [repo] : []), '--json', 'nameWithOwner,defaultBranchRef']; | ||
| const metadata = parseJson(spawnSync('gh', args, { cwd, encoding: 'utf8', env })); | ||
| const metadata = parseJson(runGh(args, { cwd, env, run: opts.run })); | ||
| if (!metadata?.nameWithOwner || !metadata?.defaultBranchRef?.name) { | ||
@@ -522,9 +535,9 @@ return { available: false, reason: 'gh-repo-unavailable', runtimeObserved: false, latestCiRun: null }; | ||
| const classicResult = spawnSync('gh', [ | ||
| const classicResult = runGh([ | ||
| 'api', `repos/${repo}/branches/${encodeURIComponent(branch)}/protection`, '--jq', | ||
| '{strict: .required_status_checks.strict, contexts: .required_status_checks.contexts, checks: .required_status_checks.checks, enforcesAdmins: .enforce_admins.enabled}', | ||
| ], { cwd, encoding: 'utf8', env }); | ||
| const rulesResult = spawnSync( | ||
| 'gh', ['api', `repos/${repo}/rules/branches/${encodeURIComponent(branch)}`], | ||
| { cwd, encoding: 'utf8', env } | ||
| ], { cwd, env, run: opts.run }); | ||
| const rulesResult = runGh( | ||
| ['api', `repos/${repo}/rules/branches/${encodeURIComponent(branch)}`], | ||
| { cwd, env, run: opts.run } | ||
| ); | ||
@@ -531,0 +544,0 @@ const classic = parseJson(classicResult); |
@@ -20,2 +20,5 @@ import { spawnSync } from 'node:child_process'; | ||
| /** Kill hung git instead of stalling CI. */ | ||
| export const SPAWN_TIMEOUT_MS = 8000; | ||
| function runGit(cwd, args) { | ||
@@ -25,2 +28,3 @@ return spawnSync('git', ['-C', cwd, ...args], { | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| timeout: SPAWN_TIMEOUT_MS, | ||
| }); | ||
@@ -27,0 +31,0 @@ } |
@@ -37,2 +37,5 @@ /** | ||
| /** Kill hung git instead of stalling CI. */ | ||
| export const SPAWN_TIMEOUT_MS = 8000; | ||
| function runGit(cwd, args) { | ||
@@ -42,2 +45,3 @@ return spawnSync('git', ['-C', cwd, ...args], { | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| timeout: SPAWN_TIMEOUT_MS, | ||
| }); | ||
@@ -44,0 +48,0 @@ } |
+30
-2
@@ -8,2 +8,30 @@ # Changelog | ||
| ## 4.6.7 — 2026-08-24 | ||
| **Patch** over **4.6.6**. Production-hardening: CODEOWNERS, eval/pack honesty, CLI extracts, | ||
| spawn timeouts, and HTML list cap. **No required config migration.** Does not close Z09. | ||
| **Status: published** (on npm `latest`; see `docs/releases/4.6.7.md`). | ||
| ### Changed | ||
| - **CODEOWNERS:** `/src/`, `/bin/`, and `/schemas/` owned by `@pedroknigge`. GitHub still | ||
| needs `require_code_owner_reviews` (or an approving-review count) for that file to | ||
| enforce; the in-tree list is the product control. | ||
| - **Eval comparative fixture:** `saas-dashboard/without-ark` is a real Presentation→Domain | ||
| **value** import. Type-only was non-blocking and made the nightly oracle go false-green. | ||
| - **npm pack JSON:** `scripts/npm-pack-report.mjs` strips ANSI and parses JSON lines that | ||
| actually have `filename`. Empty `[]` / `{}` stay empty; missing filename still throws. | ||
| - **CLI extracts:** hook payload, package-manager helpers, and check args/config/watch live | ||
| in `bin/lib/`. Module-budget maxima were not raised. | ||
| - **git/gh timeouts:** `SPAWN_TIMEOUT_MS = 8000` on git/gh `spawnSync`. Timeout is | ||
| fail-closed (`status !== 0`). | ||
| - **HTML violation cap:** beginner and full showcase lists share a cap of 12 plus | ||
| `+N more (T total)`. KPI tiles still use the full array. | ||
| ## 4.6.6 — 2026-08-22 | ||
@@ -14,3 +42,3 @@ | ||
| **Status: current** (shipping as `arkgate@4.6.6`; see `docs/releases/4.6.6.md`). | ||
| **Status: published** (see `docs/releases/4.6.6.md`). | ||
@@ -54,3 +82,3 @@ ### Changed | ||
| **Status: published** (on npm `latest` until 4.6.6 lands; see `docs/releases/4.6.5.md`). | ||
| **Status: published** (see `docs/releases/4.6.5.md`). | ||
@@ -57,0 +85,0 @@ ### Changed |
@@ -215,3 +215,4 @@ # ArkGate package surface policy | ||
| Ship notes for a version live under [releases/](https://github.com/pedroknigge/arkgate/tree/main/docs/releases) | ||
| (current: [4.6.6.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.6.6.md); | ||
| (current published: [4.6.7.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.6.7.md); | ||
| prior published: [4.6.6.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.6.6.md); | ||
| prior published: [4.6.5.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.6.5.md); | ||
@@ -218,0 +219,0 @@ prior published: [4.6.3.md](https://github.com/pedroknigge/arkgate/blob/main/docs/releases/4.6.3.md); |
+2
-2
@@ -61,4 +61,4 @@ # ArkGate documentation | ||
| Current: [releases/4.6.6.md](releases/4.6.6.md) (`arkgate@4.6.6`). | ||
| Prior: [releases/4.6.5.md](releases/4.6.5.md) · [4.6.4](releases/4.6.4.md) · [4.6.3](releases/4.6.3.md) · [4.6.2](releases/4.6.2.md) · [4.6.1](releases/4.6.1.md) · [4.6.0](releases/4.6.0.md). | ||
| Current published: [releases/4.6.7.md](releases/4.6.7.md) (`arkgate@4.6.7` on npm `latest`). | ||
| Prior: [releases/4.6.6.md](releases/4.6.6.md) · [4.6.5](releases/4.6.5.md) · [4.6.4](releases/4.6.4.md) · [4.6.3](releases/4.6.3.md) · [4.6.2](releases/4.6.2.md) · [4.6.1](releases/4.6.1.md) · [4.6.0](releases/4.6.0.md). | ||
| Older notes: [releases/](releases/). Config: [configuration.md](configuration.md). | ||
@@ -65,0 +65,0 @@ |
+1
-1
| { | ||
| "name": "arkgate", | ||
| "version": "4.6.6", | ||
| "version": "4.6.7", | ||
| "description": "One architecture config. One check. One coach.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+5
-4
@@ -19,6 +19,6 @@ <div align="center"> | ||
| > **ArkGate 4.6.6** is current (this train). **4.6.5** remains npm `latest` until publish. | ||
| > **ArkGate 4.6.7** is current on npm `latest`. | ||
| > A tree is **adopted** only with a required GitHub status running `arkgate-check --strict-merge`, | ||
| > or `.ark/adoption-stance.json` `stance: "advisory-only"`. Doctor is compact (`--doctor --all` | ||
| > for Details). [4.6.6 notes](docs/releases/4.6.6.md) · [4.6.5](docs/releases/4.6.5.md) · | ||
| > for Details). [4.6.7 notes](docs/releases/4.6.7.md) · [4.6.6](docs/releases/4.6.6.md) · | ||
| > [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md) | ||
@@ -221,4 +221,5 @@ | ||
| | Security | [SECURITY.md](SECURITY.md) | | ||
| | Current release (4.6.6) | [docs/releases/4.6.6.md](docs/releases/4.6.6.md) · [CHANGELOG](CHANGELOG.md) | | ||
| | Prior published (4.6.5 on npm `latest` until 4.6.6 lands) | [docs/releases/4.6.5.md](docs/releases/4.6.5.md) | | ||
| | Current published (4.6.7 on npm `latest`) | [docs/releases/4.6.7.md](docs/releases/4.6.7.md) · [CHANGELOG](CHANGELOG.md) | | ||
| | Prior published (4.6.6) | [docs/releases/4.6.6.md](docs/releases/4.6.6.md) | | ||
| | Prior published (4.6.5) | [docs/releases/4.6.5.md](docs/releases/4.6.5.md) | | ||
| | Prior published (4.6.3) | [docs/releases/4.6.3.md](docs/releases/4.6.3.md) | | ||
@@ -225,0 +226,0 @@ | Prior (4.6.2) | [docs/releases/4.6.2.md](docs/releases/4.6.2.md) | |
+2
-2
@@ -9,3 +9,3 @@ { | ||
| }, | ||
| "version": "4.6.6", | ||
| "version": "4.6.7", | ||
| "packages": [ | ||
@@ -15,3 +15,3 @@ { | ||
| "identifier": "arkgate", | ||
| "version": "4.6.6", | ||
| "version": "4.6.7", | ||
| "runtimeHint": "npx", | ||
@@ -18,0 +18,0 @@ "transport": { |
@@ -10,3 +10,3 @@ # ArkGate Agent Skills package | ||
| Package version when last generated context: **arkgate@4.6.6** | ||
| Package version when last generated context: **arkgate@4.6.7** | ||
| Schema: agent-skills package contract `1.0` | ||
@@ -13,0 +13,0 @@ |
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
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
3122604
0.24%214
2.88%49469
0.46%259
0.39%138
2.99%20
5.26%