@event4u/agent-config 9.33.0 → 9.34.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 (60) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +85 -167
  3. package/dist/agent-src/commands/agent-handoff.md +15 -3
  4. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
  5. package/dist/agent-src/skills/corpus-grounding/SKILL.md +21 -12
  6. package/dist/agent-src/skills/design-tokens/SKILL.md +15 -0
  7. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +24 -0
  8. package/dist/agent-src/skills/rule-writing/SKILL.md +23 -0
  9. package/dist/agent-src/skills/skill-writing/SKILL.md +80 -2
  10. package/dist/agent-src/skills/systematic-debugging/SKILL.md +5 -3
  11. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +24 -0
  12. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  13. package/dist/cli-delegate/{chunk-2WELHO3P.js → chunk-I5EAFRII.js} +35 -0
  14. package/dist/cli-delegate/{chunk-PMQ6Z3T3.js → chunk-REMEZSVP.js} +9 -2
  15. package/dist/cli-delegate/{chunk-3EWYC3YK.js → chunk-UEZ7D35A.js} +4 -3
  16. package/dist/cli-delegate/cmd_conformance.js +1 -1
  17. package/dist/cli-delegate/cmd_doctor.js +1 -1
  18. package/dist/cli-delegate/cmd_handoff.js +2 -2
  19. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  20. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  21. package/dist/cli-delegate/cmd_session_recycle.js +7 -2
  22. package/dist/discovery/deprecation-report.md +1 -1
  23. package/dist/discovery/discovery-manifest.json +11 -11
  24. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  25. package/dist/discovery/discovery-manifest.summary.md +2 -2
  26. package/dist/discovery/dormancy-report.md +17 -0
  27. package/dist/discovery/orphan-report.md +1 -1
  28. package/dist/discovery/packs.json +3 -3
  29. package/dist/discovery/trust-report.md +1 -1
  30. package/dist/discovery/workspaces.json +3 -3
  31. package/dist/hooks/dispatch.js +96 -7
  32. package/dist/mcp/registry-manifest.json +2 -2
  33. package/dist/mcp/server.json +2 -2
  34. package/dist/mcp/server.mjs +118 -10
  35. package/docs/guidelines/agent-infra/failure-signatures.md +47 -12
  36. package/package.json +2 -2
  37. package/src/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
  38. package/src/config/quorum-attendance-budget.json +1 -1
  39. package/src/scripts/_cli/cmd_session_recycle.ts +6 -1
  40. package/src/scripts/_lib/env_kill_switch.ts +37 -0
  41. package/src/scripts/_lib/security_lint.ts +18 -4
  42. package/src/scripts/_lib/self_repair.ts +60 -0
  43. package/src/scripts/_lib/self_repair_store.ts +65 -5
  44. package/src/scripts/_lib/subagent_capsule.ts +78 -1
  45. package/src/scripts/ai_council/events_log.ts +2 -2
  46. package/src/scripts/ai_council/orchestrator.ts +38 -0
  47. package/src/scripts/ai_council/prompts.ts +165 -3
  48. package/src/scripts/ai_council/quorum.ts +10 -0
  49. package/src/scripts/ai_team/review_gate.ts +2 -2
  50. package/src/scripts/build_discovery_manifest.ts +165 -0
  51. package/src/scripts/check_claims.ts +51 -2
  52. package/src/scripts/ci_time_ratio.ts +3 -1
  53. package/src/scripts/discovery_graph.ts +201 -27
  54. package/src/scripts/dispatch_r2_reviewer.ts +102 -1
  55. package/src/scripts/lint_handoffs.ts +92 -2
  56. package/src/scripts/lint_hedge_words.ts +317 -0
  57. package/src/scripts/lint_mcp_config_security.ts +1 -1
  58. package/src/scripts/lint_skill_frontmatter_safety.ts +1 -1
  59. package/src/scripts/self_repair_cli.ts +26 -1
  60. package/src/scripts/skill_linter.ts +58 -2
