@event4u/agent-config 9.33.0 → 9.35.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 (105) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +154 -145
  3. package/CONTRIBUTING.md +23 -14
  4. package/dist/agent-src/commands/agent-handoff.md +15 -3
  5. package/dist/agent-src/commands/roadmap/next.md +37 -5
  6. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
  7. package/dist/agent-src/scripts/roadmap_gates.ts +119 -4
  8. package/dist/agent-src/skills/corpus-grounding/SKILL.md +21 -12
  9. package/dist/agent-src/skills/design-tokens/SKILL.md +15 -0
  10. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +24 -0
  11. package/dist/agent-src/skills/rule-writing/SKILL.md +23 -0
  12. package/dist/agent-src/skills/skill-writing/SKILL.md +80 -2
  13. package/dist/agent-src/skills/systematic-debugging/SKILL.md +5 -3
  14. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +24 -0
  15. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +25 -0
  16. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  17. package/dist/agent-src/templates/scripts/memory_lookup.ts +1 -2
  18. package/dist/agent-src/templates/scripts/memory_signal.ts +2 -4
  19. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
  20. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
  21. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
  22. package/dist/cli/registry.js +2 -2
  23. package/dist/cli/registry.js.map +1 -1
  24. package/dist/cli-delegate/{chunk-PMQ6Z3T3.js → chunk-2TGMGLBL.js} +11 -4
  25. package/dist/cli-delegate/{chunk-YPAWXXXR.js → chunk-55RI5UXK.js} +17 -0
  26. package/dist/cli-delegate/{chunk-MA4GFWDY.js → chunk-H6F2CQFI.js} +1 -1
  27. package/dist/cli-delegate/{chunk-2WELHO3P.js → chunk-I5EAFRII.js} +35 -0
  28. package/dist/cli-delegate/{chunk-3EWYC3YK.js → chunk-UEZ7D35A.js} +4 -3
  29. package/dist/cli-delegate/{chunk-UVKNIJH2.js → chunk-ZAXH3ZWU.js} +3 -0
  30. package/dist/cli-delegate/cmd_conformance.js +2 -2
  31. package/dist/cli-delegate/cmd_doctor.js +2 -2
  32. package/dist/cli-delegate/cmd_export.js +2 -2
  33. package/dist/cli-delegate/cmd_handoff.js +3 -3
  34. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  35. package/dist/cli-delegate/cmd_prune.js +2 -2
  36. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  37. package/dist/cli-delegate/cmd_session_recycle.js +7 -2
  38. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  39. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  40. package/dist/cli-delegate/cmd_sync.js +2 -2
  41. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  42. package/dist/cli-delegate/cmd_validate.js +2 -2
  43. package/dist/discovery/deprecation-report.md +1 -1
  44. package/dist/discovery/discovery-manifest.json +13 -13
  45. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  46. package/dist/discovery/discovery-manifest.summary.md +2 -2
  47. package/dist/discovery/dormancy-report.md +17 -0
  48. package/dist/discovery/orphan-report.md +1 -1
  49. package/dist/discovery/packs.json +3 -3
  50. package/dist/discovery/trust-report.md +1 -1
  51. package/dist/discovery/workspaces.json +3 -3
  52. package/dist/hooks/dispatch.js +355 -85
  53. package/dist/install/install.mjs +3 -0
  54. package/dist/mcp/registry-manifest.json +3 -3
  55. package/dist/mcp/server.json +2 -2
  56. package/dist/mcp/server.mjs +192 -12
  57. package/dist/server/schemas/settings.js +1 -0
  58. package/dist/server/schemas/settings.js.map +1 -1
  59. package/docs/contracts/settings-classes.md +3 -2
  60. package/docs/guidelines/agent-infra/failure-signatures.md +47 -12
  61. package/package.json +2 -2
  62. package/src/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
  63. package/src/agent-src/scripts/roadmap_gates.ts +119 -4
  64. package/src/agent-src/templates/scripts/memory_lookup.ts +1 -2
  65. package/src/agent-src/templates/scripts/memory_signal.ts +2 -4
  66. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
  67. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
  68. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
  69. package/src/config/agent-settings.template.yml +11 -2
  70. package/src/config/quorum-attendance-budget.json +11 -3
  71. package/src/scripts/_cli/cmd_session_recycle.ts +6 -1
  72. package/src/scripts/_dispatch.bash +6 -2
  73. package/src/scripts/_lib/env_kill_switch.ts +37 -0
  74. package/src/scripts/_lib/security_lint.ts +18 -4
  75. package/src/scripts/_lib/self_repair.ts +60 -0
  76. package/src/scripts/_lib/self_repair_store.ts +65 -5
  77. package/src/scripts/_lib/session_register.ts +67 -1
  78. package/src/scripts/_lib/subagent_capsule.ts +78 -1
  79. package/src/scripts/ai_council/config.ts +60 -0
  80. package/src/scripts/ai_council/events_log.ts +105 -6
  81. package/src/scripts/ai_council/orchestrator.ts +38 -0
  82. package/src/scripts/ai_council/prompts.ts +165 -3
  83. package/src/scripts/ai_council/quorum.ts +87 -0
  84. package/src/scripts/ai_team/review_gate.ts +2 -2
  85. package/src/scripts/build_discovery_manifest.ts +165 -0
  86. package/src/scripts/check_claims.ts +51 -2
  87. package/src/scripts/ci_time_ratio.ts +3 -1
  88. package/src/scripts/council_cli.ts +36 -1
  89. package/src/scripts/discovery_graph.ts +201 -27
  90. package/src/scripts/dispatch_r2_reviewer.ts +102 -1
  91. package/src/scripts/hooks/turn_end_gate_hook.ts +172 -11
  92. package/src/scripts/lint_framework_leakage.ts +195 -28
  93. package/src/scripts/lint_framework_leakage_allowlist.json +94 -86
  94. package/src/scripts/lint_handoffs.ts +92 -2
  95. package/src/scripts/lint_hedge_words.ts +317 -0
  96. package/src/scripts/lint_mcp_config_security.ts +1 -1
  97. package/src/scripts/lint_skill_frontmatter_safety.ts +1 -1
  98. package/src/scripts/memory_lookup.ts +1 -2
  99. package/src/scripts/schemas/command.schema.json +4 -0
  100. package/src/scripts/schemas/skill.schema.json +4 -0
  101. package/src/scripts/self_repair_cli.ts +26 -1
  102. package/src/scripts/session_register_hook.ts +238 -18
  103. package/src/scripts/sessions_cli.ts +377 -12
  104. package/src/scripts/skill_linter.ts +58 -2
  105. package/src/server/schemas/settings.ts +3 -0
