arkgate 4.8.6 → 4.8.8

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 (45) hide show
  1. package/CHANGELOG.md +55 -4
  2. package/README.md +37 -4
  3. package/bin/ark-dashboard.mjs +423 -0
  4. package/bin/ark-mcp-runtime.mjs +8 -2
  5. package/bin/ark.mjs +51 -3
  6. package/bin/lib/analysis-engine.mjs +5 -5
  7. package/bin/lib/doctor-human.mjs +9 -0
  8. package/bin/lib/doctor-plan.mjs +5 -1
  9. package/bin/lib/html-report.mjs +4 -2
  10. package/bin/lib/layer-description.mjs +27 -0
  11. package/bin/lib/prepare-write.mjs +7 -1
  12. package/dist/{configTypes-dy5PfTqS.d.ts → configTypes-0eHpocR3.d.ts} +4 -0
  13. package/dist/{diagnosticCatalog-D_DI7qrZ.d.ts → diagnosticCatalog-DxKCTBbp.d.ts} +3 -3
  14. package/dist/eslint/index.d.ts +1 -1
  15. package/dist/index.cjs +19 -19
  16. package/dist/index.d.ts +5 -4
  17. package/dist/index.js +20 -20
  18. package/dist/nestjs/index.cjs +5 -5
  19. package/dist/nestjs/index.d.ts +3 -3
  20. package/dist/nestjs/index.js +5 -5
  21. package/dist/runtime/index.cjs +15 -15
  22. package/dist/runtime/index.d.ts +6 -6
  23. package/dist/runtime/index.js +15 -15
  24. package/dist/{types-BuM8WNqe.d.ts → types-BK47clMl.d.ts} +1 -1
  25. package/dist/{types-DrqsOiTY.d.ts → types-DxvmJO-D.d.ts} +106 -2
  26. package/docs/README.md +12 -4
  27. package/docs/agent-guide.md +36 -1
  28. package/docs/ai-gates.md +13 -1
  29. package/docs/arkorder.md +11 -1
  30. package/docs/configuration.md +25 -3
  31. package/docs/develop.md +13 -6
  32. package/docs/enthusiast/README.md +13 -2
  33. package/docs/package-surface.md +21 -5
  34. package/docs/product-voice.md +25 -2
  35. package/docs/use.md +11 -3
  36. package/package.json +3 -1
  37. package/server.json +2 -2
  38. package/templates/agent-skills/ark-adopt/SKILL.md +19 -1
  39. package/templates/agent-skills/ark-autopilot/SKILL.md +1 -1
  40. package/templates/agent-skills/ark-contract/SKILL.md +1 -1
  41. package/templates/agent-skills/ark-place/SKILL.md +18 -4
  42. package/templates/skills/ark-adopt.md +19 -1
  43. package/templates/skills/ark-autopilot.md +1 -1
  44. package/templates/skills/ark-contract.md +1 -1
  45. package/templates/skills/ark-place.md +18 -4
package/CHANGELOG.md CHANGED
@@ -3,16 +3,67 @@
3
3
  All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are documented here or
4
4
  in the immutable pre-2.0 archive linked below.
5
5
 
6
+ ## 4.8.8 — 2026-09-01
7
+
8
+ **Patch** over **4.8.7**. ArkRun gains an opt-in terminal dashboard and bounded,
9
+ payload-free inspector monitors. ArkOrder and first-contact docs gain honesty copy.
10
+ **No required config migration.** No `ark.config.json` schema bump. Does not close
11
+ `K01` / `Z09`.
12
+
13
+ **Status: prepared** (tree candidate; npm `latest` remains **4.8.7** until the
14
+ signed-tag provenance workflow succeeds).
15
+
16
+ ### Added
17
+ - Dual **`ark-dashboard` / `arkgate-dashboard`** bins render the loopback ArkRun
18
+ inspector with ANSI and polling. `ark dashboard` / `arkgate dashboard` dispatch
19
+ to the same executable; this is a view, never a gate verdict.
20
+ - ArkRun inspector snapshots report explicit store durability facts. Default
21
+ `InMemory*` stores remain visibly memory-only and never earn a durable green.
22
+ - Loopback-only **`GET /outbox`** and **`GET /workflows`** return sanitized queue
23
+ summaries and full counts. Samples are capped at 32, payloads are omitted, the
24
+ inspector still refuses production, and nothing here supplies durable storage.
25
+
26
+ ### Changed
27
+ - Existing skills and docs now teach the elimination question before a modeller
28
+ names `xiKeys`: recomputable statuses such as `paid` are derived, not frozen.
29
+ This is guidance, not a runtime invariant or new sensor; the check remains
30
+ silent on semantic entailment.
31
+ - Eight canonical public openings now share the same four plain-English story
32
+ paragraphs. STAR remains the writing method, not a visible heading or labels.
33
+
34
+ ## 4.8.7 — 2026-08-31
35
+
36
+ **Patch** over **4.8.6**. Phase **LD** — layer description projection. Optional
37
+ `layers[].description` is an **app-context caption** (what this folder is in the
38
+ product), not architecture jargon. Place, doctor, coverage, and the HTML report
39
+ show it when present. Caption-only edits do not change `policyHash`.
40
+ **No required config migration.** No `schemaVersion` bump. Does not close `K01` / `Z09`.
41
+ No new skill names.
42
+
43
+ **Status: published** (on npm `latest`; see `docs/releases/4.8.7.md`).
44
+
45
+ ### Added
46
+ - Public name for existing `layers[].description`. Example:
47
+ `"Purchase requests — from asked to received."`
48
+ - Place / prepare-write / MCP JSON, doctor JSON + human, coverage JSON, and the
49
+ HTML Purpose column project the caption when present.
50
+ - `/ark-adopt` writes the caption from the product map or glossary; `/ark-place`
51
+ prints it next to layer name + globs.
52
+
53
+ ### Changed
54
+ - `policyHash` omits `layers[].description` the same way it omits `stewards`.
55
+ Caption-only is neutral. Absence stays silent: never a residual, never a
56
+ score, never `--strict-config` fail.
57
+
6
58
  ## 4.8.6 — 2026-08-31
