arkgate 2.9.1 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +14 -2
- package/bin/ark-mcp.mjs +282 -102
- package/bin/lib/agent-gates.mjs +181 -6
- package/bin/lib/architecture-scan.mjs +19 -0
- package/bin/lib/auto-patch.mjs +264 -0
- package/bin/lib/doctor-plan.mjs +54 -0
- package/bin/lib/port-proof.mjs +309 -0
- package/bin/lib/prepare-write.mjs +130 -0
- package/bin/lib/remediation.mjs +21 -0
- package/dist/index.cjs +13 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +13 -4
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.js +13 -4
- package/dist/runtime/index.js.map +1 -1
- package/docs/agent-guide.md +14 -0
- package/docs/ai-gates.md +43 -3
- package/docs/enthusiast/how-to-agent-gates.md +8 -0
- package/docs/enthusiast/reference-commands.md +1 -1
- package/package.json +2 -1
- package/server.json +2 -2
- package/templates/skills/ark-adopt.md +57 -10
- package/templates/skills/ark-architect.md +33 -2
- package/templates/skills/ark-autopilot.md +75 -20
- package/templates/skills/ark-contract.md +36 -0
- package/templates/skills/ark-coverage.md +81 -27
- package/templates/skills/ark-explain.md +35 -2
- package/templates/skills/ark-explore.md +119 -0
- package/templates/skills/ark-fix.md +47 -2
- package/templates/skills/ark-loop.md +50 -3
- package/templates/skills/ark-place.md +36 -0
- package/templates/skills/ark-runtime.md +36 -0
- package/templates/skills/ark-think.md +63 -12
- package/templates/skills/ark-upgrade.md +33 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,78 @@ All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are do
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 2.10.0 — 2026-07-10
|
|
8
|
+
|
|
9
|
+
Track W — **Constrained write → verified repair**: write-boundary autoPatch, prepare_write,
|
|
10
|
+
loop-cost measurement, opt-in hook repair payloads, doctor write-path awareness, and a
|
|
11
|
+
proof-gated port-inject transform (judgment for auto-apply).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **W1 write-boundary autoPatch:** `validate_code` and PreToolUse `--hook` may return
|
|
16
|
+
additive `autoPatch: { source, remediationKind, confidence, valid }` for mechanical-safe
|
|
17
|
+
**import type** rewrites (`import-type-from-pure-type-module`, `import-type-of-type-exports`).
|
|
18
|
+
Post-patch revalidation must be green or the patch is discarded (never silent write).
|
|
19
|
+
Implementation: `bin/lib/auto-patch.mjs`.
|
|
20
|
+
- **W2 `ark_prepare_write` MCP tool:** place + constrain + validate + optional autoPatch +
|
|
21
|
+
judgmentBrief + contentHash in one call (`bin/lib/prepare-write.mjs`). Composes
|
|
22
|
+
`ark_place` + write gate — not a second contract.
|
|
23
|
+
- **W3 loop-cost eval harness:** `eval/loop-cost-run.mjs` / `npm run eval:loop-cost`
|
|
24
|
+
records turns-to-green, optional tokens, CHEATED (fixture-measured). Baseline
|
|
25
|
+
`eval/loop-cost-baseline.json` (medianTurnsTypeOnly=1, cheatedRate=0).
|
|
26
|
+
- **W4 opt-in hook repair payload:** `--hook-repair` / `ARK_HOOK_REPAIR=1` on deny emits
|
|
27
|
+
`ARK_REPAIR_JSON` + `ARK_AUTOPATCH_JSON` (stderr) and optional Grok `autoPatch` (stdout).
|
|
28
|
+
Default `--hook` remains hard-block prose only. Install templates (Claude/Grok) include
|
|
29
|
+
`--hook-repair`. Never silent write.
|
|
30
|
+
- **W5 doctor write-path awareness:** `ark-check --doctor` (JSON + human) surfaces
|
|
31
|
+
`writePath.mode` (`repair` | `reject-only` | `mcp-only` | `none`) and
|
|
32
|
+
`prepareWrite` / `autoPatch` flags from installed hooks/MCP. Reject-only gap is
|
|
33
|
+
additive (info) with install fix.
|
|
34
|
+
- **W6 port-proof inject binding (eval-gated):** prove+transform for
|
|
35
|
+
`port-proof-inject-binding` — single named value import used only as
|
|
36
|
+
`binding.method(...)` inside function declarations. Removes the import, emits a
|
|
37
|
+
port type, injects the binding as a parameter (call sites preserved). **Judgment for
|
|
38
|
+
auto-apply** (call arity changes; not write-path autoPatch). Fail-closed static proof;
|
|
39
|
+
rest params refuse apply. Labeled eval case. Implementation: `bin/lib/port-proof.mjs` +
|
|
40
|
+
scan flag `portProofEligible`.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **Write gate type-only edges:** `import type` / `export type` no longer hard-block
|
|
45
|
+
LAYER_IMPORT / infra heuristics on the write path (erased at runtime). Value imports
|
|
46
|
+
and peerIsolation still deny. ark-check plan continues to surface type placement debt.
|
|
47
|
+
- **`ark_prepare_write` isError:** always `isError` when proposed source is invalid
|
|
48
|
+
(autoPatch is additive recovery, not soft-success).
|
|
49
|
+
- **`resolveImportFileAbs`:** confines disk reads under project root (no path escape).
|
|
50
|
+
|
|
51
|
+
## 2.9.2 — 2026-07-09
|
|
52
|
+
|
|
53
|
+
Skill surface hardening: dual-engine, explore, STOP handoffs, AGENTS routing, subagent fan-out.
|
|
54
|
+
**No intentional CLI flag or JSON shape breaks.**
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **`/ark-explore` skill:** exploratory architecture reconnaissance — product map, entry
|
|
59
|
+
points, coupling hotspots, false-green risk, ranked *suggestions* (not only residual
|
|
60
|
+
violations). CLI remains a sensor; host agent reads the real tree.
|
|
61
|
+
- **Skill completion contract:** every `/ark-*` template ends with fixed
|
|
62
|
+
`### Completion` fields (Sensor / Opened / Result / Handoff / Incomplete?) —
|
|
63
|
+
**skill incomplete if missing**.
|
|
64
|
+
- **Hard STOP handoffs:** critical paths (false-green, concentrated edge, bulk debt,
|
|
65
|
+
wrong skill) use `STOP — do not continue this skill as complete` + named next skill.
|
|
66
|
+
- **AGENTS skill routing table:** trigger → skill map in generated `AGENTS.md` while
|
|
67
|
+
keeping `/ark-autopilot` as the default when unsure.
|
|
68
|
+
- **Subagent fan-out protocol:** every `/ark-*` skill documents optional **parallel
|
|
69
|
+
subagents** when the host supports them (disjoint read-only scopes + parent merge);
|
|
70
|
+
otherwise **fall back to sequential**. AGENTS.md repeats the rule.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- **Skills dual-engine (deterministic + exploratory):** **all** shipped skill templates
|
|
75
|
+
require dual-engine behavior (CLI sensor + real source/product pass where applicable).
|
|
76
|
+
Plan empty ≠ architecture healthy without explore. Refresh installed skills with
|
|
77
|
+
`ark-check --install-agent-gates --skills-only --force`.
|
|
78
|
+
|
|
7
79
|
## 2.9.1 — 2026-07-09
|
|
8
80
|
|
|
9
81
|
Field-install honesty: non-TTY start, baseline→CI sync, pin, false-green soft block, Grok defaults.
|
package/README.md
CHANGED
|
@@ -144,19 +144,30 @@ npx arkgate-check --coverage
|
|
|
144
144
|
Install once: `npx arkgate-check --install-agent-gates`
|
|
145
145
|
(`--tools claude,cursor,codex,grok` to pick hosts.)
|
|
146
146
|
|
|
147
|
-
**Default is always `/ark-autopilot`.** The rest are escapes, not a second curriculum
|
|
147
|
+
**Default is always `/ark-autopilot`.** The rest are escapes, not a second curriculum.
|
|
148
|
+
Generated `AGENTS.md` includes a **skill routing table** (trigger → skill). Skills are
|
|
149
|
+
**dual-engine** (CLI sensor + read real source) and end with a fixed **completion contract**;
|
|
150
|
+
critical handoffs say **STOP** and name the next skill (hosts must follow — markdown cannot chain calls).
|
|
151
|
+
When the host allows it, skills may **fan out parallel subagents** (disjoint scopes);
|
|
152
|
+
otherwise they **fall back to sequential**.
|
|
153
|
+
|
|
154
|
+
**Write path (Track W):** Prefer MCP **`ark_prepare_write`** when you have a snippet (place +
|
|
155
|
+
constrain + validate + optional `autoPatch` + `judgmentBrief`). PreToolUse hooks with
|
|
156
|
+
`--hook-repair` emit machine-readable repair payloads on deny (still hard block; never silent
|
|
157
|
+
write). See [docs/ai-gates.md](docs/ai-gates.md).
|
|
148
158
|
|
|
149
159
|
| Need | Skill |
|
|
150
160
|
|------|--------|
|
|
151
161
|
| Only the apply loop (plan already exists) | `/ark-loop` |
|
|
152
162
|
| Empty greenfield shape/scaffold | `/ark-architect` |
|
|
153
163
|
| Deep brownfield / manifest mining alone | `/ark-adopt` |
|
|
164
|
+
| Exploratory map of the real product (no apply) | `/ark-explore` |
|
|
154
165
|
| New file placement | `/ark-place` |
|
|
155
166
|
| Gate violation on a change | `/ark-fix` |
|
|
156
167
|
| Design trade-offs within the contract (no package LLM) | `/ark-think` |
|
|
157
168
|
| Edit `ark.config.json` safely | `/ark-contract` |
|
|
158
169
|
| Plain-language tour of the report | `/ark-explain` |
|
|
159
|
-
| Deep
|
|
170
|
+
| Deep coverage + opportunities audit | `/ark-coverage` |
|
|
160
171
|
| Migrate hand-rolled bus/outbox (TS) | `/ark-runtime` |
|
|
161
172
|
| Bump ArkGate + refresh all agent hosts | `/ark-upgrade` |
|
|
162
173
|
|
|
@@ -178,6 +189,7 @@ ark.config.json
|
|
|
178
189
|
- **Frameworks:** Nest / Next / express / library layouts get sensible globs on init so day-one coverage is real.
|
|
179
190
|
- **Brownfield:** baseline ratchet, refuse to freeze a wrong contract, `/ark-adopt` for mature trees.
|
|
180
191
|
- **Agents:** skills install into Claude / Cursor / Codex / Grok; `ark start` freezes an origin report under `.ark/reports/`.
|
|
192
|
+
- **Write protocol (2.10 / Track W):** mechanical-safe **autoPatch** on the write gate (`import type`); MCP **`ark_prepare_write`** (place + validate + patch + judgmentBrief); opt-in hook **`--hook-repair`** (`ARK_REPAIR_JSON`); doctor **`writePath`** (repair vs reject-only); loop-cost eval (`npm run eval:loop-cost`). Port-proof inject is **judgment** (arity change), not silent auto-apply.
|
|
181
193
|
- **TypeScript:** project compilers 5.x / 6.x / 7.x — gate falls back to a nested JS-API TypeScript when TS 7’s main export is version-only ([docs/typescript-support.md](docs/typescript-support.md)).
|
|
182
194
|
|
|
183
195
|
### Why not only ESLint / dependency-cruiser / Nx?
|
package/bin/ark-mcp.mjs
CHANGED
|
@@ -12,18 +12,27 @@
|
|
|
12
12
|
* - resource ark://manifest — the architectural contract (layers + rules, or a project
|
|
13
13
|
* manifest file when --manifest is provided)
|
|
14
14
|
* - tool validate_code — runs Ark's AI code gate on a source snippet; returns
|
|
15
|
-
* { valid, violations } and sets isError when invalid
|
|
15
|
+
* { valid, violations, autoPatch? } and sets isError when invalid.
|
|
16
|
+
* autoPatch (W1) is a gate-revalidated rewrite for mechanical-safe
|
|
17
|
+
* import-type kinds only (not W6 port-proof — signature change is judgment);
|
|
18
|
+
* discarded if post-patch still invalid.
|
|
19
|
+
* - tool ark_prepare_write — W2: place + constrain + validate + autoPatch + judgmentBrief
|
|
20
|
+
* + contentHash (composes ark_place + write gate; not a second contract).
|
|
16
21
|
* - tool ark_recommend — deterministic application-shape plan (same as
|
|
17
22
|
* ark-check --recommend --json)
|
|
18
23
|
*
|
|
19
24
|
* Usage: ark-mcp [--root <dir>] [--config ark.config.json] [--manifest <manifest.json>]
|
|
20
|
-
* ark-mcp --hook [--root <dir>] [--config ark.config.json]
|
|
25
|
+
* ark-mcp --hook [--hook-repair] [--root <dir>] [--config ark.config.json]
|
|
21
26
|
*
|
|
22
27
|
* --hook runs one-shot instead of serving: it reads a Claude Code PreToolUse payload from
|
|
23
28
|
* stdin, validates the file content a Write/Edit/MultiEdit is about to produce, and exits
|
|
24
29
|
* 2 with the violations on stderr when the write must be blocked (0 otherwise). This is
|
|
25
30
|
* the copy-paste integration for agent runtimes whose hooks run shell commands.
|
|
26
31
|
*
|
|
32
|
+
* --hook-repair (W4, also ARK_HOOK_REPAIR=1): on deny, emit machine-readable
|
|
33
|
+
* ARK_REPAIR_JSON / ARK_AUTOPATCH_JSON on stderr (and autoPatch in Grok deny JSON).
|
|
34
|
+
* Never silently writes the file — default and repair mode both hard-block.
|
|
35
|
+
*
|
|
27
36
|
* --session-context runs one-shot and prints a compact contract summary (layers, rule
|
|
28
37
|
* count, forbidden globals, baseline state, check command) to stdout. Bind it to a
|
|
29
38
|
* SessionStart hook so the agent has the architecture in context from the first token,
|
|
@@ -46,9 +55,22 @@ import {
|
|
|
46
55
|
resolveIncludeRoots,
|
|
47
56
|
} from './ark-shared.mjs';
|
|
48
57
|
import { createImportTargetResolver } from './lib/import-resolve.mjs';
|
|
58
|
+
import { validateWithAutoPatch, resolveImportFileAbs } from './lib/auto-patch.mjs';
|
|
59
|
+
import { composePrepareWrite } from './lib/prepare-write.mjs';
|
|
49
60
|
|
|
50
61
|
const arkCheckBin = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
|
|
51
62
|
|
|
63
|
+
/**
|
|
64
|
+
* W4 — opt-in hook repair payload.
|
|
65
|
+
* True when CLI `--hook-repair` or env ARK_HOOK_REPAIR is 1/true/yes.
|
|
66
|
+
* Default remains hard block with prose violations only (no machine-readable patch).
|
|
67
|
+
*/
|
|
68
|
+
function envTruthy(name) {
|
|
69
|
+
const v = process.env[name];
|
|
70
|
+
if (v == null || v === '') return false;
|
|
71
|
+
return /^(1|true|yes|on)$/i.test(String(v).trim());
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
function parseArgs(argv) {
|
|
53
75
|
const args = {
|
|
54
76
|
root: process.cwd(),
|
|
@@ -56,18 +78,27 @@ function parseArgs(argv) {
|
|
|
56
78
|
configExplicit: false,
|
|
57
79
|
manifest: undefined,
|
|
58
80
|
hook: false,
|
|
81
|
+
/** When true with --hook: emit ARK_REPAIR_JSON / ARK_AUTOPATCH_JSON (never silent write). */
|
|
82
|
+
hookRepair: false,
|
|
59
83
|
sessionContext: false,
|
|
60
84
|
};
|
|
61
85
|
for (let i = 2; i < argv.length; i += 1) {
|
|
62
86
|
const a = argv[i];
|
|
63
87
|
if (a === '--hook') args.hook = true;
|
|
64
|
-
else if (a === '--
|
|
88
|
+
else if (a === '--hook-repair') {
|
|
89
|
+
args.hook = true;
|
|
90
|
+
args.hookRepair = true;
|
|
91
|
+
} else if (a === '--session-context') args.sessionContext = true;
|
|
65
92
|
else if (a === '--root') args.root = path.resolve(argv[++i]);
|
|
66
93
|
else if (a === '--config') {
|
|
67
94
|
args.config = argv[++i];
|
|
68
95
|
args.configExplicit = true;
|
|
69
96
|
} else if (a === '--manifest') args.manifest = argv[++i];
|
|
70
97
|
}
|
|
98
|
+
// Env can enable repair without rewriting host templates (ARK_HOOK_REPAIR=1).
|
|
99
|
+
if (envTruthy('ARK_HOOK_REPAIR')) {
|
|
100
|
+
args.hookRepair = true;
|
|
101
|
+
}
|
|
71
102
|
return args;
|
|
72
103
|
}
|
|
73
104
|
|
|
@@ -191,7 +222,7 @@ function proposedSource(toolName, toolInput) {
|
|
|
191
222
|
* decision JSON on stdout. Gate plumbing problems (no stdin, malformed JSON, non-file
|
|
192
223
|
* tools, non-source files) never block the agent.
|
|
193
224
|
*/
|
|
194
|
-
function runHook(gate, config, args) {
|
|
225
|
+
function runHook(gate, config, args, ts) {
|
|
195
226
|
let payload;
|
|
196
227
|
try {
|
|
197
228
|
payload = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
@@ -213,14 +244,34 @@ function runHook(gate, config, args) {
|
|
|
213
244
|
if (typeof source !== 'string') return;
|
|
214
245
|
|
|
215
246
|
const layer = inferLayer(filePath, config, args.root);
|
|
216
|
-
const
|
|
247
|
+
const validateOnce = (src) => gate.validate(src, { layer, filePath });
|
|
248
|
+
// W1: one validation pass (+ optional autoPatch). Original write still blocked when
|
|
249
|
+
// invalid; hosts must apply autoPatch explicitly (never silent write).
|
|
250
|
+
const result = ts
|
|
251
|
+
? validateWithAutoPatch({
|
|
252
|
+
source,
|
|
253
|
+
filePath,
|
|
254
|
+
root: args.root,
|
|
255
|
+
ts,
|
|
256
|
+
validate: validateOnce,
|
|
257
|
+
resolveTargetAbs: resolveImportFileAbs,
|
|
258
|
+
})
|
|
259
|
+
: (() => {
|
|
260
|
+
const once = validateOnce(source);
|
|
261
|
+
return {
|
|
262
|
+
valid: Boolean(once.valid),
|
|
263
|
+
violations: once.violations ?? [],
|
|
264
|
+
autoPatch: null,
|
|
265
|
+
};
|
|
266
|
+
})();
|
|
217
267
|
if (result.valid) return;
|
|
218
268
|
|
|
219
269
|
// Ratchet semantics (same philosophy as ark-check --baseline): an edit is blocked only
|
|
220
270
|
// when it ADDS violations relative to the file's current on-disk state. Otherwise a
|
|
221
271
|
// pre-existing violation — frozen in a baseline or predating Ark adoption — would make
|
|
222
|
-
// every subsequent edit to that file un-writable while CI passes.
|
|
223
|
-
// numbers (edits shift them)
|
|
272
|
+
// every subsequent edit to that file un-writable while CI passes. Same-file keys ignore
|
|
273
|
+
// line numbers (edits shift them); simpler than full baselineKey (no file/layer fields
|
|
274
|
+
// needed — this file is fixed).
|
|
224
275
|
const violationKey = (violation) => `${violation.ruleId}|${violation.target ?? violation.message}`;
|
|
225
276
|
let existingKeys = new Set();
|
|
226
277
|
try {
|
|
@@ -231,7 +282,7 @@ function runHook(gate, config, args) {
|
|
|
231
282
|
} catch {
|
|
232
283
|
// New file: nothing pre-exists, every violation is new.
|
|
233
284
|
}
|
|
234
|
-
const newViolations = result.violations.filter(
|
|
285
|
+
const newViolations = (result.violations ?? []).filter(
|
|
235
286
|
(violation) => !existingKeys.has(violationKey(violation))
|
|
236
287
|
);
|
|
237
288
|
if (newViolations.length === 0) return;
|
|
@@ -246,16 +297,68 @@ function runHook(gate, config, args) {
|
|
|
246
297
|
const suggestions = [
|
|
247
298
|
...new Set(newViolations.map((violation) => violation.suggestion).filter(Boolean)),
|
|
248
299
|
];
|
|
300
|
+
const autoPatch = result.autoPatch;
|
|
301
|
+
// W4: structured repair payload is opt-in (--hook-repair / ARK_HOOK_REPAIR).
|
|
302
|
+
// Default remains hard block with prose only — hosts that cannot re-inject stay clean.
|
|
303
|
+
const repair = Boolean(args.hookRepair);
|
|
249
304
|
const message = [
|
|
250
305
|
`Ark architecture gate blocked this write to ${rel}${layer ? ` (layer: ${layer})` : ''}:`,
|
|
251
306
|
...lines,
|
|
252
307
|
...(suggestions.length > 0 ? ['Fix:', ...suggestions.map((s) => ` ${s}`)] : []),
|
|
308
|
+
...(autoPatch && repair
|
|
309
|
+
? [
|
|
310
|
+
`autoPatch available (${autoPatch.remediationKind}, confidence ${autoPatch.confidence}): ` +
|
|
311
|
+
'apply the patched source from ARK_AUTOPATCH_JSON / ARK_REPAIR_JSON on stderr' +
|
|
312
|
+
(grokStyle ? ' (or autoPatch in the deny JSON on stdout)' : '') +
|
|
313
|
+
' instead of re-drafting. Gate still denies this write (never silent apply).',
|
|
314
|
+
]
|
|
315
|
+
: []),
|
|
316
|
+
...(autoPatch && !repair
|
|
317
|
+
? [
|
|
318
|
+
`Mechanical-safe autoPatch is available (${autoPatch.remediationKind}). ` +
|
|
319
|
+
'Enable repair payload with ARK_HOOK_REPAIR=1 or --hook-repair to receive ' +
|
|
320
|
+
'machine-readable source (still hard-blocks; host re-injects).',
|
|
321
|
+
]
|
|
322
|
+
: []),
|
|
253
323
|
'Fix the violations and retry. The architecture contract is available as the ark://manifest MCP resource.',
|
|
254
324
|
].join('\n');
|
|
255
325
|
process.stderr.write(message + '\n');
|
|
326
|
+
|
|
327
|
+
if (repair) {
|
|
328
|
+
// Structured envelope for any host that can re-inject. Never writes the file.
|
|
329
|
+
const repairPayload = {
|
|
330
|
+
mode: 'repair',
|
|
331
|
+
decision: 'deny',
|
|
332
|
+
filePath: rel.split(path.sep).join('/'),
|
|
333
|
+
...(layer ? { layer } : {}),
|
|
334
|
+
...(autoPatch
|
|
335
|
+
? {
|
|
336
|
+
autoPatch: {
|
|
337
|
+
source: autoPatch.source,
|
|
338
|
+
remediationKind: autoPatch.remediationKind,
|
|
339
|
+
confidence: autoPatch.confidence,
|
|
340
|
+
valid: autoPatch.valid,
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
: { autoPatch: null }),
|
|
344
|
+
};
|
|
345
|
+
process.stderr.write(`ARK_REPAIR_JSON:${JSON.stringify(repairPayload)}\n`);
|
|
346
|
+
if (autoPatch) {
|
|
347
|
+
process.stderr.write(`ARK_AUTOPATCH_JSON:${JSON.stringify(autoPatch)}\n`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
256
351
|
// Grok Build honors { decision: "deny" } on stdout (exit 2 alone is also deny).
|
|
352
|
+
// autoPatch in stdout only when repair mode is on (same opt-in as stderr).
|
|
257
353
|
if (grokStyle) {
|
|
258
|
-
process.stdout.write(
|
|
354
|
+
process.stdout.write(
|
|
355
|
+
JSON.stringify({
|
|
356
|
+
decision: 'deny',
|
|
357
|
+
reason: message,
|
|
358
|
+
...(repair && autoPatch ? { autoPatch } : {}),
|
|
359
|
+
...(repair ? { repair: true } : {}),
|
|
360
|
+
}) + '\n'
|
|
361
|
+
);
|
|
259
362
|
}
|
|
260
363
|
process.exitCode = 2;
|
|
261
364
|
}
|
|
@@ -444,7 +547,7 @@ async function main() {
|
|
|
444
547
|
});
|
|
445
548
|
|
|
446
549
|
if (args.hook) {
|
|
447
|
-
runHook(gate, config, args);
|
|
550
|
+
runHook(gate, config, args, ts);
|
|
448
551
|
return;
|
|
449
552
|
}
|
|
450
553
|
|
|
@@ -463,7 +566,9 @@ async function main() {
|
|
|
463
566
|
"Validate a source snippet about to be written against Ark's architecture " +
|
|
464
567
|
'(forbidden infra imports, unknown intents, and layer-reference violations). ' +
|
|
465
568
|
'Bind to PreToolUse on Write/Edit to block architecturally-invalid generated code. ' +
|
|
466
|
-
'Returns { valid, violations }
|
|
569
|
+
'Returns { valid, violations, autoPatch? }. autoPatch (when present) is a ' +
|
|
570
|
+
'mechanical-safe rewrite of the source (import type conversion) that re-validates green; ' +
|
|
571
|
+
'hosts may apply it instead of re-drafting. isError is true when valid is false.',
|
|
467
572
|
inputSchema: {
|
|
468
573
|
type: 'object',
|
|
469
574
|
properties: {
|
|
@@ -518,7 +623,8 @@ async function main() {
|
|
|
518
623
|
description:
|
|
519
624
|
'Place a file in the architecture: pass filePath (preferred) and/or description. ' +
|
|
520
625
|
'Returns layer, mayImport / mustNotImport, forbiddenGlobals. Call BEFORE writing a new file. ' +
|
|
521
|
-
'If only description is given, returns a conventional path proposal under a governed layer.'
|
|
626
|
+
'If only description is given, returns a conventional path proposal under a governed layer. ' +
|
|
627
|
+
'Prefer ark_prepare_write when you already have the source snippet (place+validate+autoPatch in one call).',
|
|
522
628
|
inputSchema: {
|
|
523
629
|
type: 'object',
|
|
524
630
|
properties: {
|
|
@@ -534,6 +640,34 @@ async function main() {
|
|
|
534
640
|
},
|
|
535
641
|
},
|
|
536
642
|
},
|
|
643
|
+
{
|
|
644
|
+
name: 'ark_prepare_write',
|
|
645
|
+
description:
|
|
646
|
+
'Prepare a write against the architecture contract: place (filePath and/or description) + ' +
|
|
647
|
+
'constrain (layer, mayImport, mustNotImport, forbiddenGlobals) + validate source + optional ' +
|
|
648
|
+
'mechanical-safe autoPatch + judgmentBrief when judgment is needed + contentHash for host commit. ' +
|
|
649
|
+
'Composes ark_place + write-gate — call BEFORE Write/Edit when you have the snippet. ' +
|
|
650
|
+
'Returns { filePath, layer, valid, violations?, autoPatch?, judgmentBrief?, contentHash, ... }.',
|
|
651
|
+
inputSchema: {
|
|
652
|
+
type: 'object',
|
|
653
|
+
properties: {
|
|
654
|
+
source: { type: 'string', description: 'Full source text about to be written.' },
|
|
655
|
+
filePath: {
|
|
656
|
+
type: 'string',
|
|
657
|
+
description: 'Target path (preferred). Used for layer inference and autoPatch resolution.',
|
|
658
|
+
},
|
|
659
|
+
description: {
|
|
660
|
+
type: 'string',
|
|
661
|
+
description: 'When filePath omitted: propose a conventional path from this description.',
|
|
662
|
+
},
|
|
663
|
+
layer: {
|
|
664
|
+
type: 'string',
|
|
665
|
+
description: 'Optional explicit layer override (otherwise inferred from filePath).',
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
required: ['source'],
|
|
669
|
+
},
|
|
670
|
+
},
|
|
537
671
|
{
|
|
538
672
|
name: 'ark_recommend',
|
|
539
673
|
description:
|
|
@@ -626,13 +760,38 @@ async function main() {
|
|
|
626
760
|
if (typeof source !== 'string') {
|
|
627
761
|
return { content: [{ type: 'text', text: 'Missing required "source" argument.' }], isError: true };
|
|
628
762
|
}
|
|
629
|
-
const
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
763
|
+
const filePath = params.arguments.filePath;
|
|
764
|
+
const layer = params.arguments.layer ?? inferLayer(filePath, config, args.root);
|
|
765
|
+
const validateOnce = (src) =>
|
|
766
|
+
gate.validate(src, {
|
|
767
|
+
layer,
|
|
768
|
+
filePath,
|
|
769
|
+
});
|
|
770
|
+
// W1: attempt mechanical-safe single-file autoPatch (import type), re-validate or discard.
|
|
771
|
+
const result = validateWithAutoPatch({
|
|
772
|
+
source,
|
|
773
|
+
filePath,
|
|
774
|
+
root: args.root,
|
|
775
|
+
ts,
|
|
776
|
+
validate: validateOnce,
|
|
777
|
+
resolveTargetAbs: resolveImportFileAbs,
|
|
633
778
|
});
|
|
634
779
|
return {
|
|
635
|
-
content: [
|
|
780
|
+
content: [
|
|
781
|
+
{
|
|
782
|
+
type: 'text',
|
|
783
|
+
text: JSON.stringify(
|
|
784
|
+
{
|
|
785
|
+
valid: result.valid,
|
|
786
|
+
violations: result.violations,
|
|
787
|
+
...(result.autoPatch ? { autoPatch: result.autoPatch } : {}),
|
|
788
|
+
layer,
|
|
789
|
+
},
|
|
790
|
+
null,
|
|
791
|
+
2
|
|
792
|
+
),
|
|
793
|
+
},
|
|
794
|
+
],
|
|
636
795
|
isError: !result.valid,
|
|
637
796
|
};
|
|
638
797
|
}
|
|
@@ -691,11 +850,8 @@ async function main() {
|
|
|
691
850
|
// Deterministic placement guidance (in-process; no TS resolver needed): which layer a
|
|
692
851
|
// path falls in, and — from the same rules ark-check enforces (default allow, explicit
|
|
693
852
|
// `allowed:false` denies) — which layers it may and must not import.
|
|
694
|
-
function
|
|
695
|
-
const filePath = params?.arguments?.filePath;
|
|
696
|
-
const description = params?.arguments?.description;
|
|
853
|
+
function placeResult(filePath, description) {
|
|
697
854
|
if ((typeof filePath !== 'string' || !filePath) && typeof description === 'string' && description.trim()) {
|
|
698
|
-
// Description-only: propose a governed path under PresentationAdapters (UI default).
|
|
699
855
|
const slug = description
|
|
700
856
|
.trim()
|
|
701
857
|
.toLowerCase()
|
|
@@ -705,73 +861,39 @@ async function main() {
|
|
|
705
861
|
const proposedPath = `src/components/${slug}.tsx`;
|
|
706
862
|
const layerName = inferLayer(proposedPath, config, args.root) || 'PresentationAdapters';
|
|
707
863
|
return {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
layer: layerName,
|
|
717
|
-
governed: Boolean(inferLayer(proposedPath, config, args.root)),
|
|
718
|
-
note:
|
|
719
|
-
'filePath was omitted — proposed a conventional path from description. ' +
|
|
720
|
-
'Pass filePath explicitly for authoritative placement. Then validate_code the snippet.',
|
|
721
|
-
},
|
|
722
|
-
null,
|
|
723
|
-
2
|
|
724
|
-
),
|
|
725
|
-
},
|
|
726
|
-
],
|
|
727
|
-
isError: false,
|
|
864
|
+
filePath: proposedPath,
|
|
865
|
+
proposed: true,
|
|
866
|
+
description: description.trim(),
|
|
867
|
+
layer: layerName,
|
|
868
|
+
governed: Boolean(inferLayer(proposedPath, config, args.root)),
|
|
869
|
+
note:
|
|
870
|
+
'filePath was omitted — proposed a conventional path from description. ' +
|
|
871
|
+
'Pass filePath explicitly for authoritative placement.',
|
|
728
872
|
};
|
|
729
873
|
}
|
|
730
874
|
if (typeof filePath !== 'string' || !filePath) {
|
|
731
875
|
return {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
text:
|
|
736
|
-
'ark_place needs filePath and/or description. ' +
|
|
737
|
-
'Example: { "filePath": "src/components/Foo.tsx" } or { "description": "caption overlay UI component" }.',
|
|
738
|
-
},
|
|
739
|
-
],
|
|
740
|
-
isError: true,
|
|
876
|
+
error:
|
|
877
|
+
'Needs filePath and/or description. ' +
|
|
878
|
+
'Example: { "filePath": "src/components/Foo.tsx" } or { "description": "caption overlay UI component" }.',
|
|
741
879
|
};
|
|
742
880
|
}
|
|
743
881
|
const layerName = inferLayer(filePath, config, args.root);
|
|
744
882
|
if (!layerName) {
|
|
745
|
-
// Two distinct reasons the path matched no layer: either this project declares no
|
|
746
|
-
// path-based layers at all (the gate still enforces the default 11-layer profile by
|
|
747
|
-
// intent-name PREFIX — placement just can't be inferred from the path), or it does
|
|
748
|
-
// declare layers and this path falls outside all of them (genuinely ungoverned).
|
|
749
883
|
const noLayers = configLayers.length === 0;
|
|
750
884
|
return {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
'for the layers and validate the actual snippet with validate_code.'
|
|
764
|
-
: 'No layer pattern matches this path — code here is UNGOVERNED (no import ' +
|
|
765
|
-
'rules enforced). Place it under a directory a layer in ark.config.json ' +
|
|
766
|
-
'matches, or add a layer. See suggestedLayers for conventional homes.',
|
|
767
|
-
suggestedLayers: suggestedLayers(),
|
|
768
|
-
},
|
|
769
|
-
null,
|
|
770
|
-
2
|
|
771
|
-
),
|
|
772
|
-
},
|
|
773
|
-
],
|
|
774
|
-
isError: false,
|
|
885
|
+
filePath,
|
|
886
|
+
layer: null,
|
|
887
|
+
governed: noLayers,
|
|
888
|
+
message: noLayers
|
|
889
|
+
? 'This project declares no path-based layers in ark.config.json, so a ' +
|
|
890
|
+
'layer cannot be inferred from the path. The gate still enforces the ' +
|
|
891
|
+
'default 11-layer profile by intent-name prefix — read ark://manifest ' +
|
|
892
|
+
'for the layers and validate the actual snippet with validate_code.'
|
|
893
|
+
: 'No layer pattern matches this path — code here is UNGOVERNED (no import ' +
|
|
894
|
+
'rules enforced). Place it under a directory a layer in ark.config.json ' +
|
|
895
|
+
'matches, or add a layer. See suggestedLayers for conventional homes.',
|
|
896
|
+
suggestedLayers: suggestedLayers(),
|
|
775
897
|
};
|
|
776
898
|
}
|
|
777
899
|
const layerMeta = configLayers.find((layer) => layer.name === layerName);
|
|
@@ -782,34 +904,91 @@ async function main() {
|
|
|
782
904
|
);
|
|
783
905
|
const mayImport = otherNames.filter((name) => !mustNotImport.includes(name));
|
|
784
906
|
return {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
],
|
|
907
|
+
filePath,
|
|
908
|
+
layer: layerName,
|
|
909
|
+
governed: true,
|
|
910
|
+
description: layerMeta?.description,
|
|
911
|
+
forbiddenGlobals: layerMeta?.forbiddenGlobals ?? [],
|
|
912
|
+
...(layerMeta?.mayImportInfrastructure ? { mayImportInfrastructure: true } : {}),
|
|
913
|
+
mayImport,
|
|
914
|
+
mustNotImport,
|
|
915
|
+
note:
|
|
916
|
+
'mayImport = layers with no explicit deny (default is allow). Respect ' +
|
|
917
|
+
'forbiddenGlobals, then verify the actual snippet with validate_code or ark_prepare_write.',
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function runPlace(params) {
|
|
922
|
+
const placement = placeResult(params?.arguments?.filePath, params?.arguments?.description);
|
|
923
|
+
if (placement.error) {
|
|
924
|
+
return {
|
|
925
|
+
content: [{ type: 'text', text: `ark_place: ${placement.error}` }],
|
|
926
|
+
isError: true,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
content: [{ type: 'text', text: JSON.stringify(placement, null, 2) }],
|
|
809
931
|
isError: false,
|
|
810
932
|
};
|
|
811
933
|
}
|
|
812
934
|
|
|
935
|
+
/**
|
|
936
|
+
* W2: place + constrain + validate + autoPatch + judgmentBrief + contentHash.
|
|
937
|
+
* Composes ark_place + write-boundary gate — not a second contract.
|
|
938
|
+
*/
|
|
939
|
+
function runPrepareWrite(params) {
|
|
940
|
+
const source = params?.arguments?.source;
|
|
941
|
+
const filePath = params?.arguments?.filePath;
|
|
942
|
+
const description = params?.arguments?.description;
|
|
943
|
+
if (typeof source !== 'string') {
|
|
944
|
+
return {
|
|
945
|
+
content: [
|
|
946
|
+
{
|
|
947
|
+
type: 'text',
|
|
948
|
+
text: 'ark_prepare_write requires "source" (string). Optional: filePath, description.',
|
|
949
|
+
},
|
|
950
|
+
],
|
|
951
|
+
isError: true,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
const placement = placeResult(filePath, description);
|
|
955
|
+
if (placement.error) {
|
|
956
|
+
return {
|
|
957
|
+
content: [{ type: 'text', text: `ark_prepare_write: ${placement.error}` }],
|
|
958
|
+
isError: true,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
const layer =
|
|
962
|
+
placement.layer ||
|
|
963
|
+
params?.arguments?.layer ||
|
|
964
|
+
inferLayer(placement.filePath, config, args.root);
|
|
965
|
+
const validateOnce = (src) =>
|
|
966
|
+
gate.validate(src, {
|
|
967
|
+
layer,
|
|
968
|
+
filePath: placement.filePath,
|
|
969
|
+
});
|
|
970
|
+
const result = composePrepareWrite({
|
|
971
|
+
source,
|
|
972
|
+
placement: { ...placement, layer },
|
|
973
|
+
root: args.root,
|
|
974
|
+
ts,
|
|
975
|
+
validate: validateOnce,
|
|
976
|
+
resolveTargetAbs: resolveImportFileAbs,
|
|
977
|
+
});
|
|
978
|
+
if (!result.ok) {
|
|
979
|
+
return {
|
|
980
|
+
content: [{ type: 'text', text: result.error || 'prepare_write failed' }],
|
|
981
|
+
isError: true,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
return {
|
|
985
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
986
|
+
// Align with validate_code / --hook: proposed source still invalid → isError.
|
|
987
|
+
// autoPatch is additive recovery guidance in the body, never soft-success.
|
|
988
|
+
isError: !result.valid,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
813
992
|
function runSuggestIncludeTool() {
|
|
814
993
|
try {
|
|
815
994
|
const workspaces = detectWorkspaces(args.root);
|
|
@@ -852,6 +1031,7 @@ async function main() {
|
|
|
852
1031
|
ark_check: runCheckTool,
|
|
853
1032
|
ark_coverage: runCoverageTool,
|
|
854
1033
|
ark_place: runPlace,
|
|
1034
|
+
ark_prepare_write: runPrepareWrite,
|
|
855
1035
|
ark_recommend: runRecommendTool,
|
|
856
1036
|
ark_suggest_include: runSuggestIncludeTool,
|
|
857
1037
|
};
|