@@ -128,12 +128,42 @@ function _branch_touched_paths(root: string, base: string): Set<string> | null {
128
128
  return out;
129
129
  }
130
130
 
131
+ /**
132
+ * Paths where a repo-relative path is DATA, not a live reference — so
133
+ * rewriting it falsifies a record instead of keeping a link resolvable.
134
+ *
135
+ * Two classes, and the second is why this predicate exists at all:
136
+ *
137
+ * - `agents/evidence/**` — a committed record of what was true, or of what a
138
+ * reviewer was shown, at one point in time. Retroactively editing it makes it
139
+ * claim something that did not happen, which is the failure
140
+ * `evaluator-independence` exists to prevent ("the prompt ships with the
141
+ * verdict" is worthless if a later sweep can edit the prompt).
142
+ * - `*.patch` anywhere — a patch's `diff --git a/… b/…` and `+++ b/…` lines are
143
+ * the payload. Rewriting them detaches the patch from the commit it records
144
+ * AND makes `git apply` target a path the diff never created.
145
+ *
146
+ * Measured 2026-08-11: archiving one roadmap rewrote four frozen artefacts
147
+ * under `agents/evidence/reviews/`, including the `diff.patch` headers of a
148
+ * recorded review input. Nothing caught it — the sweep reported it as
149
+ * "4 ref(s) migrated", which reads like success.
150
+ */
151
+ const _FROZEN_RECORD_PREFIXES: readonly string[] = ['agents/evidence/'];
152
+
153
+ /** Is `rel` a frozen record whose path strings must not be rewritten? */
154
+ export function _is_frozen_record(rel: string): boolean {
155
+ const norm = rel.split(path.sep).join('/');
156
+ return _FROZEN_RECORD_PREFIXES.some((p) => norm.startsWith(p)) || norm.endsWith('.patch');
157
+ }
158
+
131
159
  /**
132
160
  * Rewrite full-path references `old_rel` → `new_rel` in tracked files.
133
161
  *
134
162
  * Only the exact repo-relative path is rewritten (bare-filename mentions are
135
163
  * left alone). The archived file's own path never matches because the search
136
- * string is the un-archived path.
164
+ * string is the un-archived path. Frozen records are skipped — see
165
+ * `_is_frozen_record`; a link left dangling in a historical record is correct,
166
+ * because the record describes a tree that no longer exists.
137
167
  */