@@ -7993,7 +7993,7 @@ function* iter_corpus(roots = DEFAULT_SCAN_ROOTS, exts = [".md"]) {
7993
7993
  function report(findings, opts) {
7994
7994
  const check_label = opts.check_label;
7995
7995
  if (findings.length === 0) {
7996
- process.stdout.write(`\u2705 ${check_label}: clean (${_corpus_note()}).
7996
+ process.stdout.write(`\u2705 ${check_label}: clean (${_corpus_note(opts.scanned_roots)}).
7997
7997
  `);
7998
7998
  return 0;
7999
7999
  }
@@ -8033,8 +8033,9 @@ function _pyG(n) {
8033
8033
  }
8034
8034
  return s;
8035
8035
  }
8036
- function _corpus_note() {
8037
- return "scanned " + DEFAULT_SCAN_ROOTS.join(", ");
8036
+ function _corpus_note(scanned_roots) {
8037
+ const roots = scanned_roots && scanned_roots.length > 0 ? scanned_roots : DEFAULT_SCAN_ROOTS;
8038
+ return "scanned " + roots.join(", ");
8038
8039
  }
8039
8040
  var PyFloat = class {
8040
8041
  constructor(value) {
@@ -16205,6 +16206,38 @@ function hasBoundaryMarker(block) {
16205
16206
  // outside the datamarking while still riding in the same block.
16206
16207
  block.trimEnd().endsWith(ENVELOPE_BOUNDARY_CLOSE);
16207
16208
  }
16209
+ var DECISION_REVERSIBILITY_TAGS = ["reversible", "irreversible"];
16210
+ function decisionTagErrors(decisions) {
16211
+ if (!Array.isArray(decisions)) {
16212
+ return [];
16213
+ }
16214
+ const errors = [];
16215
+ for (const line of decisions) {
16216
+ if (typeof line !== "string") {
16217
+ continue;
16218
+ }
16219
+ const m = /\[([^\]]*)\]\s*$/.exec(line.trimEnd());
16220
+ if (m === null) {
16221
+ continue;
16222
+ }
16223
+ const inner = (m[1] ?? "").trim();
16224
+ const lowered = inner.toLowerCase();
16225
+ if (DECISION_REVERSIBILITY_TAGS.includes(lowered)) {
16226
+ if (inner !== lowered) {
16227
+ errors.push(
16228
+ `decisions: reversibility tag must be lower-case \u2014 got \`[${inner}]\`, expected \`[${lowered}]\``
16229
+ );
16230
+ }
16231
+ continue;
16232
+ }
16233
+ if (/^i?rr?ever[a-z]*[^a-z]*$/i.test(lowered)) {
16234
+ errors.push(
16235
+ `decisions: \`[${inner}]\` is not a reversibility tag \u2014 use exactly ` + DECISION_REVERSIBILITY_TAGS.map((t) => `\`[${t}]\``).join(" or ") + ", or drop the bracket"
16236
+ );
16237
+ }
16238
+ }
16239
+ return errors;
16240
+ }
16208
16241
  var RECYCLE_ENVELOPE_KEYS = /* @__PURE__ */ new Set([
16209
16242
  "capsule_version",
16210
16243
  "variant",
@@ -16217,6 +16250,7 @@ var RECYCLE_ENVELOPE_KEYS = /* @__PURE__ */ new Set([
16217
16250
  "not_carried_forward",
16218
16251
  "decisions",
16219
16252
  "constraints",
16253
+ "do_not_touch",
16220
16254
  "open_worker_envelopes",
16221
16255
  "artifact_paths",
16222
16256
  "assumptions",
@@ -16269,7 +16303,9 @@ function validateRecycleEnvelope(input) {
16269
16303
  checkList(errors, "remaining", e["remaining"], MAX_LINE_CHARS, true);
16270
16304
  checkList(errors, "not_carried_forward", e["not_carried_forward"], MAX_LINE_CHARS, true);
16271
16305
  checkList(errors, "decisions", e["decisions"], MAX_LINE_CHARS, false);
16306
+ errors.push(...decisionTagErrors(e["decisions"]));
16272
16307
  checkList(errors, "constraints", e["constraints"], MAX_LINE_CHARS, false);
16308
+ checkList(errors, "do_not_touch", e["do_not_touch"], MAX_REF_CHARS, false);
16273
16309
  checkList(errors, "open_worker_envelopes", e["open_worker_envelopes"], MAX_REF_CHARS, false);
16274
16310
  checkList(errors, "artifact_paths", e["artifact_paths"], MAX_REF_CHARS, false);
16275
16311
  checkList(errors, "suggested_skills", e["suggested_skills"], MAX_REF_CHARS, false);
@@ -18839,6 +18875,12 @@ function claude_config_dir() {
18839
18875
  return env !== "" ? env : path32.join(os4.homedir(), ".claude");
18840
18876
  }
18841
18877
 
18878
+ // src/scripts/_lib/env_kill_switch.ts
18879
+ var _OFF_VALUES = /* @__PURE__ */ new Set(["", "0", "false", "False"]);
18880
+ function isEnvKillSwitchActive(name) {
18881
+ return !_OFF_VALUES.has(process.env[name] ?? "");
18882
+ }
18883
+
18842
18884
  // src/scripts/ai_team/config.ts
18843
18885
  var TeamConfigError = class extends Error {
18844
18886
  constructor(message) {
@@ -19048,8 +19090,7 @@ function circuit_breaker_notice(bound) {
19048
19090
  return `\u26A0\uFE0F team review-gate circuit breaker: ${bound} consecutive BLOCK verdict(s) this session (bound: \`ai_team.review_gate.max_consecutive_blocks\`). The managed gate stops re-blocking now \u2014 you decide: review the last BLOCK reason via \`/team:status\`, fix what is real, ignore what is not. An ALLOW verdict resets the counter.`;
19049
19091
  }
19050
19092
  function _kill_switch_active() {
19051
- const value = process.env[_KILL_SWITCH_ENV] ?? "";
19052
- return !(value === "" || value === "0" || value === "false" || value === "False");
19093
+ return isEnvKillSwitchActive(_KILL_SWITCH_ENV);
19053
19094
  }
19054
19095
  function _iso_seconds_z(d) {
19055
19096
  const pad = (n, w = 2) => String(n).padStart(w, "0");
@@ -19930,6 +19971,28 @@ function fingerprint2(defect_class, evidence) {
19930
19971
  const shape = evidence.toLowerCase().replace(/\d+/g, "#").replace(/[`"'‚'„"»«]/g, "").replace(/[^a-zà-ÿ#\s]/g, " ").replace(/\s+/g, " ").trim();
19931
19972
  return createHash4("sha256").update(`${defect_class}|${shape}`).digest("hex").slice(0, 16);
19932
19973
  }
19974
+ var NEW_RECORDS_PER_SOURCE_PER_WINDOW = 20;
19975
+ var CREATION_WINDOW_MS = 24 * 60 * 60 * 1e3;
19976
+ function recentCreations(existing, source, now) {
19977
+ const end = Date.parse(now);
19978
+ if (Number.isNaN(end)) {
19979
+ return 0;
19980
+ }
19981
+ return existing.filter((r) => {
19982
+ if (r.source !== source) {
19983
+ return false;
19984
+ }
19985
+ const t = Date.parse(r.first_seen);
19986
+ return Number.isNaN(t) || end - t < CREATION_WINDOW_MS && end - t >= 0;
19987
+ }).length;
19988
+ }
19989
+ function creationCapReached(existing, finding, now) {
19990
+ const fp = fingerprint2(finding.defect_class, finding.evidence);
19991
+ if (existing.some((r) => r.fingerprint === fp)) {
19992
+ return false;
19993
+ }
19994
+ return recentCreations(existing, finding.source, now) >= NEW_RECORDS_PER_SOURCE_PER_WINDOW;
19995
+ }
19933
19996
  function mergeRecord(existing, finding, now) {
19934
19997
  const fp = fingerprint2(finding.defect_class, finding.evidence);
19935
19998
  if (existing !== null && existing.fingerprint === fp) {
@@ -19995,7 +20058,7 @@ function listRecords(root) {
19995
20058
  }
19996
20059
  const out = [];
19997
20060
  for (const n of names.sort()) {
19998
- if (!n.endsWith(".json")) {
20061
+ if (!n.endsWith(".json") || n === OVERFLOW_FILE) {
19999
20062
  continue;
20000
20063
  }
20001
20064
  const rec = readRecord(root, n.slice(0, -".json".length));
@@ -20009,9 +20072,35 @@ function listRecords(root) {
20009
20072
  function openRecords(root) {
20010
20073
  return listRecords(root).filter((r) => r.status === "open");
20011
20074
  }
20075
+ var OVERFLOW_FILE = "_overflow.json";
20076
+ function overflowPath(root) {
20077
+ return path38.join(storeDir(root), OVERFLOW_FILE);
20078
+ }
20079
+ function readOverflow(root) {
20080
+ try {
20081
+ const parsed = JSON.parse(fs37.readFileSync(overflowPath(root), "utf-8"));
20082
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
20083
+ } catch {
20084
+ return {};
20085
+ }
20086
+ }
20087
+ function bumpOverflow(root, source, now) {
20088
+ const counts = readOverflow(root);
20089
+ const prev = counts[source]?.dropped ?? 0;
20090
+ counts[source] = { dropped: prev + 1, last_seen: now };
20091
+ const dir = storeDir(root);
20092
+ fs37.mkdirSync(dir, { recursive: true });
20093
+ fs37.writeFileSync(overflowPath(root), `${JSON.stringify(counts, null, 2)}
20094
+ `, "utf-8");
20095
+ }
20012
20096
  function upsertFinding(root, finding, now) {
20013
20097
  const fp = fingerprint2(finding.defect_class, finding.evidence);
20014
- const merged = mergeRecord(readRecord(root, fp), finding, now);
20098
+ const existing = readRecord(root, fp);
20099
+ if (existing === null && creationCapReached(listRecords(root), finding, now)) {
20100
+ bumpOverflow(root, finding.source, now);
20101
+ return null;
20102
+ }
20103
+ const merged = mergeRecord(existing, finding, now);
20015
20104
  writeRecord(root, merged);
20016
20105
  return merged;
20017
20106
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "discovery": {
3
3
  "artefact_count": 603,
4
- "scanner_version": "c24312e116f8"
4
+ "scanner_version": "1eaf68b6e9c9"
5
5
  },
6
6
  "generated_at": "2026-08-11",
7
7
  "package": {
@@ -9,7 +9,7 @@
9
9
  "homepage": "https://event4u-app.github.io/agent-config",
10
10
  "name": "@event4u/agent-config",
11
11
  "repository": "https://github.com/event4u-app/agent-config",
12
- "version": "9.33.0"
12
+ "version": "9.34.0"
13
13
  },
14
14
  "registries": [
15
15
  {
@@ -9,12 +9,12 @@
9
9
  "transport": {
10
10
  "type": "stdio"
11
11
  },
12
- "version": "9.33.0"
12
+ "version": "9.34.0"
13
13
  }
14
14
  ],
15
15
  "repository": {
16
16
  "source": "github",
17
17
  "url": "https://github.com/event4u-app/agent-config"
18
18
  },
19
- "version": "9.33.0"
19
+ "version": "9.34.0"
20
20
  }
@@ -9241,7 +9241,7 @@ function* iter_corpus(roots = DEFAULT_SCAN_ROOTS, exts = [".md"]) {
9241
9241
  function report(findings, opts) {
9242
9242
  const check_label = opts.check_label;
9243
9243
  if (findings.length === 0) {
9244
- process.stdout.write(`\u2705 ${check_label}: clean (${_corpus_note()}).
9244
+ process.stdout.write(`\u2705 ${check_label}: clean (${_corpus_note(opts.scanned_roots)}).
9245
9245
  `);
9246
9246
  return 0;
9247
9247
  }
@@ -9281,8 +9281,9 @@ function _pyG(n) {
9281
9281
  }
9282
9282
  return s;
9283
9283
  }
9284
- function _corpus_note() {
9285
- return "scanned " + DEFAULT_SCAN_ROOTS.join(", ");
9284
+ function _corpus_note(scanned_roots) {
9285
+ const roots = scanned_roots && scanned_roots.length > 0 ? scanned_roots : DEFAULT_SCAN_ROOTS;
9286
+ return "scanned " + roots.join(", ");
9286
9287
  }
9287
9288
  function _isPlainObject(v) {
9288
9289
  return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof PyFloat);
@@ -15361,7 +15362,7 @@ function lint_execution_metadata(execution) {
15361
15362
  }
15362
15363
  return issues;
15363
15364
  }
15364
- function lint_skill(p, text) {
15365
+ function lint_skill(p, text, absPath = null) {
15365
15366
  const issues = [];
15366
15367
  const suggestions = [];
15367
15368
  const sections = extract_sections(text);
@@ -15371,6 +15372,18 @@ function lint_skill(p, text) {
15371
15372
  issues.push(new Issue("error", "missing_section", `Missing required section: ${section}`));
15372
15373
  }
15373
15374
  }
15375
+ const skillDir = absPath !== null ? path19.dirname(absPath) : path19.isAbsolute(p) ? path19.dirname(p) : null;
15376
+ for (const { section, when, reason } of CONDITIONAL_SKILL_SECTIONS) {
15377
+ if (skillDir !== null && when(skillDir) && !sectionMatches(section, sections)) {
15378
+ issues.push(
15379
+ new Issue(
15380
+ "error",
15381
+ "missing_conditional_section",
15382
+ `Missing required section: ${section} (${reason})`
15383
+ )
15384
+ );
15385
+ }
15386
+ }
15374
15387
  for (const section of RECOMMENDED_SKILL_SECTIONS) {
15375
15388
  if (!sectionMatches(section, sections)) {
15376
15389
  issues.push(
@@ -17173,7 +17186,7 @@ function lint_file(p, repoRoot = null) {
17173
17186
  }
17174
17187
  let result;
17175
17188
  if (artifactType === "skill") {
17176
- result = lint_skill(displayPath, text);
17189
+ result = lint_skill(displayPath, text, p);
17177
17190
  } else if (artifactType === "rule") {
17178
17191
  result = lint_rule(displayPath, text);
17179
17192
  } else if (artifactType === "command") {
@@ -17998,7 +18011,7 @@ function _isCliEntry7() {
17998
18011
  return false;
17999
18012
  }
18000
18013
  }
18001
- var _HERE4, REQUIRED_PERSONA_SECTIONS_CORE, REQUIRED_PERSONA_SECTIONS_SPECIALIST, VALID_PERSONA_TIERS, PERSONA_LINE_BUDGETS, REQUIRED_USERTYPE_SECTIONS, USERTYPE_LINE_BUDGET, VALID_PERSONA_WINGS, PERSONA_LINE_BUDGETS_BY_WING, REQUIRED_SKILL_SECTIONS, SECTION_ALIASES, RECOMMENDED_SKILL_SECTIONS, RULE_BAD_SIGNS, FRUGALITY_WRITER_SKILLS, FRUGALITY_CHARTER_RELPATH, FRUGALITY_CHARTER_INDEX_RULES, VAGUE_VALIDATION_PATTERNS, TRIGGER_WARNING_PATTERNS, ORDERED_STEP_PATTERN, SECTION_PATTERN, FRONTMATTER_PATTERN, DESCRIPTION_PATTERN, STATUS_PATTERN, REPLACED_BY_PATTERN, TIER_PATTERN, TYPE_PATTERN, SOURCE_PATTERN, SENIOR_RELATED_SKILLS_PATTERN, SENIOR_RELATED_WHEN_PATTERN, SENIOR_RELATED_WHEN_NOT_PATTERN, SENIOR_PROACTIVE_PATTERN, SENIOR_OUTPUT_PATTERN, H1_PATTERN, DOUBLE_BLANK_PATTERN, VALID_RULE_TYPES, VALID_RULE_SOURCES, ROUTER_ALLOWED_TRIGGER_KEYS, KERNEL_RULE_IDS2, VALID_EXECUTION_TYPES, VALID_EXECUTION_HANDLERS, VALID_EXECUTION_SAFETY_MODES, VALID_EXECUTION_FIELDS, WING3_SPINE_SLOTS, CONTEXT_SPINE_INLINE_PATTERN, WING3_SAAS_URL_PATTERN, WING3_VENDOR_BLACKLIST, WING3_STACK_LOCKED_PATTERN, WING3_CHANNEL_TACTIC_PATTERN, WING4_SPINE_SLOTS, WING4_SAAS_URL_PATTERN, WING4_VENDOR_BLACKLIST, WING4_STAGE_AGNOSTIC_PATTERN, Issue, LintResult, _ROLE_CONTRACT_CANDIDATES, _ROLE_CONTRACT_SLUGS_CACHE, _ROLE_CONTRACT_REF_PATTERN, PROCEDURE_HEADING_PATTERN, COMMAND_FRONTMATTER_DELEGATION_KEYS, MD_LINK_PATTERN_G, NAME_PATTERN, DISABLE_MODEL_PATTERN, _INSPECT_VERB_PATTERN, last_changed_scope_reason, last_changed_base_diff_ok, _INTERACTION_NAME_PATTERNS, _INTERACTION_CONTENT_KEYWORDS, _EXEC_FILE_SIGNALS, _EXEC_CONTENT_SIGNALS, _TASK_TYPE_SIGNALS, _VERIFICATION_TOOLS, _FRUGALITY_STANDARDS_PATTERN, _FRUGALITY_CHARTER_LINK_PATTERN, _MD_LINK_PATTERN, _MALICE_CRED_EXFIL, _MALICE_REMOTE_EXEC, _MALICE_FORCE_PUSH, _MALICE_CHMOD_SECRETS, _MALICE_SHELL_INJECT, _MALICE_PATTERNS, _NAG_UPDATE_CHECK, _NAG_STAY_CURRENT, _NAG_SELF_PROMO, _NAG_BEST_SKILL, _NAG_MUST_USE, _NAG_ALWAYS_USE, _NAG_PATTERNS, _OUTPUT_SCHEMA_KEY_PATTERN, _SCHEMA_ARTEFACT_TYPES, ArgError, isCliEntry2;
18014
+ var _HERE4, REQUIRED_PERSONA_SECTIONS_CORE, REQUIRED_PERSONA_SECTIONS_SPECIALIST, VALID_PERSONA_TIERS, PERSONA_LINE_BUDGETS, REQUIRED_USERTYPE_SECTIONS, USERTYPE_LINE_BUDGET, VALID_PERSONA_WINGS, PERSONA_LINE_BUDGETS_BY_WING, REQUIRED_SKILL_SECTIONS, SECTION_ALIASES, RECOMMENDED_SKILL_SECTIONS, CONDITIONAL_SKILL_SECTIONS, RULE_BAD_SIGNS, FRUGALITY_WRITER_SKILLS, FRUGALITY_CHARTER_RELPATH, FRUGALITY_CHARTER_INDEX_RULES, VAGUE_VALIDATION_PATTERNS, TRIGGER_WARNING_PATTERNS, ORDERED_STEP_PATTERN, SECTION_PATTERN, FRONTMATTER_PATTERN, DESCRIPTION_PATTERN, STATUS_PATTERN, REPLACED_BY_PATTERN, TIER_PATTERN, TYPE_PATTERN, SOURCE_PATTERN, SENIOR_RELATED_SKILLS_PATTERN, SENIOR_RELATED_WHEN_PATTERN, SENIOR_RELATED_WHEN_NOT_PATTERN, SENIOR_PROACTIVE_PATTERN, SENIOR_OUTPUT_PATTERN, H1_PATTERN, DOUBLE_BLANK_PATTERN, VALID_RULE_TYPES, VALID_RULE_SOURCES, ROUTER_ALLOWED_TRIGGER_KEYS, KERNEL_RULE_IDS2, VALID_EXECUTION_TYPES, VALID_EXECUTION_HANDLERS, VALID_EXECUTION_SAFETY_MODES, VALID_EXECUTION_FIELDS, WING3_SPINE_SLOTS, CONTEXT_SPINE_INLINE_PATTERN, WING3_SAAS_URL_PATTERN, WING3_VENDOR_BLACKLIST, WING3_STACK_LOCKED_PATTERN, WING3_CHANNEL_TACTIC_PATTERN, WING4_SPINE_SLOTS, WING4_SAAS_URL_PATTERN, WING4_VENDOR_BLACKLIST, WING4_STAGE_AGNOSTIC_PATTERN, Issue, LintResult, _ROLE_CONTRACT_CANDIDATES, _ROLE_CONTRACT_SLUGS_CACHE, _ROLE_CONTRACT_REF_PATTERN, PROCEDURE_HEADING_PATTERN, COMMAND_FRONTMATTER_DELEGATION_KEYS, MD_LINK_PATTERN_G, NAME_PATTERN, DISABLE_MODEL_PATTERN, _INSPECT_VERB_PATTERN, last_changed_scope_reason, last_changed_base_diff_ok, _INTERACTION_NAME_PATTERNS, _INTERACTION_CONTENT_KEYWORDS, _EXEC_FILE_SIGNALS, _EXEC_CONTENT_SIGNALS, _TASK_TYPE_SIGNALS, _VERIFICATION_TOOLS, _FRUGALITY_STANDARDS_PATTERN, _FRUGALITY_CHARTER_LINK_PATTERN, _MD_LINK_PATTERN, _MALICE_CRED_EXFIL, _MALICE_REMOTE_EXEC, _MALICE_FORCE_PUSH, _MALICE_CHMOD_SECRETS, _MALICE_SHELL_INJECT, _MALICE_PATTERNS, _NAG_UPDATE_CHECK, _NAG_STAY_CURRENT, _NAG_SELF_PROMO, _NAG_BEST_SKILL, _NAG_MUST_USE, _NAG_ALWAYS_USE, _NAG_PATTERNS, _OUTPUT_SCHEMA_KEY_PATTERN, _SCHEMA_ARTEFACT_TYPES, ArgError, isCliEntry2;
18002
18015
  var init_skill_linter = __esm({
18003
18016
  "src/scripts/skill_linter.ts"() {
18004
18017
  "use strict";
@@ -18046,6 +18059,22 @@ var init_skill_linter = __esm({
18046
18059
  "Output format": /* @__PURE__ */ new Set(["Output format", "Output"])
18047
18060
  };
18048
18061
  RECOMMENDED_SKILL_SECTIONS = [];
18062
+ CONDITIONAL_SKILL_SECTIONS = [
18063
+ {
18064
+ section: "Security constraints",
18065
+ // `isDirectory()`, not `existsSync` — a FILE named `scripts` would satisfy
18066
+ // an existence test while shipping no script at all, and the predicate is
18067
+ // only false-positive-free because it means what it says.
18068
+ when: (skillDir) => {
18069
+ try {
18070
+ return fs19.statSync(path19.join(skillDir, "scripts")).isDirectory();
18071
+ } catch {
18072
+ return false;
18073
+ }
18074
+ },
18075
+ reason: "the skill ships a scripts/ directory"
18076
+ }
18077
+ ];
18049
18078
  RULE_BAD_SIGNS = ["## Procedure", "## Output format", "## Gotchas"];
18050
18079
  FRUGALITY_WRITER_SKILLS = /* @__PURE__ */ new Set([
18051
18080
  "skill-writing",
@@ -19486,6 +19515,7 @@ __export(archive_completed_roadmaps_exports, {
19486
19515
  _branch_touched_paths: () => _branch_touched_paths,
19487
19516
  _git_mv: () => _git_mv,
19488
19517
  _inbound_ref_rewrite: () => _inbound_ref_rewrite,
19518
+ _is_frozen_record: () => _is_frozen_record,
19489
19519
  _repo_root: () => _repo_root,
19490
19520
  archive_completed: () => archive_completed,
19491
19521
  main: () => main10
@@ -19535,6 +19565,10 @@ function _branch_touched_paths(root, base) {
19535
19565
  }
19536
19566
  return out;
19537
19567
  }
19568
+ function _is_frozen_record(rel) {
19569
+ const norm = rel.split(path22.sep).join("/");
19570
+ return _FROZEN_RECORD_PREFIXES.some((p) => norm.startsWith(p)) || norm.endsWith(".patch");
19571
+ }
19538
19572
  function _inbound_ref_rewrite(root, old_rel, new_rel, dry_run, tracked = true) {
19539
19573
  const changed = [];
19540
19574
  let candidates;
@@ -19552,6 +19586,9 @@ function _inbound_ref_rewrite(root, old_rel, new_rel, dry_run, tracked = true) {
19552
19586
  if (!rel || rel === old_rel) {
19553
19587
  continue;
19554
19588
  }
19589
+ if (_is_frozen_record(rel)) {
19590
+ continue;
19591
+ }
19555
19592
  const fp = path22.join(root, rel);
19556
19593
  let text;
19557
19594
  try {
@@ -19781,7 +19818,7 @@ function _isCliEntry10() {
19781
19818
  return false;
19782
19819
  }
19783
19820
  }
19784
- var _HERE6, ArgparseExit2, _PROG6;
19821
+ var _HERE6, ArgparseExit2, _FROZEN_RECORD_PREFIXES, _PROG6;
19785
19822
  var init_archive_completed_roadmaps = __esm({
19786
19823
  "src/agent-src/scripts/archive_completed_roadmaps.ts"() {
19787
19824
  "use strict";
@@ -19795,6 +19832,7 @@ var init_archive_completed_roadmaps = __esm({
19795
19832
  this.code = code;
19796
19833
  }
19797
19834
  };
19835
+ _FROZEN_RECORD_PREFIXES = ["agents/evidence/"];
19798
19836
  _PROG6 = "archive_completed_roadmaps.py";
19799
19837
  if (_isCliEntry10() || process9.argv[1] === _HERE6) {
19800
19838
  try {
@@ -25344,6 +25382,18 @@ var init_session_role = __esm({
25344
25382
  }
25345
25383
  });
25346
25384
 
25385
+ // src/scripts/_lib/env_kill_switch.ts
25386
+ function isEnvKillSwitchActive(name) {
25387
+ return !_OFF_VALUES.has(process.env[name] ?? "");
25388
+ }
25389
+ var _OFF_VALUES;
25390
+ var init_env_kill_switch = __esm({
25391
+ "src/scripts/_lib/env_kill_switch.ts"() {
25392
+ "use strict";
25393
+ _OFF_VALUES = /* @__PURE__ */ new Set(["", "0", "false", "False"]);
25394
+ }
25395
+ });
25396
+
25347
25397
  // src/scripts/ai_council/quorum.ts
25348
25398
  function resolveQuorumThreshold(total, setting = "majority") {
25349
25399
  if (!Number.isInteger(total) || total < 0) {
@@ -25430,8 +25480,7 @@ function _utf8Bytes(cp) {
25430
25480
  );
25431
25481
  }
25432
25482
  function _kill_switch_active() {
25433
- const value = process.env[_KILL_SWITCH_ENV] ?? "";
25434
- return !(value === "" || value === "0" || value === "false" || value === "False");
25483
+ return isEnvKillSwitchActive(_KILL_SWITCH_ENV);
25435
25484
  }
25436
25485
  function appendEvent(event, opts = {}) {
25437
25486
  if (_kill_switch_active()) {
@@ -25593,6 +25642,7 @@ var SCHEMA_VERSION6, _VALID_ACTIONS, _KILL_SWITCH_ENV, _HERE15, _REPO_ROOT, _DEF
25593
25642
  var init_events_log = __esm({
25594
25643
  "src/scripts/ai_council/events_log.ts"() {
25595
25644
  "use strict";
25645
+ init_env_kill_switch();
25596
25646
  init_quorum();
25597
25647
  SCHEMA_VERSION6 = 2;
25598
25648
  _VALID_ACTIONS = /* @__PURE__ */ new Set([
@@ -29945,6 +29995,7 @@ var init_review_gate = __esm({
29945
29995
  "src/scripts/ai_team/review_gate.ts"() {
29946
29996
  "use strict";
29947
29997
  init_claude_plugin();
29998
+ init_env_kill_switch();
29948
29999
  init_state_io();
29949
30000
  init_config2();
29950
30001
  GATE_LEDGER_RELPATH = path50.join("agents", "runtime", "team", "events.log");
@@ -37947,6 +37998,49 @@ function synthesis_template(mode) {
37947
37998
  `Unknown synthesis mode ${_pyRepr9(mode)}. Expected one of: [${expected.map((m) => _pyReprStr2(m)).join(", ")}]`
37948
37999
  );
37949
38000
  }
38001
+ function parse_verdict_line(text) {
38002
+ _VERDICT_RE.lastIndex = 0;
38003
+ let last = null;
38004
+ let m;
38005
+ while ((m = _VERDICT_RE.exec(text)) !== null) {
38006
+ last = m;
38007
+ }
38008
+ if (last === null) {
38009
+ return null;
38010
+ }
38011
+ const raw = (last[1] ?? "").trim();
38012
+ if (raw.length === 0) {
38013
+ return null;
38014
+ }
38015
+ if (/^<.*>$/.test(raw)) {
38016
+ return null;
38017
+ }
38018
+ return { label: raw.toLowerCase(), display: raw };
38019
+ }
38020
+ function describe_verdict_mismatch(text, tally) {
38021
+ const verdict = parse_verdict_line(text);
38022
+ if (verdict === null) {
38023
+ return null;
38024
+ }
38025
+ const optionNamedSplit = tally.options.some(
38026
+ (o) => o.label.toLowerCase() === SPLIT_VERDICT_LABEL
38027
+ );
38028
+ const claimsSplit = verdict.label === SPLIT_VERDICT_LABEL && !optionNamedSplit;
38029
+ if (tally.consensus === null) {
38030
+ if (claimsSplit) {
38031
+ return null;
38032
+ }
38033
+ const how = tally.split ? `split across ${tally.options.length} option(s)` : "no option-level stances parsed";
38034
+ return `synthesis claims "${verdict.display}" but the tally recorded no consensus (${how}); use \`${SPLIT_VERDICT_LABEL}\` or re-run the round`;
38035
+ }
38036
+ if (claimsSplit) {
38037
+ return `synthesis claims a split but the tally cleared "${tally.consensus.label}"`;
38038
+ }
38039
+ if (verdict.label !== tally.consensus.label.toLowerCase()) {
38040
+ return `synthesis claims "${verdict.display}" but the tally cleared "${tally.consensus.label}"`;
38041
+ }
38042
+ return null;
38043
+ }
37950
38044
  function _strip_host_identity(text) {
37951
38045
  if (!text) {
37952
38046
  return text;
@@ -38043,7 +38137,7 @@ ${text}`);
38043
38137
  function peer_review_synthesis_addendum() {
38044
38138
  return PEER_REVIEW_SYNTHESIS_ADDENDUM;
38045
38139
  }
38046
- var NEUTRALITY_PREAMBLE, HOST_AGENT_IDENTITY_PATTERNS, PROMPT_MODE, ROADMAP_MODE, DIFF_MODE, FILES_MODE, PR_MODE, DESIGN_MODE, OPTIMIZE_MODE, ANALYSIS_MODE, DEBATE_MODE, STANCE_LINE_CONTRACT, ANTI_CONFORMITY_DIRECTIVE, _MODE_TABLE, FINDING_EXTRACTION_PROMPT, FINDING_SCORING_PROMPT, DEFAULT_SYNTHESIS, PR_SYNTHESIS, ANALYSIS_SYNTHESIS, CREATIVE_SYNTHESIS, _SYNTHESIS_TABLE, _INPUT_MODE_TO_SYNTHESIS_KEY, PEER_REVIEW_PROMPT, PEER_REVIEW_SYNTHESIS_ADDENDUM;
38140
+ var NEUTRALITY_PREAMBLE, HOST_AGENT_IDENTITY_PATTERNS, PROMPT_MODE, ROADMAP_MODE, DIFF_MODE, FILES_MODE, PR_MODE, DESIGN_MODE, OPTIMIZE_MODE, ANALYSIS_MODE, DEBATE_MODE, STANCE_LINE_CONTRACT, ANTI_CONFORMITY_DIRECTIVE, _MODE_TABLE, FINDING_EXTRACTION_PROMPT, FINDING_SCORING_PROMPT, DEFAULT_SYNTHESIS, PR_SYNTHESIS, ANALYSIS_SYNTHESIS, CREATIVE_SYNTHESIS, _SYNTHESIS_TABLE, _INPUT_MODE_TO_SYNTHESIS_KEY, SPLIT_VERDICT_LABEL, _VERDICT_RE, VERDICT_LINE_CONTRACT, PEER_REVIEW_PROMPT, PEER_REVIEW_SYNTHESIS_ADDENDUM;
38047
38141
  var init_prompts2 = __esm({
38048
38142
  "src/scripts/ai_council/prompts.ts"() {
38049
38143
  "use strict";
@@ -38307,6 +38401,11 @@ turn.`;
38307
38401
  diff: "default",
38308
38402
  files: "default"
38309
38403
  };
38404
+ SPLIT_VERDICT_LABEL = "split";
38405
+ _VERDICT_RE = /^\s*VERDICT:\s*(.+?)\s*$/gm;
38406
+ VERDICT_LINE_CONTRACT = `Close the synthesis with EXACTLY this line, and nothing after it:
38407
+ VERDICT: <option-label>
38408
+ Use the SAME <option-label> the Vote Tally cleared. Use \`${SPLIT_VERDICT_LABEL}\` when no option cleared the threshold \u2014 an honest split is a verdict, and claiming a winner the tally did not record is a render error.`;
38310
38409
  PEER_REVIEW_PROMPT = `Below are responses from other independent reviewers to the same
38311
38410
  artefact you just reviewed. Each is labelled with a neutral identifier
38312
38411
  (\`Response-A\`, \`Response-B\`, \u2026). You do NOT know which model produced
@@ -41809,6 +41908,11 @@ ${addendum}` : _pyLStrip(addendum);
41809
41908
  template = template ? `${template}
41810
41909
 
41811
41910
  ${CHAIRMAN_FIELDS_ADDENDUM}` : CHAIRMAN_FIELDS_ADDENDUM;
41911
+ }
41912
+ if (opts.stance_tally === true && template !== "") {
41913
+ template = `${template}
41914
+
41915
+ ${VERDICT_LINE_CONTRACT}`;
41812
41916
  }
41813
41917
  let body;
41814
41918
  if (template) {
@@ -41831,6 +41935,10 @@ ${body}`;
41831
41935
  responses.filter((r) => !r.error).map((r) => ({ member: `${r.provider}:${r.model}`, text: r.text }))
41832
41936
  );
41833
41937
  blocks.push(render_vote_tally(tally));
41938
+ const mismatch = describe_verdict_mismatch(body, tally);
41939
+ if (mismatch !== null) {
41940
+ blocks.push(`> **Verdict/tally mismatch:** ${mismatch}`);
41941
+ }
41834
41942
  }
41835
41943
  blocks.push(`## Convergence / Divergence
41836
41944
 
@@ -12,18 +12,53 @@ probability cause.
12
12
 
13
13
  ## Lookup
14
14
 
15
- | Signature (what you see) | Likely cause | First check |
16
- |---|---|---|
17
- | **Tool/agent loop** — same tool called >2× with near-identical args, no progress | Missing stop condition; reading without acting; ambiguous state | Stop. State the hypothesis in one line. Act or ask — do not re-read. (`context-hygiene` 15/25 rule) |
18
- | **HTTP 429 / "rate limit" / "quota"** | Too-frequent calls; shared quota; missing backoff | Inspect response headers (`Retry-After`, `X-RateLimit-*`); add exponential backoff; check if a loop is firing the calls |
19
- | **`ECONNREFUSED` / connection refused** | Service not up; wrong host/port; container not started | Is the service running? (`docker compose ps`, health endpoint); is the port right? (`lsof -i :PORT`); is it bound to `127.0.0.1` vs `0.0.0.0`? |
20
- | **`ENOENT` / "no such file or directory"** | Wrong cwd; path relative vs absolute; file not generated yet | Print the *resolved absolute* path; check cwd; confirm a prior step actually wrote the file |
21
- | **Timeout / hang** | Blocking I/O; deadlock; awaiting a condition that never holds | What is it waiting on? Add a log right before the blocking call; bound the wait; check for a missing `await`/`return` |
22
- | **`OOM` / killed (137) / heap out of memory** | Unbounded collection; loading all rows; leak in a loop | What grows per iteration? Stream/paginate instead of loading all; check `--max-old-space-size` only after finding the growth |
23
- | **`EACCES` / permission denied** | Wrong file mode; running as wrong user; read-only mount | `ls -l` the target; check the process user; is the path a read-only volume? |
24
- | **Flaky test (passes alone, fails in suite)** | Shared state; order dependence; real clock/random; unawaited async | Run the test in isolation vs in suite; look for shared fixtures, global state, `Date.now()`/random, missing `await` |
25
- | **Works locally, fails in CI** | Env/config drift; missing service; absent secret; path/case sensitivity | Diff env (`printenv`); is the service/secret present in CI? Linux is case-sensitive — check filename casing |
26
- | **Intermittent / "sometimes"** | Race; timing; network; non-deterministic order | Reproduce with condition-based waiting, not fixed sleeps (`systematic-debugging` § Condition-based waiting); log timestamps |
15
+ | ID | Signature (what you see) | Likely cause | First check |
16
+ |---|---|---|---|
17
+ | `tool-loop` | **Tool/agent loop** — same tool called >2× with near-identical args, no progress | Missing stop condition; reading without acting; ambiguous state | Stop. State the hypothesis in one line. Act or ask — do not re-read. (`context-hygiene` 15/25 rule) |
18
+ | `rate-limited` | **HTTP 429 / "rate limit" / "quota"** | Too-frequent calls; shared quota; missing backoff | Inspect response headers (`Retry-After`, `X-RateLimit-*`); add exponential backoff; check if a loop is firing the calls |
19
+ | `connection-refused` | **`ECONNREFUSED` / connection refused** | Service not up; wrong host/port; container not started | Is the service running? (`docker compose ps`, health endpoint); is the port right? (`lsof -i :PORT`); is it bound to `127.0.0.1` vs `0.0.0.0`? |
20
+ | `path-not-found` | **`ENOENT` / "no such file or directory"** | Wrong cwd; path relative vs absolute; file not generated yet | Print the *resolved absolute* path; check cwd; confirm a prior step actually wrote the file |
21
+ | `hang` | **Timeout / hang** | Blocking I/O; deadlock; awaiting a condition that never holds | What is it waiting on? Add a log right before the blocking call; bound the wait; check for a missing `await`/`return` |
22
+ | `out-of-memory` | **`OOM` / killed (137) / heap out of memory** | Unbounded collection; loading all rows; leak in a loop | What grows per iteration? Stream/paginate instead of loading all; check `--max-old-space-size` only after finding the growth |
23
+ | `permission-denied` | **`EACCES` / permission denied** | Wrong file mode; running as wrong user; read-only mount | `ls -l` the target; check the process user; is the path a read-only volume? |
24
+ | `order-dependent-test` | **Flaky test (passes alone, fails in suite)** | Shared state; order dependence; real clock/random; unawaited async | Run the test in isolation vs in suite; look for shared fixtures, global state, `Date.now()`/random, missing `await` |
25
+ | `local-ci-drift` | **Works locally, fails in CI** | Env/config drift; missing service; absent secret; path/case sensitivity | Diff env (`printenv`); is the service/secret present in CI? Linux is case-sensitive — check filename casing |
26
+ | `race` | **Intermittent / "sometimes"** | Race; timing; network; non-deterministic order | Reproduce with condition-based waiting, not fixed sleeps (`systematic-debugging` § Condition-based waiting); log timestamps |
27
+ | `stale-capability-claim` | **Budget exhausted plus repeated attempts at a tool that is not available** | A capability the session claimed for itself instead of resolving — the council, a hook slot, a subagent primitive, an MCP server assumed present | Ask the live verb, not the tree: `agent-config hooks:status` for slot bindings, `agent-config routing:doctor` for per-field capability **provenance** (`registry` / `live-probe` / `default`), `agent-config council:status` for council availability. A `false` from `default` records that nobody answered, not that the host cannot |
28
+
29
+ ### Naming convention for IDs
30
+
31
+ An id is a short kebab handle so a signature can be **cited by name** — the
32
+ first column was prose, so nothing could reference a row.
33
+
34
+ One rule governs new ids: **where a row's signature is a stable string the
35
+ tooling already prints, the id is that string verbatim** rather than a kebab
36
+ synonym. `skill_linter.ts` emits a closed set of `Issue` codes —
37
+ `missing_section`, `missing_conditional_section`, `invalid_execution_handler` —
38
+ and a row about one of those is looked up by the code the reader just saw in the
39
+ output. No current row is of that class; the convention is stated for the next
40
+ one, so the table does not accumulate two names for the same failure.
41
+
42
+ ## Discrimination drills
43
+
44
+ A row's "first check" is only worth following if the documented signature is what
45
+ actually appears. Each drill breaks the named layer and asserts the documented
46
+ symptom — run it once when you doubt a row, or when adding one. They are text a
47
+ human runs; nothing here is a CI gate.
48
+
49
+ | ID | Drill — break this, expect that |
50
+ |---|---|
51
+ | `tool-loop` | Ask for a fact that is genuinely absent from the tree (a config key nothing defines). Expect repeated searches with shrinking variation and no edit — that shape, not an error message, is the signature. |
52
+ | `rate-limited` | Fire the provider's own rate limit with a tight loop against a cheap endpoint. Expect `429` **plus** `Retry-After`; a `429` with no such header is a different failure (a gateway, not the API). |
53
+ | `connection-refused` | Stop the service and call it (`docker compose stop <svc>`). Expect `ECONNREFUSED` immediately. If it hangs instead, the host resolves but nothing listens — that is `hang`, not this row. |
54
+ | `path-not-found` | Run a path-taking script from a different cwd. Expect `ENOENT` naming a path that is *almost* right — the near-miss is the tell that the path is relative, not missing. |
55
+ | `hang` | `await` a promise nothing resolves. Expect no output and no exit, distinguishable from `race` by being 100% reproducible. |
56
+ | `out-of-memory` | Accumulate rows in an array inside a loop over a large fixture. Expect the heap message and exit 137 — and note that the reported line is where allocation failed, not where the growth is. |
57
+ | `permission-denied` | `chmod 000` a fixture and read it. Expect `EACCES`, not `ENOENT` — confusing the two sends you looking for a missing file that is right there. |
58
+ | `order-dependent-test` | Add a module-level mutable to two tests in one file. Expect green alone, red in suite. Reversing the file order should move the failure — if it does not, the cause is shared *external* state, not order. |
59
+ | `local-ci-drift` | Rename a fixture's casing (`Foo.ts` → `foo.ts`) and import the old name. Expect green on macOS, red on Linux CI. The generalisation: anything the local filesystem is lenient about. |
60
+ | `race` | Replace a condition-based wait with a fixed `sleep` shorter than the operation. Expect intermittent failure whose rate moves with machine load — load-sensitivity is what separates this from `order-dependent-test`. |
61
+ | `stale-capability-claim` | Assert a capability without asking for it, then compare against the live verb. `agent-config routing:doctor` prints provenance per field; a field whose provenance is `default` was never established by anything — which is exactly the state a confident self-claim is indistinguishable from. |
27
62
 
28
63
  ## How to use
29
64
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
3
  "mcpName": "io.github.event4u-app/agent-config",
4
- "version": "9.33.0",
4
+ "version": "9.34.0",
5
5
  "description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including \"zero runtime daemon\".",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -77,7 +77,7 @@
77
77
  "build:mcp-catalog": "./scripts-run src/scripts/build_mcp_catalog --write --strict",
78
78
  "build": "npm run build:cli && npm run build:install-bundle && npm run build:hooks && npm run build:mcp-bundle && npm run build:cli-delegate && npm run build:ui",
79
79
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.scripts.json",
80
- "lint:ts": "eslint 'src/**/*.ts'",
80
+ "lint:ts": "eslint --cache 'src/**/*.ts'",
81
81
  "test:ts": "vitest run",
82
82
  "lint:publint": "publint",
83
83
  "prepack": "npm run build && npm run build:discovery && npm run build:mcp-manifest && node src/scripts/prepack-check.mjs",
@@ -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 {
@@ -21,7 +21,7 @@
21
21
  "solo_conclusion_rate": {
22
22
  "definition": "share of post_run passes with `solo: true` — written by the isSoloConcluded predicate in quorum.ts, never re-derived from the numbers here, so one change to the rule cannot leave this definition stale. ceil(n/2) makes 1-of-2 a legitimate conclusion; this measures how often that legitimacy is used, not whether it should be. THREE cases carry solo: true and they are not the same finding — a degraded multi-member council (configured_total > 1), a council configured with one member (configured_total = 1, dispatch = full), and a deliberate --single dispatch (dispatch = single). Split on dispatch and configured_total before reading the rate.",
23
23
  "instrument": "quorum_result lines, phase=post_run, command=run, field solo, split by dispatch + configured_total",
24
- "threshold": "none committed before data. The quorum-solo-floor blocker pre-registers the three outcomes this rate decides between — a third CLI member, a gate-scoped min_present: 2, or a published null under 5 % — and that choice is made against real data, never ahead of it. It is decided on the degraded case only; a --single pass is the operator asking for one voice, not the council failing to assemble."
24
+ "threshold": "DECIDED 2026-08-11 against real data — see ADR-224 (docs/decisions/ADR-224-gate-scoped-solo-attendance-floor.md). The quorum-solo-floor blocker pre-registered three outcomes — a third CLI member, a gate-scoped min_present: 2, or a published null under 5 %. Read at 2026-08-11T09:30Z with the definition above: 1 of 8 post_run/command=run passes carried a degraded solo conclusion = 12.5 %, so the under-5 % null did not fire and the gate-scoped floor was chosen (council 2/2). It was decided on the degraded case only; a --single pass is the operator asking for one voice, not the council failing to assemble. Still no numeric threshold is committed here, deliberately: at n=8 the 95 % interval around 1/8 spans roughly 0.3 %-53 %, so the rate established urgency and not certainty, and ADR-224's review trigger reopens the null at n=40. What this metric measures does NOT change; once the floor lands, how often it FIRES is a second and different question needing its own instrument."
25
25
  },
26
26
  "absent_reason_distribution": {
27
27
  "definition": "count per reason over absent[] entries — no_binary / no_auth / timeout / quota (AbsentReason) plus the two runtime fallbacks the CLI writes directly, `unavailable` (transport resolved to nothing) and `binary_missing` (a CLI client refused to construct). Distinguishes a council degraded by configuration from one degraded by provider behaviour, which have opposite fixes.",
@@ -57,8 +57,13 @@ export function templateEnvelope(): Record<string, unknown> {
57
57
  acceptance_criteria: ['<what done means, one line each>'],
58
58
  remaining: ['<open work, one line each>'],
59
59
  not_carried_forward: ['<what the successor must re-derive from source>'],
60
- decisions: ['<decision — one-line rationale>'],
60
+ decisions: ['<decision — one-line rationale> [reversible]'],
61
61
  constraints: ['<binding constraint>'],
62
+ // Ships EMPTY, unlike the prose lists above: the entries are path refs,
63
+ // and a `<placeholder>` left in by a hurried writer would be a fake path
64
+ // rather than obvious filler. An empty list is also the honest default —
65
+ // nothing off limits is the common case.
66
+ do_not_touch: [],
62
67
  open_worker_envelopes: [],
63
68
  artifact_paths: ['<path to a deliverable / note / evidence file>'],
64
69
  assumptions: [],