@event4u/agent-config 10.3.0 → 11.0.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 (107) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +147 -0
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze.md +2 -1
  5. package/dist/agent-src/commands/council/debate.md +13 -4
  6. package/dist/agent-src/commands/council/default.md +26 -11
  7. package/dist/agent-src/commands/council.md +1 -1
  8. package/dist/agent-src/commands/design-system/capture.md +44 -0
  9. package/dist/agent-src/commands/design-system/generate.md +75 -0
  10. package/dist/agent-src/commands/design-system/import.md +63 -0
  11. package/dist/agent-src/commands/design-system.md +66 -0
  12. package/dist/agent-src/commands/pr/create.md +51 -26
  13. package/dist/agent-src/commands/worktree/create.md +5 -3
  14. package/dist/agent-src/rules/settings-ask-protocol.md +14 -9
  15. package/dist/agent-src/skills/ai-council/SKILL.md +24 -6
  16. package/dist/agent-src/skills/corpus-grounding/SKILL.md +12 -1
  17. package/dist/agent-src/skills/corpus-grounding/scripts/decision_engine.ts +215 -7
  18. package/dist/agent-src/skills/corpus-grounding/scripts/ground.ts +39 -1
  19. package/dist/agent-src/skills/design-intelligence/ATTRIBUTION.md +19 -2
  20. package/dist/agent-src/skills/design-intelligence/data/manifest.json +36 -1
  21. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -0
  22. package/dist/agent-src/skills/design-system-capture/SKILL.md +9 -0
  23. package/dist/agent-src/skills/design-system-capture/references/design-system-json.md +70 -0
  24. package/dist/agent-src/skills/git-workflow/SKILL.md +9 -3
  25. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +9 -6
  26. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -21
  27. package/dist/agent-src/templates/agent-settings.md +0 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
  30. package/dist/cli-delegate/{chunk-BV3WR67D.js → chunk-55WZN5VF.js} +3 -6
  31. package/dist/cli-delegate/{chunk-X3VOARZD.js → chunk-L565YFXH.js} +3 -3
  32. package/dist/cli-delegate/{chunk-JMQZ7MM5.js → chunk-MNWRDMR5.js} +1 -1
  33. package/dist/cli-delegate/{chunk-IR3IVRVC.js → chunk-NLP54U3J.js} +2 -1
  34. package/dist/cli-delegate/{chunk-FV34EGEX.js → chunk-NN4KW4HZ.js} +1 -1
  35. package/dist/cli-delegate/{chunk-6ENSXSBW.js → chunk-PPQO4NGR.js} +1 -1
  36. package/dist/cli-delegate/chunk-RLN73PR5.js +157 -0
  37. package/dist/cli-delegate/{chunk-6LOPHJLD.js → chunk-RYNADUFG.js} +1 -1
  38. package/dist/cli-delegate/{chunk-I5EAFRII.js → chunk-ZPSXJTLI.js} +2 -154
  39. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  40. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  41. package/dist/cli-delegate/cmd_conformance.js +673 -215
  42. package/dist/cli-delegate/cmd_doctor.js +2 -2
  43. package/dist/cli-delegate/cmd_explain.js +2 -2
  44. package/dist/cli-delegate/cmd_export.js +4 -4
  45. package/dist/cli-delegate/cmd_handoff.js +10 -8
  46. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  47. package/dist/cli-delegate/cmd_migrate.js +2 -2
  48. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  49. package/dist/cli-delegate/cmd_prune.js +4 -4
  50. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  51. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  52. package/dist/cli-delegate/cmd_session_recycle.js +6 -4
  53. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  54. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  55. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  56. package/dist/cli-delegate/cmd_sync.js +4 -4
  57. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  58. package/dist/cli-delegate/cmd_update.js +1 -1
  59. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  60. package/dist/cli-delegate/cmd_validate.js +4 -4
  61. package/dist/cli-delegate/cmd_versions.js +1 -1
  62. package/dist/discovery/deprecation-report.md +1 -1
  63. package/dist/discovery/discovery-manifest.json +127 -22
  64. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  65. package/dist/discovery/discovery-manifest.summary.md +3 -3
  66. package/dist/discovery/dormancy-report.md +1 -1
  67. package/dist/discovery/orphan-report.md +1 -1
  68. package/dist/discovery/packs.json +9 -5
  69. package/dist/discovery/trust-report.md +2 -2
  70. package/dist/discovery/workspaces.json +8 -4
  71. package/dist/hooks/dispatch.js +994 -399
  72. package/dist/install/install.mjs +5 -7
  73. package/dist/mcp/registry-manifest.json +2 -2
  74. package/dist/mcp/server.json +2 -2
  75. package/dist/mcp/server.mjs +1104 -593
  76. package/dist/scripts/_lib/agent_settings.js +8 -0
  77. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  78. package/dist/server/schemas/settings.js +3 -4
  79. package/dist/server/schemas/settings.js.map +1 -1
  80. package/dist/ui/assets/{index-5fXOHoVh.js → index-B0aMJnuz.js} +2 -2
  81. package/dist/ui/assets/{index-5fXOHoVh.js.map → index-B0aMJnuz.js.map} +1 -1
  82. package/dist/ui/index.html +1 -1
  83. package/docs/contracts/settings-classes.md +12 -9
  84. package/package.json +1 -1
  85. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
  86. package/src/config/agent-settings.template.yml +15 -18
  87. package/src/config/gate-coverage.yml +33 -2
  88. package/src/config/gate-violation-baselines.json +2 -2
  89. package/src/config/pack-size-budget.json +3 -2
  90. package/src/scripts/_lib/agent_settings.ts +8 -0
  91. package/src/scripts/_lib/design_system_import.ts +987 -0
  92. package/src/scripts/check_branch_freshness.ts +238 -6
  93. package/src/scripts/design_system_import.ts +264 -0
  94. package/src/scripts/external_sources_denylist.json +7 -2
  95. package/src/scripts/hook_manifest.yaml +71 -2
  96. package/src/scripts/hooks/concern_registry.ts +4 -0
  97. package/src/scripts/hooks/dispatch_hook.ts +10 -0
  98. package/src/scripts/hooks/host_semantics.ts +2 -0
  99. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +212 -0
  100. package/src/scripts/hooks/subagent_ledger_hook.ts +723 -0
  101. package/src/scripts/hooks/turn_end_gate_hook.ts +39 -2
  102. package/src/scripts/lint_hook_manifest.ts +2 -0
  103. package/src/scripts/lint_rule_skill_pack_reach.ts +194 -13
  104. package/src/scripts/session_register_hook.ts +120 -10
  105. package/src/server/schemas/settings.ts +3 -6
  106. package/tests/fixtures/hooks/subagent_start.json +7 -0
  107. package/tests/fixtures/hooks/subagent_stop.json +8 -0
