arkgate 4.6.0 → 4.6.2

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/README.md +11 -5
  3. package/bin/ark-check-runtime.mjs +115 -128
  4. package/bin/ark-mcp-runtime.mjs +86 -48
  5. package/bin/ark.mjs +21 -78
  6. package/bin/lib/agent-projection.mjs +1 -1
  7. package/bin/lib/analysis-engine.mjs +3 -3
  8. package/bin/lib/ci-and-commands.mjs +11 -11
  9. package/bin/lib/config-contract.mjs +2 -0
  10. package/bin/lib/contract-smells.mjs +5 -5
  11. package/bin/lib/design-smells.mjs +1 -1
  12. package/bin/lib/doctor-advisories.mjs +9 -0
  13. package/bin/lib/doctor-next-actions.mjs +92 -0
  14. package/bin/lib/doctor-plan.mjs +61 -71
  15. package/bin/lib/field-install.mjs +1 -1
  16. package/bin/lib/first-run-help.mjs +221 -0
  17. package/bin/lib/html-report-advisories.mjs +20 -0
  18. package/bin/lib/improvement-compass-map.mjs +20 -20
  19. package/bin/lib/pilot-loop.mjs +1 -1
  20. package/bin/lib/post-green-path.mjs +2 -2
  21. package/bin/lib/prepare-change.mjs +9 -0
  22. package/bin/lib/product-copy.mjs +1 -1
  23. package/bin/lib/start-preview.mjs +17 -10
  24. package/bin/lib/status-command.mjs +19 -0
  25. package/bin/lib/status-manifest.mjs +23 -0
  26. package/bin/lib/team-parliament-io.mjs +338 -0
  27. package/bin/lib/team-parliament.mjs +383 -0
  28. package/bin/lib/upgrade-whats-new.mjs +16 -0
  29. package/bin/lib/violations.mjs +8 -4
  30. package/dist/{configTypes-CC0FEXoF.d.ts → configTypes-B8uIcLaG.d.ts} +5 -0
  31. package/dist/eslint/index.cjs +2 -2
  32. package/dist/eslint/index.d.ts +1 -1
  33. package/dist/eslint/index.js +2 -2
  34. package/dist/index.cjs +8 -8
  35. package/dist/index.d.ts +66 -2
  36. package/dist/index.js +8 -8
  37. package/docs/README.md +5 -5
  38. package/docs/agent-guide.md +21 -17
  39. package/docs/configuration.md +36 -2
  40. package/docs/develop.md +13 -1
  41. package/docs/enthusiast/README.md +1 -1
  42. package/docs/enthusiast/how-to-agent-gates.md +3 -3
  43. package/docs/enthusiast/how-to-pick-shape.md +2 -2
  44. package/docs/enthusiast/tutorial-first-project.md +4 -3
  45. package/docs/package-surface.md +5 -3
  46. package/docs/product-voice.md +48 -10
  47. package/docs/use.md +10 -5
  48. package/package.json +2 -2
  49. package/schemas/ark.config.schema.json +9 -0
  50. package/schemas/ark.status-manifest.schema.json +51 -0
  51. package/server.json +3 -3
  52. package/templates/agent-skills/README.md +1 -1
  53. package/templates/agent-skills/ark-adopt/SKILL.md +59 -23
  54. package/templates/agent-skills/ark-architect/SKILL.md +24 -145
  55. package/templates/agent-skills/ark-autopilot/SKILL.md +49 -32
  56. package/templates/agent-skills/ark-contract/SKILL.md +21 -105
  57. package/templates/agent-skills/ark-coverage/SKILL.md +7 -3
  58. package/templates/agent-skills/ark-explain/SKILL.md +8 -4
  59. package/templates/agent-skills/ark-explore/SKILL.md +38 -21
  60. package/templates/agent-skills/ark-fix/SKILL.md +34 -157
  61. package/templates/agent-skills/ark-loop/SKILL.md +31 -153
  62. package/templates/agent-skills/ark-place/SKILL.md +35 -14
  63. package/templates/agent-skills/ark-runtime/SKILL.md +3 -3
  64. package/templates/agent-skills/ark-think/SKILL.md +6 -2
  65. package/templates/agent-skills/ark-upgrade/SKILL.md +21 -10
  66. package/templates/skills/ark-adopt.md +59 -23
  67. package/templates/skills/ark-architect.md +24 -145
  68. package/templates/skills/ark-autopilot.md +49 -32
  69. package/templates/skills/ark-contract.md +21 -105
  70. package/templates/skills/ark-coverage.md +7 -3
  71. package/templates/skills/ark-explain.md +8 -4
  72. package/templates/skills/ark-explore.md +38 -21
  73. package/templates/skills/ark-fix.md +34 -157
  74. package/templates/skills/ark-loop.md +31 -153
  75. package/templates/skills/ark-place.md +35 -14
  76. package/templates/skills/ark-runtime.md +3 -3
  77. package/templates/skills/ark-think.md +6 -2
  78. package/templates/skills/ark-upgrade.md +21 -10
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Team parliament I/O — git base refs, changed paths, pin/contract/baseline compare.
3
+ * Pure classification lives in team-parliament.mjs (Domain).
4
+ */
5
+ import { spawnSync } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { createHash } from 'node:crypto';
9
+ import {
10
+ baselineKeysFromDocument,
11
+ classifyBaselineKeyDelta,
12
+ classifyChangeSet,
13
+ evaluateTeamGate,
14
+ formatVsBaseLine,
15
+ isTeamPersona,
16
+ mapPolicyClassToKind,
17
+ parseCodeownersHandles,
18
+ personaCheckBudget,
19
+ resolveStewardHandle,
20
+ suggestStewards,
21
+ } from './team-parliament.mjs';
22
+
23
+ export {
24
+ baselineKeysFromDocument,
25
+ classifyBaselineKeyDelta,
26
+ classifyChangeSet,
27
+ evaluateTeamGate,
28
+ formatVsBaseLine,
29
+ isTeamPersona,
30
+ mapPolicyClassToKind,
31
+ parseCodeownersHandles,
32
+ personaCheckBudget,
33
+ resolveStewardHandle,
34
+ suggestStewards,
35
+ };
36
+
37
+ function runGit(cwd, args) {
38
+ return spawnSync('git', ['-C', cwd, ...args], {
39
+ encoding: 'utf8',
40
+ stdio: ['ignore', 'pipe', 'pipe'],
41
+ });
42
+ }
43
+
44
+ export function safeGitRef(value) {
45
+ return (
46
+ typeof value === 'string' &&
47
+ /^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/.test(value) &&
48
+ !value.includes('..')
49
+ );
50
+ }
51
+
52
+ export function contractSessionFrom(args, env = process.env) {
53
+ if (args?.contractSession === true) return true;
54
+ const raw = env.ARK_CONTRACT_SESSION;
55
+ return raw === '1' || raw === 'true';
56
+ }
57
+
58
+ export function resolveTeamAuthor(args, env = process.env) {
59
+ return resolveStewardHandle({
60
+ explicit: typeof args?.author === 'string' ? args.author : null,
61
+ githubActor: env.GITHUB_ACTOR,
62
+ arkSteward: env.ARK_STEWARD,
63
+ authorEmail: env.GIT_AUTHOR_EMAIL,
64
+ gitName: env.GIT_AUTHOR_NAME,
65
+ });
66
+ }
67
+
68
+ export function discoverTeamBaseRef(root, preferred) {
69
+ if (safeGitRef(preferred)) return preferred;
70
+ const candidates = ['origin/dev', 'origin/main', 'origin/master', 'dev', 'main'];
71
+ for (const candidate of candidates) {
72
+ const exists = runGit(root, ['rev-parse', '--verify', `${candidate}^{commit}`]);
73
+ if (exists.status === 0) return candidate;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ export function listChangedPaths(root, baseRef) {
79
+ if (!safeGitRef(baseRef)) {
80
+ return { ok: false, paths: [], error: 'Invalid or missing git base ref.' };
81
+ }
82
+ const verify = runGit(root, ['rev-parse', '--verify', `${baseRef}^{commit}`]);
83
+ if (verify.status !== 0) {
84
+ return { ok: false, paths: [], error: `Cannot resolve git ref ${baseRef}.` };
85
+ }
86
+ const diff = runGit(root, ['diff', '--name-only', `${baseRef}...HEAD`]);
87
+ const unstaged = runGit(root, ['diff', '--name-only']);
88
+ const staged = runGit(root, ['diff', '--name-only', '--cached']);
89
+ const untracked = runGit(root, ['ls-files', '--others', '--exclude-standard']);
90
+ if (diff.status !== 0) {
91
+ return { ok: false, paths: [], error: diff.stderr.trim() || `git diff ${baseRef} failed.` };
92
+ }
93
+ const paths = [
94
+ ...diff.stdout.split('\n'),
95
+ ...unstaged.stdout.split('\n'),
96
+ ...staged.stdout.split('\n'),
97
+ ...untracked.stdout.split('\n'),
98
+ ]
99
+ .map((line) => line.trim().replace(/\\/g, '/'))
100
+ .filter(Boolean);
101
+ return { ok: true, paths: [...new Set(paths)].sort(), error: null };
102
+ }
103
+
104
+ export function gitShowText(root, baseRef, relPath) {
105
+ if (!safeGitRef(baseRef) || !relPath) return null;
106
+ const shown = runGit(root, ['show', `${baseRef}:${relPath}`]);
107
+ if (shown.status !== 0) return null;
108
+ return shown.stdout;
109
+ }
110
+
111
+ export function readJsonMaybe(text) {
112
+ if (typeof text !== 'string' || !text.trim()) return null;
113
+ try {
114
+ return JSON.parse(text);
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ export function arkgatePinFromPackageJson(raw) {
121
+ if (!raw || typeof raw !== 'object') return null;
122
+ const bags = [raw.dependencies, raw.devDependencies, raw.optionalDependencies];
123
+ for (const bag of bags) {
124
+ if (bag && typeof bag.arkgate === 'string' && bag.arkgate.trim()) return bag.arkgate.trim();
125
+ }
126
+ if (raw.name === 'arkgate' && typeof raw.version === 'string') return raw.version;
127
+ return null;
128
+ }
129
+
130
+ function sha256Text(text) {
131
+ return `sha256:${createHash('sha256').update(text, 'utf8').digest('hex')}`;
132
+ }
133
+
134
+ export function collectVsBaseFacts({ root, baseRef, configRel = 'ark.config.json' }) {
135
+ const localConfigPath = path.join(root, configRel);
136
+ const localPkgPath = path.join(root, 'package.json');
137
+ const localBaselinePath = path.join(root, '.ark-baseline.json');
138
+ const localConfig = fs.existsSync(localConfigPath) ? fs.readFileSync(localConfigPath, 'utf8') : '';
139
+ const localPkg = fs.existsSync(localPkgPath)
140
+ ? readJsonMaybe(fs.readFileSync(localPkgPath, 'utf8'))
141
+ : null;
142
+ const localBaseline = fs.existsSync(localBaselinePath)
143
+ ? readJsonMaybe(fs.readFileSync(localBaselinePath, 'utf8'))
144
+ : null;
145
+ const baseConfig = gitShowText(root, baseRef, configRel) ?? '';
146
+ const basePkg = readJsonMaybe(gitShowText(root, baseRef, 'package.json'));
147
+ const baseBaseline = readJsonMaybe(gitShowText(root, baseRef, '.ark-baseline.json'));
148
+ const localKeys = baselineKeysFromDocument(localBaseline);
149
+ const baseKeys = baselineKeysFromDocument(baseBaseline);
150
+ const grew = classifyBaselineKeyDelta(baseKeys, localKeys).grow.length > 0;
151
+ const facts = {
152
+ baseRef,
153
+ pinLocal: arkgatePinFromPackageJson(localPkg),
154
+ pinBase: arkgatePinFromPackageJson(basePkg),
155
+ contractEqual: Boolean(localConfig) && localConfig === baseConfig,
156
+ baselineGrew: grew,
157
+ };
158
+ if (localConfig && baseConfig && !facts.contractEqual) {
159
+ facts.contractEqual = sha256Text(localConfig) === sha256Text(baseConfig);
160
+ }
161
+ return { ...facts, line: formatVsBaseLine(facts) };
162
+ }
163
+
164
+ export function teamStewardsFromConfig(config) {
165
+ return Array.isArray(config?.stewards) ? config.stewards.filter((s) => typeof s === 'string') : [];
166
+ }
167
+
168
+ export function applyPersonaDefaults(args) {
169
+ const next = { ...args };
170
+ if (!isTeamPersona(next.persona)) return next;
171
+ const budget = personaCheckBudget(next.persona);
172
+ if (budget.scan === 'none' || budget.scan === 'changed' || budget.scan === 'changed+ungoverned') {
173
+ next.changed = true;
174
+ }
175
+ if (budget.scan === 'changed+ungoverned') next.failUngoverned = true;
176
+ if (budget.contractDiff) next.contractDiff = true;
177
+ if (next.persona === 'steward') next.contractSession = true;
178
+ return next;
179
+ }
180
+
181
+ export function bindTeamBaseRefs(args, root) {
182
+ const next = applyPersonaDefaults(args);
183
+ const teamBase = discoverTeamBaseRef(
184
+ root,
185
+ next.base || next.against || next.policyBaseRef || next.baseRef
186
+ );
187
+ if (teamBase) {
188
+ if (!next.against && (next.changed || next.contractDiff || next.persona)) {
189
+ next.against = teamBase;
190
+ }
191
+ if (!next.policyBaseRef && (next.contractDiff || next.changed || next.against)) {
192
+ next.policyBaseRef = teamBase;
193
+ }
194
+ if (!next.baseRef && next.failOnNewSmells) next.baseRef = teamBase;
195
+ }
196
+ return { args: next, teamBase };
197
+ }
198
+
199
+ export function teamCheckRequested(args, config) {
200
+ return Boolean(
201
+ args.changed ||
202
+ args.contractDiff ||
203
+ args.against ||
204
+ args.persona ||
205
+ args.contractSession ||
206
+ (args.strictMerge && teamStewardsFromConfig(config).length > 0)
207
+ );
208
+ }
209
+
210
+ export function runTeamPreflight({ root, args, config, policyDelta, teamBase }) {
211
+ if (!teamCheckRequested(args, config)) {
212
+ return { halt: null, teamParliament: null, changedPaths: [] };
213
+ }
214
+ const againstRef = args.against || teamBase;
215
+ const listed = againstRef ? listChangedPaths(root, againstRef) : { ok: true, paths: [], error: null };
216
+ const changedPaths = listed.ok ? listed.paths : [];
217
+ const changeSet = classifyChangeSet(changedPaths);
218
+ const baseBaselineRaw = againstRef
219
+ ? readJsonMaybe(gitShowText(root, againstRef, '.ark-baseline.json'))
220
+ : null;
221
+ const localBaselinePath = path.join(root, args.baseline || '.ark-baseline.json');
222
+ const localBaselineRaw = fs.existsSync(localBaselinePath)
223
+ ? readJsonMaybe(fs.readFileSync(localBaselinePath, 'utf8'))
224
+ : null;
225
+ const baselineDelta = classifyBaselineKeyDelta(
226
+ baselineKeysFromDocument(baseBaselineRaw),
227
+ baselineKeysFromDocument(localBaselineRaw)
228
+ );
229
+ const policyKind = mapPolicyClassToKind(policyDelta?.classification ?? null);
230
+ const verdict = evaluateTeamGate({
231
+ changeSet,
232
+ contractSession: contractSessionFrom(args),
233
+ policyKind,
234
+ baselineGrowCount: baselineDelta.grow.length,
235
+ stewards: teamStewardsFromConfig(config),
236
+ author: resolveTeamAuthor(args),
237
+ });
238
+ const teamParliament = {
239
+ baseRef: againstRef,
240
+ changeSet,
241
+ policyKind,
242
+ baselineGrow: baselineDelta.grow.length,
243
+ baselineShrink: baselineDelta.shrink.length,
244
+ ...verdict,
245
+ changedPathError: listed.ok ? null : listed.error,
246
+ };
247
+ if (!listed.ok && (args.changed || args.against || args.contractDiff)) {
248
+ return {
249
+ halt: { exitCode: 2, message: listed.error || 'Cannot resolve team base ref.', teamParliament },
250
+ teamParliament,
251
+ changedPaths,
252
+ };
253
+ }
254
+ if (verdict.deny) {
255
+ return {
256
+ halt: { exitCode: 1, message: verdict.message, teamParliament, fail: true },
257
+ teamParliament,
258
+ changedPaths,
259
+ };
260
+ }
261
+ if (args.changed && changeSet.productPaths.length === 0 && !changeSet.hasLaw) {
262
+ return {
263
+ halt: { exitCode: 0, cheap: true, teamParliament },
264
+ teamParliament,
265
+ changedPaths,
266
+ };
267
+ }
268
+ return { halt: null, teamParliament, changedPaths };
269
+ }
270
+
271
+ export function ungovernedDumpMessage(dumped) {
272
+ return `New ungoverned source in this diff: ${dumped.slice(0, 8).join(', ')}${dumped.length > 8 ? '…' : ''}. Classify via /ark-adopt (contract session) or move into a governed layer.`;
273
+ }
274
+
275
+ export function filterChangedGovernedFiles(allGovernedFiles, root, changedPaths, normalizeRel) {
276
+ if (!changedPaths?.length) return allGovernedFiles;
277
+ const changedSet = new Set(changedPaths);
278
+ return allGovernedFiles.filter((abs) => changedSet.has(normalizeRel(path.relative(root, abs))));
279
+ }
280
+
281
+ export function applyAgainstRatchet({
282
+ violations,
283
+ againstRef,
284
+ root,
285
+ changed,
286
+ changedPaths,
287
+ occurrenceKeys,
288
+ }) {
289
+ const baseRaw = readJsonMaybe(gitShowText(root, againstRef, '.ark-baseline.json'));
290
+ const baseKeys = new Set(baselineKeysFromDocument(baseRaw));
291
+ const vsBaseActive = violations.filter((_, index) => !baseKeys.has(occurrenceKeys[index]));
292
+ const changedSet = new Set(changedPaths ?? []);
293
+ const activeViolations = changed
294
+ ? vsBaseActive.filter((violation) =>
295
+ changedSet.has(String(violation.file || '').replace(/\\/g, '/'))
296
+ )
297
+ : vsBaseActive;
298
+ return {
299
+ activeViolations,
300
+ suppressed: violations.filter((violation) => !activeViolations.includes(violation)),
301
+ };
302
+ }
303
+
304
+ function gitAuthors(root) {
305
+ const log = runGit(root, ['log', '--format=%aN<%aE>', '--max-count=300']);
306
+ if (log.status !== 0) return [];
307
+ const ids = [];
308
+ for (const line of log.stdout.split('\n')) {
309
+ const match = line.trim().match(/^(.*)<([^>]+)>$/);
310
+ if (!match) continue;
311
+ const name = match[1].trim();
312
+ const email = match[2].trim();
313
+ ids.push(email || name);
314
+ }
315
+ return ids;
316
+ }
317
+
318
+ function readCodeowners(root) {
319
+ for (const rel of ['CODEOWNERS', '.github/CODEOWNERS', 'docs/CODEOWNERS']) {
320
+ const full = path.join(root, rel);
321
+ if (!fs.existsSync(full)) continue;
322
+ try {
323
+ return parseCodeownersHandles(fs.readFileSync(full, 'utf8'));
324
+ } catch {
325
+ return [];
326
+ }
327
+ }
328
+ return [];
329
+ }
330
+
331
+ /** Advisory only. Never flips a gate. Missing git is honest empty, not green. */
332
+ export function collectStewardNudge(root, config) {
333
+ return suggestStewards({
334
+ existingStewards: teamStewardsFromConfig(config),
335
+ gitAuthors: gitAuthors(root),
336
+ codeowners: readCodeowners(root),
337
+ });
338
+ }