arkgate 3.0.2 → 3.0.4

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.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Q04 — productized pilot loop: extraction card → one pilot → re-doctor.
3
+ *
4
+ * Selects a single next pilot from patternBets / design smells, emits an
5
+ * extraction-card payload, and compares residual after re-doctor on pilot paths.
6
+ * Judgment only — never mechanical-safe; never multi-pilot batch apply.
7
+ */
8
+ import { buildPatternBetsFromSmells } from './design-smells.mjs';
9
+
10
+ /** Stable product id for JSON / tests. */
11
+ export const PILOT_LOOP_ID = 'one-pilot-redoctor';
12
+
13
+ /** Smell priority when choosing the single next pilot (lower = earlier). */
14
+ const SMELL_PRIORITY = {
15
+ 'facade-sql-in-routes': 0,
16
+ 'io-under-application': 1,
17
+ 'handler-in-persistence': 2,
18
+ 'domain-logic-in-ui': 3,
19
+ 'god-module': 4,
20
+ 'soft-contract': 5,
21
+ 'mixed-pattern-cluster': 6,
22
+ };
23
+
24
+ const DEFAULT_DO_NOT = [
25
+ 'rewrite queries / touch schema / migrations',
26
+ 'weaken ark.config.json to silence the smell',
27
+ 'auto-apply as mechanical-safe or invent new mechanical-safe kinds',
28
+ 'big-bang the whole monorepo',
29
+ ];
30
+
31
+ /**
32
+ * Evidence entries that are real file paths (not layout: / layer: tokens).
33
+ * @param {string[]} evidence
34
+ * @returns {string[]}
35
+ */
36
+ export function fileEvidencePaths(evidence = []) {
37
+ return (evidence || []).filter(
38
+ (e) =>
39
+ typeof e === 'string' &&
40
+ e.length > 0 &&
41
+ !e.startsWith('layout:') &&
42
+ !e.startsWith('layer:') &&
43
+ !e.startsWith('rule:')
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Score a pattern bet for "do this pilot first".
49
+ * Prefers concrete src/ files and higher-impact smell ids.
50
+ * @param {object} bet
51
+ * @param {number} index
52
+ */
53
+ function scoreBet(bet, index) {
54
+ const files = fileEvidencePaths(bet?.evidence);
55
+ const smellPri = SMELL_PRIORITY[bet?.smellId] ?? 50;
56
+ // Higher score wins; concrete files dominate; then smell priority; stable by index.
57
+ return files.length * 100 - smellPri * 10 - index;
58
+ }
59
+
60
+ /**
61
+ * Build extraction-card fields from one pattern bet (P03/P05 vocabulary).
62
+ * @param {object} bet
63
+ * @param {string[]} [preferredFiles]
64
+ */
65
+ export function extractionCardFromBet(bet, preferredFiles) {
66
+ if (!bet || typeof bet !== 'object') return null;
67
+ const files = preferredFiles?.length
68
+ ? preferredFiles
69
+ : fileEvidencePaths(bet.evidence);
70
+ const evidence = files.length ? files : (bet.evidence || []).slice(0, 8);
71
+ const pilotTarget =
72
+ files[0] ||
73
+ (typeof bet.pilot === 'string' ? bet.pilot : null) ||
74
+ 'src/**';
75
+
76
+ return {
77
+ id: PILOT_LOOP_ID,
78
+ patternBetId: bet.id || `pattern-b:${bet.smellId || 'unknown'}`,
79
+ smellId: bet.smellId || 'unknown',
80
+ pilot: typeof bet.pilot === 'string' ? bet.pilot : pilotTarget,
81
+ pilotTarget,
82
+ evidence,
83
+ move:
84
+ typeof bet.fix === 'string' && bet.fix.trim()
85
+ ? bet.fix.trim()
86
+ : 'Apply one bounded extraction for this smell on pilot paths only',
87
+ doNot: [...DEFAULT_DO_NOT],
88
+ successSignal:
89
+ typeof bet.successSignal === 'string'
90
+ ? bet.successSignal
91
+ : 'Smell evidence paths cleared on pilot without weakening the contract',
92
+ killSwitch:
93
+ typeof bet.killSwitch === 'string'
94
+ ? bet.killSwitch
95
+ : 'If pilot increases edge violations without design clarity, stop and re-map with /ark-explore',
96
+ neverMechanicalSafe: true,
97
+ class: 'judgment',
98
+ loopStep: 'one-pilot',
99
+ reDoctor: 'ark-check --doctor --json',
100
+ rePlan: 'ark-check --plan --json',
101
+ next:
102
+ '/ark-fix (one cluster) | /ark-autopilot (user ok on B) | re-doctor after pilot',
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Select **one** next pilot from pattern bets (or build bets from smells).
108
+ * @param {object[] | null | undefined} patternBets
109
+ * @param {{ designSmells?: object[] }} [options]
110
+ * @returns {null | ReturnType<typeof extractionCardFromBet>}
111
+ */
112
+ export function selectNextPilot(patternBets, options = {}) {
113
+ let bets = Array.isArray(patternBets) ? [...patternBets] : [];
114
+ if (bets.length === 0 && Array.isArray(options.designSmells) && options.designSmells.length) {
115
+ bets = buildPatternBetsFromSmells(options.designSmells);
116
+ }
117
+ if (bets.length === 0) return null;
118
+
119
+ let best = null;
120
+ let bestScore = -Infinity;
121
+ for (let i = 0; i < bets.length; i++) {
122
+ const bet = bets[i];
123
+ if (!bet || bet.neverMechanicalSafe === false) continue;
124
+ // Skip anything that claims mechanical-safe (honesty).
125
+ if (bet.class === 'mechanical-safe') continue;
126
+ const files = fileEvidencePaths(bet.evidence);
127
+ const sc = scoreBet(bet, i);
128
+ if (sc > bestScore) {
129
+ bestScore = sc;
130
+ best = { bet, files };
131
+ }
132
+ }
133
+ if (!best) return null;
134
+ return extractionCardFromBet(best.bet, best.files);
135
+ }
136
+
137
+ /**
138
+ * Human-readable extraction card block (P05 template parity).
139
+ * @param {ReturnType<typeof extractionCardFromBet>} card
140
+ * @returns {string | null}
141
+ */
142
+ export function formatExtractionCard(card) {
143
+ if (!card) return null;
144
+ const doNot = (card.doNot || DEFAULT_DO_NOT).map((d) => ` - ${d}`).join('\n');
145
+ return [
146
+ '### Extraction card',
147
+ `Pilot: ${card.pilotTarget || card.pilot}`,
148
+ `Smell: ${card.smellId}`,
149
+ `Move: ${card.move}`,
150
+ 'Do not:',
151
+ doNot,
152
+ `Success: ${card.successSignal}`,
153
+ `Kill-switch: ${card.killSwitch}`,
154
+ `Next: ${card.next}`,
155
+ '(Q04 pilot loop: one pilot at a time → re-doctor; never mechanical-safe)',
156
+ ].join('\n');
157
+ }
158
+
159
+ /**
160
+ * Doctor/plan JSON summary of the active pilot loop step.
161
+ * @param {{
162
+ * designWeak?: boolean,
163
+ * patternBets?: object[],
164
+ * designSmells?: object[],
165
+ * }} opts
166
+ */
167
+ export function summarizePilotLoop(opts = {}) {
168
+ const designWeak = opts.designWeak === true;
169
+ if (!designWeak) {
170
+ return {
171
+ active: false,
172
+ id: PILOT_LOOP_ID,
173
+ reason: 'not-design-weak',
174
+ oneAtATime: true,
175
+ neverMechanicalSafe: true,
176
+ };
177
+ }
178
+
179
+ const nextPilot = selectNextPilot(opts.patternBets, {
180
+ designSmells: opts.designSmells,
181
+ });
182
+ if (!nextPilot) {
183
+ return {
184
+ active: false,
185
+ id: PILOT_LOOP_ID,
186
+ reason: 'no-pattern-bets',
187
+ oneAtATime: true,
188
+ neverMechanicalSafe: true,
189
+ };
190
+ }
191
+
192
+ const remaining = Array.isArray(opts.patternBets) ? opts.patternBets.length : 0;
193
+
194
+ return {
195
+ active: true,
196
+ id: PILOT_LOOP_ID,
197
+ oneAtATime: true,
198
+ neverMechanicalSafe: true,
199
+ remainingBets: remaining,
200
+ nextPilot,
201
+ instruction:
202
+ 'Apply ONE pilot from nextPilot (extraction card), then re-doctor. ' +
203
+ 'Do not multi-pilot batch. patternBets never mechanical-safe. ' +
204
+ 'Success = reduced smell evidence on pilot paths; residual outside pilot may remain.',
205
+ cardText: formatExtractionCard(nextPilot),
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Compare design-smell residual on the pilot after a single change.
211
+ * Drives real before/after smell arrays (from detectDesignSmells).
212
+ *
213
+ * @param {{
214
+ * beforeSmells: object[],
215
+ * afterSmells: object[],
216
+ * nextPilot: { smellId: string, evidence?: string[], pilotTarget?: string, pilot?: string },
217
+ * }} args
218
+ */
219
+ export function comparePilotResidual({ beforeSmells, afterSmells, nextPilot }) {
220
+ const smellId = nextPilot?.smellId;
221
+ const pilotFiles = fileEvidencePaths(nextPilot?.evidence || []);
222
+ if (nextPilot?.pilotTarget && !pilotFiles.includes(nextPilot.pilotTarget)) {
223
+ if (
224
+ typeof nextPilot.pilotTarget === 'string' &&
225
+ !nextPilot.pilotTarget.startsWith('layout:') &&
226
+ !nextPilot.pilotTarget.includes('**')
227
+ ) {
228
+ pilotFiles.push(nextPilot.pilotTarget);
229
+ }
230
+ }
231
+
232
+ const beforeSmell = (beforeSmells || []).find((s) => s.id === smellId);
233
+ const afterSmell = (afterSmells || []).find((s) => s.id === smellId);
234
+
235
+ const beforeAll = fileEvidencePaths(beforeSmell?.evidence);
236
+ const afterAll = fileEvidencePaths(afterSmell?.evidence);
237
+
238
+ // Evidence on the pilot file set (exact path match).
239
+ const beforeOnPilot = pilotFiles.length
240
+ ? pilotFiles.filter((p) => beforeAll.includes(p))
241
+ : beforeAll;
242
+ const afterOnPilot = pilotFiles.length
243
+ ? pilotFiles.filter((p) => afterAll.includes(p))
244
+ : afterAll;
245
+
246
+ const pilotSmellCleared = !afterSmell;
247
+ const reduced =
248
+ afterOnPilot.length < beforeOnPilot.length ||
249
+ (pilotSmellCleared && beforeOnPilot.length > 0);
250
+
251
+ return {
252
+ smellId,
253
+ pilotFiles,
254
+ beforeEvidenceCount: beforeOnPilot.length,
255
+ afterEvidenceCount: afterOnPilot.length,
256
+ beforeEvidence: beforeOnPilot,
257
+ afterEvidence: afterOnPilot,
258
+ beforeSmellPresent: Boolean(beforeSmell),
259
+ afterSmellPresent: Boolean(afterSmell),
260
+ pilotSmellCleared,
261
+ reduced,
262
+ // Global residual may remain — honest Shape work.
263
+ beforeSmellCount: (beforeSmells || []).length,
264
+ afterSmellCount: (afterSmells || []).length,
265
+ };
266
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Q01 — Single post-green product path (“clarify for AI” / Shape).
3
+ *
4
+ * When edges are clean but design residual remains, doctor + agent routing name
5
+ * ONE door that chains map → dual-plan B. No skill shopping; no new skill basename.
6
+ * Plan B stays never mechanical-safe.
7
+ */
8
+
9
+ /** Stable product id for JSON / tests. */
10
+ export const POST_GREEN_PATH_ID = 'clarify-for-ai';
11
+
12
+ /** Primary skill entry (map + dual-plan seed). Apply is second step of the same path. */
13
+ export const POST_GREEN_PRIMARY_SKILL = '/ark-explore';
14
+
15
+ /**
16
+ * Canonical human / agent next-action string (single door).
17
+ * Chained: explore shape-focus then autopilot only to apply B with user OK.
18
+ */
19
+ export const POST_GREEN_PRIMARY_ACTION =
20
+ 'Clarify for AI (Shape): /ark-explore shape-focus → dual-plan B, then /ark-autopilot only to apply B with your OK — never empty plan A = done; patternBets never mechanical-safe';
21
+
22
+ /** Short label for tables / metrics. */
23
+ export const POST_GREEN_PRIMARY_SHORT =
24
+ '/ark-explore shape-focus → /ark-autopilot (apply B with OK) # clarify for AI';
25
+
26
+ /**
27
+ * @param {{ designWeak?: boolean } | null | undefined} designFitness
28
+ * @returns {null | {
29
+ * id: string,
30
+ * primary: true,
31
+ * skill: string,
32
+ * applySkill: string,
33
+ * flow: string,
34
+ * action: string,
35
+ * short: string,
36
+ * neverMechanicalSafe: true,
37
+ * healthyFinishedForbidden: true,
38
+ * }}
39
+ */
40
+ export function buildPostGreenNextAction(designFitness) {
41
+ if (!designFitness?.designWeak) return null;
42
+ return {
43
+ id: POST_GREEN_PATH_ID,
44
+ primary: true,
45
+ skill: POST_GREEN_PRIMARY_SKILL,
46
+ applySkill: '/ark-autopilot',
47
+ flow: 'shape-focus',
48
+ action: POST_GREEN_PRIMARY_ACTION,
49
+ short: POST_GREEN_PRIMARY_SHORT,
50
+ neverMechanicalSafe: true,
51
+ healthyFinishedForbidden: true,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Put the single post-green door first; drop competing Shape guidance strings.
57
+ * @param {string[]} actions
58
+ * @param {ReturnType<typeof buildPostGreenNextAction>} postGreen
59
+ * @returns {string[]}
60
+ */
61
+ export function mergePostGreenTopActions(actions, postGreen) {
62
+ const list = [...(actions || [])].filter(Boolean);
63
+ if (!postGreen?.action) return [...new Set(list)];
64
+
65
+ const competing =
66
+ /\/ark-explore|\/ark-autopilot|shape residual|dual-plan B|pattern bet|shape-focus|clarify for ai|design-weak/i;
67
+ const filtered = list.filter((a) => !competing.test(a));
68
+ return [postGreen.action, ...new Set(filtered)];
69
+ }
70
+
71
+ /**
72
+ * Whether doctor may print “Healthy — nothing to do”.
73
+ * @param {{ designWeak?: boolean } | null | undefined} designFitness
74
+ * @param {string[]} topActions
75
+ */
76
+ export function isDoctorHealthyNothingToDo(designFitness, topActions = []) {
77
+ if (designFitness?.designWeak) return false;
78
+ return !topActions.some(Boolean);
79
+ }
@@ -115,6 +115,8 @@ export function composePrepareWrite(opts) {
115
115
  ...(placement?.message ? { placementMessage: placement.message } : {}),
116
116
  ...(placement?.note ? { placementNote: placement.note } : {}),
117
117
  ...(placement?.description ? { description: placement.description } : {}),
118
+ // Q03: pass through golden pattern from ark_place (advisory; absent is normal).
119
+ ...(placement?.goldenPattern ? { goldenPattern: placement.goldenPattern } : {}),
118
120
  valid: gate.valid,
119
121
  violations: gate.violations,
120
122
  ...(gate.autoPatch ? { autoPatch: gate.autoPatch } : {}),
@@ -43,10 +43,11 @@ export function detectActiveAgentHost(env = process.env) {
43
43
  .toLowerCase();
44
44
  if (explicit) return explicit;
45
45
 
46
- // Grok / xAI Build
46
+ // Grok / xAI Build (include GROK_AGENT — common session signal missing in older detect)
47
47
  if (
48
48
  envTruthy(env.GROK_BUILD) ||
49
49
  envTruthy(env.XAI_GROK) ||
50
+ envTruthy(env.GROK_AGENT) ||
50
51
  env.GROK_WORKSPACE_ROOT ||
51
52
  env.GROK_SESSION_ID
52
53
  ) {
@@ -30,21 +30,32 @@ export function detectWritePathCapabilities(root, explicitHost) {
30
30
 
31
31
  const tools = installToolsForHost(activeHost);
32
32
  let gap = null;
33
+ // Repo inventory (any host) can show hard/advisory write while activeHost is
34
+ // unknown (plain shell / `npx ark-check --report` outside an agent session).
35
+ // Session projection stays mode=none (other hosts' hooks are not a guarantee for
36
+ // this process) — but do not open an adoption gap: gates exist on disk.
37
+ const inventoryHasWriteBoundary =
38
+ Boolean(inventory?.capabilities?.['hard-write']) ||
39
+ Boolean(inventory?.capabilities?.['advisory-write']);
33
40
  if (mode === 'none') {
34
- gap = {
35
- id: 'write-path-none',
36
- severity: 'warn',
37
- message:
38
- `Active host ${activeHost} has no hard write boundary or advisory Ark MCP. ` +
39
- (capabilities['merge-gate']
40
- ? 'The CI check remains separate and does not block local writes.'
41
- : 'No Ark CI check was detected either.'),
42
- fix: arkCommand(
43
- root,
44
- 'ark-check',
45
- `--install-agent-gates --tools ${tools}`
46
- ),
47
- };
41
+ if (activeHost === 'unknown' && inventoryHasWriteBoundary) {
42
+ gap = null;
43
+ } else {
44
+ gap = {
45
+ id: 'write-path-none',
46
+ severity: 'warn',
47
+ message:
48
+ `Active host ${activeHost} has no hard write boundary or advisory Ark MCP. ` +
49
+ (capabilities['merge-gate']
50
+ ? 'The CI check remains separate and does not block local writes.'
51
+ : 'No Ark CI check was detected either.'),
52
+ fix: arkCommand(
53
+ root,
54
+ 'ark-check',
55
+ `--install-agent-gates --tools ${tools}`
56
+ ),
57
+ };
58
+ }
48
59
  } else if (mode === 'reject-only') {
49
60
  gap = {
50
61
  id: 'write-path-reject-only',
package/dist/index.cjs CHANGED
@@ -50,7 +50,7 @@ __export(gate_exports, {
50
50
  module.exports = __toCommonJS(gate_exports);
51
51
 
52
52
  // src/version.ts
53
- var version = "3.0.2";
53
+ var version = "3.0.4";
54
54
 
55
55
  // src/domain/adapterContract.ts
56
56
  var ARK_ANALYSIS_RESULT_SCHEMA_VERSION = "1.0";
package/dist/index.d.cts CHANGED
@@ -2,7 +2,7 @@ import { a as ArkConfigRule, b as ArkConfigLayer, A as ArkConfig, c as ArkConfig
2
2
  export { d as ARK_CONFIG_SCHEMA, e as ARK_CONFIG_SCHEMA_VERSION, l as loadArkConfigContract, p as parseArkConfigJson } from './configContract-BxSIwVRo.cjs';
3
3
 
4
4
  /** ArkGate library version — single source of truth. */
5
- declare const version = "3.0.2";
5
+ declare const version = "3.0.4";
6
6
 
7
7
  /** Versioned public result contract shared by every ArkGate enforcement adapter. */
8
8
  declare const ARK_ANALYSIS_RESULT_SCHEMA_VERSION: "1.0";
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { a as ArkConfigRule, b as ArkConfigLayer, A as ArkConfig, c as ArkConfig
2
2
  export { d as ARK_CONFIG_SCHEMA, e as ARK_CONFIG_SCHEMA_VERSION, l as loadArkConfigContract, p as parseArkConfigJson } from './configContract-BxSIwVRo.js';
3
3
 
4
4
  /** ArkGate library version — single source of truth. */
5
- declare const version = "3.0.2";
5
+ declare const version = "3.0.4";
6
6
 
7
7
  /** Versioned public result contract shared by every ArkGate enforcement adapter. */
8
8
  declare const ARK_ANALYSIS_RESULT_SCHEMA_VERSION: "1.0";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var version = "3.0.2";
2
+ var version = "3.0.4";
3
3
 
4
4
  // src/domain/adapterContract.ts
5
5
  var ARK_ANALYSIS_RESULT_SCHEMA_VERSION = "1.0";
@@ -82,19 +82,45 @@ explore then apply A + propose/apply-with-ok B. `/ark-loop` = plan A only. Empty
82
82
  “architecture healthy” if design-weak residual remains. Full routing table: full-install
83
83
  `AGENTS.md` / [README skill table](../README.md#other-skills-only-when-you-need-them).
84
84
 
85
- **Design fitness (3.0.1+):** after edges are clean, doctor can still report **ENFORCE · design-weak**.
85
+ **Design fitness (3.0.1+ / Phase Q 3.0.3):** after edges are clean, doctor can still report **ENFORCE · design-weak**.
86
86
 
87
87
  ```bash
88
- npx ark-check --doctor --json # doctor.designFitness + doctor.designSmells[]
89
- npx ark-check --plan --json # plan.goal.designWeak + plan.patternBets[] (never mechanical-safe)
88
+ npx ark-check --doctor --json # designFitness, designSmells[].outcome, postGreenPath, goldenPattern, pilotLoop
89
+ npx ark-check --plan --json # plan.goal.designWeak + plan.patternBets[] + plan.pilotLoop
90
90
  ```
91
91
 
92
- Smell ids (stable): `io-under-application`, `handler-in-persistence`, `god-module`,
93
- `domain-logic-in-ui`, `facade-sql-in-routes`, `mixed-pattern-cluster`, `soft-contract`.
94
- Each has `evidence[]` paths. Plan **B** bets include `pilot`, `successSignal`, `killSwitch`,
95
- and **`neverMechanicalSafe: true`** — loop/autoPatch must ignore them. For judgment I/O moves
96
- use **extraction cards** ([brownfield-adoption.md](brownfield-adoption.md) §6). Multi-PR residual
97
- may optionally be persisted as a short Shape plan under the repo; not a gate requirement.
92
+ **Post-green path (Q01):** when design-weak, doctor sets `postGreenPath` / `primaryNextAction`
93
+ (`clarify-for-ai`) — **one** Shape door: `/ark-explore` shape-focus → dual-plan B, then
94
+ `/ark-autopilot` only to apply B with your OK. Do not skill-shop coverage/think for the same residual.
95
+
96
+ **Pilot loop (Q04):** when design-weak, `pilotLoop.nextPilot` is **one** extraction card
97
+ (pilot target, move, success, kill-switch). Apply **that one pilot only**, then re-doctor.
98
+ Success = reduced smell evidence on pilot paths; residual outside the pilot may remain.
99
+ Never multi-pilot batch; never mechanical-safe; never claim healthy finished while design-weak.
100
+
101
+ **AI-velocity evidence (Q05):** deterministic fixture bench (no live LLM) compares the same
102
+ feature add on design-weak vs golden-path trees. Run `npm run eval:ai-velocity`; metric is
103
+ `placementTurns` (agent-equivalent steps to the DomainModel home). Method is stored next to
104
+ the number in `eval/ai-velocity-report.json`. See [eval/README.md](../eval/README.md).
105
+
106
+ Smell **ids** (stable JSON) plus **outcome** lines (plain language, Q02) on each
107
+ `designSmells[]` object — prefer `outcome` for humans; keep `id` for automation:
108
+
109
+ | id | Outcome (what to do / why the AI struggles) |
110
+ |----|-----------------------------------------------|
111
+ | `io-under-application` | Business code reaches DB/APIs directly — put I/O behind a port/adapter |
112
+ | `handler-in-persistence` | HTTP handlers under storage folders — move handlers to API/UI |
113
+ | `god-module` | Huge multi-job files — split the pilot by concern |
114
+ | `domain-logic-in-ui` | can*/calculate* in UI — move pure rules into Domain |
115
+ | `facade-sql-in-routes` | Routes import ORM/SQL — keep queries in repository/adapter |
116
+ | `mixed-pattern-cluster` | Several layout styles — pick one golden pattern + pilot |
117
+ | `soft-contract` | Layers without deny rules — add real walls, not soft green |
118
+
119
+ Each smell also has `evidence[]` paths and `message` (technical detail). Plan **B** bets include
120
+ `pilot`, `successSignal`, `killSwitch`, and **`neverMechanicalSafe: true`** — loop/autoPatch must
121
+ ignore them. For judgment I/O moves use **extraction cards**
122
+ ([brownfield-adoption.md](brownfield-adoption.md) §6). Multi-PR residual may optionally be
123
+ persisted as a short Shape plan under the repo; not a gate requirement.
98
124
 
99
125
  **Full-skill agent co-pilot:** after explicitly installing the `/ark-*` pack, use
100
126
  `/ark-autopilot` (explore-first, dual plan A remediation + B pattern bets). Recon without
@@ -223,16 +249,42 @@ reference, and explanation for the full path (recommend → init → gallery →
223
249
  5. Use `/ark-place` or `ark_place` for individual files after the contract exists.
224
250
  6. Verify with `ark-check --root . --config ark.config.json --strict`.
225
251
 
252
+ ### Golden pattern for new code (Q03)
253
+
254
+ When the team has picked **one** layout style for *new* files (after Shape / pilot),
255
+ you may record it as an optional side-car:
256
+
257
+ ```json
258
+ // .ark/golden-pattern.json
259
+ {
260
+ "schemaVersion": "1",
261
+ "name": "vertical-slice features",
262
+ "norm": "New features live under src/features/<slice>/; shared only in src/shared/.",
263
+ "newCodeHome": "src/features/",
264
+ "examplePath": "src/features/billing/createInvoice.ts"
265
+ }
266
+ ```
267
+
268
+ | Rule | Meaning |
269
+ |------|---------|
270
+ | **Optional** | Missing file is fine — no claim, no error. |
271
+ | **Advisory** | `ark_place` / `ark_prepare_write` and doctor attach `goldenPattern` for **new** code only. |
272
+ | **Not a gate** | Does **not** ENFORCE, does **not** clear design-weak, does not replace `ark.config.json`. |
273
+ | **Malformed** | Invalid JSON or missing `name`/`norm` → `invalid: true`; fix or delete — do not treat as guidance. |
274
+
275
+ Legacy paths stay migrate-on-touch; the golden norm limits where agents put **new** code.
276
+
226
277
  ### Write protocol (2.10+ / Track W)
227
278
 
228
279
  Prefer preparing the write before the host commits it to disk:
229
280
 
230
281
  | Surface | Role |
231
282
  |---------|------|
232
- | MCP **`ark_prepare_write`** | Place + constrain + validate + optional `autoPatch` + `judgmentBrief` + contentHash in one call |
283
+ | MCP **`ark_prepare_write`** | Place + constrain + validate + optional `autoPatch` + `judgmentBrief` + contentHash + optional `goldenPattern` in one call |
233
284
  | Write-gate **`autoPatch`** | Mechanical-safe **import type** rewrites only; post-patch revalidation green or discarded |
234
285
  | PreToolUse **`--hook-repair`** | On deny: `ARK_REPAIR_JSON` / `ARK_AUTOPATCH_JSON` on stderr (still exit 2 — never silent write) |
235
286
  | Doctor **`writePath`** | Reports `repair` \| `reject-only` \| `mcp-only` \| `none` for installed gates |
287
+ | Doctor **`goldenPattern`** | Optional Q03 advisory summary (`present` / `invalid`); never clears design-weak |
236
288
 
237
289
  Port-proof inject binding is **judgment** for auto-apply (signature/arity change), not write-path autoPatch.
238
290
  Full reference: [ai-gates.md](ai-gates.md). Loop-cost harness: `npm run eval:loop-cost`.
@@ -117,7 +117,28 @@ ark-check --doctor --json # designFitness.designWeak + designSmells[].evidence
117
117
  ark-check --plan --json # patternBets[] with neverMechanicalSafe: true
118
118
  ```
119
119
 
120
- Fixture for CI honesty: `tests/fixtures/design-weak-enforce/` (empty plan A + non-empty B).
120
+ ### Pilot loop (Q04) one pilot at a time → re-doctor
121
+
122
+ When doctor/plan report **ENFORCE · design-weak**, JSON also includes **`pilotLoop`**:
123
+
124
+ | Field | Meaning |
125
+ |-------|---------|
126
+ | `pilotLoop.active` | `true` when design-weak and at least one pattern bet exists |
127
+ | `pilotLoop.nextPilot` | **One** extraction card (same fields as §6) ranked from `patternBets` |
128
+ | `pilotLoop.oneAtATime` | Always true — do not multi-pilot batch |
129
+ | `pilotLoop.neverMechanicalSafe` | Always true — judgment only; re-doctor is the success sensor |
130
+
131
+ **Loop:**
132
+
133
+ 1. Read `pilotLoop.nextPilot` (or fill the §6 card by hand).
134
+ 2. Apply **only** that pilot (bounded path(s) in `evidence` / `pilotTarget`).
135
+ 3. Re-run `ark-check --doctor --json` (and `--plan --json`).
136
+ 4. Success on this step = **reduced smell evidence on the pilot paths** (or that smell cleared). Residual outside the pilot may remain — that is honest Shape work, not failure.
137
+ 5. Pick the next `nextPilot` only after re-doctor; stop on kill-switch.
138
+
139
+ Do **not** claim “healthy finished” while `designWeak` remains. Do **not** auto-apply pattern bets as mechanical-safe.
140
+
141
+ Fixture for CI honesty: `tests/fixtures/design-weak-enforce/` (empty plan A + non-empty B + pilotLoop).
121
142
 
122
143
  ### Optional: durable Shape plan (multi-PR)
123
144
 
@@ -15,8 +15,12 @@ This document is the consumer contract for **what is stable** vs **what is exper
15
15
  | Surface | How you use it | Stability notes |
16
16
  |---------|----------------|-----------------|
17
17
  | **CLI** | `arkgate` / `arkgate-check` (aliases `ark` / `ark-check`) | Flags and human text may improve; **JSON output shapes** for `--json` (check, doctor, plan, coverage, recommend) are stable within a major. Additive fields OK; removals/renames are major. |
18
- | **Doctor design fitness (P02+)** | `ark-check --doctor --json` → `doctor.designFitness`, `doctor.designSmells[]` | Additive. Stable smell `id`s: `io-under-application`, `handler-in-persistence`, `god-module`, `domain-logic-in-ui`, `facade-sql-in-routes`, `mixed-pattern-cluster`, `soft-contract`. Each smell has `evidence[]` paths and `fix`. Does **not** fail the gate by itself. |
18
+ | **Doctor design fitness (P02+)** | `ark-check --doctor --json` → `doctor.designFitness`, `doctor.designSmells[]` | Additive. Stable smell `id`s: `io-under-application`, `handler-in-persistence`, `god-module`, `domain-logic-in-ui`, `facade-sql-in-routes`, `mixed-pattern-cluster`, `soft-contract`. Each smell has `evidence[]`, `fix`, technical `message`, and plain-language **`outcome`** (Q02). Does **not** fail the gate by itself. |
19
+ | **Post-green path (Q01)** | `doctor.postGreenPath`, `doctor.primaryNextAction`, `doctor.healthyFinishedForbidden` | Additive when `designFitness.designWeak`. Single Shape door (`id: clarify-for-ai`): explore shape-focus → dual-plan B → autopilot only with OK. Never empty plan A = healthy finished. |
20
+ | **Golden pattern (Q03)** | Optional `.ark/golden-pattern.json`; doctor JSON `doctor.goldenPattern`; MCP `ark_place` / `ark_prepare_write` → `goldenPattern` | Additive, **advisory for NEW code only**. Required fields: `name`, `norm`; optional `newCodeHome`, `examplePath`, `schemaVersion`. **Absent is normal** (no claim). Never ENFORCE; never clears design-weak. Malformed → `invalid: true`, not silent guidance. |
19
21
  | **Plan pattern B (P03+)** | `ark-check --plan --json` → `plan.patternBets[]`, `plan.goal.designWeak` | Additive. Each bet: `id`, `smellId`, `pilot`, `evidence`, `successSignal`, `killSwitch`, **`neverMechanicalSafe: true`**, `class: "judgment"`. **Never** auto-applied by loop/autoPatch; not a `remediationKind` mechanical-safe. `goal.met` remains edge honesty only. |
22
+ | **Pilot loop (Q04)** | `plan.pilotLoop` / `doctor.pilotLoop` | Additive. When design-weak: `active`, `oneAtATime`, `neverMechanicalSafe`, **`nextPilot`** extraction-card fields (`pilotTarget`, `smellId`, `move`, `successSignal`, `killSwitch`, `doNot[]`). **One pilot → re-doctor**; never multi-pilot batch; never mechanical-safe. |
23
+ | **AI-velocity eval (Q05)** | `npm run eval:ai-velocity` → `eval/ai-velocity-report.json` | Fixture-measured (no live LLM). Same feature scenario on design-weak vs golden-path arms; metric **`placementTurns`** (agent-equivalent). Golden must be strictly better. Method string lives next to the number. Does not weaken the gate. |
20
24
  | **MCP tools** | `arkgate-mcp` / `ark://…` resources | Tool names and primary argument shapes are stable within a major. |
21
25
  | **`ark.config.json`** | Layer globs, rules, include/exclude, forbiddenGlobals, intent prefixes, `peerIsolation`, `dynamicImportAllowlist`, `safety` thresholds | Versioned by `schemaVersion`; unknown fields fail closed and migrations preserve the previous supported major. |
22
26
  | **`arkgate/schema/analysis-result`** | Public CLI/MCP/hook diagnostic envelope (`schemaVersion`, `valid`, `diagnostics`) | Versioned JSON Schema; committed v1 compatibility fixture protects rule, severity, location, and evidence fields. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkgate",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "ArkGate — architecture co-pilot for AI TypeScript (write gate, CI gate, plan/loop)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -104,6 +104,7 @@
104
104
  "eval:corpus": "node eval/validate-corpus.mjs",
105
105
  "eval:comparative": "node eval/comparative-run.mjs",
106
106
  "eval:loop-cost": "node eval/loop-cost-run.mjs",
107
+ "eval:ai-velocity": "node eval/ai-velocity-run.mjs",
107
108
  "eval:adoption": "node eval/adoption-run.mjs",
108
109
  "test:adoption-harness": "vitest run tests/unit/eval/adoptionHarness.test.ts",
109
110
  "bench:scale": "node scripts/ark-scale-bench.mjs",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/pedroknigge/arkgate",
7
7
  "source": "github"
8
8
  },
9
- "version": "3.0.2",
9
+ "version": "3.0.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "arkgate",
14
- "version": "3.0.2",
14
+ "version": "3.0.4",
15
15
  "runtimeHint": "npx",
16
16
  "transport": {
17
17
  "type": "stdio"
@@ -20,8 +20,12 @@ the explore pass and dual-plan section B (pattern / Shape bets).
20
20
  | “Make architecture sound” end-to-end | Map only, no apply → `/ark-explore` |
21
21
  | Brownfield or greenfield with apply | Only fitness numbers → `/ark-coverage` |
22
22
  | User wants A + B planned and A executed | Single edge fix → `/ark-fix`; plan A only → `/ark-loop` |
23
+ | **Apply half of Q01 post-green path** (after explore map / when user wants full apply) | Skipping explore when doctor primary is Shape map-first |
23
24
  | Spaghetti under ENFORCE: Shape work with user ok on B | Contract false-green first → `/ark-adopt` / `/ark-contract` STOP paths |
24
25
 
26
+ **Q01:** doctor’s single door is `/ark-explore` shape-focus → dual-plan B, **then** this skill only
27
+ to apply B with OK. Prefer that order when `postGreenPath` / design-weak is the primary residual.
28
+
25
29
  ## Related onboarding
26
30
 
27
31
  - **Greenfield:** `/ark-architect` or `ark-check --recommend` / `ark start`.
@@ -47,6 +51,9 @@ decision-grade explore pass **and** without opening violating files.
47
51
  4. **Open every file** in plan A `steps[]` (and `target` if present) before classifying a fix.
48
52
  5. **“Así te lo re-soluciono”** for each A cluster and each B pattern bet.
49
53
  6. Apply A → re-run ark-check → rollback on regression. **Never auto-apply B** as mechanical-safe.
54
+ 7. **Q04 pilot loop for B:** when design-weak, take **`pilotLoop.nextPilot`** (one extraction card)
55
+ → apply **only** that pilot with user OK → **re-doctor**. Never multi-pilot batch B; residual
56
+ outside the pilot may remain and must not be called “healthy finished.”
50
57
 
51
58
 
52
59
  ## Subagent fan-out (optional, host-dependent)