@@ -31,6 +31,26 @@
31
31
  * It never fetches, merges, or rebases for you. Choosing how to catch up is a
32
32
  * git-history decision (`git-history-discipline`), and a gate that rewrites
33
33
  * history to make itself pass is a worse failure than the one it reports.
34
+ *
35
+ * WHICH BASE — asked of the forge, not assumed.
36
+ *
37
+ * The first version hardcoded `main`. That is right for most branches here and
38
+ * wrong for every branch whose PR targets something else: a stacked PR, a
39
+ * release line, a consumer repo on `master`. On those the gate produced a false
40
+ * GREEN — "current with origin/main" while the branch sat behind the base it
41
+ * would actually merge into — which is worse than no gate, because it answers a
42
+ * question nobody asked in the voice of the one they did.
43
+ *
44
+ * So the base is resolved, in order: an explicit `--base`, then the base of the
45
+ * OPEN PR for this branch as the forge reports it, then the repo's default
46
+ * branch from `origin/HEAD`, then `main`. Every message names the base AND
47
+ * where it came from, because "I checked main because that is the PR's base"
48
+ * and "I checked main because I could not ask" must never print the same.
49
+ *
50
+ * When the forge cannot be asked (no `gh`, not authenticated, no GitHub remote)
51
+ * the run says so out loud even under `--quiet`: it has verified the default
52
+ * base and has NOT ruled out an open PR against a different one. That is a
53
+ * partial answer, and partial answers are reported as partial.
34
54
  */
35
55
  import { execFileSync } from "node:child_process";
36
56
  import fs from "node:fs";
@@ -43,9 +63,11 @@ import { reportScanned } from "./_lib/scan_scope.js";
43
63
 
44
64
  // ledger-exempt: single remote-ref probe — the entire scope is ONE ls-remote answer (0 or 1 refs) resolved to one aggregate ancestor verdict, and every empty path already publishes its reason via reportScanned allowEmpty; there is no per-target collection to account.
45
65
  const DEFAULT_BASE = "main";
46
- const SELF_TEST_MIN_CASES = 3;
47
- const SELF_TEST_MIN_REJECT = 1;
66
+ const SELF_TEST_MIN_CASES = 5;
67
+ const SELF_TEST_MIN_REJECT = 2;
48
68
  const SCRIPT_REL = "src/scripts/check_branch_freshness.ts";
69
+ /** One forge round trip on the push path — long enough for a cold `gh`, short enough not to stall a push. */
70
+ const FORGE_TIMEOUT_MS = 10_000;
49
71
 