7
59
 
8
60
  **Patch** over **4.8.5**. Phase **LV** — ArkOrder valved loop. First `release()`
9
61
  freezes ξ; later pattern change is `proposeRelease` then `apply`. σ identity is
10
62
  not ξ identity. ingest returns a closed residual. Capacity is data. ArkRun
11
63
  carries the decision tape. **No required config migration.** Does not close
12
- `K01` / `Z09`. No `/ark-order` skill. Do not npm-publish from this item until
13
- the maintainer tag.
64
+ `K01` / `Z09`. No `/ark-order` skill.
14
65
 
15
- **Status: prepared** (tree ready; npm `latest` remains **4.8.5** until publish).
66
+ **Status: published** (see `docs/releases/4.8.6.md`).
16
67
 
17
68
  ### Added
18
69
  - **`apply(ProposeResult)`** on `createOrderPlane`. Unvalved second freeze of a
@@ -42,7 +93,7 @@ now has a canonical page. The runtime half (shadow / replay / compare) is
42
93
  ArkRun and in-memory. **No required config migration.** Does not close `K01` /
43
94
  `Z09`.
44
95
 
45
- **Status: published** (on npm `latest`; see `docs/releases/4.8.5.md`).
96
+ **Status: published** (see `docs/releases/4.8.5.md`).
46
97
 
47
98
  ### Added
