arkgate 3.8.3 → 3.9.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 +63 -0
- package/README.md +94 -345
- package/bin/ark-mcp-runtime.mjs +137 -11
- package/bin/lib/agent-gates.mjs +4 -0
- package/bin/lib/ci-and-commands.mjs +28 -21
- package/bin/lib/doctor-plan.mjs +37 -28
- package/bin/lib/hook-templates.mjs +13 -9
- package/bin/lib/host-support-matrix.mjs +64 -4
- package/bin/lib/install-migrate.mjs +92 -0
- package/bin/lib/managed-upgrade.mjs +2 -0
- package/bin/lib/mcp-adoption.mjs +60 -2
- package/bin/lib/post-green-path.mjs +2 -2
- package/bin/lib/skill-install.mjs +46 -2
- package/bin/lib/start-preview.mjs +13 -1
- package/bin/lib/write-path-capabilities.mjs +67 -18
- package/bin/lib/write-path-detect.mjs +11 -7
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/README.md +70 -0
- package/docs/agent-guide.md +59 -17
- package/docs/ai-gates.md +97 -11
- package/docs/develop.md +127 -0
- package/docs/enthusiast/README.md +2 -0
- package/docs/package-surface.md +3 -3
- package/docs/product-voice.md +194 -0
- package/docs/use.md +88 -0
- package/package.json +5 -1
- package/server.json +2 -2
- package/templates/hooks/opencode-ark-write-gate.mjs +85 -0
- package/templates/skills/ark-autopilot.md +20 -7
- package/templates/skills/ark-explore.md +17 -4
package/bin/ark-mcp-runtime.mjs
CHANGED
|
@@ -177,13 +177,90 @@ function isResolvedAnalysisInput(relativePath, args, compilerInputs = new Set())
|
|
|
177
177
|
return /(?:^|\/)configs?\/[^/]+\.jsonc?$/i.test(relative);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Map Google Antigravity write tools (PascalCase args) onto Claude Write/Edit/MultiEdit.
|
|
182
|
+
* @returns {{ toolName: string, toolInput: object }|null}
|
|
183
|
+
*/
|
|
184
|
+
function mapAntigravityToolCall(toolCall) {
|
|
185
|
+
if (!toolCall || typeof toolCall !== 'object') return null;
|
|
186
|
+
const name = toolCall.name ?? '';
|
|
187
|
+
const args = toolCall.args && typeof toolCall.args === 'object' ? toolCall.args : {};
|
|
188
|
+
const filePath = args.TargetFile ?? args.targetFile ?? args.file_path ?? args.path;
|
|
189
|
+
if (name === 'write_to_file') {
|
|
190
|
+
return {
|
|
191
|
+
toolName: 'Write',
|
|
192
|
+
toolInput: {
|
|
193
|
+
file_path: filePath,
|
|
194
|
+
content: args.CodeContent ?? args.codeContent ?? args.content ?? '',
|
|
195
|
+
},
|
|
196
|
+
operation: 'write_to_file',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (name === 'replace_file_content') {
|
|
200
|
+
return {
|
|
201
|
+
toolName: 'Edit',
|
|
202
|
+
toolInput: {
|
|
203
|
+
file_path: filePath,
|
|
204
|
+
old_string: args.TargetContent ?? args.targetContent ?? args.old_string ?? '',
|
|
205
|
+
new_string: args.ReplacementContent ?? args.replacementContent ?? args.new_string ?? '',
|
|
206
|
+
replace_all: Boolean(args.AllowMultiple ?? args.allowMultiple),
|
|
207
|
+
},
|
|
208
|
+
operation: 'replace_file_content',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (name === 'multi_replace_file_content') {
|
|
212
|
+
const chunks = Array.isArray(args.ReplacementChunks)
|
|
213
|
+
? args.ReplacementChunks
|
|
214
|
+
: Array.isArray(args.replacementChunks)
|
|
215
|
+
? args.replacementChunks
|
|
216
|
+
: [];
|
|
217
|
+
return {
|
|
218
|
+
toolName: 'MultiEdit',
|
|
219
|
+
toolInput: {
|
|
220
|
+
file_path: filePath,
|
|
221
|
+
edits: chunks.map((chunk) => ({
|
|
222
|
+
old_string: chunk?.TargetContent ?? chunk?.targetContent ?? chunk?.old_string ?? '',
|
|
223
|
+
new_string:
|
|
224
|
+
chunk?.ReplacementContent ?? chunk?.replacementContent ?? chunk?.new_string ?? '',
|
|
225
|
+
replace_all: Boolean(chunk?.AllowMultiple ?? chunk?.allowMultiple),
|
|
226
|
+
})),
|
|
227
|
+
},
|
|
228
|
+
operation: 'multi_replace_file_content',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
toolName: name,
|
|
233
|
+
toolInput: { ...args, file_path: filePath },
|
|
234
|
+
operation: name,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
180
238
|
/**
|
|
181
239
|
* Normalize agent PreToolUse payloads.
|
|
182
240
|
* Claude Code: { tool_name, tool_input: { file_path, content | old_string/new_string } }
|
|
183
241
|
* Grok Build: { toolName, toolInput: { file_path, content | old_string/new_string } }
|
|
184
242
|
* (aliases Write/Edit/MultiEdit → write/search_replace; matcher keeps both)
|
|
243
|
+
* Antigravity: { toolCall: { name, args: { TargetFile, CodeContent, … } } }
|
|
185
244
|
*/
|
|
186
245
|
function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_HOOK_EVENT)) {
|
|
246
|
+
const antigravityStyle =
|
|
247
|
+
payload != null && typeof payload === 'object' && 'toolCall' in payload;
|
|
248
|
+
if (antigravityStyle) {
|
|
249
|
+
const mapped = mapAntigravityToolCall(payload.toolCall);
|
|
250
|
+
const filePath =
|
|
251
|
+
mapped?.toolInput?.file_path ??
|
|
252
|
+
mapped?.toolInput?.filePath ??
|
|
253
|
+
mapped?.toolInput?.path ??
|
|
254
|
+
mapped?.toolInput?.target_file;
|
|
255
|
+
return {
|
|
256
|
+
toolName: mapped?.toolName ?? '',
|
|
257
|
+
toolInput: { ...(mapped?.toolInput ?? {}), file_path: filePath },
|
|
258
|
+
grokStyle: true, // decision JSON on stdout (deny)
|
|
259
|
+
antigravityStyle: true,
|
|
260
|
+
operation: mapped?.operation ?? mapped?.toolName ?? null,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
187
264
|
const rawName = payload?.tool_name ?? payload?.toolName ?? '';
|
|
188
265
|
const toolInput = payload?.tool_input ?? payload?.toolInput ?? {};
|
|
189
266
|
const nameMap = {
|
|
@@ -194,6 +271,9 @@ function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_
|
|
|
194
271
|
MultiEdit: 'MultiEdit',
|
|
195
272
|
ApplyPatch: 'ApplyPatch',
|
|
196
273
|
apply_patch: 'ApplyPatch',
|
|
274
|
+
write_to_file: 'Write',
|
|
275
|
+
replace_file_content: 'Edit',
|
|
276
|
+
multi_replace_file_content: 'MultiEdit',
|
|
197
277
|
};
|
|
198
278
|
const toolName = nameMap[rawName] ?? rawName;
|
|
199
279
|
const filePath =
|
|
@@ -205,6 +285,8 @@ function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_
|
|
|
205
285
|
grokStyle:
|
|
206
286
|
grokHookEvent ||
|
|
207
287
|
(payload != null && typeof payload === 'object' && 'toolName' in payload),
|
|
288
|
+
antigravityStyle: false,
|
|
289
|
+
operation: null,
|
|
208
290
|
};
|
|
209
291
|
}
|
|
210
292
|
|
|
@@ -409,8 +491,14 @@ function processHookOutput() {
|
|
|
409
491
|
};
|
|
410
492
|
}
|
|
411
493
|
|
|
494
|
+
/** Antigravity PreToolUse requires stdout `decision` on every response (allow included). */
|
|
495
|
+
function emitAntigravityAllow(output, antigravityStyle) {
|
|
496
|
+
if (!antigravityStyle) return;
|
|
497
|
+
output.stdout(`${JSON.stringify({ decision: 'allow' })}\n`);
|
|
498
|
+
}
|
|
499
|
+
|
|
412
500
|
function runHookPayload(payload, gate, config, args, ts, attemptContext, output = processHookOutput()) {
|
|
413
|
-
const { toolName, toolInput, grokStyle } = normalizeHookPayload(
|
|
501
|
+
const { toolName, toolInput, grokStyle, antigravityStyle, operation } = normalizeHookPayload(
|
|
414
502
|
payload,
|
|
415
503
|
attemptContext?.grokHookEvent ?? Boolean(process.env.GROK_HOOK_EVENT)
|
|
416
504
|
);
|
|
@@ -419,7 +507,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
419
507
|
const parsedPatch = codexPatchWrites(patch, args.root);
|
|
420
508
|
// Codex ApplyPatch is only preflighted when Ark can reconstruct every file operation.
|
|
421
509
|
// An incomplete reconstruction must not be mislabeled as atomic or hard enforcement.
|
|
422
|
-
if (!parsedPatch.complete)
|
|
510
|
+
if (!parsedPatch.complete) {
|
|
511
|
+
emitAntigravityAllow(output, antigravityStyle);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
423
514
|
const patchWrites = parsedPatch.writes;
|
|
424
515
|
const sourceWrites = patchWrites.filter((change) =>
|
|
425
516
|
isGovernableSourceFile(path.basename(String(change.path)))
|
|
@@ -482,7 +573,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
482
573
|
.join(', ')}; source-only virtual preflight cannot model those contents.`
|
|
483
574
|
);
|
|
484
575
|
}
|
|
485
|
-
if (changes.length === 0)
|
|
576
|
+
if (changes.length === 0) {
|
|
577
|
+
emitAntigravityAllow(output, antigravityStyle);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
486
580
|
result = prepareChangeFromRoot({
|
|
487
581
|
root: args.root,
|
|
488
582
|
config,
|
|
@@ -537,7 +631,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
537
631
|
const designDelta = args.failOnNewSmells
|
|
538
632
|
? evaluateWriteDesignDelta({ root: args.root, config, changes, ts })
|
|
539
633
|
: null;
|
|
540
|
-
if (result.valid && (designDelta?.valid ?? true))
|
|
634
|
+
if (result.valid && (designDelta?.valid ?? true)) {
|
|
635
|
+
emitAntigravityAllow(output, antigravityStyle);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
541
638
|
const message = [
|
|
542
639
|
`Ark architecture gate blocked this complete ${toolName} (${changes.length} governed file(s)):`,
|
|
543
640
|
...result.diagnostics.map(
|
|
@@ -567,16 +664,27 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
567
664
|
return;
|
|
568
665
|
}
|
|
569
666
|
const filePath = toolInput.file_path;
|
|
570
|
-
if (!['Write', 'Edit', 'MultiEdit'].includes(toolName))
|
|
667
|
+
if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) {
|
|
668
|
+
// Non-file tools: fail-open. Antigravity still needs an explicit allow decision.
|
|
669
|
+
emitAntigravityAllow(output, antigravityStyle);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
571
672
|
if (typeof filePath !== 'string' || !SOURCE_FILE.test(filePath) || filePath.endsWith('.d.ts')) {
|
|
673
|
+
emitAntigravityAllow(output, antigravityStyle);
|
|
572
674
|
return;
|
|
573
675
|
}
|
|
574
676
|
const rel = path.relative(args.root, path.resolve(filePath));
|
|
575
677
|
const segments = rel.split(path.sep);
|
|
576
|
-
if (segments[0] === '..' || segments.includes('node_modules'))
|
|
678
|
+
if (segments[0] === '..' || segments.includes('node_modules')) {
|
|
679
|
+
emitAntigravityAllow(output, antigravityStyle);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
577
682
|
|
|
578
683
|
const source = proposedSource(toolName, toolInput);
|
|
579
|
-
if (typeof source !== 'string')
|
|
684
|
+
if (typeof source !== 'string') {
|
|
685
|
+
emitAntigravityAllow(output, antigravityStyle);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
580
688
|
|
|
581
689
|
const layer = inferLayer(filePath, config, args.root);
|
|
582
690
|
const validateOnce = (src) =>
|
|
@@ -611,7 +719,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
611
719
|
ts,
|
|
612
720
|
})
|
|
613
721
|
: null;
|
|
614
|
-
if (result.valid && (designDelta?.valid ?? true))
|
|
722
|
+
if (result.valid && (designDelta?.valid ?? true)) {
|
|
723
|
+
emitAntigravityAllow(output, antigravityStyle);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
615
726
|
|
|
616
727
|
// Ratchet semantics (same philosophy as ark-check --baseline): an edit is blocked only
|
|
617
728
|
// when it ADDS violations relative to the file's current on-disk state. Otherwise a
|
|
@@ -639,7 +750,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
639
750
|
existingCounts.set(key, remaining - 1);
|
|
640
751
|
return false;
|
|
641
752
|
});
|
|
642
|
-
if (newViolations.length === 0 && (designDelta?.valid ?? true))
|
|
753
|
+
if (newViolations.length === 0 && (designDelta?.valid ?? true)) {
|
|
754
|
+
emitAntigravityAllow(output, antigravityStyle);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
643
757
|
const combinedViolations = [...newViolations, ...designDeltaViolations(designDelta)];
|
|
644
758
|
const adapterResult = createAdapterResult({
|
|
645
759
|
valid: false,
|
|
@@ -698,9 +812,21 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
698
812
|
filePath: normalizedRel,
|
|
699
813
|
enforcement: hookEnforcement(
|
|
700
814
|
args.root,
|
|
701
|
-
attemptContext?.host ??
|
|
815
|
+
attemptContext?.host ??
|
|
816
|
+
(antigravityStyle ? 'antigravity' : grokStyle ? 'grok' : 'claude'),
|
|
702
817
|
attemptContext?.operation ??
|
|
703
|
-
|
|
818
|
+
operation ??
|
|
819
|
+
(antigravityStyle
|
|
820
|
+
? toolName === 'Edit'
|
|
821
|
+
? 'replace_file_content'
|
|
822
|
+
: toolName === 'MultiEdit'
|
|
823
|
+
? 'multi_replace_file_content'
|
|
824
|
+
: 'write_to_file'
|
|
825
|
+
: grokStyle
|
|
826
|
+
? toolName === 'Edit'
|
|
827
|
+
? 'search_replace'
|
|
828
|
+
: 'write'
|
|
829
|
+
: toolName),
|
|
704
830
|
toolName === 'Write' || Boolean(attemptContext?.completePatch)
|
|
705
831
|
),
|
|
706
832
|
...(layer ? { layer } : {}),
|
package/bin/lib/agent-gates.mjs
CHANGED
|
@@ -23,10 +23,14 @@ export {
|
|
|
23
23
|
|
|
24
24
|
export {
|
|
25
25
|
PREFERRED_MCP_BIN,
|
|
26
|
+
antigravityHooks,
|
|
26
27
|
claudeSettings,
|
|
27
28
|
codexProjectConfig,
|
|
28
29
|
grokHooks,
|
|
29
30
|
grokProjectConfig,
|
|
31
|
+
mergeAntigravityArkHook,
|
|
32
|
+
mergeOpencodeArkMcp,
|
|
33
|
+
opencodeProjectConfig,
|
|
30
34
|
} from './hook-templates.mjs';
|
|
31
35
|
|
|
32
36
|
export { detectWritePathCapabilities } from './write-path-detect.mjs';
|
|
@@ -128,16 +128,13 @@ export function agentInstructions(root) {
|
|
|
128
128
|
|
|
129
129
|
## Default agent flow (if unsure, do only this)
|
|
130
130
|
|
|
131
|
-
1.
|
|
132
|
-
2.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Do **not** invent a second architecture curriculum outside the routing table below — when a
|
|
136
|
-
trigger matches, use that skill; when unsure, stay on autopilot.
|
|
137
|
-
3. Status anytime: \`${doctorCmd}\` (status light + next action — not a mode picker).
|
|
138
|
-
4. After ordinary feature edits: run \`${checkCmd}\`. On violations → **\`/ark-fix\`** (or
|
|
139
|
-
\`/ark-place\` for new files, \`/ark-contract\` only if the contract itself is wrong).
|
|
131
|
+
1. Status anytime: \`${doctorCmd}\` — **control plane** (one status light, one next action; not a mode picker).
|
|
132
|
+
2. If \`ark.config.json\` is missing: run \`${startCmd}\` once (preview), then \`${startCmd} --apply\`.
|
|
133
|
+
3. Guided end-to-end work (“make architecture sound”): **\`/ark-autopilot\`** — explore → dual plan A (edges) + B (shape) → mechanical-safe fixes; B only with user OK. Day-zero origin is frozen by \`ark start\`/\`ark init\` (or autopilot if missing) **before** agent docs.
|
|
134
|
+
4. After ordinary feature edits: run \`${checkCmd}\`. On violations → **\`/ark-fix\`** (or \`/ark-place\` for new files, \`/ark-contract\` only if the contract itself is wrong).
|
|
140
135
|
|
|
136
|
+
Do **not** skill-shop the full table for routine work. When unsure, do doctor top action #1 only
|
|
137
|
+
(re-run doctor after). Do **not** jump to \`/ark-autopilot\` unless #1 or a STOP handoff names it.
|
|
141
138
|
Skills are **dual-engine**: deterministic CLI sensors + exploratory read of *this* repo — not JSON-only wrappers.
|
|
142
139
|
When a skill says **STOP — do not continue this skill as complete**, stop and invoke the named handoff skill.
|
|
143
140
|
|
|
@@ -151,14 +148,16 @@ scouts (disjoint path scopes) and merge in the parent. If the host does **not**,
|
|
|
151
148
|
**fall back to sequential** — one cluster/step at a time. Never parallel-write the same
|
|
152
149
|
files; never weaken the gate via subagents.
|
|
153
150
|
|
|
154
|
-
## Skill routing (triggers → skill)
|
|
151
|
+
## Skill routing (expert depth — triggers → skill)
|
|
155
152
|
|
|
156
|
-
Do **not** run overlapping skills for the same job.
|
|
153
|
+
**Escapes, not a second curriculum.** Do **not** run overlapping skills for the same job.
|
|
154
|
+
Pick **one** primary skill. Prefer doctor top action #1 when unsure.
|
|
157
155
|
|
|
158
156
|
| When | Invoke | Not this |
|
|
159
157
|
|------|--------|----------|
|
|
160
|
-
| Unsure
|
|
161
|
-
|
|
|
158
|
+
| Unsure what to do next | **Doctor top action #1** (\`${doctorCmd}\`), then re-run doctor | skill-shopping, defaulting to autopilot |
|
|
159
|
+
| Make architecture sound (guided apply path) | **/ark-autopilot** | explore-only, coverage-only |
|
|
160
|
+
| **Messy / spaghetti / design-weak after green / Shape residual** | **Single path:** \`/ark-explore\` shape-focus → dual-plan B, then \`/ark-autopilot\` only to apply B with OK | coverage, think, loop-as-done, skill-shopping |
|
|
162
161
|
| Map / residual / dual-plan seed only (no apply, already know you want recon) | \`/ark-explore\` | coverage (fitness only) |
|
|
163
162
|
| Greenfield shape / empty tree | \`/ark-architect\` | adopt |
|
|
164
163
|
| Brownfield / wrong contract / false-green | \`/ark-adopt\` then \`/ark-contract\` if globs wrong | architect |
|
|
@@ -209,20 +208,28 @@ export function compactAgentInstructions(root, host = null) {
|
|
|
209
208
|
'ark-check',
|
|
210
209
|
`--install-agent-gates --skills-only --tools ${selectedHost === 'none' ? '<host>' : selectedHost}`
|
|
211
210
|
);
|
|
211
|
+
// Progressive disclosure: primary path only. Full /ark-* catalog is expert depth
|
|
212
|
+
// (install via --skills-only). See docs/product-voice.md.
|
|
212
213
|
return `# Ark Enforcement
|
|
213
214
|
|
|
214
215
|
<!-- arkgate:compact-router host=${selectedHost} -->
|
|
215
216
|
## Compact router
|
|
216
217
|
|
|
217
|
-
|
|
218
|
-
agent router. Before editing TypeScript or JavaScript, read \`ark://manifest\`
|
|
219
|
-
when available; use \`ark_place\` for new files and \`validate_code\` after edits.
|
|
220
|
-
If MCP is unavailable, inspect \`ark.config.json\` and run \`${checkCmd}\`.
|
|
218
|
+
**Primary path (do this):**
|
|
221
219
|
|
|
222
|
-
|
|
223
|
-
\`${
|
|
224
|
-
|
|
225
|
-
|
|
220
|
+
1. Status anytime: \`${doctorCmd}\` — one status light, one next action (control plane).
|
|
221
|
+
2. Day to day: read \`ark://manifest\` when MCP is available; place new files with \`ark_place\`; validate after edits; run \`${checkCmd}\`. On a gate deny, fix the architecture — do not weaken the contract.
|
|
222
|
+
3. If MCP is unavailable: inspect \`ark.config.json\` and run \`${checkCmd}\`.
|
|
223
|
+
|
|
224
|
+
The selected host is \`${selectedHost}\`. Host registration and CI are installed with this file.
|
|
225
|
+
This compact router is enough for normal feature work.
|
|
226
|
+
|
|
227
|
+
## Expert depth (optional)
|
|
228
|
+
|
|
229
|
+
Full \`/ark-*\` skills (including guided end-to-end \`/ark-autopilot\`) are **not** the default
|
|
230
|
+
curriculum. Install them only when doctor top action #1 or a STOP handoff names a skill:
|
|
231
|
+
|
|
232
|
+
\`${installSkills}\`
|
|
226
233
|
`;
|
|
227
234
|
}
|
|
228
235
|
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
mergePostGreenTopActions,
|
|
37
37
|
isDoctorHealthyNothingToDo,
|
|
38
38
|
} from './post-green-path.mjs';
|
|
39
|
+
import { doctorWritePathHonestyMessage } from './host-support-matrix.mjs';
|
|
39
40
|
import {
|
|
40
41
|
computePureLayerOptInNudge,
|
|
41
42
|
loadGoldenPattern,
|
|
@@ -578,14 +579,20 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
578
579
|
console.log('');
|
|
579
580
|
console.log(color.bold('Operating mode'));
|
|
580
581
|
// Modes are detected states, not user-picked settings. Plain-language "what you do next".
|
|
581
|
-
|
|
582
|
+
// Never paint green (ok) under design residual — edges clean ≠ design done (product-voice).
|
|
583
|
+
const modeMark =
|
|
584
|
+
mode === 'enforce' && !designFitness.designWeak
|
|
585
|
+
? ok
|
|
586
|
+
: warn;
|
|
587
|
+
// Status lights are detected states, not user-picked settings (see docs/product-voice.md).
|
|
588
|
+
// modeTitle alone names the light — bodies must not re-prefix Suggest/Adapt/Enforce.
|
|
582
589
|
const modeHelp = {
|
|
583
590
|
suggest:
|
|
584
|
-
'
|
|
591
|
+
'thin or new tree; the contract is not yet the control plane. You do not pick this light. Next: ark start (preview), then ark start --apply; re-check with --doctor.',
|
|
585
592
|
adapt:
|
|
586
|
-
'
|
|
593
|
+
'contract and tree still disagree, or debt is open. Write path does not fully protect you yet. You do not pick this light. Next: do doctor top action #1 (often /ark-adopt, /ark-contract, or /ark-autopilot).',
|
|
587
594
|
enforce:
|
|
588
|
-
'
|
|
595
|
+
'honest coverage and clean checked edges. You arrived here; you never turn Enforce on. Next: keep the host write path and CI check; only NEW violations should fail.',
|
|
589
596
|
};
|
|
590
597
|
const modeTitle =
|
|
591
598
|
mode === 'enforce' && designFitness.designWeak
|
|
@@ -595,7 +602,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
595
602
|
modeMark,
|
|
596
603
|
`${modeTitle} — ${
|
|
597
604
|
designFitness.designWeak
|
|
598
|
-
? '
|
|
605
|
+
? 'checked edges are honest; design smells remain. Green is not elegant design. You do not pick this light. Next: one Shape door — /ark-explore shape-focus → dual-plan B; apply B only with /ark-autopilot and your OK. Empty plan A is not done.'
|
|
599
606
|
: modeHelp[mode]
|
|
600
607
|
}`
|
|
601
608
|
);
|
|
@@ -692,34 +699,25 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
692
699
|
if (showNewHere) {
|
|
693
700
|
console.log('');
|
|
694
701
|
console.log(color.bold('New here?'));
|
|
702
|
+
// Suggest residual: start → doctor only (not a competing recommend/architect curriculum).
|
|
703
|
+
line(ok, `Primary path: ${arkCommand(root, 'ark', 'start')} (preview) → ${arkCommand(root, 'ark', 'start --apply')} → re-run --doctor`);
|
|
695
704
|
if (recommendation) {
|
|
696
|
-
line(warn, `
|
|
697
|
-
if (recommendation.galleryStarter) {
|
|
698
|
-
line(ok, `Gallery starter: ${recommendation.galleryStarter}`);
|
|
699
|
-
}
|
|
705
|
+
line(warn, `Sensor shape hint (not a second curriculum): ${recommendation.archetype} — ${recommendation.label} (preset ${recommendation.preset})`);
|
|
706
|
+
if (recommendation.galleryStarter) line(ok, `Gallery starter (optional): ${recommendation.galleryStarter}`);
|
|
700
707
|
if (recommendation.policyPack) {
|
|
701
|
-
line(ok, `Policy pack: ${arkCommand(root, 'ark-check', `--apply-policy-pack ${recommendation.policyPack}`)}`);
|
|
708
|
+
line(ok, `Policy pack (optional expert): ${arkCommand(root, 'ark-check', `--apply-policy-pack ${recommendation.policyPack}`)}`);
|
|
702
709
|
}
|
|
703
710
|
if (recommendation.signals?.nestFramework) {
|
|
704
|
-
line(
|
|
705
|
-
ok,
|
|
706
|
-
'Nest modular monolith → prefer hexagonal (or ddd-bounded-contexts if you have src/contexts/*)'
|
|
707
|
-
);
|
|
711
|
+
line(ok, 'Nest modular monolith → prefer hexagonal (or ddd-bounded-contexts if you have src/contexts/*)');
|
|
708
712
|
}
|
|
709
713
|
if (recommendation.signals?.monorepoTooling?.length) {
|
|
710
|
-
line(
|
|
711
|
-
ok,
|
|
712
|
-
`Monorepo tooling (${recommendation.signals.monorepoTooling.join(', ')}) → preset monorepo (apps/packages/libs)`
|
|
713
|
-
);
|
|
714
|
+
line(ok, `Monorepo tooling (${recommendation.signals.monorepoTooling.join(', ')}) → preset monorepo (apps/packages/libs)`);
|
|
714
715
|
}
|
|
715
716
|
} else {
|
|
716
|
-
line(warn, 'Low governed coverage or fresh config —
|
|
717
|
-
}
|
|
718
|
-
line(ok, `See the plan: ${arkCommand(root, 'ark-check', '--recommend')}`);
|
|
719
|
-
if (recommendation?.archetype) {
|
|
720
|
-
line(ok, `Quick setup: ${arkCommand(root, 'ark', `init --archetype ${recommendation.archetype} --yes`)}`);
|
|
717
|
+
line(warn, 'Low governed coverage or fresh config — finish start, then re-run doctor before adding layers of code.');
|
|
721
718
|
}
|
|
722
|
-
|
|
719
|
+
line(ok, `Optional sensor detail: ${arkCommand(root, 'ark-check', '--recommend')}`);
|
|
720
|
+
actions.unshift('finish ark start (preview + --apply), then re-run --doctor');
|
|
723
721
|
}
|
|
724
722
|
|
|
725
723
|
console.log('');
|
|
@@ -731,8 +729,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
731
729
|
warn,
|
|
732
730
|
'No active violations — coverage is still thin, so green is not yet honest enforcement'
|
|
733
731
|
);
|
|
732
|
+
} else if (designFitness.designWeak) {
|
|
733
|
+
line(warn, 'None on checked edges — edges match the contract; design residual remains (ENFORCE · design-weak). Not healthy finished.');
|
|
734
734
|
} else {
|
|
735
|
-
line(ok, 'None — the code matches the contract');
|
|
735
|
+
line(ok, 'None — the code matches the contract on checked edges');
|
|
736
736
|
}
|
|
737
737
|
} else {
|
|
738
738
|
const typeNote = summary.typeOnlyCount > 0 ? ` (${summary.valueCount} value · ${summary.typeOnlyCount} type-only)` : '';
|
|
@@ -770,6 +770,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
770
770
|
line(' ', `Active host: ${writePath.activeHost}`);
|
|
771
771
|
line(' ', `Supported profile: ${writePath.supportSummary}`);
|
|
772
772
|
line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`);
|
|
773
|
+
const honestyLine = doctorWritePathHonestyMessage(writePath.activeHost, capabilities['hard-write']);
|
|
774
|
+
if (honestyLine) line(warn, honestyLine);
|
|
773
775
|
if (writePath.sessionNote) {
|
|
774
776
|
line(warn, writePath.sessionNote);
|
|
775
777
|
}
|
|
@@ -932,18 +934,25 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
932
934
|
const uniqueActions = mergePostGreenTopActions(actions, postGreenPath);
|
|
933
935
|
if (isDoctorHealthyNothingToDo(designFitness, uniqueActions)) {
|
|
934
936
|
console.log(color.green('✔ Healthy — nothing to do.'));
|
|
937
|
+
console.log(color.dim(' Contract edges and design residual are clear. Keep write path + CI.'));
|
|
935
938
|
} else {
|
|
936
939
|
if (designFitness.designWeak && uniqueActions.length === 0 && postGreenPath) {
|
|
937
940
|
uniqueActions.push(postGreenPath.action);
|
|
938
941
|
}
|
|
939
|
-
console.log(color.bold(`
|
|
940
|
-
|
|
942
|
+
console.log(color.bold(`Primary next action`));
|
|
943
|
+
console.log(` 1. ${uniqueActions[0]}`);
|
|
944
|
+
if (uniqueActions.length > 1) {
|
|
945
|
+
console.log(color.bold(`Also (${uniqueActions.length - 1}):`));
|
|
946
|
+
uniqueActions.slice(1).forEach((action, index) => console.log(` ${index + 2}. ${action}`));
|
|
947
|
+
}
|
|
941
948
|
if (postGreenPath) {
|
|
942
949
|
console.log(
|
|
943
950
|
color.dim(
|
|
944
|
-
'
|
|
951
|
+
' Shape residual is the primary door under ENFORCE · design-weak — do not skill-shop explore vs coverage vs think.'
|
|
945
952
|
)
|
|
946
953
|
);
|
|
954
|
+
} else {
|
|
955
|
+
console.log(color.dim(' Doctor is the control plane: do #1 first, then re-run --doctor.'));
|
|
947
956
|
}
|
|
948
957
|
}
|
|
949
958
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// Generated from hook-templates.source.mjs — run npm run generate:packaged-tooling.
|
|
2
|
-
import{execCommandParts as i,execRunner as
|
|
3
|
-
`}function
|
|
4
|
-
`}function
|
|
2
|
+
import{execCommandParts as i,execRunner as s}from"../ark-shared.mjs";const c="arkgate-mcp";function l(e){const r=s(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${c} --session-context --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`}]}]}},null,2)}
|
|
3
|
+
`}function p(e){const r=s(e),o="${CODEX_PROJECT_DIR:-${PWD:-.}}";return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root "${o}" --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "${o}" --config ark.config.json`}]}]}},null,2)}
|
|
4
|
+
`}function g(e){const{command:r,args:o}=i(e,c,["--root",".","--config","ark.config.json"]),t=a=>a.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),n=o.map(a=>`"${t(a)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
|
|
5
5
|
# Restart Codex after changes; MCP servers are loaded when the project session starts.
|
|
6
6
|
[mcp_servers.ark]
|
|
7
|
-
command = "${t(
|
|
8
|
-
args = [${
|
|
9
|
-
`}function
|
|
7
|
+
command = "${t(r)}"
|
|
8
|
+
args = [${n}]
|
|
9
|
+
`}function f(e){const{command:r,args:o}=i(e,c,["--root",".","--config","ark.config.json"]),t=o.map(n=>`"${n.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
|
|
10
10
|
# Restart Grok (or /mcps \u2192 refresh) after changes. Also loads repo-root .mcp.json.
|
|
11
11
|
[mcp_servers.ark]
|
|
12
|
-
command = "${
|
|
12
|
+
command = "${r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
|
|
13
13
|
args = [${t}]
|
|
14
|
-
`}function
|
|
15
|
-
`}
|
|
14
|
+
`}function u(e){const r=s(e),o="${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}";return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root "${o}" --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "${o}" --config ark.config.json`}]}]}},null,2)}
|
|
15
|
+
`}function k(e){const r=s(e);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root "\${PWD:-.}" --config ark.config.json`}]}]}},null,2)}
|
|
16
|
+
`}function d(e){const{command:r,args:o}=i(e,c,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...o],enabled:!0}}},null,2)}
|
|
17
|
+
`}function $(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!t||typeof t!="object"||Array.isArray(t))return null;const n=t["ark-write-gate"];if(!n||typeof n!="object")return null;const a={...o,"ark-write-gate":n};return`${JSON.stringify(a,null,2)}
|
|
18
|
+
`}function h(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o))return null;const n={...o};!n.$schema&&t.$schema&&(n.$schema=t.$schema);const a=o.mcp&&typeof o.mcp=="object"&&!Array.isArray(o.mcp)?{...o.mcp}:{};return a.ark=t.mcp.ark,n.mcp=a,`${JSON.stringify(n,null,2)}
|
|
19
|
+
`}export{c as PREFERRED_MCP_BIN,k as antigravityHooks,l as claudeSettings,p as codexHooks,g as codexProjectConfig,u as grokHooks,f as grokProjectConfig,$ as mergeAntigravityArkHook,h as mergeOpencodeArkMcp,d as opencodeProjectConfig};
|
|
@@ -38,6 +38,16 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
|
|
|
38
38
|
true,
|
|
39
39
|
true
|
|
40
40
|
),
|
|
41
|
+
// Google Antigravity: official PreToolUse deny is a hard block. Claim hard only when
|
|
42
|
+
// installed + trusted and the listed write tools are covered by the adapter.
|
|
43
|
+
antigravity: hostProfile(
|
|
44
|
+
'Google Antigravity',
|
|
45
|
+
'.agents/hooks.json',
|
|
46
|
+
'PreToolUse `write_to_file` / `replace_file_content` / `multi_replace_file_content`',
|
|
47
|
+
['write_to_file', 'replace_file_content', 'multi_replace_file_content'],
|
|
48
|
+
true,
|
|
49
|
+
true
|
|
50
|
+
),
|
|
41
51
|
cursor: hostProfile('Cursor', null, null, [], false, false),
|
|
42
52
|
codex: hostProfile(
|
|
43
53
|
'OpenAI Codex',
|
|
@@ -47,6 +57,16 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
|
|
|
47
57
|
false,
|
|
48
58
|
false
|
|
49
59
|
),
|
|
60
|
+
// OpenCode: first-class MCP + permissions; plugin tool.execute.before is incomplete
|
|
61
|
+
// (subagent holes). Never claim hard write.
|
|
62
|
+
opencode: hostProfile(
|
|
63
|
+
'OpenCode',
|
|
64
|
+
null,
|
|
65
|
+
'Advisory MCP + optional experimental plugin (`tool.execute.before`); not a hard boundary',
|
|
66
|
+
[],
|
|
67
|
+
false,
|
|
68
|
+
false
|
|
69
|
+
),
|
|
50
70
|
});
|
|
51
71
|
|
|
52
72
|
export const HOST_SUPPORT_HOSTS = Object.freeze(Object.keys(HOST_SUPPORT_MATRIX));
|
|
@@ -70,18 +90,58 @@ export function renderHostSupportMatrixMarkdown() {
|
|
|
70
90
|
const rows = HOST_SUPPORT_HOSTS.map((host) => {
|
|
71
91
|
const profile = HOST_SUPPORT_MATRIX[host];
|
|
72
92
|
const capabilities = profile.capabilities;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
// Fail-closed honesty: Cursor/Codex/OpenCode never claim hard write; CI is required-status.
|
|
94
|
+
// hookSurface already includes "PreToolUse …" — do not prefix PreToolUse again.
|
|
95
|
+
let local;
|
|
96
|
+
if (capabilities['hard-write']) {
|
|
97
|
+
local = `**Hard** block for listed ops (${profile.hookSurface}) when installed + trusted`;
|
|
98
|
+
} else if (host === 'codex') {
|
|
99
|
+
local =
|
|
100
|
+
'**Advisory / best-effort** at write (not equivalent to Claude/Grok hard block)';
|
|
101
|
+
} else if (host === 'opencode') {
|
|
102
|
+
local =
|
|
103
|
+
'**Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary)';
|
|
104
|
+
} else {
|
|
105
|
+
local = '**Advisory only** at write (no hard hook)';
|
|
106
|
+
}
|
|
76
107
|
const repair = capabilities['repair-payload']
|
|
77
108
|
? 'Emitted on hook deny; host must re-inject'
|
|
78
109
|
: 'No hard-boundary payload';
|
|
79
|
-
|
|
110
|
+
const merge = capabilities['hard-write']
|
|
111
|
+
? '**Required status** = hard merge boundary (`arkgate-check --strict-merge`)'
|
|
112
|
+
: '**Required status** = hard merge boundary (same CI)';
|
|
113
|
+
return `| ${profile.label} | ${local} | Advisory; the agent must call it | ${merge} | ${repair} |`;
|
|
80
114
|
}).join('\n');
|
|
81
115
|
|
|
82
116
|
return `| Host | Local write boundary | MCP validation | CI / merge path | Repair payload |
|
|
83
117
|
|------|----------------------|----------------|-----------------|----------------|
|
|
84
118
|
${rows}
|
|
85
119
|
|
|
120
|
+
**Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
|
|
121
|
+
merge check — not “CI file present.” Cursor/Codex/OpenCode never get a fake hard write claim.
|
|
122
|
+
|
|
86
123
|
This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair payloads never write code silently: the host must re-inject the candidate and ArkGate revalidates it. Run \`arkgate-check --doctor\` for the evidence actually detected in the current repository.`;
|
|
87
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Doctor human one-liner for active-host write honesty (fail-closed).
|
|
128
|
+
* @returns {string|null}
|
|
129
|
+
*/
|
|
130
|
+
export function doctorWritePathHonestyMessage(activeHost, hardWriteActive) {
|
|
131
|
+
const host = typeof activeHost === 'string' ? activeHost.trim().toLowerCase() : '';
|
|
132
|
+
if (host === 'cursor') {
|
|
133
|
+
return 'Cursor: write path is advisory (MCP/rules; no hard PreToolUse). Required CI status (arkgate-check --strict-merge) is the hard merge boundary.';
|
|
134
|
+
}
|
|
135
|
+
if (host === 'codex') {
|
|
136
|
+
return 'Codex: write path is advisory / best-effort at write (not Claude/Grok hard). Required CI status (arkgate-check --strict-merge) is the hard merge boundary.';
|
|
137
|
+
}
|
|
138
|
+
if (host === 'opencode') {
|
|
139
|
+
return 'OpenCode: write path is advisory / best-effort (MCP + optional plugin; not Claude/Grok/Antigravity hard). Required CI status (arkgate-check --strict-merge) is the hard merge boundary.';
|
|
140
|
+
}
|
|
141
|
+
if ((host === 'claude' || host === 'grok' || host === 'antigravity') && !hardWriteActive) {
|
|
142
|
+
const label =
|
|
143
|
+
host === 'claude' ? 'Claude' : host === 'grok' ? 'Grok' : 'Antigravity';
|
|
144
|
+
return `${label}: hard PreToolUse is supported for listed ops when installed + trusted; without runtime-observed hook evidence, hard is unverified. Required CI remains the merge hard boundary.`;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|