138
168
  function _inbound_ref_rewrite(
139
169
  root: string,
@@ -165,6 +195,10 @@ function _inbound_ref_rewrite(
165
195
  // skip the roadmap file itself
166
196
  continue;
167
197
  }
198
+ if (_is_frozen_record(rel)) {
199
+ // A record of a past state — rewriting it falsifies the record.
200
+ continue;
201
+ }
168
202
  const fp = path.join(root, rel);
169
203
  let text: string;
170
204
  try {
@@ -46,6 +46,13 @@
46
46
  * ./agent-config gates --all # every open blocker, grouped by owner
47
47
  * ./agent-config gates --json # machine-readable
48
48
  * ./agent-config gates --reply # reply-close form; empty when none
49
+ * ./agent-config gates --pending # staged actions awaiting confirmation
50
+ *
51
+ * `--pending` reads a different source — the staged-confirmation store, not the
52
+ * roadmap tree — and stays out of `--reply` on purpose. ADR-222 fixes the
53
+ * reply-close form at exactly ONE decision rendered in full; folding a second
54
+ * source into it would silently change which decision "the one" is, and that
55
+ * contract is not this step's to move.
49
56
  */
50
57
 
51
58
  import * as path from 'node:path';
@@ -58,6 +65,8 @@ import {
58
65
  blocker_needs_user as needsUser,
59
66
  type Blocker,
60
67
  } from './update_roadmap_progress.js';
68
+ import { listPending } from '../templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.js';
69
+ import type { StagedAction } from '../templates/scripts/work_engine/hooks/builtin/staged_confirmation.js';
61
70
 
62
71
  const _HERE = fileURLToPath(import.meta.url);
63
72
 
@@ -327,6 +336,89 @@ function renderJson(entries: readonly Entry[], all: boolean): string {
327
336
  );
328
337
  }
329
338
 
339
+ /**
340
+ * `--pending`: staged actions awaiting a human confirmation.
341
+ *
342
+ * A second source under the same verb, and the reason it belongs here rather
343
+ * than in a new command is the verb's own definition — "open decisions that
344
+ * need you, as actions". A staged irreversible action IS one, and the most
345
+ * literal kind: the roadmap blockers above are decisions a human owes a plan,
346
+ * this is a decision a human owes an action that is already loaded and waiting.
347
+ *
348
+ * Expired stages are counted, never listed as actionable. An expired stage
349
+ * cannot execute (`stageStatus` derives that from the clock, not from a sweep
350
+ * having run), so rendering it beside a live one would put two rows in front of
351
+ * the reader where only one is a decision.
352
+ */
353
+ function renderPending(root: string, now: number): string {
354
+ const rows = listPending(root, now);
355
+ const live = rows.filter((r) => r.status === 'pending');
356
+ const expired = rows.filter((r) => r.status === 'expired');
357
+ const lines: string[] = [];
358
+
359
+ if (live.length === 0) {
360
+ lines.push(
361
+ expired.length === 0
362
+ ? 'No staged actions awaiting confirmation.'
363
+ : `Nothing awaits your confirmation. ${expired.length} stage` +
364
+ `${expired.length !== 1 ? 's' : ''} expired unconfirmed — the action never fired.`,
365
+ );
366
+ return lines.join('\n') + '\n';
367
+ }
368
+
369
+ lines.push(
370
+ `${live.length} staged action${live.length !== 1 ? 's' : ''} await` +
371
+ `${live.length === 1 ? 's' : ''} your confirmation` +
372
+ (expired.length > 0 ? ` · ${expired.length} expired unconfirmed` : ''),
373
+ );
374
+ let n = 0;
375
+ for (const { stage } of live) {
376
+ n += 1;
377
+ lines.push(...renderStage(stage, n));
378
+ }
379
+ lines.push('');
380
+ return lines.join('\n') + '\n';
381
+ }
382
+
383
+ function renderStage(stage: StagedAction, index: number): string[] {
384
+ const head = `${index} · ${stage.action}`;
385
+ const tail = `token: ${stage.token}`;
386
+ const dashes = Math.max(3, WIDTH - head.length - tail.length - 6);
387
+ return [
388
+ '',
389
+ `── ${head} ${'─'.repeat(dashes)} ${tail}`.trimEnd(),
390
+ // The object is the first field on purpose: an approval that does not
391
+ // name the exact object is the thing non-destructive-by-default forbids,
392
+ // so it is what the reader must see before anything else.
393
+ ...field('Object:', stage.object),
394
+ ...field('Staged by:', stage.source),
395
+ ...field('Expires:', stage.expires_at),
396
+ ];
397
+ }
398
+
399
+ function renderPendingJson(root: string, now: number): string {
400
+ const rows = listPending(root, now);
401
+ return (
402
+ JSON.stringify(
403
+ {
404
+ awaitingYou: rows.filter((r) => r.status === 'pending').length,
405
+ expired: rows.filter((r) => r.status === 'expired').length,
406
+ staged: rows.map(({ stage, status }) => ({
407
+ token: stage.token,
408
+ action: stage.action,
409
+ object: stage.object,
410
+ source: stage.source,
411
+ stagedAt: stage.staged_at,
412
+ expiresAt: stage.expires_at,
413
+ status,
414
+ })),
415
+ },
416
+ null,
417
+ 2,
418
+ ) + '\n'
419
+ );
420
+ }
421
+
330
422
  function _isDir(p: string): boolean {
331
423
  try {
332
424
  return fs.statSync(p).isDirectory();
@@ -340,8 +432,8 @@ function _isDir(p: string): boolean {
340
432
  * `agents/roadmaps`, otherwise fall back to the git toplevel (monorepo
341
433
  * sub-project support).
342
434
  */
343
- function _resolveRepoRoot(start: string): string {
344
- if (_isDir(path.join(start, 'agents', 'roadmaps'))) {
435
+ function _resolveRepoRoot(start: string, marker = path.join('agents', 'roadmaps')): string {
436
+ if (_isDir(path.join(start, marker))) {
345
437
  return start;
346
438
  }
347
439
  try {
@@ -351,7 +443,7 @@ function _resolveRepoRoot(start: string): string {
351
443
  timeout: 10_000,
352
444
  });
353
445
  const top = (r.stdout || '').trim();
354
- if (r.status === 0 && top !== '' && _isDir(path.join(top, 'agents', 'roadmaps'))) {
446
+ if (r.status === 0 && top !== '' && _isDir(path.join(top, marker))) {
355
447
  return top;
356
448
  }
357
449
  } catch {
@@ -365,6 +457,18 @@ function main(argv?: readonly string[]): number {
365
457
  const all = args.includes('--all');
366
458
  const json = args.includes('--json');
367
459
  const reply = args.includes('--reply');
460
+ const pending = args.includes('--pending');
461
+
462
+ // `--pending` resolves against `agents/runtime/`, not `agents/roadmaps/`,
463
+ // and is answered BEFORE the roadmaps-directory exit below: a staged action
464
+ // is independent of whether this project plans in roadmaps at all, so
465
+ // "no roadmaps here" is not an answer to "what awaits my confirmation".
466
+ if (pending) {
467
+ const root = _resolveRepoRoot(process.cwd(), path.join('agents', 'runtime'));
468
+ const now = Date.now();
469
+ process.stdout.write(json ? renderPendingJson(root, now) : renderPending(root, now));
470
+ return 0;
471
+ }
368
472
 
369
473
  const repoRoot = _resolveRepoRoot(process.cwd());
370
474
  const roadmapRoot = path.join(repoRoot, 'agents', 'roadmaps');
@@ -412,5 +516,16 @@ if (_isCliEntry()) {
412
516
  process.exitCode = main();
413
517
  }
414
518
 
415
- export { main, needsUser, regroupTodo, wrap, collectEntries, render, renderJson, renderReply };
519
+ export {
520
+ main,
521
+ needsUser,
522
+ regroupTodo,
523
+ wrap,
524
+ collectEntries,
525
+ render,
526
+ renderJson,
527
+ renderReply,
528
+ renderPending,
529
+ renderPendingJson,
530
+ };
416
531
  export type { Entry };
@@ -184,18 +184,27 @@ Each domain ships `data/manifest.json` beside its CSVs — declaring its
184
184
  - BM25 tokenizer drops tokens ≤2 chars — "UI", "a11y" style queries need
185
185
  longer companions.
186
186
 
187
- ## Runtime-safety review (Step 1.6 record)
188
-
189
- - **Read-only by default** — the engine opens corpus CSVs under the
190
- manifest's directory only (`resolve_data_path` refuses absolute paths
191
- and `..` escapes).
192
- - **Single write surface** — `--persist DIR` (opt-in) writes markdown
193
- under the caller-chosen `DIR`; nothing else writes.
194
- - **No network, no subprocess, no secrets** — pure stdlib; embeddings /
195
- remote retrievers are intentionally not implemented (ADR-061 §2).
196
- - `rules_module` executes a manifest-adjacent TypeScript module (dynamic
197
- `import()`) — same trust domain as the skill that ships the manifest;
198
- containment enforced by `resolve_data_path`.
187
+ ## Security constraints (Step 1.6 runtime-safety record)
188
+
189
+ Four scripts ship under `scripts/`: `ground.ts` (CLI), `bm25_search.ts`,
190
+ `decision_engine.ts`, `schema_validator.ts`.
191
+
192
+ - **What they may touch** — corpus CSVs and the manifest under the
193
+ manifest's own directory. `resolve_data_path` refuses absolute paths and
194
+ `..` escapes, so the manifest directory is the read boundary.
195
+ - **What they must never do** — read outside that directory, reach the
196
+ network, or spawn a subprocess. Embeddings and remote retrievers are
197
+ intentionally not implemented (ADR-061 §2), and that is a containment
198
+ decision, not a missing feature.
199
+ - **Default invocation** — read-only. The single write surface is opt-in
200
+ `--persist DIR`, which writes `MASTER.md` (and `pages/<page>.md` when
201
+ `--page` is given) under `DIR/design-system/<project-slug>/`. Nothing
202
+ else writes.
203
+ - **Outbound** — nothing. No network access at all.
204
+
205
+ One qualified exception: `rules_module` executes a manifest-adjacent
206
+ TypeScript module via dynamic `import()`. That is the same trust domain as
207
+ the skill shipping the manifest, and its containment is `resolve_data_path`.
199
208
 
200
209
  ## See also
201
210
 
@@ -113,6 +113,21 @@ four-operation split assigns to *rules/linters*, not the corpus).
113
113
  3. `validate` report — exit 0 evidence, or the violations list handed to
114
114
  the polish round as `token_violation` findings.
115
115
 
116
+ ## Security constraints
117
+
118
+ `scripts/tokens.ts` is the only shipped script.
119
+
120
+ - **What it may touch** — the `tokens.json` config path it is given, the
121
+ directory it is asked to `validate`, and the `--output` path it is asked
122
+ to write. Nothing outside the paths named on the command line.
123
+ - **What it must never do** — reach the network, spawn a subprocess, or
124
+ write to a path the caller did not name. It never edits the generated CSS
125
+ it produced earlier; regeneration replaces, it does not patch.
126
+ - **Default invocation** — read-only. `generate` prints to stdout unless
127
+ `--output PATH` is passed; `--output` is the flag that makes it mutating,
128
+ and it creates only that file's parent directory. `validate` never writes.
129
+ - **Outbound** — nothing. No network access.
130
+
116
131
  ## Do NOT
117
132
 
118
133
  - Do NOT hand-edit generated CSS — `tokens.json` is the single source.
@@ -299,6 +299,30 @@ carve-out for the tool-specific part.
299
299
  values the component no longer accepts is stale documentation; keep them beside
300
300
  the component and update them in the same change.
301
301
 
302
+ ## Security constraints
303
+
304
+ `scripts/shadcn_add.ts` is the only shipped script, and it is the single
305
+ subprocess-plus-network surface in this suite — treat it accordingly.
306
+
307
+ - **What it may touch** — the project rooted at `--project-root` (default
308
+ `cwd`): it reads `components.json` there and lets the upstream CLI write
309
+ the generated primitives under that root. Nothing outside it.
310
+ - **What it must never do** — run without the exact `npx` command and the
311
+ component list having been shown to the user first, per the gate above.
312
+ Never pass `--overwrite` unprompted; never treat a non-zero exit as
313
+ success; never work around a missing `npx` silently
314
+ ([`missing-tool-handling`](../../rules/missing-tool-handling.md)).
315
+ - **Default invocation** — mutating. A bare `shadcn_add <component>`
316
+ spawns `npx shadcn@latest add <component>` and writes into the project.
317
+ `--dry-run` is the read-only path and prints the command it would run;
318
+ `--list` is read-only too. Use `--dry-run` for the proposal step.
319
+ - **Outbound** — yes, and this is the point of the gate: `npx` resolves
320
+ `shadcn@latest` from the public npm registry on every live run, so both
321
+ the code fetched and the components written are chosen upstream, at run
322
+ time, not pinned here. That is the egress leg of the lethal trifecta
323
+ ([`lethal-trifecta-guard`](../../rules/lethal-trifecta-guard.md)); the
324
+ human confirmation is what keeps it off an autonomous path.
325
+
302
326
  ## Do NOT
303
327
 
304
328
  - Do NOT install `shadcn-ui` from npm — primitives are scaffolded.
@@ -355,3 +355,26 @@ Good description (trigger-shaped, names domain + symptoms):
355
355
  Bad description (no trigger, too vague):
356
356
 
357
357
  > "Commit conventions"
358
+
359
+ ### Contrastive-example slot (optional, for the rule body)
360
+
361
+ The pair above governs descriptions. A rule whose obligation is easy to agree
362
+ with and hard to apply — what counts as a cheap question, when an interrupt is
363
+ an interrupt, which reply mirrors the user's language — needs the same treatment
364
+ for the **behaviour**, not just the frontmatter.
365
+
366
+ Six live corpora already carry those pairs:
367
+ [`direct-answers-demos`](../../../docs/guidelines/agent-infra/direct-answers-demos.md),
368
+ [`asking-and-brevity-examples`](../../../docs/guidelines/agent-infra/asking-and-brevity-examples.md),
369
+ [`language-and-tone-examples`](../../../docs/guidelines/agent-infra/language-and-tone-examples.md),
370
+ and `autonomy-examples` / `interrupt-examples` / `cheap-question-mechanics` under
371
+ `src/agent-src/contexts/execution/`. Follow one; do not invent a format.
372
+
373
+ The shape they share: the **wrong** version in the form it actually gets
374
+ written, the **right** version, and one line of **why** — the why is what makes
375
+ it a rule rather than a memorised case.
376
+
377
+ Where the pairs go is a size decision, not a taste one. A rule body is capped at
378
+ 200 lines hard / 120 soft, so more than two or three pairs belong in a guideline
379
+ or context file the rule points at — same split
380
+ [`skill-writing`](../skill-writing/SKILL.md) § Contrastive-example slot uses.
@@ -530,7 +530,7 @@ tool skill*, **never** a new skill per pitfall and never a generated grid (see
530
530
  [`size-enforcement`](../../rules/size-enforcement.md) § Per-tool pitfall
531
531
  content).
532
532
 
533
- ## Rationalizations-to-reject section (required, security-stop-routed skills)
533
+ ## Rationalizations-to-reject section (recommended pattern, security-stop-routed skills)
534
534
 
535
535
  A skill routed by `security-sensitive-stop` already carries a forbidden-moves or
536
536
  failure-mode list. Those describe **what is wrong**. They do not answer the
@@ -553,7 +553,7 @@ the skip actually happens.
553
553
  Keep it to the arguments that have really been made. An invented rationalization
554
554
  teaches nothing and dilutes the ones that recur.
555
555
 
556
- ## Non-negotiable-deliverable section (required, adjacent-technology clusters)
556
+ ## Non-negotiable-deliverable section (recommended pattern, adjacent-technology clusters)
557
557
 
558
558
  Some skills sit next to a technology that is a *tempting wrong answer* — close
559
559
  enough to look interchangeable, different enough that picking it silently loses
@@ -644,6 +644,84 @@ This duplicates the rules on purpose, and the duplication is the point: the
644
644
  rules are the enforcement, the section is what survives the artifact leaving
645
645
  their reach.
646
646
 
647
+ ## Mechanism-teaching section (optional pattern, skills whose subject has one)
648
+
649
+ A procedure tells the agent what to do in the cases you thought of. A
650
+ **mechanism** tells it why those steps work, which is the only thing that
651
+ transfers to the case you did not. Where a skill's subject has a mechanism worth
652
+ one screen — how the cascade resolves, why the queue drops the message, what the
653
+ scheduler actually orders — state it in a `## How it works` section, ahead of the
654
+ procedure that depends on it.
655
+
656
+ Keep it to one screen and to mechanisms only: no history, no alternatives-considered,
657
+ no restating the procedure in prose. If it takes more than a screen, the skill is
658
+ carrying a guideline's worth of material and should point at one instead.
659
+
660
+ The test for whether it earns its place: name a plausible situation the procedure
661
+ does not cover, and check whether this section is enough to answer it. If the
662
+ answer is still "read the source", the section is decoration.
663
+
664
+ Optional, and it gets no gate: "teaches a mechanism" is a prose judgement, and a
665
+ predicate over prose is the false-positive class this file's other patterns avoid.
666
+
667
+ ## Illustrative-not-verbatim marker (optional pattern, reference code)
668
+
669
+ A code block in a skill is read as copy-me by default. When a block is teaching a
670
+ *shape* — a signature to mirror, a structure to follow, pseudocode standing in for
671
+ the real call — say so on the line above it:
672
+
673
+ ```
674
+ <!-- illustrative: shape only, not a working snippet -->
675
+ ```
676
+
677
+ One line, immediately above the fence, in the skill's own words if you prefer.
678
+ What matters is that the reader can tell shape-teaching code from code that runs,
679
+ because the failure mode is silent: an agent pastes the illustration, it almost
680
+ works, and the debugging starts from the wrong premise.
681
+
682
+ The inverse deserves the same care — a block that IS meant to be run verbatim,
683
+ where a reader might assume otherwise, says so too. See also `### 4. Add
684
+ safe/unsafe example`, which governs a different axis: safe-vs-unsafe, not
685
+ runnable-vs-illustrative.
686
+
687
+ ## Headline metric + closing report (optional pattern, optimization skills)
688
+
689
+ A skill whose job is to make something better — smaller, faster, cheaper, quieter
690
+ — has to say **which single number it moves**. Without one, "optimized" is a
691
+ claim nobody can check and every run reports success.
692
+
693
+ So an optimization skill names, up front:
694
+
695
+ 1. **The headline metric.** One number, with its unit and how it is measured.
696
+ "Initial-context tokens, measured by `audit-tokens`" — not "context cost".
697
+ 2. **The closing report shape.** Before, after, delta, and the command that
698
+ produced both. The same command for both readings, or the delta is not a delta.
699
+
700
+ State what the metric does **not** capture, in one line. An optimization that
701
+ moves the headline number by degrading something it does not measure is the
702
+ failure this pattern exists to make visible, and only the author knows where that
703
+ edge is.
704
+
705
+ ## Contrastive-example slot (optional pattern, authoring and behaviour skills)
706
+
707
+ Where a skill's guidance is easy to agree with and hard to apply — tone, phrasing,
708
+ question shape, what counts as evidence — a wrong/right pair does what prose
709
+ cannot: it shows the near-miss. Six live corpora already carry them:
710
+ [`direct-answers-demos`](../../../docs/guidelines/agent-infra/direct-answers-demos.md),
711
+ [`asking-and-brevity-examples`](../../../docs/guidelines/agent-infra/asking-and-brevity-examples.md),
712
+ [`language-and-tone-examples`](../../../docs/guidelines/agent-infra/language-and-tone-examples.md),
713
+ and `autonomy-examples` / `interrupt-examples` / `cheap-question-mechanics` under
714
+ `src/agent-src/contexts/execution/`.
715
+
716
+ Follow one of those rather than inventing a format. The shape they share: the
717
+ **wrong** version first, in the form it actually gets written; the **right**
718
+ version second; and one line of **why** — which is the load-bearing part, because
719
+ a pair without it teaches the specific case and not the rule.
720
+
721
+ Put the corpus in a guideline or context file and point at it from the skill when
722
+ it outgrows a section. A near-miss corpus is reference material read on demand,
723
+ not always-loaded prose.
724
+
647
725
  ## Do NOT
648
726
 
649
727
  * Write documentation-style, pointer-only, or too-broad skills ("Laravel skill", "Django skill")
@@ -222,10 +222,12 @@ right line beats five minutes of IDE breakpoints.
222
222
 
223
223
  **Known failure signature?** If the symptom has a recognisable shape — a
224
224
  tool/agent loop, HTTP 429, `ECONNREFUSED`, `ENOENT`, timeout/hang, OOM, flaky
225
- test, or works-locally-fails-in-CI — consult the symptom → cause → first-check
225
+ test, works-locally-fails-in-CI, or repeated attempts at a tool that is not
226
+ available — consult the symptom → cause → first-check
226
227
  lookup in [`failure-signatures`](../../../docs/guidelines/agent-infra/failure-signatures.md)
227
- to shortcut Phase 2 (Isolate) with the highest-probability first check. It
228
- supplements the loop; it does not replace it.
228
+ to shortcut Phase 2 (Isolate) with the highest-probability first check. Each row
229
+ carries a stable id you can cite, and a discrimination drill that produces its
230
+ documented symptom. It supplements the loop; it does not replace it.
229
231
 
230
232
  ## Condition-based waiting (intermittent bugs)
231
233
 
@@ -157,8 +157,32 @@ Risks: <arbitrary values, !important, dark-mode gaps>
157
157
 
158
158
  When `DESIGN.md` declares `## Taste Dials`, honour them: Variance → layout-family spread + asymmetry tolerance; Motion → animation budget + reduced-motion posture; Density → spacing scale + information-per-viewport. Absent → follow the design brief's inferred dials.
159
159
 
160
+ ## Security constraints
161
+
162
+ `scripts/tailwind_config_gen.ts` is the only shipped script.
163
+
164
+ - **What it may touch** — exactly one file: `--output PATH`, or
165
+ `tailwind.config.ts` / `.js` in the current working directory when no
166
+ `--output` is given. It reads nothing from the project.
167
+ - **What it must never do** — write anywhere other than that one path,
168
+ reach the network, or spawn a subprocess. It does none of these.
169
+ - **Default invocation** — **mutating**, and stated here because it is a
170
+ real exception rather than a comfortable one: a bare invocation writes
171
+ `tailwind.config.ts` into `cwd`, overwriting an existing config without
172
+ prompting. `--validate-only` is the read-only path — it prints the
173
+ generated config to stdout and writes nothing. Run `--validate-only`
174
+ first, and pass `--output` when the target is not the cwd default.
175
+ This default contradicts `skill-writing`'s "never mutate on a bare
176
+ invocation" line; it is recorded rather than silently tolerated, and
177
+ changing the script's default is a caller-visible behaviour change that
178
+ belongs in its own change, not here.
179
+ - **Outbound** — nothing. No network access.
180
+
160
181
  ## Do NOT
161
182
 
183
+ - Do NOT run `tailwind_config_gen` bare in a project that already has a
184
+ `tailwind.config.*` — it overwrites without asking. Use
185
+ `--validate-only` first, then `--output` at the intended path.
162
186
  - Do NOT add `!important` to win a specificity fight; restructure
163
187
  the cascade or extract the conflicting style.
164
188
  - Do NOT introduce a new colour outside `tailwind.config` without
@@ -163,6 +163,27 @@ expect([403, 404]).toContain((await api.get(`/users/${otherTenantUser.id}`)).sta
163
163
 
164
164
  Deriving from seeded data is necessary but **not sufficient** — a test that only reads back the exact fields it seeded is still overfit (a mutation in the code survives it). A real test also exercises boundary (empty / null / max / Unicode), error (missing / invalid), and, on security-sensitive paths, an abuse case (IDOR, injection string, XSS payload). Coverage of *cases*, not just lines.
165
165
 
166
+ **The adversarial variant, and it is the one that looks safest.** A *negative* test — the abuse case just named — fails the same way, but nothing about it reads as weak: it names an attack, it asserts a refusal, it goes green. It passes because **something other than the control under test** produced that refusal. Two shapes, both from one real review:
167
+
168
+ - **The hostile value sits only in the argument.** A guard rejecting a path-traversing `token` was pinned by planting a fixture whose own `token` field held the *legitimate* value while `'../outside'` was passed as the argument. Downstream, an ordinary token comparison mismatched and returned exactly the expected refusal — delete the guard and the test still passes. The hostile value belongs in the **field under test**, not only in the call.
169
+ - **The assertion cannot fail.** The same test asserted the planted file still existed after a rejected claim. The two directories were siblings, so both `../x.json` joins resolved to one path and the rename could never have moved it — a green no implementation could turn red.
170
+
171
+ Gate:
172
+
173
+ ```
174
+ BEFORE trusting a negative test:
175
+ Delete or invert the control it claims to pin. Does it FAIL?
176
+ RESTORE THE CONTROL IMMEDIATELY — before any other edit, before any commit.
177
+ The mutation is a probe, never a change. A deleted guard left deleted is
178
+ a shipped vulnerability produced by a test-quality check.
179
+ IF it still passes: it is pinning something else. Name what, then fix the test.
180
+ IF an assertion cannot fail for ANY implementation: delete THE ASSERTION —
181
+ it is decoration. Only after proving vacuity, and never to turn a red test
182
+ green — that is Anti-Pattern 7 below.
183
+ ```
184
+
185
+ No mutation-testing rig required: comment the control out, run that one spec, put it back. A negative test nobody has ever watched fail is a claim, not evidence.
186
+
166
187
  ### Anti-Pattern 7 — Gaming the green (test-integrity)
167
188
 
168
189
  Symptom: a test fails, and the "fix" edits the *test* instead of the code — `.skip` / `it.skip` / `xfail` / `@Disabled` on the failing case, deleting the failing assertion, loosening `toBe(x)` to `toBeTruthy()`, or rewriting the `expected` value to whatever the (possibly buggy) code now emits. The suite goes green while the behavior stays broken — the worst outcome, because green now *hides* the bug. This is the automation-bias trap: under pressure to finish, the agent optimizes the signal (green) instead of the target (correct behavior).
@@ -211,6 +232,7 @@ This is the test-surface instance of the `autonomous-execution` N=3 / allowlist-
211
232
  - Do NOT mark a story complete until at least one test was watched failing first.
212
233
  - Do NOT hardcode an expected value the code will emit, or pin a regex to an incidental fixed date/id — derive from inputs and seeded data, and vary the input across cases.
213
234
  - Do NOT skip / `xfail` / delete a failing test, or rewrite its `expected` to the code's current output, to get a green run — fix the code, or state a real spec change in the diff.
235
+ - Do NOT trust a negative test you have never watched fail — delete the control it pins, confirm the test goes red, restore the control immediately.
214
236
 
215
237
  ## Auto-trigger keywords
216
238
 
@@ -225,6 +247,9 @@ This is the test-surface instance of the `autonomous-execution` N=3 / allowlist-
225
247
  - gaming the green
226
248
  - skip failing test
227
249
  - test integrity
250
+ - negative test
251
+ - vacuous assertion
252
+ - abuse-case test
228
253
 
229
254
  ## Provenance
230
255
 
@@ -39,7 +39,7 @@ schema_version: 1
39
39
  # CI guard: a release bump of `package.json` must update this value
40
40
  # in lockstep — see scripts/check_template_pin_drift.ts (road-to-
41
41
  # portable-runtime-and-update-check P3.3).
42
- agent_config_version: "9.33.0"
42
+ agent_config_version: "9.35.0"
43
43
 
44
44
  # --- Project identity ---
45
45
  project:
@@ -388,8 +388,7 @@ function* _iter_knowledge_entries(): Generator<[string, Record<string, unknown>]
388
388
  /**
389
389
  * Naive relevance score: max over keys of (glob-match | substring).
390
390
  *
391
- * Good enough for the `absent` path where retrieval is best-effort.
392
- * The `present` path returns a real score from agent-memory.
391
+ * Retrieval is best-effort by design — this is the only scoring path.
393
392
  */
394
393
  export function _score(entry: Record<string, unknown>, keys: string[]): number {
395
394
  if (keys.length === 0) {
@@ -214,10 +214,8 @@ export interface SignalRecord {
214
214
  /**
215
215
  * Append a signal entry. Returns the written record, or null when skipped.
216
216
  *
217
- * On `present` backend, routing to the package is a no-op here today —
218
- * the package adapter is wired in `road-to-agent-memory-integration.md`
219
- * Phase 3. For now, the file path is the single source of truth so
220
- * merge-safety is preserved in every mode.
217
+ * The file path is the single source of truth: there is no external backend
218
+ * to route to, so merge-safety holds in every mode.
221
219
  */
222
220
  export function emit(
223
221
  entry_type: string,
@@ -41,6 +41,22 @@ type Any = unknown;
41
41
 
42
42
  const _BLOCK_REASON_PREFIX = 'decision_gate';
43
43
 
44
+ /**
45
+ * Stage an action for confirmation and return the surface lines that name it.
46
+ *
47
+ * An injected collaborator rather than a direct import of
48
+ * `staged_confirmation_store.js`, for two reasons. The gate is called from a
49
+ * hook bus with no repo root and no clock of its own, and — the load-bearing
50
+ * one — an absent stager keeps the `ask` path byte-identical to what ships
51
+ * today. The confirmation primitive is deliberately UNBOUND until blocker
52
+ * `confirmation-degraded-host-semantics` is decided (5 of 8 hosts have no
53
+ * `pre_tool_use` slot), so reaching it has to be an explicit opt-in and not a
54
+ * behaviour change that arrives with an upgrade.
55
+ */
56
+ export interface ConfirmationStager {
57
+ stage(input: { action: string; object: string; source: string }): readonly string[];
58
+ }
59
+
44
60
  /**
45
61
  * Evaluate decision-engine gates on every `AFTER_STEP`.
46
62
  *
@@ -49,9 +65,11 @@ const _BLOCK_REASON_PREFIX = 'decision_gate';
49
65
  */
50
66
  export class DecisionGateHook {
51
67
  private readonly _settings: DecisionEngineSettings;
68
+ private readonly _stager: ConfirmationStager | null;
52
69
 
53
- constructor(settings: DecisionEngineSettings) {
70
+ constructor(settings: DecisionEngineSettings, stager: ConfirmationStager | null = null) {
54
71
  this._settings = settings;
72
+ this._stager = stager;
55
73
  }
56
74
 
57
75
  /** Register the gate callback on `AFTER_STEP`. */
@@ -109,10 +127,22 @@ export class DecisionGateHook {
109
127
  DecisionGateHook._surface(decision, 'ask_timeout'),
110
128
  );
111
129
  }
112
- throw new HookHalt(
113
- `${_BLOCK_REASON_PREFIX}:${decision.gate_id}`,
114
- DecisionGateHook._surface(decision),
115
- );
130
+ // `stop` and `ask` share this exit: both halt, which is the policy —
131
+ // ask and action are strictly sequential, so an interactive gate does
132
+ // not act while it asks. What `ask` adds, and only when a stager is
133
+ // injected, is a token on the surface so the answer can be matched to
134
+ // THIS question and consumed exactly once. The halt reason is
135
+ // deliberately unchanged: it is the identity downstream readers match on.
136
+ let surface = DecisionGateHook._surface(decision);
137
+ if (action === 'ask' && this._stager !== null) {
138
+ const extra = this._stager.stage({
139
+ action: `work_engine:advance:${decision.gate_id}`,
140
+ object: `phase ${decision.phase}`,
141
+ source: _BLOCK_REASON_PREFIX,
142
+ });
143
+ surface = [...surface, '', ...extra];
144
+ }
145
+ throw new HookHalt(`${_BLOCK_REASON_PREFIX}:${decision.gate_id}`, surface);
116
146
  }
117
147
 
118
148
  // -- formatting helpers -------------------------------------------