50
72
  function git(args: string[]): string | null {
51
73
  try {
@@ -55,6 +77,134 @@ function git(args: string[]): string | null {
55
77
  }
56
78
  }
57
79
 
80
+ export type GhResult = { ok: true; stdout: string } | { ok: false; reason: string };
81
+ export type GhRunner = (args: string[]) => GhResult;
82
+
83
+ /**
84
+ * What the forge said about an open PR for this branch.
85
+ *
86
+ * Three-valued on purpose. `none` (the forge answered: no open PR) and
87
+ * `unavailable` (the forge could not be asked) lead to the same base and to
88
+ * DIFFERENT confidence, and collapsing them is exactly how a gate reports a
89
+ * green it did not earn.
90
+ */
91
+ export type ForgeAnswer =
92
+ | { kind: "pr"; base: string; number: number }
93
+ | { kind: "none" }
94
+ | { kind: "unavailable"; reason: string };
95
+
96
+ /** Where the base name came from — printed with every verdict. */
97
+ export type BaseSource = "flag" | "pull-request" | "origin-head" | "fallback";
98
+
99
+ export interface BaseResolution {
100
+ readonly base: string;
101
+ readonly source: BaseSource;
102
+ readonly pr?: number;
103
+ /** Present iff an open PR against a different base could not be ruled out. */
104
+ readonly unverified?: string;
105
+ }
106
+
107
+ function gh(args: string[]): GhResult {
108
+ try {
109
+ const out = execFileSync("gh", args, {
110
+ encoding: "utf8",
111
+ stdio: ["ignore", "pipe", "ignore"],
112
+ timeout: FORGE_TIMEOUT_MS,
113
+ });
114
+ return { ok: true, stdout: out.toString() };
115
+ } catch (err) {
116
+ const code = (err as NodeJS.ErrnoException).code;
117
+ if (code === "ENOENT") {
118
+ return { ok: false, reason: "the gh CLI is not installed" };
119
+ }
120
+ if (code === "ETIMEDOUT") {
121
+ return { ok: false, reason: `gh did not answer within ${FORGE_TIMEOUT_MS / 1000}s` };
122
+ }
123
+ return { ok: false, reason: "gh could not answer — not authenticated, or no GitHub remote" };
124
+ }
125
+ }
126
+
127
+ /**
128
+ * The base of the open PR whose head is `branch`.
129
+ *
130
+ * `pr list --head` rather than `pr view`: `pr view` will happily resolve a
131
+ * CLOSED PR for the same branch, and a merged PR's base is not the base the
132
+ * next push has to be current with.
133
+ */
134
+ export function askForgeForBase(branch: string, run: GhRunner = gh): ForgeAnswer {
135
+ const res = run([
136
+ "pr", "list", "--head", branch, "--state", "open",
137
+ "--json", "number,baseRefName", "--limit", "1",
138
+ ]);
139
+ if (!res.ok) {
140
+ return { kind: "unavailable", reason: res.reason };
141
+ }
142
+ let rows: unknown;
143
+ try {
144
+ rows = JSON.parse(res.stdout);
145
+ } catch {
146
+ return { kind: "unavailable", reason: "gh returned output that is not JSON" };
147
+ }
148
+ if (!Array.isArray(rows) || rows.length === 0) {
149
+ return { kind: "none" };
150
+ }
151
+ const row = rows[0] as { baseRefName?: unknown; number?: unknown };
152
+ const base = typeof row.baseRefName === "string" ? row.baseRefName.trim() : "";
153
+ if (base === "") {
154
+ return { kind: "unavailable", reason: "gh answered without a baseRefName" };
155
+ }
156
+ return { kind: "pr", base, number: typeof row.number === "number" ? row.number : 0 };
157
+ }
158
+
159
+ /** The repo's own default branch, so a consumer on `master` is not told about `main`. */
160
+ export function repoDefaultBase(): { base: string; source: "origin-head" | "fallback" } {
161
+ const ref = git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
162
+ if (ref !== null && ref.startsWith("origin/")) {
163
+ const name = ref.slice("origin/".length).trim();
164
+ if (name !== "") {
165
+ return { base: name, source: "origin-head" };
166
+ }
167
+ }
168
+ return { base: DEFAULT_BASE, source: "fallback" };
169
+ }
170
+
171
+ /** `--base` wins, then the open PR's base, then the repo default. */
172
+ export function resolveBase(
173
+ argv: readonly string[],
174
+ branch: string,
175
+ forge: (b: string) => ForgeAnswer = (b) => askForgeForBase(b),
176
+ ): BaseResolution {
177
+ const flag = argv.indexOf("--base");
178
+ if (flag >= 0) {
179
+ const explicit = argv[flag + 1];
180
+ if (explicit !== undefined && explicit !== "") {
181
+ return { base: explicit, source: "flag" };
182
+ }
183
+ }
184
+ const answer = forge(branch);
185
+ if (answer.kind === "pr") {
186
+ return { base: answer.base, source: "pull-request", pr: answer.number };
187
+ }
188
+ const fallback = repoDefaultBase();
189
+ return answer.kind === "unavailable"
190
+ ? { base: fallback.base, source: fallback.source, unverified: answer.reason }
191
+ : { base: fallback.base, source: fallback.source };
192
+ }
193
+
194
+ /** Human-readable provenance — never omitted, so no verdict is ambiguous about what it checked. */
195
+ export function describeBase(r: BaseResolution): string {
196
+ switch (r.source) {
197
+ case "flag":
198
+ return "base given on the command line";
199
+ case "pull-request":
200
+ return `base of open PR #${r.pr ?? 0}`;
201
+ case "origin-head":
202
+ return "repo default branch (origin/HEAD)";
203
+ case "fallback":
204
+ return `repo default (assumed ${DEFAULT_BASE} — origin/HEAD is not set)`;
205
+ }
206
+ }
207
+
58
208
  /** The SHA the server reports for `<base>` right now — never a tracking ref. */
59
209
  export function remoteHead(base: string, remote = "origin"): string | null {
60
210
  const out = git(["ls-remote", "--heads", remote, base]);
@@ -121,6 +271,47 @@ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
121
271
  return work;
122
272
  }
123
273
 
274
+ /**
275
+ * A repo where the branch's real base is NOT the default branch: `feat/x` is
276
+ * cut from `develop`, `develop` then advances on the remote, and `main` never
277
+ * moves.
278
+ *
279
+ * This is the fixture that demonstrates the defect the base resolution exists
280
+ * to remove. Checked against `main` the branch is perfectly current — a GREEN
281
+ * that is true and irrelevant. Checked against `develop`, the base it will
282
+ * actually merge into, it is behind. Both verdicts come from the same tree, so
283
+ * a regression that reinstates a hardcoded base flips exactly one of them.
284
+ */
285
+ function buildTwoBaseFixture(root: string): string {
286
+ const origin = path.join(root, "origin.git");
287
+ const work = path.join(root, "work");
288
+ fs.mkdirSync(origin, { recursive: true });
289
+ const g = (args: string[], at: string): void => {
290
+ execFileSync("git", args, { cwd: at, stdio: "ignore" });
291
+ };
292
+ const write = (at: string, name: string, body: string): void => {
293
+ fs.writeFileSync(path.join(at, name), body);
294
+ g(["add", "-A"], at);
295
+ g(["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", name], at);
296
+ };
297
+ g(["init", "--bare", "-b", DEFAULT_BASE], origin);
298
+ g(["init", "-b", DEFAULT_BASE, work], root);
299
+ g(["remote", "add", "origin", origin], work);
300
+ write(work, "a.txt", "one");
301
+ g(["push", "-u", "origin", DEFAULT_BASE], work);
302
+ g(["checkout", "-b", "develop"], work);
303
+ g(["push", "-u", "origin", "develop"], work);
304
+ g(["checkout", "-b", "feat/x"], work);
305
+ write(work, "b.txt", "two");
306
+ // A second clone advances `develop` only — `main` stays exactly where it was.
307
+ const other = path.join(root, "other");
308
+ g(["clone", origin, other], root);
309
+ g(["checkout", "develop"], other);
310
+ write(other, "c.txt", "three");
311
+ g(["push"], other);
312
+ return work;
313
+ }
314
+
124
315
  export function selfTest(repoRoot: string): number {
125
316
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "cbf-selftest-"));
126
317
  const run = (advanceRemote: boolean, onBase: boolean): number => {
@@ -134,12 +325,29 @@ export function selfTest(repoRoot: string): number {
134
325
  delete process.env["GITHUB_ACTIONS"];
135
326
  return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"], work);
136
327
  };
328
+ const runTwoBase = (base: string): number => {
329
+ const dir = fs.mkdtempSync(path.join(root, "twobase-"));
330
+ const work = buildTwoBaseFixture(dir);
331
+ delete process.env["CI"];
332
+ delete process.env["GITHUB_ACTIONS"];
333
+ return runGateCli(repoRoot, SCRIPT_REL, ["--quiet", "--base", base], work);
334
+ };
137
335
  const cases: SelfTestCase[] = [
138
336
  {
139
337
  name: "a branch whose tracking ref is stale while the remote has moved is REFUSED",
140
338
  expect: "reject",
141
339
  run: () => run(true, false),
142
340
  },
341
+ {
342
+ name: "a branch behind its REAL base (develop) is refused when that base is checked",
343
+ expect: "reject",
344
+ run: () => runTwoBase("develop"),
345
+ },
346
+ {
347
+ name: "the same branch passes against main — the false green a hardcoded base produces",
348
+ expect: "accept",
349
+ run: () => runTwoBase(DEFAULT_BASE),
350
+ },
143
351
  {
144
352
  name: "a branch containing the remote base passes",
145
353
  expect: "accept",
@@ -186,8 +394,6 @@ export function main(argv: string[] = process.argv.slice(2)): number {
186
394
  return selfTest(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."));
187
395
  }
188
396
  const quiet = argv.includes("--quiet");
189
- const baseArg = argv.indexOf("--base");
190
- const base = baseArg >= 0 ? (argv[baseArg + 1] ?? DEFAULT_BASE) : DEFAULT_BASE;
191
397
 
192
398
  // CI merges or refuses on the server; a second opinion here would be noise.
193
399
  if (process.env["CI"] === "true" || process.env["GITHUB_ACTIONS"] === "true") {
@@ -200,11 +406,27 @@ export function main(argv: string[] = process.argv.slice(2)): number {
200
406
  scanReport(0, "detached HEAD — there is no branch whose freshness could be asked");
201
407
  return 0;
202
408
  }
409
+
410
+ // Resolved AFTER the branch is known: the PR's base is a fact about THIS branch.
411
+ const resolved = resolveBase(argv, branch);
412
+ const base = resolved.base;
413
+
203
414
  if (branch === base) {
204
415
  scanReport(0, `standing on ${base} itself — a branch cannot be behind itself`);
205
416
  return 0;
206
417
  }
207
418
 
419
+ // Loud even under --quiet: this run verified the default base and did NOT
420
+ // rule out an open PR against a different one. A partial answer is reported
421
+ // as partial, never folded into the plain green below.
422
+ if (resolved.unverified !== undefined) {
423
+ process.stdout.write(
424
+ `⚠️ check_branch_freshness: could not ask the forge for this branch's PR base ` +
425
+ `(${resolved.unverified}) — checking origin/${base}, the ${describeBase(resolved)}. ` +
426
+ "If an open PR targets a different base, THAT base is NOT verified here.\n",
427
+ );
428
+ }
429
+
208
430
  const sha = remoteHead(base);
209
431
  if (sha === null) {
210
432
  // Loud, not silent. An offline run has not verified the invariant, and a
@@ -221,19 +443,29 @@ export function main(argv: string[] = process.argv.slice(2)): number {
221
443
  scanReport(1);
222
444
  if (containsCommit(sha) === true) {
223
445
  if (!quiet) {
224
- process.stdout.write(`✅ branch is current with origin/${base} (${sha.slice(0, 9)})` + "\n");
446
+ process.stdout.write(
447
+ `✅ branch is current with origin/${base} (${sha.slice(0, 9)}) — ${describeBase(resolved)}\n`,
448
+ );
225
449
  }
226
450
  return 0;
227
451
  }
228
452
 
229
- console.error(`❌ branch is BEHIND origin/${base} — the remote is at ${sha.slice(0, 9)}.`);
453
+ console.error(
454
+ `❌ branch is BEHIND origin/${base} — the remote is at ${sha.slice(0, 9)}. ` +
455
+ `(${describeBase(resolved)})`,
456
+ );
230
457
  console.error("");
231
458
  console.error(" Pushing now opens a PR that may conflict, and worse: another branch");
232
459
  console.error(" may already have shipped what this one is implementing.");
233
460
  console.error("");
234
461
  console.error(` git fetch origin && git merge origin/${base}`);
462
+ console.error(" ./agent-config roadmap:progress # regenerate AFTER every merge, not only on conflict");
235
463
  console.error(" then re-run the gates before pushing.");
236
464
  console.error("");
465
+ console.error(" The dashboard line is not optional housekeeping: the roadmap dashboard is a");
466
+ console.error(" GENERATED file, so a merge that lands cleanly still leaves it describing");
467
+ console.error(" neither side's roadmap set. A clean auto-merge of a generated file is still wrong.");
468
+ console.error("");
237
469
  console.error(
238
470
  " This gate asks the REMOTE, not your tracking ref — a fetch from earlier in",
239
471
  );
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Import an extracted design system into the `design-system.json` contract.
4
+ *
5
+ * Producer-side half of the contract in
6
+ * `design-system-capture/references/design-system-json.md`
7
+ * (road-to-design-system-onramp Phase 1). Three input lanes — a native
8
+ * artifact, a W3C DTCG token file, or an extraction tool's raw JSON — one
9
+ * output shape.
10
+ *
11
+ * **Pure and offline by construction.** It reads a file, transforms it, and
12
+ * writes to stdout. No network, no browser, no crawl: the package owns the
13
+ * contract and not the crawler (council 2026-06-28), and a transform that
14
+ * fetched would quietly become the thing the lock forbids.
15
+ *
16
+ * The output is a **proposal**, never an applied change. It is handed to
17
+ * `design-system-capture`, whose import step confirms it per field against
18
+ * `DESIGN.md` and flags — never auto-applies — anything that conflicts with a
19
+ * registered brand token.
20
+ *
21
+ * CLI:
22
+ *
23
+ * ./scripts-run src/scripts/design_system_import &lt;file&gt; [options]
24
+ *
25
+ * --lane {native,dtcg,dembrandt} Force a lane instead of detecting it.
26
+ * --source-kind {url,repo,dir} Provenance for formats that carry none.
27
+ * --source-ref REF Provenance ref (the URL / repo / path).
28
+ * --captured-at ISO Capture time; omitted records "unknown".
29
+ * --format {json,summary} json (default) or a human-readable summary.
30
+ *
31
+ * Exit codes:
32
+ *
33
+ * 0 — imported; the artifact is on stdout (or its summary).
34
+ * 1 — the input was rejected (no lane matched, or no provenance).
35
+ * 2 — argument error.
36
+ * 3 — internal error.
37
+ */
38
+
39
+ import * as fs from 'node:fs';
40
+ import * as path from 'node:path';
41
+ import { fileURLToPath, pathToFileURL } from 'node:url';
42
+
43
+ import {
44
+ type ImportOutcome,
45
+ type Lane,
46
+ type ProvenanceOverride,
47
+ importDesignSystem,
48
+ } from './_lib/design_system_import.js';
49
+
50
+ const _HERE = fileURLToPath(import.meta.url);
51
+
52
+ const LANES = new Set(['native', 'dtcg', 'dembrandt']);
53
+ const SOURCE_KINDS = new Set(['url', 'repo', 'dir']);
54
+ const FORMATS = new Set(['json', 'summary']);
55
+
56
+ const USAGE =
57
+ 'usage: design_system_import <file> [--lane {native,dtcg,dembrandt}] ' +
58
+ '[--source-kind {url,repo,dir}] [--source-ref REF] [--captured-at ISO] ' +
59
+ '[--format {json,summary}]';
60
+
61
+ interface Args {
62
+ file: string;
63
+ lane: Lane | null;
64
+ sourceKind: 'url' | 'repo' | 'dir' | null;
65
+ sourceRef: string | null;
66
+ capturedAt: string | null;
67
+ format: 'json' | 'summary';
68
+ }
69
+
70
+ function argError(message: string): never {
71
+ process.stderr.write(`${USAGE}\ndesign_system_import: error: ${message}\n`);
72
+ process.exit(2);
73
+ }
74
+
75
+ export function parse_args(argv: readonly string[]): Args {
76
+ const args: Args = {
77
+ file: '',
78
+ lane: null,
79
+ sourceKind: null,
80
+ sourceRef: null,
81
+ capturedAt: null,
82
+ format: 'json',
83
+ };
84
+ const positional: string[] = [];
85
+ for (let i = 0; i < argv.length; i++) {
86
+ const a = argv[i] as string;
87
+ const eq = a.indexOf('=');
88
+ const flag = a.startsWith('--') && eq > 0 ? a.slice(0, eq) : a;
89
+ const inline = a.startsWith('--') && eq > 0 ? a.slice(eq + 1) : null;
90
+ const take = (): string => {
91
+ if (inline !== null) return inline;
92
+ const next = argv[++i];
93
+ if (next === undefined) argError(`argument ${flag}: expected one argument`);
94
+ return next;
95
+ };
96
+ switch (flag) {
97
+ case '-h':
98
+ case '--help':
99
+ process.stdout.write(`${USAGE}\n`);
100
+ process.exit(0);
101
+ break;
102
+ case '--lane': {
103
+ const v = take();
104
+ if (!LANES.has(v)) {
105
+ argError(`argument --lane: invalid choice: '${v}' (choose from 'native', 'dtcg', 'dembrandt')`);
106
+ }
107
+ args.lane = v as Lane;
108
+ break;
109
+ }
110
+ case '--source-kind': {
111
+ const v = take();
112
+ if (!SOURCE_KINDS.has(v)) {
113
+ argError(`argument --source-kind: invalid choice: '${v}' (choose from 'url', 'repo', 'dir')`);
114
+ }
115
+ args.sourceKind = v as 'url' | 'repo' | 'dir';
116
+ break;
117
+ }
118
+ case '--source-ref':
119
+ args.sourceRef = take();
120
+ break;
121
+ case '--captured-at':
122
+ args.capturedAt = take();
123
+ break;
124
+ case '--format': {
125
+ const v = take();
126
+ if (!FORMATS.has(v)) {
127
+ argError(`argument --format: invalid choice: '${v}' (choose from 'json', 'summary')`);
128
+ }
129
+ args.format = v as 'json' | 'summary';
130
+ break;
131
+ }
132
+ default:
133
+ if (a.startsWith('-')) argError(`unrecognized arguments: ${a}`);
134
+ positional.push(a);
135
+ }
136
+ }
137
+ if (positional.length === 0) argError('the following arguments are required: file');
138
+ if (positional.length > 1) argError(`unrecognized arguments: ${positional.slice(1).join(' ')}`);
139
+ args.file = positional[0] as string;
140
+ // Half a provenance is worse than none: it reads as complete in the output
141
+ // while naming nothing traceable, and the contract's whole point is that a
142
+ // reader can go back to the source.
143
+ if ((args.sourceKind === null) !== (args.sourceRef === null)) {
144
+ argError('--source-kind and --source-ref must be given together');
145
+ }
146
+ // `--captured-at` alone was accepted and then ignored: the override object
147
+ // is only built when the kind/ref pair is present. Silently discarding a
148
+ // flag the user typed is worse than refusing it.
149
+ if (args.capturedAt !== null && args.sourceKind === null) {
150
+ argError('--captured-at requires --source-kind and --source-ref');
151
+ }
152
+ return args;
153
+ }
154
+
155
+ /** One line per fact, so a human can see what was mapped without reading JSON. */
156
+ export function render_summary(outcome: ImportOutcome): string {
157
+ if (!outcome.ok) {
158
+ return `REJECTED (${outcome.lane ?? 'no lane'})\n ${outcome.reason}`;
159
+ }
160
+ const ds = outcome.design_system;
161
+ const lines: string[] = [];
162
+ lines.push(`lane: ${outcome.lane}`);
163
+ lines.push(`source: ${ds.source.kind} ${ds.source.ref} @ ${ds.source.captured_at} (provenance from ${ds.source._meta?.provenance_origin ?? 'input'})`);
164
+ const counts: string[] = [];
165
+ const light = Object.keys(ds.colors?.light ?? {}).length;
166
+ const dark = Object.keys(ds.colors?.dark ?? {}).length;
167
+ if (light + dark > 0) counts.push(`colors: ${light} light / ${dark} dark`);
168
+ if (ds.typography?.families?.length) counts.push(`font families: ${ds.typography.families.length}`);
169
+ if (ds.typography?.scale?.length) counts.push(`type steps: ${ds.typography.scale.length}`);
170
+ if (ds.spacing?.scale?.length) counts.push(`spacing steps: ${ds.spacing.scale.length}`);
171
+ if (ds.radius) counts.push(`radius roles: ${Object.keys(ds.radius).length}`);
172
+ if (ds.shadow) counts.push(`shadow roles: ${Object.keys(ds.shadow).length}`);
173
+ if (ds.motion?.durations) counts.push(`motion durations: ${Object.keys(ds.motion.durations).length}`);
174
+ if (ds.motion?.easings) counts.push(`motion easings: ${Object.keys(ds.motion.easings).length}`);
175
+ if (ds.components?.length) counts.push(`components: ${ds.components.length}`);
176
+ lines.push(counts.length > 0 ? `mapped — ${counts.join(', ')}` : 'mapped — nothing (observation only)');
177
+ for (const note of outcome.notes) lines.push(`note: ${note}`);
178
+ lines.push('');
179
+ lines.push('This artifact is observed, not authoritative. Hand it to design-system-capture,');
180
+ lines.push('which confirms it per field; a value conflicting with a registered brand token is');
181
+ lines.push('flagged, never auto-applied.');
182
+ return lines.join('\n');
183
+ }
184
+
185
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
186
+ const args = parse_args(argv);
187
+
188
+ let raw: string;
189
+ try {
190
+ raw = fs.readFileSync(args.file, 'utf-8');
191
+ } catch (exc) {
192
+ const msg = exc instanceof Error ? exc.message : String(exc);
193
+ process.stderr.write(`design_system_import: cannot read ${args.file}: ${msg}\n`);
194
+ return 1;
195
+ }
196
+
197
+ let parsed: unknown;
198
+ try {
199
+ parsed = JSON.parse(raw);
200
+ } catch (exc) {
201
+ const msg = exc instanceof Error ? exc.message : String(exc);
202
+ process.stderr.write(`design_system_import: ${args.file} is not valid JSON: ${msg}\n`);
203
+ return 1;
204
+ }
205
+
206
+ const provenance: ProvenanceOverride | undefined =
207
+ args.sourceKind !== null && args.sourceRef !== null
208
+ ? {
209
+ kind: args.sourceKind,
210
+ ref: args.sourceRef,
211
+ ...(args.capturedAt !== null ? { captured_at: args.capturedAt } : {}),
212
+ }
213
+ : undefined;
214
+
215
+ const outcome = importDesignSystem(parsed, provenance, args.lane ?? undefined);
216
+
217
+ // The input's own provenance wins over a caller override — defensible, and
218
+ // previously invisible: a user who typed --source-ref was never told the
219
+ // flag had no effect on this file.
220
+ if (
221
+ outcome.ok &&
222
+ provenance !== undefined &&
223
+ outcome.design_system.source._meta?.provenance_origin === 'input'
224
+ ) {
225
+ outcome.notes.push(
226
+ `the file carries its own source block, so it wins over --source-ref "${provenance.ref}" — the supplied provenance was not used`,
227
+ );
228
+ }
229
+
230
+ if (!outcome.ok) {
231
+ process.stderr.write(`${render_summary(outcome)}\n`);
232
+ return 1;
233
+ }
234
+ if (args.format === 'summary') {
235
+ process.stdout.write(`${render_summary(outcome)}\n`);
236
+ } else {
237
+ process.stdout.write(`${JSON.stringify(outcome.design_system, null, 2)}\n`);
238
+ for (const note of outcome.notes) process.stderr.write(`note: ${note}\n`);
239
+ }
240
+ return 0;
241
+ }
242
+
243
+ function _isCliEntry(): boolean {
244
+ if (process.argv[1] === undefined) return false;
245
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
246
+ if (import.meta.url === argvUrl) return true;
247
+ try {
248
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
249
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
250
+ return here === argv;
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+
256
+ if (_isCliEntry() || process.argv[1] === _HERE) {
257
+ try {
258
+ process.exit(main());
259
+ } catch (exc) {
260
+ const msg = exc instanceof Error ? exc.message : String(exc);
261
+ process.stderr.write(`design_system_import: internal error: ${msg}\n`);
262
+ process.exit(3);
263
+ }
264
+ }
@@ -95,7 +95,10 @@
95
95
  "agents/roadmaps/archive/road-to-subagent-value-realization.md",
96
96
  "agents/roadmap-assets/road-to-image-brand-typography.assets.md",
97
97
  "CREDITS.md",
98
- "agents/roadmaps/archive/road-to-ecosystem-harvest-index.md"
98
+ "agents/roadmaps/archive/road-to-ecosystem-harvest-index.md",
99
+ "provenance/borrows.jsonl",
100
+ "docs/THIRD-PARTY-NOTICES.md",
101
+ "agents/evidence/reviews/*.review-input/diff.patch"
99
102
  ],
100
103
  "skip_reason": {
101
104
  "vendored_cluster": "design-intelligence/corpus-grounding/design-tokens/react-shadcn-ui/tailwind-engineer + cost/*.mjs ship vendored Apache/MIT code; their upstream attribution is license-required and must NOT be stripped (ADR-061). CREDITS.md is the repo-level attribution file FOR that same cluster — it names the upstreams because the licenses require it, which is the one case source-confidentiality explicitly exempts. It was missing here, so the gate flagged the very attribution the rule mandates.",
@@ -103,7 +106,9 @@
103
106
  "generated_bundles": "content.json* bundle the vendored-cluster skill bodies above.",
104
107
  "self": "the linter + its denylist + the source-confidentiality rule necessarily contain the deny tokens as data.",
105
108
  "validate_safe_paths": "telegraph-derived (MIT) ported code; upstream attribution is license-required.",
106
- "archived_roadmaps_newly_tracked": "Archived roadmaps (and their assets) that predate the source-confidentiality sweep became tracked via the 2026-07 agents-dir gitignore hygiene change. They are frozen historical records; the claudekit mentions are ADR-061 license-attribution work and the remaining names are historical planning references. New roadmaps stay subject to the gate. road-to-ecosystem-harvest-index.md is the sharpest case: the flagged line is its own acceptance criterion, 'No harvest-family artifact names a real external source' — it quotes the names in order to assert their absence, so the gate fires on the sentence that records the cleanup. road-to-opt-retrieval-and-memory.md was REMOVED from this list on 2026-07-28 (road-to-native-code-intelligence Phase 6): its two clear-text mentions named a gitignored borrow-catalogue inbox file, which carries no historical value that a neutral descriptor loses — so the mentions were rewritten and the exception retired rather than kept. Retiring an exception is preferred over holding one whenever the named token is not license-required attribution and not load-bearing for the record."
109
+ "archived_roadmaps_newly_tracked": "Archived roadmaps (and their assets) that predate the source-confidentiality sweep became tracked via the 2026-07 agents-dir gitignore hygiene change. They are frozen historical records; the claudekit mentions are ADR-061 license-attribution work and the remaining names are historical planning references. New roadmaps stay subject to the gate. road-to-ecosystem-harvest-index.md is the sharpest case: the flagged line is its own acceptance criterion, 'No harvest-family artifact names a real external source' — it quotes the names in order to assert their absence, so the gate fires on the sentence that records the cleanup. road-to-opt-retrieval-and-memory.md was REMOVED from this list on 2026-07-28 (road-to-native-code-intelligence Phase 6): its two clear-text mentions named a gitignored borrow-catalogue inbox file, which carries no historical value that a neutral descriptor loses — so the mentions were rewritten and the exception retired rather than kept. Retiring an exception is preferred over holding one whenever the named token is not license-required attribution and not load-bearing for the record.",
110
+ "provenance_ledger": "provenance/borrows.jsonl and the docs/THIRD-PARTY-NOTICES.md it generates are the license-attribution surfaces for CODE borrows, and code-provenance REQUIRES the upstream name in them: the ledger's `source_url` is a mandatory field (provenance/README.md), and lint_provenance fails when the notices file drifts from it. That is the same license-required-attribution case source-confidentiality already exempts for the vendored cluster and CREDITS.md — the rule forbids derivation-attribution, not the notice a license obliges. The collision only surfaced on 2026-08-13 because the ledger had been empty until its first record; before that there was no name to flag. Scope is deliberately these two paths and not `provenance/*`: the harvest ledger next door governs KNOWLEDGE claims, where anonymisation is the rule and no license compels a name.",
111
+ "r2_review_diff_capture": "The R2 completion-review dispatcher writes a verbatim `diff.patch` of the branch under review into a tracked input package. It is a MECHANICAL CAPTURE, not authored text: any source name inside it was already written somewhere else in the same branch, where this gate does see it. A branch that touches the vendored cluster's ATTRIBUTION.md — carved out here precisely because its license requires the name — therefore reproduces that name in the capture and fails the gate for content the gate already allowed at its source. Scope is the capture ONLY: `findings.md` and `prompt.md` are authored prose and stay under the gate, so a reviewer who names a harvest source in their findings is still caught."
107
112
  },
108
113
  "_excluded_tokens": {
109
114
  "agent-os": "collides with our own coined 'Agent-OS primitive' term (universal-skills) — too FP-prone to auto-deny; scrub external 'agent-os' mentions by review.",