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.
- package/CHANGELOG.md +63 -1
- package/README.md +30 -2
- package/bin/ark-check.mjs +10 -0
- package/bin/ark-mcp.mjs +14 -7
- package/bin/lib/ai-velocity.mjs +293 -0
- package/bin/lib/ci-and-commands.mjs +8 -6
- package/bin/lib/design-smells.mjs +122 -58
- package/bin/lib/doctor-plan.mjs +104 -7
- package/bin/lib/golden-pattern.mjs +184 -0
- package/bin/lib/html-report-depth.mjs +282 -0
- package/bin/lib/html-report.mjs +214 -21
- package/bin/lib/pilot-loop.mjs +266 -0
- package/bin/lib/post-green-path.mjs +79 -0
- package/bin/lib/prepare-write.mjs +2 -0
- package/bin/lib/skill-install.mjs +2 -1
- package/bin/lib/write-path-detect.mjs +25 -14
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/agent-guide.md +62 -10
- package/docs/brownfield-adoption.md +22 -1
- package/docs/package-surface.md +5 -1
- package/package.json +2 -1
- package/server.json +2 -2
- package/templates/skills/ark-autopilot.md +7 -0
- package/templates/skills/ark-explain.md +34 -3
- package/templates/skills/ark-explore.md +15 -5
- package/templates/skills/ark-place.md +4 -1
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
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.
|
|
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.
|
|
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
package/docs/agent-guide.md
CHANGED
|
@@ -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 #
|
|
89
|
-
npx ark-check --plan --json # plan.goal.designWeak + plan.patternBets[]
|
|
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
|
-
|
|
93
|
-
`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
|
package/docs/package-surface.md
CHANGED
|
@@ -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[]`
|
|
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.
|
|
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.
|
|
9
|
+
"version": "3.0.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "arkgate",
|
|
14
|
-
"version": "3.0.
|
|
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)
|