arkgate 4.2.1 → 4.4.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +85 -3
  2. package/README.md +24 -8
  3. package/bin/ark-check-runtime.mjs +16 -1
  4. package/bin/ark-mcp-runtime.mjs +64 -0
  5. package/bin/ark.mjs +55 -1
  6. package/bin/lib/adapter-contract.mjs +88 -5
  7. package/bin/lib/agent-projection-command.mjs +396 -0
  8. package/bin/lib/agent-projection.mjs +319 -0
  9. package/bin/lib/agent-skills-package.mjs +266 -0
  10. package/bin/lib/baseline-key.mjs +32 -0
  11. package/bin/lib/ci-and-commands.mjs +55 -5
  12. package/bin/lib/diagnostic-catalog.mjs +155 -0
  13. package/bin/lib/doctor-plan.mjs +25 -0
  14. package/bin/lib/html-report-advisories.mjs +33 -0
  15. package/bin/lib/html-report-depth.mjs +24 -0
  16. package/bin/lib/improvement-compass-doctor.mjs +106 -0
  17. package/bin/lib/improvement-compass.mjs +630 -0
  18. package/bin/lib/status-command.mjs +369 -0
  19. package/bin/lib/status-manifest.mjs +431 -0
  20. package/dist/eslint/index.cjs +3 -3
  21. package/dist/eslint/index.js +3 -3
  22. package/dist/index.cjs +46 -11
  23. package/dist/index.d.ts +886 -6
  24. package/dist/index.js +46 -11
  25. package/docs/README.md +9 -8
  26. package/docs/agent-guide.md +128 -14
  27. package/docs/configuration.md +7 -0
  28. package/docs/develop.md +12 -1
  29. package/docs/diagnostics.md +606 -0
  30. package/docs/package-surface.md +44 -31
  31. package/docs/product-voice.md +71 -0
  32. package/docs/use.md +60 -1
  33. package/package.json +7 -1
  34. package/schemas/ark.analysis-result.schema.json +14 -1
  35. package/schemas/ark.status-manifest.schema.json +270 -0
  36. package/server.json +2 -2
  37. package/templates/agent-skills/README.md +59 -0
  38. package/templates/agent-skills/ark-adopt/SKILL.md +191 -0
  39. package/templates/agent-skills/ark-architect/SKILL.md +195 -0
  40. package/templates/agent-skills/ark-autopilot/SKILL.md +262 -0
  41. package/templates/agent-skills/ark-contract/SKILL.md +156 -0
  42. package/templates/agent-skills/ark-coverage/SKILL.md +187 -0
  43. package/templates/agent-skills/ark-explain/SKILL.md +230 -0
  44. package/templates/agent-skills/ark-explore/SKILL.md +397 -0
  45. package/templates/agent-skills/ark-fix/SKILL.md +205 -0
  46. package/templates/agent-skills/ark-loop/SKILL.md +200 -0
  47. package/templates/agent-skills/ark-place/SKILL.md +182 -0
  48. package/templates/agent-skills/ark-runtime/SKILL.md +127 -0
  49. package/templates/agent-skills/ark-think/SKILL.md +153 -0
  50. package/templates/agent-skills/ark-upgrade/SKILL.md +238 -0
  51. package/templates/skills/ark-adopt.md +20 -0
  52. package/templates/skills/ark-architect.md +21 -1
  53. package/templates/skills/ark-autopilot.md +25 -5
  54. package/templates/skills/ark-contract.md +20 -0
  55. package/templates/skills/ark-coverage.md +20 -0
  56. package/templates/skills/ark-explain.md +20 -0
  57. package/templates/skills/ark-explore.md +23 -3
  58. package/templates/skills/ark-fix.md +22 -2
  59. package/templates/skills/ark-loop.md +22 -2
  60. package/templates/skills/ark-place.md +20 -0
  61. package/templates/skills/ark-runtime.md +7 -0
  62. package/templates/skills/ark-think.md +20 -0
  63. package/templates/skills/ark-upgrade.md +20 -0
