@event4u/agent-config 10.3.0 → 10.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 (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +78 -0
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze.md +2 -1
  5. package/dist/agent-src/commands/design-system/capture.md +44 -0
  6. package/dist/agent-src/commands/design-system/generate.md +53 -0
  7. package/dist/agent-src/commands/design-system/import.md +63 -0
  8. package/dist/agent-src/commands/design-system.md +66 -0
  9. package/dist/agent-src/skills/design-system-capture/SKILL.md +9 -0
  10. package/dist/agent-src/skills/design-system-capture/references/design-system-json.md +70 -0
  11. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  12. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
  13. package/dist/cli-delegate/chunk-RLN73PR5.js +157 -0
  14. package/dist/cli-delegate/{chunk-I5EAFRII.js → chunk-ZPSXJTLI.js} +2 -154
  15. package/dist/cli-delegate/cmd_conformance.js +671 -213
  16. package/dist/cli-delegate/cmd_handoff.js +8 -6
  17. package/dist/cli-delegate/cmd_session_recycle.js +5 -3
  18. package/dist/discovery/deprecation-report.md +1 -1
  19. package/dist/discovery/discovery-manifest.json +116 -11
  20. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  21. package/dist/discovery/discovery-manifest.summary.md +3 -3
  22. package/dist/discovery/dormancy-report.md +1 -1
  23. package/dist/discovery/orphan-report.md +1 -1
  24. package/dist/discovery/packs.json +9 -5
  25. package/dist/discovery/trust-report.md +2 -2
  26. package/dist/discovery/workspaces.json +8 -4
  27. package/dist/hooks/dispatch.js +940 -387
  28. package/dist/mcp/registry-manifest.json +2 -2
  29. package/dist/mcp/server.json +2 -2
  30. package/dist/mcp/server.mjs +1102 -592
  31. package/package.json +1 -1
  32. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
  33. package/src/config/gate-coverage.yml +23 -2
  34. package/src/config/gate-violation-baselines.json +2 -2
  35. package/src/scripts/_lib/design_system_import.ts +987 -0
  36. package/src/scripts/design_system_import.ts +264 -0
  37. package/src/scripts/hook_manifest.yaml +71 -2
  38. package/src/scripts/hooks/concern_registry.ts +4 -0
  39. package/src/scripts/hooks/dispatch_hook.ts +10 -0
  40. package/src/scripts/hooks/host_semantics.ts +2 -0
  41. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +212 -0
  42. package/src/scripts/hooks/subagent_ledger_hook.ts +723 -0
  43. package/src/scripts/hooks/turn_end_gate_hook.ts +39 -2
  44. package/src/scripts/lint_hook_manifest.ts +2 -0
  45. package/src/scripts/lint_rule_skill_pack_reach.ts +194 -13
  46. package/tests/fixtures/hooks/subagent_start.json +7 -0
  47. package/tests/fixtures/hooks/subagent_stop.json +8 -0
@@ -129,6 +129,7 @@ import { isSafeTranscriptPath } from './end_review_nudge_hook.js';
129
129
  import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
130
130
  import { readHookStdin } from './hook_stdin.js';
131
131
  import { atomic_write_json, is_replay_mode } from './state_io.js';
132
+ import { openRecordStats } from './subagent_ledger_hook.js';
132
133
 
133
134
  /** Dispatcher-internal block code. Pinned to 1 by `concern_block_exit_parity`. */
134
135
  const EXIT_BLOCK = 1;
@@ -842,6 +843,39 @@ export function main(): number {
842
843
 
843
844
  const workspaceRoot = str(envelope['workspace_root'] as JsonValue | undefined) || process.cwd();
844
845
 
846
+ // Layer 1b — an open subagent dispatch is an EXPLICIT allow
847
+ // (road-to-subagent-lifecycle-integrity Phase 3 Step 2).
848
+ //
849
+ // A turn waiting on an async subagent has, by construction, outstanding
850
+ // work the model cannot finish in this turn. Refusing it is the upstream
851
+ // Stop-hook x async-subagent loop shape (anthropics/claude-code#55754):
852
+ // the gate refuses, the model still cannot proceed because the dispatch
853
+ // has not returned, and the refusal repeats.
854
+ //
855
+ // This is an allow path and can never become a deny path — it only ever
856
+ // lets a turn END that would otherwise have been refused. It reads the
857
+ // Phase-1 ledger; a ledger that is absent, empty or unreadable yields zero
858
+ // open records and changes nothing, so the gate degrades to exactly its
859
+ // previous behaviour rather than failing open in the dangerous direction.
860
+ //
861
+ // R2 round 2, finding 2: it used to be an early `return EXIT_ALLOW`, which
862
+ // suppressed ALL FOUR detectors. A pending dispatch explains a promissory
863
+ // closing (A) and an unsettled completion claim (D); it explains nothing
864
+ // about a language mismatch (B) or an unverified edit (C), and silencing
865
+ // those was scope Step 2 never asked for. Applied per detector below.
866
+ //
867
+ // R2 round 2, finding 1: the count is TTL-filtered inside
868
+ // `openRecordStats`. A leaked record from a dispatch that never returned
869
+ // would otherwise read as "a dispatch is open" forever and, because this
870
+ // branch is an ALLOW, disable those two detectors indefinitely with no
871
+ // signal — the ledger's own leak inherited with the opposite polarity.
872
+ let dispatchOpen = false;
873
+ try {
874
+ dispatchOpen = openRecordStats(workspaceRoot).open_count > 0;
875
+ } catch {
876
+ // An unreadable ledger is not a reason to change the verdict.
877
+ }
878
+
845
879
  const transcriptPath = str(
846
880
  (payload['transcript_path'] ?? payload['transcriptPath']) as JsonValue | undefined,
847
881
  );
@@ -881,7 +915,10 @@ export function main(): number {
881
915
  // second, configurable notion of warranted layered on top of it.
882
916
  const findings: Finding[] = [];
883
917
  for (const f of [
884
- detectPromissory(lastAssistant),
918
+ // A and D are the completion-adjacent pair a pending dispatch excuses
919
+ // (Phase 3 Step 2, narrowed by R2 round 2 finding 2). B and C are not
920
+ // excused by anything about a dispatch and run unchanged.
921
+ dispatchOpen ? null : detectPromissory(lastAssistant),
885
922
  detectLanguage(lastAssistant, readLanguagePin(workspaceRoot)),
886
923
  detectUnverifiedEdit(toolCalls),
887
924
  // Round 7 § Phase 1 — detector D, in the same unconditional list as the
@@ -891,7 +928,7 @@ export function main(): number {
891
928
  // gating is where the comment above says gating belongs — inside the
892
929
  // detector: no CI observed, or a settled read, or no completion claim
893
930
  // ⇒ no finding.
894
- detectCompletionClaim(lastAssistant, readCiSettled(workspaceRoot)),
931
+ dispatchOpen ? null : detectCompletionClaim(lastAssistant, readCiSettled(workspaceRoot)),
895
932
  ]) {
896
933
  if (f) findings.push(f);
897
934
  }
@@ -55,6 +55,8 @@ const EVENT_VOCABULARY: ReadonlySet<string> = new Set([
55
55
  "stop",
56
56
  "pre_compact",
57
57
  "agent_error",
58
+ "subagent_start",
59
+ "subagent_stop",
58
60
  ]);
59
61
 
60
62
  // Known platform identifiers.
@@ -51,19 +51,40 @@
51
51
  * findings under `--strict` · 2 usage or IO error. A scan that walked no rules
52
52
  * exits 2 rather than green — a gate that scanned nothing must never read as a
53
53
  * pass.
54
+ *
55
+ * `--self-test` proves those rejections still fire in the binary a contributor
56
+ * runs, not merely in the imported functions `tests/scripts/pack_reach.test.ts`
57
+ * exercises. `--root <dir>` exists for it: the three inputs are ordinary tree
58
+ * reads, so a temporary fixture repo can reproduce every verdict, which is why
59
+ * this gate adopts the harness rather than claiming the exemption.
54
60
  */
55
61
 
56
62
  import * as fs from 'node:fs';
63
+ import * as os from 'node:os';
57
64
  import * as path from 'node:path';
58
65
  import { fileURLToPath } from 'node:url';
59
66
 
60
67
  import { parse as parseYaml } from 'yaml';
61
68
 
69
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
70
+
62
71
  const _HERE = fileURLToPath(import.meta.url);
63
72
  const REPO = path.resolve(path.dirname(_HERE), '..', '..');
64
- const RULES_DIR = path.join(REPO, 'src', 'rules');
65
- const SKILLS_DIR = path.join(REPO, 'src', 'skills');
66
- const PACKS_FILE = path.join(REPO, 'src', 'config', 'discovery', 'packs.yml');
73
+
74
+ interface Roots {
75
+ rulesDir: string;
76
+ skillsDir: string;
77
+ packsFile: string;
78
+ }
79
+
80
+ /** The three inputs, resolved against `root` so a fixture tree can stand in. */
81
+ function rootsFor(root: string): Roots {
82
+ return {
83
+ rulesDir: path.join(root, 'src', 'rules'),
84
+ skillsDir: path.join(root, 'src', 'skills'),
85
+ packsFile: path.join(root, 'src', 'config', 'discovery', 'packs.yml'),
86
+ };
87
+ }
67
88
 
68
89
  export interface PackDef {
69
90
  id: string;
@@ -212,10 +233,165 @@ export function analyze(
212
233
  return findings;
213
234
  }
214
235
 
215
- function main(): number {
216
- const packs = readPacks(PACKS_FILE);
217
- const rules = readRules(RULES_DIR);
218
- const skillPacks = readSkillPacks(SKILLS_DIR);
236
+ interface Args {
237
+ quiet: boolean;
238
+ strict: boolean;
239
+ selfTest: boolean;
240
+ root: string;
241
+ }
242
+
243
+ function _parseArgs(argv: readonly string[]): Args {
244
+ const args: Args = { quiet: false, strict: false, selfTest: false, root: REPO };
245
+ for (let i = 0; i < argv.length; i += 1) {
246
+ const a = argv[i];
247
+ if (a === '--quiet') args.quiet = true;
248
+ else if (a === '--strict') args.strict = true;
249
+ else if (a === '--self-test') args.selfTest = true;
250
+ else if (a === '--root') {
251
+ // Never fall back to the real repo. A self-test whose root argument
252
+ // went missing would otherwise scan the live tree instead of its
253
+ // fixture and could still exit 0 — a green run proving nothing,
254
+ // which is the exact failure `_lib/gate_self_test.ts` exists for.
255
+ const value = argv[i + 1];
256
+ if (value === undefined || value.startsWith('--')) {
257
+ throw new Error('--root requires a directory argument');
258
+ }
259
+ args.root = value;
260
+ i += 1;
261
+ }
262
+ }
263
+ return args;
264
+ }
265
+
266
+ /**
267
+ * Build a fixture tree. `packsYml` is written verbatim; each rule and skill is
268
+ * a minimal frontmatter block, which is all `readRules` / `readSkillPacks`
269
+ * ever look at.
270
+ */
271
+ function _fixture(
272
+ tmp: string,
273
+ name: string,
274
+ packsYml: string,
275
+ rules: ReadonlyArray<{ name: string; packs?: string[]; routes: string[] }>,
276
+ skills: ReadonlyArray<{ name: string; packs?: string[] }>,
277
+ ): string {
278
+ const root = path.join(tmp, name);
279
+ const { rulesDir, skillsDir, packsFile } = rootsFor(root);
280
+ fs.mkdirSync(rulesDir, { recursive: true });
281
+ fs.mkdirSync(skillsDir, { recursive: true });
282
+ fs.mkdirSync(path.dirname(packsFile), { recursive: true });
283
+ fs.writeFileSync(packsFile, packsYml, 'utf-8');
284
+ const list = (key: string, values?: string[]): string =>
285
+ values === undefined ? '' : `${key}: [${values.join(', ')}]\n`;
286
+ for (const rule of rules) {
287
+ fs.writeFileSync(
288
+ path.join(rulesDir, `${rule.name}.md`),
289
+ `---\n${list('packs', rule.packs)}${list('routes_to', rule.routes)}---\n\n# ${rule.name}\n`,
290
+ 'utf-8',
291
+ );
292
+ }
293
+ for (const skill of skills) {
294
+ fs.mkdirSync(path.join(skillsDir, skill.name), { recursive: true });
295
+ fs.writeFileSync(
296
+ path.join(skillsDir, skill.name, 'SKILL.md'),
297
+ `---\n${list('packs', skill.packs)}---\n\n# ${skill.name}\n`,
298
+ 'utf-8',
299
+ );
300
+ }
301
+ return root;
302
+ }
303
+
304
+ function _selfTest(): number {
305
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'lrspr-selftest-'));
306
+ const runRaw = (args: readonly string[]): number =>
307
+ runGateCli(REPO, 'src/scripts/lint_rule_skill_pack_reach.ts', args, REPO);
308
+ const run = (root: string, extra: readonly string[] = []): number =>
309
+ runRaw(['--root', root, '--quiet', ...extra]);
310
+
311
+ // `alpha` stands alone; `beta` pulls `alpha` in transitively.
312
+ const PACKS = '- id: alpha\n- id: beta\n requires: [alpha]\n- id: gamma\n';
313
+
314
+ try {
315
+ const unreachable = _fixture(
316
+ tmp,
317
+ 'unreachable',
318
+ PACKS,
319
+ [{ name: 'r', packs: ['gamma'], routes: ['skill:s'] }],
320
+ [{ name: 's', packs: ['alpha'] }],
321
+ );
322
+ const viaRequires = _fixture(
323
+ tmp,
324
+ 'via-requires',
325
+ PACKS,
326
+ [{ name: 'r', packs: ['beta'], routes: ['skill:s'] }],
327
+ [{ name: 's', packs: ['alpha'] }],
328
+ );
329
+ const unscopedRule = _fixture(
330
+ tmp,
331
+ 'unscoped-rule',
332
+ PACKS,
333
+ [{ name: 'r', routes: ['skill:s'] }],
334
+ [{ name: 's', packs: ['alpha'] }],
335
+ );
336
+ const unscopedSkill = _fixture(
337
+ tmp,
338
+ 'unscoped-skill',
339
+ PACKS,
340
+ [{ name: 'r', packs: ['gamma'], routes: ['skill:s'] }],
341
+ [{ name: 's' }],
342
+ );
343
+ const empty = _fixture(tmp, 'empty', PACKS, [], [{ name: 's', packs: ['alpha'] }]);
344
+
345
+ return runSelfTest({
346
+ gate: 'lint_rule_skill_pack_reach',
347
+ minCases: 6,
348
+ minRejectCases: 3,
349
+ cases: [
350
+ {
351
+ name: 'a rule routing into a pack its own closure never reaches is rejected under --strict',
352
+ expect: 'reject',
353
+ run: () => run(unreachable, ['--strict']),
354
+ },
355
+ {
356
+ name: 'an empty rule corpus refuses to report a pass',
357
+ expect: 'reject',
358
+ run: () => run(empty, ['--strict']),
359
+ },
360
+ {
361
+ name: 'the same route passes when `requires` pulls the skill pack in transitively',
362
+ expect: 'accept',
363
+ run: () => run(viaRequires, ['--strict']),
364
+ },
365
+ {
366
+ name: 'an unscoped rule ships everywhere, so its route is always reachable',
367
+ expect: 'accept',
368
+ run: () => run(unscopedRule, ['--strict']),
369
+ },
370
+ {
371
+ name: 'an unscoped skill ships everywhere, so no route into it can be unreachable',
372
+ expect: 'accept',
373
+ run: () => run(unscopedSkill, ['--strict']),
374
+ },
375
+ {
376
+ name: '--root with no value is refused rather than silently scanning the real tree',
377
+ expect: 'reject',
378
+ run: () => runRaw(['--quiet', '--root']),
379
+ },
380
+ ],
381
+ });
382
+ } finally {
383
+ fs.rmSync(tmp, { recursive: true, force: true });
384
+ }
385
+ }
386
+
387
+ function main(argv: readonly string[] = process.argv.slice(2)): number {
388
+ const args = _parseArgs(argv);
389
+ if (args.selfTest) return _selfTest();
390
+
391
+ const { rulesDir, skillsDir, packsFile } = rootsFor(args.root);
392
+ const packs = readPacks(packsFile);
393
+ const rules = readRules(rulesDir);
394
+ const skillPacks = readSkillPacks(skillsDir);
219
395
 
220
396
  if (rules.length === 0 || skillPacks.size === 0 || packs.size === 0) {
221
397
  process.stderr.write(
@@ -231,8 +407,7 @@ function main(): number {
231
407
  // --quiet suppresses the per-finding lines but never the count line: this
232
408
  // gate is expected to stay non-empty for a while, and a quiet run that
233
409
  // printed nothing at all would read as clean.
234
- const quiet = process.argv.includes('--quiet');
235
- if (!quiet) {
410
+ if (!args.quiet) {
236
411
  for (const finding of errors) {
237
412
  process.stdout.write(`❌ ${finding.subject} — ${finding.detail}\n`);
238
413
  }
@@ -241,16 +416,22 @@ function main(): number {
241
416
  }
242
417
  }
243
418
 
244
- const strict = process.argv.includes('--strict');
419
+ // Two lines on purpose. `check_gate_coverage`'s SCANNED_RE is
420
+ // `/^\s*scanned:\s*(\d+)\s*$/m` — the count must END the line — so the
421
+ // detailed breakdown below can never satisfy it. This gate was registered
422
+ // `enforced` with `min_scanned: 90` and had therefore reported `null` since
423
+ // 924cad87f: an enforced floor that no output could ever meet. Emit the
424
+ // machine-readable contract line first, then the human breakdown.
425
+ process.stdout.write(`scanned: ${rules.length}\n`);
245
426
  process.stdout.write(
246
- `scanned: ${rules.length} rule(s), ${skillPacks.size} skill(s), ${packs.size} pack(s) — ${errors.length} unreachable-route, ${advisories.length} unrouted-skill\n`,
427
+ `${rules.length} rule(s), ${skillPacks.size} skill(s), ${packs.size} pack(s) — ${errors.length} unreachable-route, ${advisories.length} unrouted-skill\n`,
247
428
  );
248
- if (errors.length > 0 && !strict) {
429
+ if (errors.length > 0 && !args.strict) {
249
430
  process.stdout.write(
250
431
  `advisory run: ${errors.length} unreachable-route finding(s) reported, exit 0. Promote with --strict once the set is empty.\n`,
251
432
  );
252
433
  }
253
- return strict && errors.length > 0 ? 1 : 0;
434
+ return args.strict && errors.length > 0 ? 1 : 0;
254
435
  }
255
436
 
256
437
  if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(_HERE)) {
@@ -0,0 +1,7 @@
1
+ {
2
+ "session_id": "fixture-subagent-start-001",
3
+ "agent_id": "fixture-agent-0001",
4
+ "agent_type": "Explore",
5
+ "cwd": "/workspace",
6
+ "platform_hint": "claude"
7
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "session_id": "fixture-subagent-stop-001",
3
+ "agent_id": "fixture-agent-0001",
4
+ "agent_type": "Explore",
5
+ "last_assistant_message": "{\"summary\":\"fixture envelope\",\"handoff\":\"none\",\"confidence\":\"high\",\"findings\":[],\"risks\":[]}",
6
+ "cwd": "/workspace",
7
+ "platform_hint": "claude"
8
+ }