arkgate 4.8.18 → 4.8.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +153 -0
- package/README.md +5 -6
- package/bin/ark-check-runtime.mjs +23 -0
- package/bin/ark-layer-match.mjs +141 -9
- package/bin/ark-mcp-runtime.mjs +3 -3
- package/bin/ark-shared.mjs +187 -16
- package/bin/ark.mjs +23 -19
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/ci-and-commands.mjs +2 -2
- package/bin/lib/config-contract.mjs +1 -0
- package/bin/lib/doctor-advisories.mjs +2 -1
- package/bin/lib/doctor-human.mjs +2 -0
- package/bin/lib/doctor-next-actions.mjs +35 -0
- package/bin/lib/doctor-plan.mjs +8 -8
- package/bin/lib/enforcement-honesty.mjs +30 -2
- package/bin/lib/gate-files.mjs +22 -1
- package/bin/lib/host-support-matrix.mjs +1 -1
- package/bin/lib/html-report-depth.mjs +6 -2
- package/bin/lib/install-migrate.mjs +3 -4
- package/bin/lib/invariant-coverage.mjs +44 -13
- package/bin/lib/managed-upgrade.mjs +30 -5
- package/bin/lib/mcp-adoption.mjs +4 -1
- package/bin/lib/package-manager.mjs +5 -2
- package/bin/lib/physical-cohesion.mjs +68 -51
- package/bin/lib/presets.mjs +14 -14
- package/bin/lib/reshape-decisions.mjs +2 -2
- package/bin/lib/rules-under-contract.mjs +40 -4
- package/bin/lib/scan-files.mjs +3 -26
- package/bin/lib/skill-install.mjs +3 -2
- package/bin/lib/start-install-recovery.mjs +156 -0
- package/bin/lib/start-preview.mjs +17 -16
- package/bin/lib/write-path-detect.mjs +3 -2
- package/dist/{configTypes-VD0qcubY.d.ts → configTypes-VVknqcTJ.d.ts} +2 -0
- package/dist/{diagnosticCatalog-BNxKdcN4.d.ts → diagnosticCatalog-BPYWNyJR.d.ts} +9 -3
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +12 -4
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +18 -18
- package/dist/index.d.ts +8 -4
- package/dist/index.js +20 -20
- package/dist/nestjs/index.cjs +5 -5
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +5 -5
- package/dist/runtime/index.cjs +10 -10
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +10 -10
- package/dist/{types-BSzRy2X1.d.ts → types-Cs-46DRu.d.ts} +1 -1
- package/dist/{types-D5GT5ZT8.d.ts → types-p_fCp6P-.d.ts} +1 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +3 -1
- package/docs/ai-gates.md +7 -3
- package/docs/configuration.md +20 -5
- package/docs/develop.md +1 -2
- package/docs/package-surface.md +2 -2
- package/docs/use.md +1 -2
- package/package.json +3 -3
- package/schemas/ark.config.schema.json +6 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +15 -1
- package/templates/agent-skills/ark-autopilot/SKILL.md +2 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +1 -1
- package/templates/agent-skills/ark-explain/SKILL.md +1 -1
- package/templates/agent-skills/ark-explore/SKILL.md +13 -1
- package/templates/agent-skills/ark-place/SKILL.md +2 -0
- package/templates/agent-skills/ark-upgrade/SKILL.md +5 -3
- package/templates/hooks/opencode-ark-write-gate.mjs +2 -2
- package/templates/skills/ark-adopt.md +15 -1
- package/templates/skills/ark-autopilot.md +2 -0
- package/templates/skills/ark-coverage.md +1 -1
- package/templates/skills/ark-explain.md +1 -1
- package/templates/skills/ark-explore.md +13 -1
- package/templates/skills/ark-place.md +2 -0
- package/templates/skills/ark-upgrade.md +5 -3
|
@@ -201,7 +201,7 @@ const AGENT_CONTRACT = {
|
|
|
201
201
|
],
|
|
202
202
|
// Cursor-only guidance: the write-time validate_code tool is available in
|
|
203
203
|
// Cursor's runtime but has no equivalent in a plain AGENTS.md read.
|
|
204
|
-
cursorValidateStep: `
|
|
204
|
+
cursorValidateStep: `The write gate is the host pre-hook when \`.cursor/hooks.json\` is installed, trusted, and \`failClosed\`. Call \`ark_prepare_write\` / \`validate_code\` only as fallback if that hook is missing or fail-open.`,
|
|
205
205
|
};
|
|
206
206
|
|
|
207
207
|
/**
|
|
@@ -413,7 +413,7 @@ Contener · Guiar · Ordenar — contain the write, guide the next step, order l
|
|
|
413
413
|
1. Status anytime: \`${doctorCmd}\` — what's wrong and what to do first.
|
|
414
414
|
2. Read the **Improvement compass** section (not a score). Name residual lenses in plain language when present (SoC, DIP, domain, …). Out-of-scope lenses (performance, app security tooling, full resilience) stay honest — do not invent Ark enforcement for them.
|
|
415
415
|
3. CLI-first: if the local CLI already resolved this project root, do not wait on MCP “still connecting”. Identity handshake is optional in that case. When you do trust MCP evidence: call \`ark_identity\` with \`project.expectedRoot\` set to this project's exact absolute root, then reuse that root plus the returned \`projectIdentity.projectId\` on every Ark MCP call. A descendant path is authoritative only with that matching id. Missing tool, non-\`matched\` binding, or wrong root means this is not proven to be the right project: restart the host and use the local CLI meanwhile.
|
|
416
|
-
4. Day to day: call \`ark_manifest\` with the same project expectation (or skip if CLI already bound the root); place new files with \`ark_place\` (filePath required — fail-closed);
|
|
416
|
+
4. Day to day: call \`ark_manifest\` with the same project expectation (or skip if CLI already bound the root); place new files with \`ark_place\` (filePath required — fail-closed); run \`${checkCmd}\`. The host pre-hook is the write gate when installed; MCP \`ark_prepare_write\` is fallback if that hook is missing or fail-open. The \`ark://manifest\` resource is compatibility-only and always unverified. On a deny, fix the illegal import — do not weaken \`ark.config.json\`.
|
|
417
417
|
5. If MCP is unavailable: inspect \`ark.config.json\` and run \`${checkCmd}\`. The host-readable projection is AGENTS.md and CLAUDE.md (same schema).
|
|
418
418
|
|
|
419
419
|
**Single door when residual remains:**
|
|
@@ -200,7 +200,8 @@ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, par
|
|
|
200
200
|
physicalCohesion,
|
|
201
201
|
files,
|
|
202
202
|
root,
|
|
203
|
-
decisionMemory
|
|
203
|
+
decisionMemory,
|
|
204
|
+
{ layers: config?.layers, rules: rules ?? config?.rules }
|
|
204
205
|
);
|
|
205
206
|
// Prefer architecture facts paths when available; still union the doctor walk
|
|
206
207
|
// so an empty-catalog residual can see Domain files the facts subset missed.
|
package/bin/lib/doctor-human.mjs
CHANGED
|
@@ -147,6 +147,8 @@ export function printDoctorCompactHuman(view) {
|
|
|
147
147
|
if (skippableCi) line(warn, skippableCi);
|
|
148
148
|
else if (writePathHonesty?.nativeFailClosed === false) {
|
|
149
149
|
line(warn, writePathHonesty.message || 'Write hook is fail-open — if the checker cannot run, the write still lands.');
|
|
150
|
+
} else if (listedMissing.length === 0 && writePath.gap?.id === 'write-path-mcp-only') {
|
|
151
|
+
line(warn, writePath.gap.message || 'Fell back to MCP prepare; required CI is the merge boundary.');
|
|
150
152
|
} else if (listedMissing.length === 0 && (writePath.gap || writePathHonesty?.softWriteHost)) {
|
|
151
153
|
line(warn, 'Local writes are advisory; required CI is the merge boundary.');
|
|
152
154
|
}
|
|
@@ -9,6 +9,36 @@ import { mergePostGreenTopActions } from './post-green-path.mjs';
|
|
|
9
9
|
import { ADOPTED_NOT, NOT_ADOPTED_NEXT_ACTION } from './adoption-stance.mjs';
|
|
10
10
|
import { REQUIRED_GATE_WORKFLOW } from './gate-files.mjs';
|
|
11
11
|
|
|
12
|
+
/** Package pin may exist; local resolve still failed. Lead with install, not CI. */
|
|
13
|
+
export const PACKAGE_UNRESOLVED_NEXT_ACTION =
|
|
14
|
+
'Install arkgate in this project first (npx --package=arkgate, or pnpm add -D arkgate -w / yarn add -D arkgate -W at a workspace root), then re-run --doctor';
|
|
15
|
+
|
|
16
|
+
export function packageUnresolvedNextAction(ctx) {
|
|
17
|
+
if (ctx?.packageInstalled !== false || ctx?.selfHost === true) return null;
|
|
18
|
+
const code = ctx?.packageVersionTruth?.code;
|
|
19
|
+
if (code === 'PACKAGE_PIN_SELF_HOST' || code === 'PACKAGE_PIN_ABSENT') return null;
|
|
20
|
+
// Failed start --apply: pin is in package.json, node_modules did not resolve.
|
|
21
|
+
if (code === 'PACKAGE_PIN_MATCHES' || code === 'PACKAGE_PIN_BEHIND_CLI' || ctx?.packagePinned === true) {
|
|
22
|
+
return PACKAGE_UNRESOLVED_NEXT_ACTION;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Compact doctor JSON #1 — same rank as collectDoctorNextActions when package is missing. */
|
|
28
|
+
export function preferredDoctorPrimaryNextAction({
|
|
29
|
+
adopted,
|
|
30
|
+
packageInstalled,
|
|
31
|
+
selfHost,
|
|
32
|
+
packageVersionTruth,
|
|
33
|
+
postGreenPath,
|
|
34
|
+
dualTruthNext,
|
|
35
|
+
} = {}) {
|
|
36
|
+
return (
|
|
37
|
+
packageUnresolvedNextAction({ packageInstalled, selfHost, packageVersionTruth }) ||
|
|
38
|
+
(adopted === 'not-adopted' ? NOT_ADOPTED_NEXT_ACTION : postGreenPath?.action ?? dualTruthNext ?? null)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
12
42
|
function missingGateFiles(ctx) {
|
|
13
43
|
const list = Array.isArray(ctx.gatesMissing) ? ctx.gatesMissing : [];
|
|
14
44
|
if (ctx.ciNotFailClosed) {
|
|
@@ -119,6 +149,8 @@ export function collectDoctorNextActions(ctx) {
|
|
|
119
149
|
if (notAdopted || ctx.adopted === ADOPTED_NOT || ctx.adopted == null) {
|
|
120
150
|
actions.push(ctx.notAdoptedNextAction || NOT_ADOPTED_NEXT_ACTION);
|
|
121
151
|
}
|
|
152
|
+
const resolvePkg = packageUnresolvedNextAction(ctx);
|
|
153
|
+
if (resolvePkg) actions.push(resolvePkg);
|
|
122
154
|
const nudge = ctx.stewardNudge;
|
|
123
155
|
if (
|
|
124
156
|
nudge &&
|
|
@@ -259,6 +291,9 @@ export function collectDoctorNextActions(ctx) {
|
|
|
259
291
|
if (ctx.designFitness.designWeak && unique.length === 0 && ctx.postGreenPath) {
|
|
260
292
|
unique.push(ctx.postGreenPath.action);
|
|
261
293
|
}
|
|
294
|
+
if (resolvePkg) {
|
|
295
|
+
return [resolvePkg, ...unique.filter((a) => a !== resolvePkg)];
|
|
296
|
+
}
|
|
262
297
|
if (overlapAction) {
|
|
263
298
|
return [overlapAction, ...unique.filter((a) => a !== overlapAction)];
|
|
264
299
|
}
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import * as arkShared from '../ark-shared.mjs';
|
|
|
13
13
|
import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
14
14
|
import { describePackageVersionDualTruth } from './field-install.mjs';
|
|
15
15
|
import { detectAgentHomeGaps } from './agent-homes.mjs';
|
|
16
|
-
import { collectDoctorNextActions } from './doctor-next-actions.mjs';
|
|
16
|
+
import { collectDoctorNextActions, preferredDoctorPrimaryNextAction } from './doctor-next-actions.mjs';
|
|
17
17
|
import { printDoctorCompactHuman, printDoctorDetailsHuman } from './doctor-human.mjs';
|
|
18
18
|
import { collectLayerOwnerResidual, layerGuidanceLine, placementDescriptionFields } from './layer-description.mjs';
|
|
19
19
|
import { collectAdrPresenceResidual, printAdrPresenceHint } from './adr-presence.mjs';
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
import {
|
|
60
60
|
buildCoverageHonesty,
|
|
61
61
|
computeDoctorEnforcementHonesty,
|
|
62
|
+
physicalCohesionResidualRemains,
|
|
62
63
|
} from './enforcement-honesty.mjs';
|
|
63
64
|
import {
|
|
64
65
|
computePureLayerOptInNudge,
|
|
@@ -653,6 +654,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
653
654
|
const statusTransitionCatalog = collectStatusTransitionCatalogResidual({ root, config, files, statesTransitions });
|
|
654
655
|
const noDomainFrontend = collectNoDomainFrontendResidual({ config, coverage: cov, designSmells });
|
|
655
656
|
const { invariantCoverageRoots, invariantTestsPath } = collectInvariantCoverageResiduals({ adopted: isAdopted(adopted) || options.requireGates === true, coverage: config?.coverage, config, root });
|
|
657
|
+
const selfHost = packageVersionTruth?.selfHost === true || packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST';
|
|
656
658
|
const { coverageHonesty, baselineHonesty, writePathHonesty, productHonesty } =
|
|
657
659
|
computeDoctorEnforcementHonesty({
|
|
658
660
|
governedPercent: cov.governed.percent,
|
|
@@ -681,13 +683,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
681
683
|
: rulesUnderContract?.active === true || arkRun?.active === true || doctorAdvisories.arkOrder?.active === true
|
|
682
684
|
? { active: true, extraMergeTeeth: arkRun?.extraMergeTeeth === true || doctorAdvisories.arkOrder?.extraMergeTeeth === true }
|
|
683
685
|
: null,
|
|
684
|
-
primaryNextAction:
|
|
685
|
-
adopted === 'not-adopted' ? NOT_ADOPTED_NEXT_ACTION : postGreenPath?.action ?? dualTruthNext,
|
|
686
|
+
primaryNextAction: preferredDoctorPrimaryNextAction({ adopted, packageInstalled, selfHost, packageVersionTruth, postGreenPath, dualTruthNext }),
|
|
686
687
|
operatingMode,
|
|
687
688
|
packageInstalled,
|
|
688
|
-
selfHost
|
|
689
|
-
packageVersionTruth?.selfHost === true ||
|
|
690
|
-
packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
|
|
689
|
+
selfHost,
|
|
691
690
|
nativeFailClosed: writePath.nativeFailClosed,
|
|
692
691
|
nativeFailClosedPolicy: writePath.nativeFailClosedPolicy,
|
|
693
692
|
adopted,
|
|
@@ -695,6 +694,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
695
694
|
github: githubForBoundary,
|
|
696
695
|
adoptionStance: stanceFile,
|
|
697
696
|
stewardNudge: doctorAdvisories.stewardNudge,
|
|
697
|
+
physicalCohesionResidual: physicalCohesionResidualRemains(doctorAdvisories.physicalCohesion),
|
|
698
698
|
});
|
|
699
699
|
|
|
700
700
|
// Improvement compass: projection only — never feeds ok/valid/goal.met.
|
|
@@ -752,7 +752,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
752
752
|
adoptionStance: adopted,
|
|
753
753
|
...(adopted === 'not-adopted'
|
|
754
754
|
? {
|
|
755
|
-
primaryNextAction: NOT_ADOPTED_NEXT_ACTION,
|
|
755
|
+
primaryNextAction: productHonesty.primaryNextAction || NOT_ADOPTED_NEXT_ACTION,
|
|
756
756
|
...(postGreenPath ? DESIGN_WEAK_HONESTY_FLAGS : {}),
|
|
757
757
|
}
|
|
758
758
|
: postGreenPath
|
|
@@ -908,7 +908,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
908
908
|
coverageHonesty,
|
|
909
909
|
cov,
|
|
910
910
|
packageVersionTruth,
|
|
911
|
-
dualTruthNext,
|
|
911
|
+
dualTruthNext, packageInstalled, selfHost,
|
|
912
912
|
activeCount,
|
|
913
913
|
writePath,
|
|
914
914
|
gatesMissing,
|
|
@@ -288,14 +288,26 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
|
|
|
288
288
|
* primaryNextAction?: string | null,
|
|
289
289
|
* operatingMode?: string | null,
|
|
290
290
|
* activeBlockingViolations?: number | null,
|
|
291
|
+
* physicalCohesionResidual?: boolean,
|
|
291
292
|
* }} input
|
|
292
293
|
*/
|
|
294
|
+
export function physicalCohesionResidualRemains(cohesion) {
|
|
295
|
+
const findings = Array.isArray(cohesion?.findings) ? cohesion.findings : [];
|
|
296
|
+
if (findings.length === 0) return false;
|
|
297
|
+
const covered = new Set();
|
|
298
|
+
for (const row of cohesion?.reshapeDecisions?.current ?? []) {
|
|
299
|
+
if (row?.suppressesPilot === true && typeof row.concept === 'string') covered.add(row.concept);
|
|
300
|
+
}
|
|
301
|
+
return findings.some((finding) => !covered.has(finding?.concept));
|
|
302
|
+
}
|
|
303
|
+
|
|
293
304
|
export function buildProductHonesty(input = {}) {
|
|
294
305
|
const reasons = [];
|
|
295
306
|
const cov = input.coverageHonesty;
|
|
296
307
|
const base = input.baselineHonesty;
|
|
297
308
|
const write = input.writePathHonesty;
|
|
298
309
|
const designWeak = input.designWeak === true;
|
|
310
|
+
const cohesionResidual = input.physicalCohesionResidual === true;
|
|
299
311
|
const dualTruth = input.packageVersionTruth?.dualTruth === true;
|
|
300
312
|
const pinCode = input.packageVersionTruth?.code || write?.packagePinCode || null;
|
|
301
313
|
const pinAbsent =
|
|
@@ -365,6 +377,14 @@ export function buildProductHonesty(input = {}) {
|
|
|
365
377
|
});
|
|
366
378
|
}
|
|
367
379
|
|
|
380
|
+
if (cohesionResidual) {
|
|
381
|
+
reasons.push({
|
|
382
|
+
id: 'physical-cohesion-residual',
|
|
383
|
+
message:
|
|
384
|
+
'Physical cohesion residual remains — the gate verdict and exit code are unchanged, and the reshape card is one judgment, not a queue to empty.',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
368
388
|
if (dualTruth) {
|
|
369
389
|
reasons.push({
|
|
370
390
|
id: 'package-version-dual-truth',
|
|
@@ -551,6 +571,12 @@ export function buildProductHonesty(input = {}) {
|
|
|
551
571
|
headline = 'Advisory-only adoption — merge status is not required';
|
|
552
572
|
} else if (!unfinished) {
|
|
553
573
|
headline = 'Honesty clear on residual signals';
|
|
574
|
+
} else if (
|
|
575
|
+
wholeTreeGoverned &&
|
|
576
|
+
architectureReasons.length === 1 &&
|
|
577
|
+
architectureReasons[0].id === 'physical-cohesion-residual'
|
|
578
|
+
) {
|
|
579
|
+
headline = 'Residual remaining';
|
|
554
580
|
} else if (coverageIncomplete) {
|
|
555
581
|
headline = 'Not finished / not whole-tree guarantee';
|
|
556
582
|
} else {
|
|
@@ -592,8 +618,8 @@ export function buildProductHonesty(input = {}) {
|
|
|
592
618
|
: 'unknown';
|
|
593
619
|
|
|
594
620
|
return {
|
|
595
|
-
finished: !unfinished && wholeTreeGoverned && !designWeak && activeBlocking === 0,
|
|
596
|
-
elegant: !designWeak && !base?.dirtyBaselineRisk && !designSmellsOpenEdges && activeBlocking === 0,
|
|
621
|
+
finished: !unfinished && wholeTreeGoverned && !designWeak && !cohesionResidual && activeBlocking === 0,
|
|
622
|
+
elegant: !designWeak && !cohesionResidual && !base?.dirtyBaselineRisk && !designSmellsOpenEdges && activeBlocking === 0,
|
|
597
623
|
wholeTreeGuarantee:
|
|
598
624
|
wholeTreeGoverned &&
|
|
599
625
|
!designWeak &&
|
|
@@ -656,6 +682,7 @@ export function computeDoctorEnforcementHonesty({
|
|
|
656
682
|
stewardNudge,
|
|
657
683
|
nativeFailClosed,
|
|
658
684
|
nativeFailClosedPolicy,
|
|
685
|
+
physicalCohesionResidual,
|
|
659
686
|
} = {}) {
|
|
660
687
|
const coverageHonesty = buildCoverageHonesty({
|
|
661
688
|
percent: governedPercent,
|
|
@@ -713,6 +740,7 @@ export function computeDoctorEnforcementHonesty({
|
|
|
713
740
|
considerMergeBoundary: true,
|
|
714
741
|
emptyStewards,
|
|
715
742
|
stewardNudge,
|
|
743
|
+
physicalCohesionResidual,
|
|
716
744
|
});
|
|
717
745
|
return {
|
|
718
746
|
coverageHonesty,
|
package/bin/lib/gate-files.mjs
CHANGED
|
@@ -152,9 +152,30 @@ export function isCompactRouterAgentsContent(text) {
|
|
|
152
152
|
return typeof text === 'string' && COMPACT_ROUTER.test(text);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
function hasCursorFailClosedHook(root) {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = readJson(path.join(root, '.cursor', 'hooks.json'));
|
|
158
|
+
const entries = Array.isArray(parsed?.hooks?.preToolUse) ? parsed.hooks.preToolUse : [];
|
|
159
|
+
return entries.some(
|
|
160
|
+
(entry) =>
|
|
161
|
+
entry &&
|
|
162
|
+
typeof entry === 'object' &&
|
|
163
|
+
entry.failClosed === true &&
|
|
164
|
+
typeof entry.command === 'string' &&
|
|
165
|
+
/(?:^|\s)--hook(?:\s|$)/.test(entry.command)
|
|
166
|
+
);
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function hasCursorCompactRegistration(root) {
|
|
173
|
+
return hasArkMcpRegistration(root, '.cursor/mcp.json') && hasCursorFailClosedHook(root);
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
function hasCompactHostRegistration(root, host) {
|
|
156
177
|
if (host === 'none') return hasArkMcpRegistration(root);
|
|
157
|
-
if (host === 'cursor') return
|
|
178
|
+
if (host === 'cursor') return hasCursorCompactRegistration(root);
|
|
158
179
|
if (host === 'codex') return hasCodexCompactRegistration(root);
|
|
159
180
|
const files = COMPACT_HOST_FILES[host];
|
|
160
181
|
return Boolean(files) && files.every((relativePath) => fs.existsSync(path.join(root, relativePath)));
|
|
@@ -197,7 +197,7 @@ Codex hard write covers only a complete local \`apply_patch\`; Cursor covers onl
|
|
|
197
197
|
installed + trusted, while shell/direct
|
|
198
198
|
filesystem writes, hosted or specialized opt-out paths, and human edits still rely on CI.
|
|
199
199
|
|
|
200
|
-
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.`;
|
|
200
|
+
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. **Write-gate order:** install and trust the host pre-hook first. MCP prepare is fallback when that path is missing or fail-open. 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.`;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
/**
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
buildBaselineHonesty,
|
|
18
18
|
buildWritePathHonesty,
|
|
19
19
|
buildProductHonesty,
|
|
20
|
+
physicalCohesionResidualRemains,
|
|
20
21
|
} from './enforcement-honesty.mjs';
|
|
21
22
|
import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
22
23
|
import { attachExtraDoctorSections } from './doctor-advisories.mjs';
|
|
@@ -25,6 +26,7 @@ import { describePackageVersionDualTruth } from './field-install.mjs';
|
|
|
25
26
|
import { buildDoctorImprovementCompass } from './improvement-compass-doctor.mjs';
|
|
26
27
|
import { buildDeepModuleCoachAdvisory } from './deep-module-coach.mjs';
|
|
27
28
|
import { computePhysicalCohesion } from './physical-cohesion.mjs';
|
|
29
|
+
import { computeReshapeDecisionMemory } from './reshape-decisions.mjs';
|
|
28
30
|
import { POST_GREEN_LEDE, operatingModeTitle } from './product-copy.mjs';
|
|
29
31
|
|
|
30
32
|
function esc(value) {
|
|
@@ -153,6 +155,8 @@ export function buildReportDepthPayload(
|
|
|
153
155
|
: packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT'
|
|
154
156
|
? 'Add arkgate to package.json and install so CI/npx resolve this CLI (PACKAGE_PIN_ABSENT)'
|
|
155
157
|
: null;
|
|
158
|
+
const physicalCohesion = computePhysicalCohesion(root, files);
|
|
159
|
+
physicalCohesion.reshapeDecisions = computeReshapeDecisionMemory(root, files).summary;
|
|
156
160
|
const productHonesty = buildProductHonesty({
|
|
157
161
|
coverageHonesty,
|
|
158
162
|
baselineHonesty,
|
|
@@ -169,9 +173,9 @@ export function buildReportDepthPayload(
|
|
|
169
173
|
: null,
|
|
170
174
|
primaryNextAction: postGreenPath?.action ?? dualTruthNext,
|
|
171
175
|
activeBlockingViolations: activeBlockingCount,
|
|
176
|
+
physicalCohesionResidual: physicalCohesionResidualRemains(physicalCohesion),
|
|
172
177
|
});
|
|
173
178
|
// Doctor parity: same physical-cohesion + baseline stale facts as runDoctor.
|
|
174
|
-
const physicalCohesion = computePhysicalCohesion(root, files);
|
|
175
179
|
const baselineStale =
|
|
176
180
|
typeof baselineSplit.baselineStale === 'number' ? baselineSplit.baselineStale : null;
|
|
177
181
|
// Improvement compass — same projection as doctor; notAScore; never a gate input.
|
|
@@ -224,7 +228,7 @@ export function writePathModeHint(mode) {
|
|
|
224
228
|
case 'reject-only':
|
|
225
229
|
return 'Hard write boundary without repair payload; edits can be blocked without guided re-entry.';
|
|
226
230
|
case 'mcp-only':
|
|
227
|
-
return '
|
|
231
|
+
return 'Fell back to MCP prepare — advisory prepare-write/autoPatch; no hard PreToolUse for this host.';
|
|
228
232
|
case 'none':
|
|
229
233
|
return 'No hard write boundary or advisory MCP for the active host (or host is unknown in this process).';
|
|
230
234
|
default:
|
|
@@ -200,10 +200,9 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
|
|
|
200
200
|
})
|
|
201
201
|
);
|
|
202
202
|
if (selectedTools.has('cursor')) {
|
|
203
|
-
|
|
204
|
-
// whole-file for managed upgrade/manifest (json-merge is install-time only via
|
|
205
|
-
// mergeCursorArkHook below — same pattern as Antigravity hooks).
|
|
203
|
+
// Write-gate order: pre-hook first, MCP prepare as fallback + identity.
|
|
206
204
|
add('.cursor/hooks.json', cursorHooks(root));
|
|
205
|
+
add('.cursor/mcp.json', mcpJson(root));
|
|
207
206
|
if (!compact) add('.cursor/rules/ark.mdc', cursorRule(root));
|
|
208
207
|
}
|
|
209
208
|
if (selectedTools.has('claude')) add('.claude/settings.json', claudeSettings(root));
|
|
@@ -213,8 +212,8 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
|
|
|
213
212
|
if (!compact) add('docs/ark-codex-config.toml', codexTomlSnippet(root));
|
|
214
213
|
}
|
|
215
214
|
if (selectedTools.has('grok')) {
|
|
216
|
-
add('.grok/config.toml', grokProjectConfig(root));
|
|
217
215
|
add('.grok/hooks/ark-write-gate.json', grokHooks(root));
|
|
216
|
+
add('.grok/config.toml', grokProjectConfig(root));
|
|
218
217
|
}
|
|
219
218
|
if (selectedTools.has('antigravity')) {
|
|
220
219
|
add('.agents/hooks.json', antigravityHooks(root));
|
|
@@ -19,7 +19,7 @@ export const INVARIANT_COVERAGE_ROOTS_MESSAGE = 'A domain invariant is enforced,
|
|
|
19
19
|
* `omitBudget` drops the budget clause and the load totals for messages whose
|
|
20
20
|
* own text already carries them — the same number twice reads as two facts.
|
|
21
21
|
*/
|
|
22
|
-
function formatCoverageDiscards(stats, omitBudget = false) {
|
|
22
|
+
export function formatCoverageDiscards(stats, omitBudget = false) {
|
|
23
23
|
if (!stats)
|
|
24
24
|
return '';
|
|
25
25
|
const d = stats.discarded;
|
|
@@ -62,22 +62,50 @@ function titleMatchesInvariant(content, id) {
|
|
|
62
62
|
const re = new RegExp(`(?:describe|it|test|context)\\s*\\(\\s*['"\`][^'"\`]*${escaped}[^'"\`]*['"\`]`, 'i');
|
|
63
63
|
return re.test(content) || content.includes(id);
|
|
64
64
|
}
|
|
65
|
-
function
|
|
65
|
+
function escapeRegExp(value) {
|
|
66
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* True when `name` is declared here. Imports and calls do not count.
|
|
70
|
+
* `export const name =` is the same binding shape sensors already treat as a
|
|
71
|
+
* declaration (arrow or function). The last segment of `Aggregate.method` is
|
|
72
|
+
* the name that must be declared.
|
|
73
|
+
*/
|
|
74
|
+
function declaresIdentifier(content, name) {
|
|
75
|
+
const n = escapeRegExp(name);
|
|
76
|
+
const functionDecl = new RegExp(`(?:^|[\\s;{}])(?:export\\s+(?:default\\s+)?)?(?:declare\\s+)?(?:async\\s+)?function\\s*\\*?\\s*${n}\\s*[(<]`);
|
|
77
|
+
const classDecl = new RegExp(`(?:^|[\\s;{}])(?:export\\s+(?:default\\s+)?)?(?:abstract\\s+)?class\\s+${n}\\b`);
|
|
78
|
+
const constDecl = new RegExp(`(?:^|[\\s;{}])export\\s+(?:const|let|var)\\s+${n}\\s*=`);
|
|
79
|
+
const methodDecl = new RegExp(`(?:^|[\\n;{}])\\s*(?:(?:public|private|protected|static|async|readonly|override|abstract|get|set|declare)\\s+)*${n}\\s*(?:<[^>\\n]*>)?\\s*\\([^;{}]*\\)\\s*(?::\\s*[^;{]+)?\\s*\\{`);
|
|
80
|
+
return (functionDecl.test(content) ||
|
|
81
|
+
classDecl.test(content) ||
|
|
82
|
+
constDecl.test(content) ||
|
|
83
|
+
methodDecl.test(content));
|
|
84
|
+
}
|
|
85
|
+
/** Witness path, or undefined when no non-test file declares `symbol`. */
|
|
86
|
+
function symbolPresent(fileContents, testFiles, symbol) {
|
|
66
87
|
if (!symbol)
|
|
67
|
-
return
|
|
68
|
-
// Support Aggregate.method or bare method name.
|
|
88
|
+
return undefined;
|
|
69
89
|
const parts = symbol.split('.');
|
|
70
|
-
const needle = parts[parts.length - 1];
|
|
90
|
+
const needle = parts[parts.length - 1] ?? '';
|
|
91
|
+
if (!needle)
|
|
92
|
+
return undefined;
|
|
71
93
|
const className = parts.length > 1 ? parts[0] : null;
|
|
72
|
-
|
|
94
|
+
const tests = new Set(testFiles.map((file) => file.replace(/\\/g, '/').replace(/^\.\//, '')));
|
|
95
|
+
const files = Object.keys(fileContents).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
const normalized = file.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
98
|
+
if (tests.has(normalized))
|
|
99
|
+
continue;
|
|
100
|
+
const content = fileContents[file];
|
|
101
|
+
if (!content)
|
|
102
|
+
continue;
|
|
73
103
|
if (className && !content.includes(className))
|
|
74
104
|
continue;
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
105
|
+
if (declaresIdentifier(content, needle))
|
|
106
|
+
return file;
|
|
79
107
|
}
|
|
80
|
-
return
|
|
108
|
+
return undefined;
|
|
81
109
|
}
|
|
82
110
|
export function evaluateInvariantCoverage(input) {
|
|
83
111
|
const invariants = input.arkRules.invariants ?? [];
|
|
@@ -131,9 +159,11 @@ export function evaluateInvariantCoverage(input) {
|
|
|
131
159
|
if (testEvidenceFile !== undefined)
|
|
132
160
|
evidence.push('test-title');
|
|
133
161
|
}
|
|
134
|
-
|
|
162
|
+
const symbolEvidenceFile = symbol
|
|
163
|
+
? symbolPresent(input.fileContents, testFiles, symbol)
|
|
164
|
+
: undefined;
|
|
165
|
+
if (symbolEvidenceFile)
|
|
135
166
|
evidence.push('symbol');
|
|
136
|
-
}
|
|
137
167
|
// Covered if any requested evidence is present.
|
|
138
168
|
// When coverage declares neither test nor symbol, require at least description-only advisory presence = not covered.
|
|
139
169
|
const requiresEvidence = inv.coverage?.test === true || Boolean(symbol) || inv.coverage === undefined;
|
|
@@ -154,6 +184,7 @@ export function evaluateInvariantCoverage(input) {
|
|
|
154
184
|
partial,
|
|
155
185
|
description: inv.description,
|
|
156
186
|
...(testEvidenceFile !== undefined ? { testEvidenceFile } : {}),
|
|
187
|
+
...(symbolEvidenceFile !== undefined ? { symbolEvidenceFile } : {}),
|
|
157
188
|
...(outsideDeclaredRoots !== undefined ? { outsideDeclaredRoots } : {}),
|
|
158
189
|
coverageRootsDeclared: rootsDeclared,
|
|
159
190
|
});
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
arkPackageVersion,
|
|
18
18
|
detectActiveAgentHost,
|
|
19
19
|
normalizeToolsList,
|
|
20
|
+
planSkillInstall,
|
|
20
21
|
skillContentIdentity,
|
|
21
22
|
} from './skill-install.mjs';
|
|
22
23
|
|
|
@@ -396,8 +397,11 @@ function summaryFor(assets, manifestChanged) {
|
|
|
396
397
|
for (const asset of assets) states[asset.state] = (states[asset.state] ?? 0) + 1;
|
|
397
398
|
const applying = assets.filter((asset) => asset.willApply);
|
|
398
399
|
const wouldWrite = applying.length;
|
|
399
|
-
//
|
|
400
|
-
|
|
400
|
+
// Same-body stamp drift (arkVersion / description prefix) stays state=current
|
|
401
|
+
// and is scheduled via planSkillInstall stamp-refresh — not a second pipeline.
|
|
402
|
+
const metadataRefresh = applying.filter(
|
|
403
|
+
(asset) => asset.state === 'current' && asset.reason === 'stamp-refresh'
|
|
404
|
+
).length;
|
|
401
405
|
const customizedPreserved = assets.filter((asset) => asset.state === 'customized').length;
|
|
402
406
|
const fileChanges = applying.length;
|
|
403
407
|
return {
|
|
@@ -477,9 +481,23 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
477
481
|
refreshSkills &&
|
|
478
482
|
catalogAsset.kind === 'skill' &&
|
|
479
483
|
classified.state === 'customized';
|
|
484
|
+
// HS01 / #284: identity-current skill whose arkVersion / description prefix
|
|
485
|
+
// lagged the package — reuse planSkillInstall stamp-refresh, do not reclassify.
|
|
486
|
+
const stampRefresh =
|
|
487
|
+
!unparsedScope &&
|
|
488
|
+
catalogAsset.kind === 'skill' &&
|
|
489
|
+
classified.state === 'current' &&
|
|
490
|
+
currentScoped != null &&
|
|
491
|
+
planSkillInstall({
|
|
492
|
+
existingContent: currentScoped,
|
|
493
|
+
targetContent: desiredScoped,
|
|
494
|
+
packageVersion: catalog.version ?? arkPackageVersion(),
|
|
495
|
+
scope: 'repo',
|
|
496
|
+
}).reason === 'stamp-refresh';
|
|
480
497
|
const canApply =
|
|
481
498
|
classified.state === 'stale' ||
|
|
482
499
|
skillRefresh ||
|
|
500
|
+
stampRefresh ||
|
|
483
501
|
(classified.state === 'missing' && (!recorded || accepted)) ||
|
|
484
502
|
(classified.state === 'conflicted' && accepted);
|
|
485
503
|
const blocked = classified.requiresConsent && !accepted && !skillRefresh;
|
|
@@ -490,7 +508,11 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
490
508
|
kind: catalogAsset.kind,
|
|
491
509
|
scope: catalogAsset.scope,
|
|
492
510
|
...classified,
|
|
493
|
-
...(unparsedScope
|
|
511
|
+
...(unparsedScope
|
|
512
|
+
? { reason: 'unparsed managed TOML scope preserved' }
|
|
513
|
+
: stampRefresh
|
|
514
|
+
? { reason: 'stamp-refresh' }
|
|
515
|
+
: {}),
|
|
494
516
|
action: canApply ? (currentScoped == null ? 'create' : 'update') : 'none',
|
|
495
517
|
willApply: canApply,
|
|
496
518
|
blocked,
|
|
@@ -970,8 +992,11 @@ export function renderManagedUpgrade(plan, options = {}) {
|
|
|
970
992
|
const customizedPreserved = summary.customizedPreserved ?? summary.states?.customized ?? 0;
|
|
971
993
|
const blocked = summary.blocked ?? 0;
|
|
972
994
|
console.log(
|
|
973
|
-
`Managed assets: ${managedAssets}; would write: ${wouldWrite}
|
|
974
|
-
|
|
995
|
+
`Managed assets: ${managedAssets}; would write: ${wouldWrite}` +
|
|
996
|
+
(summary.metadataRefresh
|
|
997
|
+
? `; metadata refresh: ${summary.metadataRefresh}`
|
|
998
|
+
: '') +
|
|
999
|
+
`; customized preserved: ${customizedPreserved}; blocked conflicts/deletions: ${blocked}.`
|
|
975
1000
|
);
|
|
976
1001
|
const honesty =
|
|
977
1002
|
plan.selfService ??
|
package/bin/lib/mcp-adoption.mjs
CHANGED
|
@@ -191,7 +191,10 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
191
191
|
host: 'cursor',
|
|
192
192
|
dir: '.cursor',
|
|
193
193
|
skill: (n) => path.join(root, '.cursor', 'commands', `${n}.md`),
|
|
194
|
-
extras: [
|
|
194
|
+
extras: [
|
|
195
|
+
['.cursor/hooks.json', 'write-gate hook'],
|
|
196
|
+
['.cursor/mcp.json', 'MCP config (fallback)'],
|
|
197
|
+
],
|
|
195
198
|
toolsFlag: 'cursor',
|
|
196
199
|
},
|
|
197
200
|
{
|
|
@@ -94,9 +94,12 @@ export function arkCommand(root, bin, argsStr = '') {
|
|
|
94
94
|
/**
|
|
95
95
|
* Run an Ark binary from package `arkgate` when local bins may be missing.
|
|
96
96
|
* Bare `npx arkgate-check` 404s — npx treats that bin name as its own package.
|
|
97
|
+
* Pass `packageSpec` (e.g. `arkgate@4.8.19`) when Age blocked a local add.
|
|
97
98
|
*/
|
|
98
|
-
export function arkPackageRecoveryCommand(bin, argsStr = '') {
|
|
99
|
-
|
|
99
|
+
export function arkPackageRecoveryCommand(bin, argsStr = '', packageSpec) {
|
|
100
|
+
const spec =
|
|
101
|
+
typeof packageSpec === 'string' && packageSpec.trim() ? packageSpec.trim() : 'arkgate';
|
|
102
|
+
return `npx --package=${spec} ${bin}${argsStr ? ` ${argsStr}` : ''}`;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
/**
|