@@ -0,0 +1,369 @@
1
+ /**
2
+ * ACS03 — gather session/project evidence for `ark status` / MCP `ark_status`.
3
+ *
4
+ * Fail-closed and CI-safe: never prompts (no readline), never invents hard write,
5
+ * never invents a numeric score. Pure assembly lives in Domain statusManifest.
6
+ */
7
+ import { createHash } from 'node:crypto';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ import { buildStatusManifest } from './status-manifest.mjs';
13
+ import { createProjectId } from './project-identity.mjs';
14
+ import { resolveEffectiveProjectRoot } from './project-root.mjs';
15
+ import { detectWritePathCapabilities } from './write-path-detect.mjs';
16
+ import { buildWritePathHonesty } from './enforcement-honesty.mjs';
17
+ import { HOST_SUPPORT_MATRIX } from './host-support-matrix.mjs';
18
+ import { detectActiveAgentHost } from './skill-install.mjs';
19
+ import { readBaseline } from './violations.mjs';
20
+ import { reportsDir, readJsonSafe } from './html-report.mjs';
21
+ import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
22
+
23
+ function sha256Hex(value) {
24
+ return createHash('sha256').update(value, 'utf8').digest('hex');
25
+ }
26
+
27
+ function packageVersion() {
28
+ try {
29
+ const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
30
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
31
+ return typeof pkg.version === 'string' ? pkg.version : 'unknown';
32
+ } catch {
33
+ return 'unknown';
34
+ }
35
+ }
36
+
37
+ function realpathOrResolve(p) {
38
+ try {
39
+ return fs.realpathSync(p);
40
+ } catch {
41
+ return path.resolve(p);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Classify expectedRoot against resolvedRoot after canonicalization.
47
+ * @param {string} resolvedRoot
48
+ * @param {string|undefined} expectedRoot
49
+ * @returns {'exact'|'descendant'|'outside'|'unknown'}
50
+ */
51
+ export function classifyExpectedRootRelation(resolvedRoot, expectedRoot) {
52
+ if (expectedRoot == null || expectedRoot === '') return 'unknown';
53
+ if (typeof expectedRoot !== 'string' || !path.isAbsolute(expectedRoot)) return 'unknown';
54
+ let expected;
55
+ let resolved;
56
+ try {
57
+ expected = realpathOrResolve(expectedRoot);
58
+ resolved = realpathOrResolve(resolvedRoot);
59
+ } catch {
60
+ return 'unknown';
61
+ }
62
+ if (expected === resolved) return 'exact';
63
+ const rel = path.relative(resolved, expected);
64
+ if (rel === '') return 'exact';
65
+ if (!rel.startsWith('..') && !path.isAbsolute(rel)) return 'descendant';
66
+ return 'outside';
67
+ }
68
+
69
+ /**
70
+ * Count baseline keys that belong to the ArkRules plane only.
71
+ * Full baseline size is used for lastCheck.frozenResidual (all frozen debt);
72
+ * rules.frozenResidual must not over-count layer/capability freezes as ArkRules residual.
73
+ * @param {{ exists?: boolean, keys?: Set<string>|Iterable<string> }|null|undefined} baseline
74
+ * @returns {number|null}
75
+ */
76
+ export function countArkruleFrozenKeys(baseline) {
77
+ if (!baseline?.exists || !baseline.keys) return baseline?.exists ? 0 : null;
78
+ let n = 0;
79
+ for (const key of baseline.keys) {
80
+ if (typeof key !== 'string' || key.length === 0) continue;
81
+ // baselineKey format: ruleId|file|fromLayer|toLayer|target
82
+ if (
83
+ key.startsWith('ARKRULE_') ||
84
+ key.startsWith('INVARIANT_UNCOVERED|')
85
+ ) {
86
+ n += 1;
87
+ }
88
+ }
89
+ return n;
90
+ }
91
+
92
+ /**
93
+ * Map latest report snapshot + baseline into last-check facts.
94
+ * @param {object|null} latest
95
+ * @param {{ exists: boolean, keys: Set<string> }} baseline
96
+ */
97
+ export function lastCheckFactsFromSnapshot(latest, baseline) {
98
+ const frozenResidual = baseline?.exists ? baseline.keys.size : null;
99
+ if (!latest || typeof latest !== 'object') {
100
+ return {
101
+ lastCheckAt: null,
102
+ lastCheckVerdict: null,
103
+ activeViolations: null,
104
+ frozenResidual,
105
+ };
106
+ }
107
+ const at =
108
+ typeof latest.generatedAt === 'string'
109
+ ? latest.generatedAt
110
+ : typeof latest.at === 'string'
111
+ ? latest.at
112
+ : null;
113
+ const active =
114
+ typeof latest.activeViolations === 'number'
115
+ ? latest.activeViolations
116
+ : typeof latest.violations?.active === 'number'
117
+ ? latest.violations.active
118
+ : null;
119
+ let verdict = null;
120
+ if (latest.ok === true && (active == null || active === 0)) verdict = 'pass';
121
+ else if (latest.ok === false || (typeof active === 'number' && active > 0)) verdict = 'fail';
122
+ else if (latest.completeness === 'partial' || latest.completeness === 'unavailable') {
123
+ verdict = 'incomplete';
124
+ } else if (latest.ok === true) verdict = 'pass';
125
+ return {
126
+ lastCheckAt: at,
127
+ lastCheckVerdict: verdict,
128
+ activeViolations: active,
129
+ frozenResidual,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Collect status facts from disk (no prompts).
135
+ * @param {{
136
+ * root?: string,
137
+ * config?: string,
138
+ * expectedRoot?: string,
139
+ * expectedProjectId?: string,
140
+ * host?: string,
141
+ * arkgateVersion?: string,
142
+ * env?: NodeJS.ProcessEnv,
143
+ * }} [options]
144
+ */
145
+ export function collectStatusFacts(options = {}) {
146
+ const startRoot = path.resolve(options.root || process.cwd());
147
+ const configName = options.config || 'ark.config.json';
148
+ const resolved = resolveEffectiveProjectRoot(startRoot, {
149
+ configName,
150
+ writeMode: false,
151
+ });
152
+ const resolvedRoot = path.resolve(resolved.root || startRoot);
153
+ const configPath = resolved.configFound
154
+ ? path.resolve(resolved.configPath)
155
+ : path.join(resolvedRoot, typeof configName === 'string' ? path.basename(configName) : 'ark.config.json');
156
+ const configExists = fs.existsSync(configPath);
157
+
158
+ let config = null;
159
+ if (configExists) {
160
+ try {
161
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
162
+ } catch {
163
+ config = null;
164
+ }
165
+ }
166
+
167
+ const projectId = configExists
168
+ ? createProjectId(realpathOrResolve(resolvedRoot), realpathOrResolve(configPath), sha256Hex)
169
+ : null;
170
+
171
+ const expectation =
172
+ options.expectedRoot != null || options.expectedProjectId != null
173
+ ? {
174
+ ...(options.expectedRoot != null ? { expectedRoot: options.expectedRoot } : {}),
175
+ ...(options.expectedProjectId != null
176
+ ? { expectedProjectId: options.expectedProjectId }
177
+ : {}),
178
+ }
179
+ : null;
180
+
181
+ const expectedRootRelation = expectation?.expectedRoot
182
+ ? classifyExpectedRootRelation(resolvedRoot, expectation.expectedRoot)
183
+ : null;
184
+
185
+ const env = options.env || process.env;
186
+ const activeHost =
187
+ (typeof options.host === 'string' && options.host.trim()) ||
188
+ detectActiveAgentHost(env) ||
189
+ 'unknown';
190
+
191
+ let writePath = null;
192
+ let writePathUnavailable = false;
193
+ try {
194
+ writePath = detectWritePathCapabilities(resolvedRoot, activeHost === 'unknown' ? undefined : activeHost);
195
+ } catch {
196
+ writePathUnavailable = true;
197
+ }
198
+
199
+ const hardWriteActive = writePath?.enforcementState?.localWrite?.hard === true;
200
+ const hostKey =
201
+ typeof writePath?.activeHost === 'string'
202
+ ? writePath.activeHost.trim().toLowerCase()
203
+ : String(activeHost).trim().toLowerCase();
204
+ const matrix = HOST_SUPPORT_MATRIX[hostKey] ?? null;
205
+ // Soft only when the host is known and matrix hard-write is false (Cursor/Codex/OpenCode).
206
+ const softWriteHost = Boolean(matrix && matrix.capabilities?.['hard-write'] !== true);
207
+ const writePathHonesty = buildWritePathHonesty(hostKey, hardWriteActive, {
208
+ packageInstalled: writePath?.enforcementState?.localWrite?.installed !== false,
209
+ });
210
+
211
+ const latestPath = path.join(reportsDir(resolvedRoot), 'latest.json');
212
+ const latest = readJsonSafe(latestPath);
213
+ const baseline = readBaseline(resolvedRoot, '.ark-baseline.json');
214
+ const lastCheck = lastCheckFactsFromSnapshot(latest, baseline);
215
+
216
+ const arkRulesLoaded = Boolean(
217
+ config?.arkRules && typeof config.arkRules === 'object' && Object.keys(config.arkRules).length > 0
218
+ );
219
+
220
+ let rulesInventoried = null;
221
+ let rulesUnderContract = null;
222
+ let rulesFrozenResidual = null;
223
+ if (configExists && config) {
224
+ try {
225
+ const summary = summarizeRulesUnderContract(resolvedRoot, config);
226
+ if (summary?.active === false) {
227
+ rulesInventoried = 0;
228
+ rulesUnderContract = 0;
229
+ rulesFrozenResidual = 0;
230
+ } else if (summary && summary.active !== false) {
231
+ const structure = Number(summary.structureRules) || 0;
232
+ const invariants = Number(summary.invariants) || 0;
233
+ const covered = Number(summary.coveredInvariants) || 0;
234
+ rulesInventoried = structure + invariants;
235
+ rulesUnderContract = structure + covered;
236
+ rulesFrozenResidual = baseline.exists ? countArkruleFrozenKeys(baseline) : 0;
237
+ }
238
+ } catch {
239
+ // Counts stay null when inventory cannot be loaded — honest absence, not zero score.
240
+ }
241
+ }
242
+
243
+ const arkruleFrozenFallback = countArkruleFrozenKeys(baseline);
244
+
245
+ return {
246
+ arkgateVersion: options.arkgateVersion || packageVersion(),
247
+ resolvedRoot: realpathOrResolve(resolvedRoot),
248
+ resolvedConfigPath: configExists ? realpathOrResolve(configPath) : null,
249
+ projectId,
250
+ expectation,
251
+ expectedRootRelation,
252
+ activeHost: hostKey || null,
253
+ hardWriteActive,
254
+ softWriteHost: softWriteHost || writePathHonesty.softWriteHost === true,
255
+ writePathUnavailable,
256
+ honestLabel: writePathHonesty.message || null,
257
+ lastCheckAt: lastCheck.lastCheckAt,
258
+ lastCheckVerdict: lastCheck.lastCheckVerdict,
259
+ activeViolations: lastCheck.activeViolations,
260
+ frozenResidual: lastCheck.frozenResidual,
261
+ arkRulesLoaded,
262
+ rulesInventoried,
263
+ rulesUnderContract,
264
+ rulesFrozenResidual:
265
+ rulesFrozenResidual != null
266
+ ? rulesFrozenResidual
267
+ : arkRulesLoaded && arkruleFrozenFallback != null
268
+ ? arkruleFrozenFallback
269
+ : arkRulesLoaded
270
+ ? 0
271
+ : null,
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Build the public status manifest for a project root.
277
+ * @param {Parameters<typeof collectStatusFacts>[0]} [options]
278
+ */
279
+ export function buildProjectStatusManifest(options = {}) {
280
+ return buildStatusManifest(collectStatusFacts(options));
281
+ }
282
+
283
+ /**
284
+ * CLI entry: always non-interactive. Prefer JSON for agents; human lines without --json.
285
+ * Exit 0 when identity is not mismatch; exit 1 on mismatch (stale / wrong project).
286
+ * @param {{
287
+ * root?: string,
288
+ * config?: string,
289
+ * json?: boolean,
290
+ * expectedRoot?: string,
291
+ * expectedProjectId?: string,
292
+ * host?: string,
293
+ * arkgateVersion?: string,
294
+ * write?: (line: string) => void,
295
+ * writeErr?: (line: string) => void,
296
+ * }} args
297
+ */
298
+ export function runStatusCommand(args = {}) {
299
+ const write = args.write ?? ((line) => console.log(line));
300
+ const writeErr = args.writeErr ?? ((line) => console.error(line));
301
+
302
+ // CI / non-TTY: never hang. Status never uses readline.
303
+ const asJson = args.json === true || process.env.CI === '1' || process.env.CI === 'true';
304
+
305
+ try {
306
+ const manifest = buildProjectStatusManifest({
307
+ root: args.root,
308
+ config: args.config,
309
+ expectedRoot: args.expectedRoot,
310
+ expectedProjectId: args.expectedProjectId,
311
+ host: args.host,
312
+ arkgateVersion: args.arkgateVersion,
313
+ });
314
+
315
+ if (asJson || args.json) {
316
+ write(JSON.stringify(manifest, null, 2));
317
+ } else {
318
+ write(`ArkGate status ${manifest.arkgateVersion} — schema ${manifest.schemaVersion}`);
319
+ write(
320
+ ` identity: ${manifest.projectIdentity.binding}` +
321
+ (manifest.projectIdentity.authoritative ? ' (authoritative)' : '') +
322
+ (manifest.projectIdentity.projectId
323
+ ? ` · ${manifest.projectIdentity.projectId.slice(0, 18)}…`
324
+ : ' · (no project id)')
325
+ );
326
+ write(` root: ${manifest.projectIdentity.resolvedRoot}`);
327
+ write(
328
+ ` activation: ${manifest.activation.writePath}` +
329
+ (manifest.activation.host ? ` · host=${manifest.activation.host}` : '')
330
+ );
331
+ write(` ${manifest.activation.honestLabel}`);
332
+ const lc = manifest.lastCheck;
333
+ write(
334
+ ` lastCheck: ${lc.verdict ?? 'none'}` +
335
+ (lc.at ? ` @ ${lc.at}` : '') +
336
+ (lc.activeViolations != null ? ` · active=${lc.activeViolations}` : '') +
337
+ (lc.frozenResidual != null ? ` · frozen=${lc.frozenResidual}` : '')
338
+ );
339
+ write(
340
+ ` rules: loaded=${manifest.rules.arkRulesLoaded}` +
341
+ (manifest.rules.inventoried != null ? ` · inventoried=${manifest.rules.inventoried}` : '') +
342
+ (manifest.rules.underContract != null
343
+ ? ` · underContract=${manifest.rules.underContract}`
344
+ : '') +
345
+ (manifest.rules.frozenResidual != null
346
+ ? ` · frozen=${manifest.rules.frozenResidual}`
347
+ : '')
348
+ );
349
+ write(` next: [${manifest.nextAction.id}] ${manifest.nextAction.summary}`);
350
+ }
351
+
352
+ if (manifest.projectIdentity.binding === 'mismatch') return 1;
353
+ return 0;
354
+ } catch (error) {
355
+ const message = error instanceof Error ? error.message : String(error);
356
+ if (asJson || args.json) {
357
+ write(
358
+ JSON.stringify({
359
+ schemaVersion: '1.0',
360
+ error: message,
361
+ ok: false,
362
+ })
363
+ );
364
+ } else {
365
+ writeErr(message);
366
+ }
367
+ return 2;
368
+ }
369
+ }