48
99
  - **`docs/arkorder.md`:** named APIs adopters were redesigning (`createOrderPlane`,
package/README.md CHANGED
@@ -7,6 +7,14 @@
7
7
  Not an API Gateway. Not a folder linter. If the check is not required on the PR, the config
8
8
  is just documentation.
9
9
 
10
+ AI can build fast—and make a mess just as fast.
11
+
12
+ Keep the product easy to understand, change, and trust.
13
+
14
+ ArkGate stops bad shortcuts. ArkRules protects how each part should behave. ArkRun keeps work moving. ArkOrder protects the few big choices that should not change by accident.
15
+
16
+ Safer changes, fewer surprises, and extra protection only when you choose it.
17
+
10
18
  Works with Cursor, Claude, Codex, and Grok.
11
19
 
12
20
  [![Website](https://img.shields.io/badge/website-arkgate.online-0a0a0a)](https://www.arkgate.online/)
@@ -29,7 +37,7 @@ Works with Cursor, Claude, Codex, and Grok.
29
37
 
30
38
  </div>
31
39
 
32
- > **ArkGate 4.8.5** is on npm `latest`. This tree is **4.8.6** (prepared patch; not published).
40
+ > **ArkGate 4.8.8** is prepared on this tree; npm `latest` remains **4.8.7**.
33
41
  > Write. Check. Ship. Adopted = required GitHub
34
42
  > status running `arkgate-check --strict-merge`, or an explicit `advisory-only` stance.
35
43
  > Status is compact (`arkgate-check --doctor`; `--all` for Details). Optional **ArkRun**
@@ -37,7 +45,7 @@ Works with Cursor, Claude, Codex, and Grok.
37
45
  > (`arkgate/order`) stops the agent rewriting the few slow product decisions as CRUD;
38
46
  > later pattern change is `proposeRelease` then `apply`.
39
47
  > `@arkgate/runtime` is deprecated.
40
- > [4.8.6](docs/releases/4.8.6.md) · [4.8.5](docs/releases/4.8.5.md) · [4.8.4](docs/releases/4.8.4.md) · [4.8.3](docs/releases/4.8.3.md) · [4.8.2](docs/releases/4.8.2.md) · [4.8.1](docs/releases/4.8.1.md) · [4.8.0](docs/releases/4.8.0.md) · [Docs hub](docs/README.md) · [Voice](docs/product-voice.md)
48
+ > [4.8.8 prepared](docs/releases/4.8.8.md) · [4.8.7 published](docs/releases/4.8.7.md) · [4.8.6](docs/releases/4.8.6.md) · [4.8.5](docs/releases/4.8.5.md) · [4.8.4](docs/releases/4.8.4.md) · [4.8.3](docs/releases/4.8.3.md) · [4.8.2](docs/releases/4.8.2.md) · [4.8.1](docs/releases/4.8.1.md) · [4.8.0](docs/releases/4.8.0.md) · [Docs hub](docs/README.md) · [Voice](docs/product-voice.md)
41
49
 
42
50
  ---
43
51
 
@@ -245,6 +253,8 @@ npx arkgate-check --install-agent-gates --tools claude,cursor,codex,grok,antigra
245
253
  # npx arkgate-check --install-agent-gates --skills-only --agent-homes --force
246
254
  # optional: same 13 skills via Agent Skills ecosystem (no new names)
247
255
  # npx skills add ./node_modules/arkgate/templates/agent-skills
256
+ # optional ArkRun: poll the loopback inspector (ANSI TUI — not a gate)
257
+ # npx ark-dashboard --url http://127.0.0.1:<port>/snapshot
248
258
  ```
249
259
 
250
260
  More: [docs/develop.md](docs/develop.md) · skills install: [docs/agent-guide.md](docs/agent-guide.md#install-skills-ark-and-ecosystem) · enthusiast track: [docs/enthusiast/](docs/enthusiast/README.md)
@@ -261,6 +271,26 @@ for decoupling.
261
271
  lives in memory and **dies on restart**. Fine for local. Not Postgres, not an
262
272
  outbox, not Temporal. `@arkgate/runtime` is deprecated.
263
273
 
274
+ ### Dev inspector and observability dashboard
275
+
276
+ Opt-in `startInspector()` binds **loopback only** (`127.0.0.1`), refuses
277
+ `NODE_ENV=production`, and serves JSON facts — not a TUI. Alongside
278
+ `GET /snapshot`, `GET /events` (SSE), and `GET /graph`, the inspector exposes
279
+ queue monitors:
280
+
281
+ | Path | Body (JSON) |
282
+ |------|-------------|
283
+ | `GET /outbox` | Pending/failed outbox **summaries** + counts (`available`, `pendingCount`, `failedCount`, `pending`, `failed`) — no event payloads |
284
+ | `GET /workflows` | Workflow **summaries** + counts (`available`, `total`, `runningCount`, …, `workflows`) |
285
+
286
+ Poll those facts from the dual bins **`ark-dashboard`** / **`arkgate-dashboard`**
287
+ (`bin/ark-dashboard.mjs`). ANSI + polling only (no React/Ink/Blessed). Point
288
+ `--url` / `-u` at the inspector snapshot (default
289
+ `http://127.0.0.1:3000/snapshot`); the dashboard also fetches sibling `/outbox`
290
+ and `/workflows`. `--interval` / `-i` is clamped to 200–60000 ms (default 2000).
291
+ Also available as `ark dashboard` / `arkgate dashboard` (passthrough to the same bin). Kernel stays
292
+ JSON-only; presentation stays in `bin/`.
293
+
264
294
  ### Durability stance
265
295
 
266
296
  Default stores (`InMemoryEventBuffer`, `InMemoryAuditStore`, `InMemoryReadModelStore`,
@@ -282,6 +312,7 @@ because “what may be the plan” was never a rule.
282
312
 
283
313
  **ArkOrder** (`arkgate/order`) is that rule. Off unless you add `arkOrder`.
284
314
  Name the slow keys (`xiKeys`: plan, protocol, cost-code bound — not `projectId`).
315
+ A status you can recompute from data you already have is not a slow decision. Derive it. Do not freeze it.
285
316
  Posting an invoice is absorbed. Changing plan is `proposeRelease` then `apply`.
286
317
  `refreshSigma`; ingest residual `absorb | escalate_up | hold` + `reasonCode`;
287
318
  capacity pack as data; in-memory `ReleaseStore`; `ingestTravelAction`; ArkRun
@@ -309,8 +340,10 @@ three keys. Compact starters leave it off. Details:
309
340
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
310
341
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
311
342
  | Security | [SECURITY.md](SECURITY.md) |
312
- | Tree prepared (4.8.6, not published) | [docs/releases/4.8.6.md](docs/releases/4.8.6.md) · [CHANGELOG](CHANGELOG.md) |
313
- | Current published (4.8.5 on npm `latest`) | [docs/releases/4.8.5.md](docs/releases/4.8.5.md) · [CHANGELOG](CHANGELOG.md) |
343
+ | Prepared (4.8.8; not published) | [docs/releases/4.8.8.md](docs/releases/4.8.8.md) · [CHANGELOG](CHANGELOG.md) |
344
+ | Current published (4.8.7 on npm `latest`) | [docs/releases/4.8.7.md](docs/releases/4.8.7.md) · [CHANGELOG](CHANGELOG.md) |
345
+ | Prior published (4.8.6) | [docs/releases/4.8.6.md](docs/releases/4.8.6.md) |
346
+ | Prior published (4.8.5) | [docs/releases/4.8.5.md](docs/releases/4.8.5.md) |
314
347
  | Prior published (4.8.4) | [docs/releases/4.8.4.md](docs/releases/4.8.4.md) |
315
348
  | Prior published (4.8.3) | [docs/releases/4.8.3.md](docs/releases/4.8.3.md) |
316
349
  | Prior published (4.8.2) | [docs/releases/4.8.2.md](docs/releases/4.8.2.md) |
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+
4
+ const DEFAULT_INTERVAL_MS = '2000';
5
+ const DEFAULT_SNAPSHOT_URL = 'http://127.0.0.1:3000/snapshot';
6
+ const DEFAULT_FETCH_TIMEOUT_MS = 5_000;
7
+ const MAX_FIELD_LEN = 120;
8
+
9
+ const args = parseArgs({
10
+ options: {
11
+ interval: {
12
+ type: 'string',
13
+ short: 'i',
14
+ default: DEFAULT_INTERVAL_MS
15
+ },
16
+ url: {
17
+ type: 'string',
18
+ short: 'u',
19
+ default: DEFAULT_SNAPSHOT_URL
20
+ },
21
+ timeout: {
22
+ type: 'string',
23
+ short: 't',
24
+ default: String(DEFAULT_FETCH_TIMEOUT_MS)
25
+ }
26
+ },
27
+ allowPositionals: true
28
+ });
29
+
30
+ const parsedInterval = parseInt(args.values.interval, 10);
31
+ const interval =
32
+ Number.isFinite(parsedInterval) && parsedInterval >= 200 && parsedInterval <= 60_000
33
+ ? parsedInterval
34
+ : Number(DEFAULT_INTERVAL_MS);
35
+ const targetUrl = args.values.url || DEFAULT_SNAPSHOT_URL;
36
+ const parsedTimeout = parseInt(args.values.timeout, 10);
37
+ const fetchTimeoutMs =
38
+ Number.isFinite(parsedTimeout) && parsedTimeout >= 200 && parsedTimeout <= 60_000
39
+ ? parsedTimeout
40
+ : DEFAULT_FETCH_TIMEOUT_MS;
41
+
42
+ const RESET = '\x1b[0m';
43
+ const RED = '\x1b[31m';
44
+ const GREEN = '\x1b[32m';
45
+ const YELLOW = '\x1b[33m';
46
+ const MUTED = '\x1b[2m';
47
+ const SAMPLE_LIMIT = 8;
48
+
49
+ const DRIFT_FLOW_KEYS = [
50
+ 'declaredButUnobserved',
51
+ 'observedButUndeclared',
52
+ 'unknownSources',
53
+ ];
54
+ const DRIFT_ID_KEYS = [
55
+ 'unregisteredObservedSources',
56
+ 'unregisteredObservedIntents',
57
+ 'registeredButNeverObserved',
58
+ ];
59
+
60
+ /** Strip ANSI CSI / OSC / other C0+C1 terminal control sequences; bound length. */
61
+ function sanitizeField(value, maxLen = MAX_FIELD_LEN) {
62
+ let text = value === undefined || value === null ? '' : String(value);
63
+ // OSC: ESC ] … BEL or ESC ]
64
+ text = text.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?/g, '');
65
+ // CSI / Fe sequences
66
+ text = text.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '');
67
+ // Remaining ESC + final byte / 8-bit C1
68
+ text = text.replace(/\u001b[@-Z\\-_]/g, '');
69
+ text = text.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '');
70
+ if (text.length > maxLen) {
71
+ return `${text.slice(0, Math.max(0, maxLen - 1))}…`;
72
+ }
73
+ return text;
74
+ }
75
+
76
+ function siblingUrl(snapshotUrl, suffix) {
77
+ try {
78
+ const u = new URL(snapshotUrl);
79
+ let basePath = u.pathname;
80
+ if (basePath.endsWith('/snapshot')) {
81
+ basePath = basePath.slice(0, -'/snapshot'.length);
82
+ } else if (basePath.endsWith('/snapshot/')) {
83
+ basePath = basePath.slice(0, -'/snapshot/'.length);
84
+ }
85
+ u.pathname = `${basePath}${suffix}`;
86
+ return u.toString();
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * @returns {{ ok: true, data: unknown } | { ok: false, kind: 'waiting' | 'failure', error?: string }}
94
+ */
95
+ async function fetchJsonResult(url) {
96
+ if (!url) return { ok: false, kind: 'waiting' };
97
+ const controller = new AbortController();
98
+ const timer = setTimeout(() => controller.abort(), fetchTimeoutMs);
99
+ try {
100
+ const res = await fetch(url, { signal: controller.signal });
101
+ if (!res.ok) {
102
+ return { ok: false, kind: 'failure', error: `HTTP ${res.status}` };
103
+ }
104
+ const data = await res.json();
105
+ return { ok: true, data };
106
+ } catch (error) {
107
+ const message = error instanceof Error ? error.message : String(error);
108
+ const aborted =
109
+ (error && typeof error === 'object' && error.name === 'AbortError') ||
110
+ /aborted|timeout/i.test(message);
111
+ return {
112
+ ok: false,
113
+ kind: 'failure',
114
+ error: sanitizeField(aborted ? `timeout after ${fetchTimeoutMs}ms` : message),
115
+ };
116
+ } finally {
117
+ clearTimeout(timer);
118
+ }
119
+ }
120
+
121
+ function isObservabilityReady(obs) {
122
+ if (!obs || typeof obs !== 'object' || Array.isArray(obs)) return false;
123
+ if (Object.keys(obs).length === 0) return false;
124
+ return [...DRIFT_FLOW_KEYS, ...DRIFT_ID_KEYS].some((k) => Array.isArray(obs[k]));
125
+ }
126
+
127
+ function formatFlow(flow) {
128
+ if (!flow || typeof flow !== 'object') return sanitizeField(flow);
129
+ const from = sanitizeField(flow.from ?? '?');
130
+ const to = sanitizeField(flow.to ?? '?');
131
+ return `${from}→${to}`;
132
+ }
133
+
134
+ function listSamples(items, formatter) {
135
+ const sample = items.slice(0, SAMPLE_LIMIT);
136
+ for (const item of sample) {
137
+ console.log(` • ${formatter(item)}`);
138
+ }
139
+ if (items.length > SAMPLE_LIMIT) {
140
+ console.log(` … +${items.length - SAMPLE_LIMIT} more`);
141
+ }
142
+ }
143
+
144
+ function renderDriftRadar(observability) {
145
+ console.log(`\n--- Drift Radar ---`);
146
+ if (!isObservabilityReady(observability)) {
147
+ console.log(`${MUTED}Waiting for observability…${RESET}`);
148
+ return;
149
+ }
150
+
151
+ const flows = Object.fromEntries(
152
+ DRIFT_FLOW_KEYS.map((k) => [k, Array.isArray(observability[k]) ? observability[k] : []]),
153
+ );
154
+ const ids = Object.fromEntries(
155
+ DRIFT_ID_KEYS.map((k) => [k, Array.isArray(observability[k]) ? observability[k] : []]),
156
+ );
157
+
158
+ const counts = {
159
+ declaredButUnobserved: flows.declaredButUnobserved.length,
160
+ observedButUndeclared: flows.observedButUndeclared.length,
161
+ unknownSources: flows.unknownSources.length,
162
+ unregisteredObservedSources: ids.unregisteredObservedSources.length,
163
+ unregisteredObservedIntents: ids.unregisteredObservedIntents.length,
164
+ registeredButNeverObserved: ids.registeredButNeverObserved.length,
165
+ };
166
+ const totalDrift = Object.values(counts).reduce((a, b) => a + b, 0);
167
+
168
+ if (observability.generatedAt) {
169
+ console.log(`Generated: ${sanitizeField(observability.generatedAt)}`);
170
+ }
171
+
172
+ console.log(
173
+ `Counts: declaredButUnobserved=${counts.declaredButUnobserved} ` +
174
+ `observedButUndeclared=${counts.observedButUndeclared} ` +
175
+ `unknownSources=${counts.unknownSources} ` +
176
+ `unregisteredSources=${counts.unregisteredObservedSources} ` +
177
+ `unregisteredIntents=${counts.unregisteredObservedIntents} ` +
178
+ `neverObserved=${counts.registeredButNeverObserved}`,
179
+ );
180
+
181
+ if (totalDrift === 0) {
182
+ console.log(`${GREEN}No drift${RESET}`);
183
+ return;
184
+ }
185
+
186
+ const critical =
187
+ counts.observedButUndeclared > 0 ||
188
+ counts.unknownSources > 0 ||
189
+ counts.unregisteredObservedSources > 0 ||
190
+ counts.unregisteredObservedIntents > 0;
191
+ const color = critical ? RED : YELLOW;
192
+ console.log(`${color}[DRIFT] ${totalDrift} issue(s) detected${RESET}`);
193
+
194
+ for (const key of DRIFT_FLOW_KEYS) {
195
+ const items = flows[key];
196
+ if (items.length === 0) continue;
197
+ console.log(` ${YELLOW}${key}${RESET} (${items.length}):`);
198
+ listSamples(items, formatFlow);
199
+ }
200
+ for (const key of DRIFT_ID_KEYS) {
201
+ const items = ids[key];
202
+ if (items.length === 0) continue;
203
+ console.log(` ${YELLOW}${key}${RESET} (${items.length}):`);
204
+ listSamples(items, (id) => sanitizeField(id));
205
+ }
206
+ }
207
+
208
+ function formatOutboxRow(row) {
209
+ const intent = row.intent ? ` ${sanitizeField(row.intent)}` : '';
210
+ const err = row.error ? ` err=${sanitizeField(row.error)}` : '';
211
+ return `${sanitizeField(row.status)} ${sanitizeField(row.id)}${intent} attempts=${row.attempts ?? 0}${err}`;
212
+ }
213
+
214
+ function formatWorkflowRow(row) {
215
+ const step = row.currentStep ? ` @${sanitizeField(row.currentStep)}` : '';
216
+ const err = row.error ? ` err=${sanitizeField(row.error)}` : '';
217
+ return `${sanitizeField(row.status)} ${sanitizeField(row.name)} (${sanitizeField(row.id)})${step}${err}`;
218
+ }
219
+
220
+ function isOutboxReady(outbox) {
221
+ return Boolean(outbox && typeof outbox === 'object' && outbox.available === true);
222
+ }
223
+
224
+ function isWorkflowsReady(workflows) {
225
+ return Boolean(workflows && typeof workflows === 'object' && workflows.available === true);
226
+ }
227
+
228
+ function renderFetchStatus(label, result) {
229
+ if (result.ok) return;
230
+ if (result.kind === 'waiting') {
231
+ console.log(`${MUTED}${label}: Waiting…${RESET}`);
232
+ return;
233
+ }
234
+ const detail = result.error ? ` (${result.error})` : '';
235
+ console.log(`${RED}${label}: Failure${detail}${RESET}`);
236
+ }
237
+
238
+ function renderQueuesAndWorkflows(outboxResult, workflowsResult, snapshotOutbox, snapshotWorkflows) {
239
+ console.log(`\n--- Queues & Workflows ---`);
240
+
241
+ const outbox = isOutboxReady(outboxResult.ok ? outboxResult.data : null)
242
+ ? outboxResult.data
243
+ : isOutboxReady(snapshotOutbox)
244
+ ? snapshotOutbox
245
+ : null;
246
+ const workflows = isWorkflowsReady(workflowsResult.ok ? workflowsResult.data : null)
247
+ ? workflowsResult.data
248
+ : isWorkflowsReady(snapshotWorkflows)
249
+ ? snapshotWorkflows
250
+ : null;
251
+
252
+ if (!outbox && !workflows) {
253
+ if (!outboxResult.ok && !workflowsResult.ok) {
254
+ renderFetchStatus('Outbox', outboxResult);
255
+ renderFetchStatus('Workflows', workflowsResult);
256
+ return;
257
+ }
258
+ console.log(`${MUTED}Waiting for queues & workflows…${RESET}`);
259
+ return;
260
+ }
261
+
262
+ if (!outbox) {
263
+ renderFetchStatus('Outbox', outboxResult.ok ? { ok: false, kind: 'waiting' } : outboxResult);
264
+ } else {
265
+ const pending = Array.isArray(outbox.pending) ? outbox.pending : [];
266
+ const failed = Array.isArray(outbox.failed) ? outbox.failed : [];
267
+ const pendingCount = outbox.pendingCount ?? pending.length;
268
+ const failedCount = outbox.failedCount ?? failed.length;
269
+ console.log(`Outbox: pending=${pendingCount} failed=${failedCount}`);
270
+
271
+ if (pendingCount === 0 && failedCount === 0) {
272
+ console.log(`${GREEN}Outbox empty${RESET}`);
273
+ } else {
274
+ if (failedCount > 0) {
275
+ console.log(`${RED}[OUTBOX] ${failedCount} failed${RESET}`);
276
+ listSamples(failed, (row) => `${RED}${formatOutboxRow(row)}${RESET}`);
277
+ }
278
+ if (pendingCount > 0) {
279
+ console.log(`${YELLOW}[OUTBOX] ${pendingCount} pending${RESET}`);
280
+ listSamples(pending, (row) => `${YELLOW}${formatOutboxRow(row)}${RESET}`);
281
+ }
282
+ }
283
+ }
284
+
285
+ if (!workflows) {
286
+ renderFetchStatus(
287
+ 'Workflows',
288
+ workflowsResult.ok ? { ok: false, kind: 'waiting' } : workflowsResult,
289
+ );
290
+ return;
291
+ }
292
+
293
+ const rows = Array.isArray(workflows.workflows) ? workflows.workflows : [];
294
+ const running = workflows.runningCount ?? rows.filter((w) => w.status === 'running').length;
295
+ const compensating =
296
+ workflows.compensatingCount ?? rows.filter((w) => w.status === 'compensating').length;
297
+ const failedWf = workflows.failedCount ?? rows.filter((w) => w.status === 'failed').length;
298
+ const pendingWf =
299
+ workflows.pendingCount ??
300
+ rows.filter((w) => w.status === 'idle' || w.status === 'waiting').length;
301
+ console.log(
302
+ `Workflows: total=${workflows.total ?? rows.length} running=${running} ` +
303
+ `compensating=${compensating} failed=${failedWf} pending=${pendingWf}`,
304
+ );
305
+
306
+ if (failedWf === 0 && running === 0 && compensating === 0 && pendingWf === 0) {
307
+ console.log(`${GREEN}No active workflows${RESET}`);
308
+ return;
309
+ }
310
+
311
+ if (failedWf > 0) {
312
+ console.log(`${RED}[WORKFLOWS] ${failedWf} failed${RESET}`);
313
+ listSamples(
314
+ rows.filter((w) => w.status === 'failed'),
315
+ (row) => `${RED}${formatWorkflowRow(row)}${RESET}`,
316
+ );
317
+ }
318
+ const yellowRows = rows.filter((w) =>
319
+ w.status === 'running' ||
320
+ w.status === 'compensating' ||
321
+ w.status === 'idle' ||
322
+ w.status === 'waiting',
323
+ );
324
+ if (yellowRows.length > 0) {
325
+ console.log(`${YELLOW}[WORKFLOWS] ${yellowRows.length} pending/running/compensating${RESET}`);
326
+ listSamples(yellowRows, (row) => `${YELLOW}${formatWorkflowRow(row)}${RESET}`);
327
+ }
328
+ }
329
+
330
+ function durabilityStores(snapshot) {
331
+ const stores = snapshot?.hardening?.durability?.stores;
332
+ return Array.isArray(stores) ? stores : [];
333
+ }
334
+
335
+ function renderHardening(snapshotResult) {
336
+ console.log(`--- Hardening Status ---`);
337
+ if (!snapshotResult.ok) {
338
+ if (snapshotResult.kind === 'waiting') {
339
+ console.log(`${YELLOW}Waiting for kernel...${RESET}`);
340
+ } else {
341
+ const detail = snapshotResult.error ? ` (${snapshotResult.error})` : '';
342
+ console.log(`${RED}Failure contacting kernel${detail}${RESET}`);
343
+ }
344
+ return;
345
+ }
346
+
347
+ const snapshot = snapshotResult.data;
348
+ const stores = durabilityStores(snapshot);
349
+ const memoryStores = stores.filter((s) => s && s.kind === 'memory');
350
+ const durableStores = stores.filter((s) => s && s.kind === 'durable');
351
+
352
+ if (stores.length === 0) {
353
+ // Missing durability facts must never read as green OK.
354
+ console.log(`${YELLOW}[WARNING] Store durability facts unavailable${RESET}`);
355
+ return;
356
+ }
357
+
358
+ if (memoryStores.length > 0) {
359
+ console.log(`${RED}[WARNING] Memory defaults in use!${RESET}`);
360
+ for (const store of memoryStores) {
361
+ const role = sanitizeField(store.role ?? 'store');
362
+ const id = sanitizeField(store.id ?? store.name ?? 'unknown');
363
+ console.log(` - ${YELLOW}${role}: ${id}${RESET}`);
364
+ }
365
+ if (durableStores.length > 0) {
366
+ for (const store of durableStores) {
367
+ const role = sanitizeField(store.role ?? 'store');
368
+ const id = sanitizeField(store.id ?? store.name ?? 'unknown');
369
+ console.log(` - ${MUTED}${role}: ${id} (durable)${RESET}`);
370
+ }
371
+ }
372
+ return;
373
+ }
374
+
375
+ console.log(`${GREEN}[OK] Durable Stores Configured${RESET}`);
376
+ for (const store of durableStores) {
377
+ const role = sanitizeField(store.role ?? 'store');
378
+ const id = sanitizeField(store.id ?? store.name ?? 'unknown');
379
+ console.log(` - ${MUTED}${role}: ${id}${RESET}`);
380
+ }
381
+ }
382
+
383
+ async function render() {
384
+ process.stdout.write('\x1b[2J\x1b[H');
385
+ console.log(`ArkGate Observability Dashboard`);
386
+ console.log(`Time: ${sanitizeField(new Date().toISOString())}`);
387
+ console.log(`Endpoint: ${sanitizeField(targetUrl)}\n`);
388
+
389
+ const outboxUrl = siblingUrl(targetUrl, '/outbox');
390
+ const workflowsUrl = siblingUrl(targetUrl, '/workflows');
391
+ const [snapshotResult, outboxResult, workflowsResult] = await Promise.all([
392
+ fetchJsonResult(targetUrl),
393
+ fetchJsonResult(outboxUrl),
394
+ fetchJsonResult(workflowsUrl),
395
+ ]);
396
+
397
+ renderHardening(snapshotResult);
398
+
399
+ const snapshot = snapshotResult.ok ? snapshotResult.data : null;
400
+ renderDriftRadar(snapshot?.observability);
401
+ renderQueuesAndWorkflows(
402
+ outboxResult,
403
+ workflowsResult,
404
+ snapshot?.outbox,
405
+ snapshot?.workflows,
406
+ );
407
+ }
408
+
409
+ async function startDashboard() {
410
+ console.log(
411
+ `Starting ArkGate Observability Dashboard (polling every ${interval}ms, fetch timeout ${fetchTimeoutMs}ms)`,
412
+ );
413
+
414
+ while (true) {
415
+ await render();
416
+ await new Promise((resolve) => setTimeout(resolve, interval));
417
+ }
418
+ }
419
+
420
+ startDashboard().catch((err) => {
421
+ console.error(err);
422
+ process.exit(1);
423
+ });
@@ -27,6 +27,7 @@ import { effectiveCapabilityDeny, stableSerialize } from './lib/analysis-engine.
27
27
  import { createImportTargetResolver } from './lib/import-resolve.mjs';
28
28
  import { validateWithAutoPatch, resolveImportFileAbs } from './lib/auto-patch.mjs';
29
29
  import { composePrepareWrite } from './lib/prepare-write.mjs';
30
+ import { placementDescriptionFields } from './lib/layer-description.mjs';
30
31
  import { loadArkConfigContract } from './lib/config-contract.mjs';
31
32
  import { loadEffectiveArkRulesFromDisk } from './lib/effective-contract-load.mjs';
32
33
  import {
@@ -1925,6 +1926,7 @@ export async function runArkMcp({ hookInput } = {}) {
1925
1926
  'Place a file in the architecture: filePath is required (fail-closed without it — never invents components/*.tsx or defaults to Presentation). ' +
1926
1927
  'Returns layer, mayImport / mustNotImport, forbiddenGlobals, and goldenPattern ' +
1927
1928
  '(load-bearing for NEW code when .ark/golden-pattern.json exists — adopt generates it). ' +
1929
+ 'When the matched layer has layers[].description, the JSON includes description; the field is omitted when absent. ' +
1928
1930
  'Call BEFORE writing a new file. ' +
1929
1931
  'Prefer ark_prepare_write when you already have the source snippet (place+validate+autoPatch in one call).',
1930
1932
  inputSchema: {
@@ -1950,7 +1952,8 @@ export async function runArkMcp({ hookInput } = {}) {
1950
1952
  'mechanical-safe autoPatch + judgmentBrief when judgment is needed + contentHash for host commit. ' +
1951
1953
  'Also returns the versioned new/worsened designDelta for the proposed full file. ' +
1952
1954
  'Composes ark_place + write-gate — call BEFORE Write/Edit when you have the snippet. ' +
1953
- 'Returns { filePath, layer, valid, violations?, autoPatch?, judgmentBrief?, contentHash, ... }.',
1955
+ 'When the matched layer has layers[].description, the JSON includes description; the field is omitted when absent. ' +
1956
+ 'Returns { filePath, layer, description?, valid, violations?, autoPatch?, judgmentBrief?, contentHash, ... }.',
1954
1957
  inputSchema: {
1955
1958
  type: 'object',
1956
1959
  properties: {
@@ -2393,6 +2396,9 @@ export async function runArkMcp({ hookInput } = {}) {
2393
2396
  // `allowed:false` denies) — which layers it may and must not import.
2394
2397
  // Q03: when present, attach optional `.ark/golden-pattern.json` (advisory for NEW code only).
2395
2398
  function placeResult(filePath, description) {
2399
+ // Input `description` is the pre-existing "what you are building" hint — not
2400
+ // layers[].description — and must not be copied onto the caption key (ADR 0035 D5).
2401
+ void description;
2396
2402
  const golden = loadGoldenPattern(args.root);
2397
2403
  const withGolden = (placement) => attachGoldenToPlacement(placement, golden);
2398
2404
 
@@ -2433,7 +2439,7 @@ export async function runArkMcp({ hookInput } = {}) {
2433
2439
  filePath,
2434
2440
  layer: layerName,
2435
2441
  governed: true,
2436
- description: layerMeta?.description,
2442
+ ...placementDescriptionFields(layerMeta),
2437
2443
  forbiddenGlobals: layerMeta?.forbiddenGlobals ?? [],
2438
2444
  ...(layerMeta?.mayImportInfrastructure ? { mayImportInfrastructure: true } : {}),
2439
2445
  mayImport,