@event4u/agent-config 8.11.0 → 8.13.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 (87) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -0
  3. package/CHANGELOG.md +64 -266
  4. package/README.md +2 -2
  5. package/dist/agent-src/commands/pr/create.md +26 -0
  6. package/dist/agent-src/commands/team/adversarial.md +96 -0
  7. package/dist/agent-src/commands/team/delegate.md +109 -0
  8. package/dist/agent-src/commands/team/review.md +84 -0
  9. package/dist/agent-src/commands/team/status.md +92 -0
  10. package/dist/agent-src/commands/team.md +147 -0
  11. package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
  12. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +106 -94
  13. package/dist/agent-src/rules/architecture.md +6 -51
  14. package/dist/agent-src/rules/code-comment-discipline.md +10 -94
  15. package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
  16. package/dist/agent-src/rules/improve-before-implement.md +8 -65
  17. package/dist/agent-src/rules/no-roadmap-references.md +13 -77
  18. package/dist/agent-src/rules/persona-governance.md +5 -47
  19. package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
  20. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
  21. package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
  22. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +83 -0
  23. package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
  24. package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
  25. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  26. package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
  27. package/dist/discovery/deprecation-report.md +1 -1
  28. package/dist/discovery/discovery-manifest.json +159 -21
  29. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  30. package/dist/discovery/discovery-manifest.summary.md +3 -3
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +11 -6
  33. package/dist/discovery/trust-report.md +2 -2
  34. package/dist/discovery/workspaces.json +10 -5
  35. package/dist/install/install.mjs +38 -0
  36. package/dist/mcp/registry-manifest.json +2 -2
  37. package/dist/router.json +1 -1
  38. package/dist/server/schemas/settings.js +7 -0
  39. package/dist/server/schemas/settings.js.map +1 -1
  40. package/docs/CLAIMS.md +31 -3
  41. package/docs/architecture.md +2 -2
  42. package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
  43. package/docs/benchmark.md +39 -0
  44. package/docs/catalog.md +10 -4
  45. package/docs/command-flows.md +6 -1
  46. package/docs/contracts/ai-council-config.md +9 -4
  47. package/docs/contracts/ai-team-config.md +191 -0
  48. package/docs/contracts/command-clusters.md +1 -0
  49. package/docs/contracts/persona-schema.md +49 -0
  50. package/docs/contracts/provider-lifecycle.md +29 -4
  51. package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
  52. package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
  53. package/docs/decisions/INDEX.md +2 -0
  54. package/docs/design/council-vs-solo-baseline.md +92 -0
  55. package/docs/design/utilization-window-criteria.md +51 -0
  56. package/docs/featured-skills.md +1 -1
  57. package/docs/getting-started-by-role.md +3 -1
  58. package/docs/getting-started.md +31 -1
  59. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
  60. package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
  61. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
  62. package/docs/guidelines/code-clarity.md +86 -1
  63. package/docs/maintainers/branch-protection.md +105 -0
  64. package/docs/maintainers/system-map.md +85 -0
  65. package/docs/proof.md +15 -4
  66. package/package.json +1 -1
  67. package/src/config/agent-settings.template.yml +49 -0
  68. package/src/scripts/_cli/cmd_doctor.ts +291 -1
  69. package/src/scripts/_lib/claude_plugin.ts +39 -4
  70. package/src/scripts/_lib/knowledge_global.ts +44 -0
  71. package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
  72. package/src/scripts/ai_council/orchestrator.ts +188 -1
  73. package/src/scripts/ai_team/config.ts +162 -0
  74. package/src/scripts/bench_persona_placebo.ts +308 -0
  75. package/src/scripts/bench_quality_rerun.ts +357 -0
  76. package/src/scripts/build_proof.ts +9 -0
  77. package/src/scripts/check_artefact_count_messaging.ts +56 -0
  78. package/src/scripts/check_knowledge_cards.ts +33 -4
  79. package/src/scripts/check_portability.ts +1 -0
  80. package/src/scripts/complexity_report.ts +776 -0
  81. package/src/scripts/consumer_matrix.ts +251 -12
  82. package/src/scripts/council_cli.ts +56 -1
  83. package/src/scripts/explain_run.ts +593 -0
  84. package/src/scripts/install.ts +39 -0
  85. package/src/scripts/knowledge_global_cli.ts +234 -45
  86. package/src/scripts/rule_backlinks.ts +350 -0
  87. package/src/server/schemas/settings.ts +17 -0
@@ -65,7 +65,8 @@ import {
65
65
  synthesis_template,
66
66
  system_prompt_for,
67
67
  } from './prompts.js';
68
- import { render_vote_tally, tally_stances } from './stance_tally.js';
68
+ import { count_dissenters, dissent_quota_met, is_near_duplicate } from './debate_gates.js';
69
+ import { parse_stance_line, render_vote_tally, tally_stances } from './stance_tally.js';
69
70
 
70
71
  // ── Python-format / stdlib parity helpers ────────────────────────────────
71
72
  //
@@ -419,6 +420,14 @@ export interface ConsultOptions {
419
420
  * round's prompt is byte-identical to today.
420
421
  */
421
422
  stance_tally?: boolean;
423
+ /**
424
+ * Phase 1 stance-repair transport (2026-07-12 council policy): confirm per
425
+ * member in interactive runs; `null`/absent → a missing/unparseable stance
426
+ * stays a repair-marker in the tally, never an unplanned billable call.
427
+ */
428
+ on_stance_repair?: ((member: string) => boolean) | null;
429
+ /** Receives each stance-repair response so the caller can account its cost. */
430
+ on_stance_repair_result?: ((r: CouncilResponse) => void) | null;
422
431
  }
423
432
 
424
433
  /**
@@ -515,6 +524,46 @@ export function consult(
515
524
  }
516
525
  }
517
526
 
527
+ // Phase 1 stance repair: one bounded stance-line-only re-prompt per member
528
+ // whose final reply lacks a parseable STANCE line — dispatched only through
529
+ // the on_stance_repair transport, reusing _run_round (spend gate + ledger +
530
+ // stamping unchanged). The repaired stance line is APPENDED to the member's
531
+ // final text so the deterministic tally can read it.
532
+ const on_stance_repair = opts.on_stance_repair ?? null;
533
+ if (stance_tally && on_stance_repair !== null) {
534
+ for (let i = 0; i < last_results.length; i++) {
535
+ const r = last_results[i] as CouncilResponse;
536
+ if (r.error !== null || r.text.trim() === '' || parse_stance_line(r.text) !== null) {
537
+ continue;
538
+ }
539
+ const member = members[i];
540
+ if (member === undefined || !on_stance_repair(member.name)) {
541
+ continue;
542
+ }
543
+ const repairQ = new CouncilQuestion({
544
+ mode: question.mode,
545
+ user_prompt:
546
+ `Your reply is missing the mandatory closing stance line. ` +
547
+ `Reply with ONLY that single line for your final position.\n\n${STANCE_LINE_CONTRACT}`,
548
+ max_tokens: question.max_tokens,
549
+ });
550
+ const rr = _run_round([member], repairQ, resolvedBudget, spent, {
551
+ table,
552
+ on_overrun,
553
+ project,
554
+ original_ask,
555
+ advisor_plans,
556
+ });
557
+ const rep = rr[0];
558
+ if (rep !== undefined) {
559
+ opts.on_stance_repair_result?.(rep);
560
+ if (rep.error === null && parse_stance_line(rep.text) !== null) {
561
+ r.text = `${r.text.trim()}\n\n${rep.text.trim()}`;
562
+ }
563
+ }
564
+ }
565
+ }
566
+
518
567
  return last_results;
519
568
  }
520
569
 
@@ -895,6 +944,90 @@ function _augment_for_debate_round(
895
944
  );
896
945
  }
897
946
 
947
+ /**
948
+ * Phase 3 post-round enforcement (2026-07-12 council policy): deterministic
949
+ * detectors flag members whose round added nothing (novelty near-duplicate of
950
+ * their own prior round) or when the round collapsed toward agreement (dissent
951
+ * quota unmet → the most-recently-converged member). At most ONE bounded repair
952
+ * re-prompt per member per round, dispatched only through the `on_repair`
953
+ * transport (confirm-interactive / auto-fire under --auto-continue); the repair
954
+ * call reuses `_run_round`, so the projected-spend gate, ledger, and metadata
955
+ * stamping apply unchanged. A successful repair REPLACES the member's entry so
956
+ * downstream rounds/persistence see the repaired reply.
957
+ */
958
+ function _run_debate_gate_repairs(
959
+ members: ExternalAIClient[],
960
+ results: CouncilResponse[],
961
+ prior: CouncilResponse[],
962
+ round_question: CouncilQuestion,
963
+ budget: CostBudget,
964
+ spent: Spent,
965
+ round_opts: {
966
+ table: PriceTable | null;
967
+ on_overrun: OnOverrunCallback | null;
968
+ project: ProjectContext | null;
969
+ original_ask: string;
970
+ advisor_plans: Map<string, AdvisorPlan> | null;
971
+ },
972
+ on_repair: ((member: string, reason: string) => boolean) | null,
973
+ ): CouncilResponse[] {
974
+ const flagged: Array<{ idx: number; reason: string }> = [];
975
+ const okText = (r: CouncilResponse | undefined): string =>
976
+ r !== undefined && r.error === null ? r.text : '';
977
+ for (let i = 0; i < results.length; i++) {
978
+ const curr = okText(results[i]);
979
+ const prev = okText(prior[i]);
980
+ if (curr !== '' && prev !== '' && is_near_duplicate(prev, curr)) {
981
+ flagged.push({ idx: i, reason: 'novelty: near-duplicate of your own prior round' });
982
+ }
983
+ }
984
+ const texts = results.map((r) => okText(r));
985
+ if (!dissent_quota_met(texts.filter((t) => t !== ''))) {
986
+ // Most-recently-converged heuristic: the LAST member with a real reply
987
+ // that carries no objection marker.
988
+ for (let i = results.length - 1; i >= 0; i--) {
989
+ const t = okText(results[i]);
990
+ if (t !== '' && count_dissenters([t]) === 0 && !flagged.some((f) => f.idx === i)) {
991
+ flagged.push({ idx: i, reason: 'dissent quota unmet: state a genuine objection or confirm with a named reason' });
992
+ break;
993
+ }
994
+ }
995
+ }
996
+ if (flagged.length === 0 || on_repair === null) {
997
+ return results;
998
+ }
999
+ const repaired = new Set<number>();
1000
+ const out = [...results];
1001
+ for (const f of flagged) {
1002
+ if (repaired.has(f.idx)) {
1003
+ continue; // hard cap: <= 1 repair per member per round
1004
+ }
1005
+ const member = members[f.idx];
1006
+ if (member === undefined) {
1007
+ continue;
1008
+ }
1009
+ if (!on_repair(member.name, f.reason)) {
1010
+ continue;
1011
+ }
1012
+ repaired.add(f.idx);
1013
+ const repairQ = new CouncilQuestion({
1014
+ mode: round_question.mode,
1015
+ user_prompt:
1016
+ `${round_question.user_prompt}\n\n---\n\n` +
1017
+ `REPAIR RE-PROMPT (${f.reason}). Your previous reply this round did not ` +
1018
+ `advance the debate. Respond again: either a genuinely updated position ` +
1019
+ `naming the specific flaw that moved you, or an explicit defense with NEW evidence.`,
1020
+ max_tokens: round_question.max_tokens,
1021
+ });
1022
+ const rr = _run_round([member], repairQ, budget, spent, round_opts);
1023
+ const r0 = rr[0];
1024
+ if (r0 !== undefined && r0.error === null && r0.text.trim() !== '') {
1025
+ out[f.idx] = r0;
1026
+ }
1027
+ }
1028
+ return out;
1029
+ }
1030
+
898
1031
  export interface RunDebateOptions {
899
1032
  budget?: CostBudget | null;
900
1033
  table?: PriceTable | null;
@@ -914,6 +1047,22 @@ export interface RunDebateOptions {
914
1047
  * prompt is byte-identical to today.
915
1048
  */
916
1049
  debate_gates?: boolean;
1050
+ /**
1051
+ * Phase 3 repair transport, per the 2026-07-12 council policy
1052
+ * (`debate_gates.repair_action`): the CLI passes `() => true` under
1053
+ * `--auto-continue` (auto-fire under the cap) or an interactive one-line
1054
+ * confirm otherwise. `null`/absent → gates detect but never dispatch
1055
+ * (no unplanned spend without an explicit transport).
1056
+ */
1057
+ on_repair?: ((member: string, reason: string) => boolean) | null;
1058
+ /**
1059
+ * Phase 3 restate pass (`ai_council.restate.enabled` / `--restate`): a
1060
+ * pre-round-1 billable call per member collecting a <=50-word restatement
1061
+ * + one alternative framing. Default off -> no extra calls, byte-identical.
1062
+ */
1063
+ restate?: boolean;
1064
+ /** Receives the restate responses (rendering, divergence flags, cost). */
1065
+ on_restate?: ((responses: CouncilResponse[]) => void) | null;
917
1066
  }
918
1067
 
919
1068
  /**
@@ -975,6 +1124,27 @@ export function run_debate(
975
1124
  const all_rounds: CouncilResponse[][] = [];
976
1125
  let current_user_prompt = question.user_prompt;
977
1126
 
1127
+ // Phase 3 restate: one pre-round-1 call per member. Runs BEFORE any debate
1128
+ // spend so a diverging restatement can be flagged before round-2 cost.
1129
+ if (opts.restate === true) {
1130
+ const restateQ = new CouncilQuestion({
1131
+ mode: question.mode,
1132
+ user_prompt:
1133
+ `Before the debate begins: RESTATE the question below in at most 50 words, ` +
1134
+ `then offer ONE alternative framing of it. Do not argue a position yet.\n\n---\n\n` +
1135
+ `${question.user_prompt}`,
1136
+ max_tokens: question.max_tokens,
1137
+ });
1138
+ const restated = _run_round(members, restateQ, budget, spent, {
1139
+ table,
1140
+ on_overrun,
1141
+ project,
1142
+ original_ask,
1143
+ advisor_plans,
1144
+ });
1145
+ opts.on_restate?.(restated);
1146
+ }
1147
+
978
1148
  for (let round_idx = 0; round_idx < max_rounds; round_idx++) {
979
1149
  const round_number = round_idx + 1;
980
1150
  let results: CouncilResponse[];
@@ -1000,6 +1170,23 @@ export function run_debate(
1000
1170
  });
1001
1171
  }
1002
1172
 
1173
+ if (opts.debate_gates === true && round_idx > 0 && all_rounds.length > 0) {
1174
+ results = _run_debate_gate_repairs(
1175
+ members,
1176
+ results,
1177
+ all_rounds[all_rounds.length - 1] as CouncilResponse[],
1178
+ // round_idx > 0 here, so current_user_prompt IS this round's prompt.
1179
+ new CouncilQuestion({
1180
+ mode: question.mode,
1181
+ user_prompt: current_user_prompt,
1182
+ max_tokens: question.max_tokens,
1183
+ }),
1184
+ budget,
1185
+ spent,
1186
+ { table, on_overrun, project, original_ask, advisor_plans },
1187
+ opts.on_repair ?? null,
1188
+ );
1189
+ }
1003
1190
  all_rounds.push(results);
1004
1191
  if (on_round_complete !== null) {
1005
1192
  on_round_complete(round_number, results);
@@ -0,0 +1,162 @@
1
+ /**
2
+ * ai_team configuration loader — single source of truth for the `/team`
3
+ * cross-model review family.
4
+ *
5
+ * Mirrors the fail-closed validation posture of the council loader
6
+ * (`src/scripts/ai_council/config.ts`) at a fraction of its surface: the
7
+ * `ai_team` block has five flat keys and no nested sub-schemas, so this
8
+ * loader is deliberately lean — defaults, per-key type checks, and hard
9
+ * unknown-key rejection. Contract: `docs/contracts/ai-team-config.md`.
10
+ *
11
+ * Unlike the council (ALWAYS user-global `.ai-council.yml`, ADR-104),
12
+ * `ai_team` lives in the PROJECT settings cascade (`.agent-settings.yml`
13
+ * via `load_agent_settings`) — team mode is a per-project posture
14
+ * (default-off), not a per-developer credential store; the codex CLI
15
+ * carries the subscription auth itself.
16
+ *
17
+ * Validation contract (all enforced at load time, fail-closed):
18
+ *
19
+ * 1. `ai_team` absent → defaults (feature off). Non-mapping → error.
20
+ * 2. Unknown keys under `ai_team` are rejected — a typo must never
21
+ * silently disable a gate (`allow_delegate` misspelled = delegation
22
+ * stays off AND the load fails loudly).
23
+ * 3. `enabled`, `allow_delegate`, `suppress_setup_hint` are booleans.
24
+ * 4. `model` is a non-empty string. `'auto'` = pass no `--model` flag
25
+ * (the codex CLI default applies); any other value passes through
26
+ * verbatim.
27
+ * 5. `max_calls_per_day` is a non-negative integer (bools rejected).
28
+ */
29
+ import { load_agent_settings, type SettingsDict } from '../_lib/agent_settings.js';
30
+
31
+ /** Raised when the `ai_team` block of `.agent-settings.yml` violates the schema. */
32
+ export class TeamConfigError extends Error {
33
+ constructor(message: string) {
34
+ super(message);
35
+ this.name = 'TeamConfigError';
36
+ }
37
+ }
38
+
39
+ export interface AiTeamConfig {
40
+ readonly enabled: boolean;
41
+ readonly model: string;
42
+ readonly allow_delegate: boolean;
43
+ readonly max_calls_per_day: number;
44
+ readonly suppress_setup_hint: boolean;
45
+ }
46
+
47
+ /** Shipped defaults — byte-parity with `src/config/agent-settings.template.yml`. */
48
+ export const AI_TEAM_DEFAULTS: AiTeamConfig = Object.freeze({
49
+ enabled: false,
50
+ model: 'auto',
51
+ allow_delegate: false,
52
+ max_calls_per_day: 50,
53
+ suppress_setup_hint: false,
54
+ });
55
+
56
+ /**
57
+ * Sentinel: `model: 'auto'` means "pass NO --model flag" so the codex
58
+ * CLI's own default applies (tracks the subscription's strongest model
59
+ * instead of pinning a stale ID).
60
+ */
61
+ export const AI_TEAM_MODEL_AUTO = 'auto';
62
+
63
+ const _KNOWN_KEYS: ReadonlySet<string> = new Set([
64
+ 'enabled',
65
+ 'model',
66
+ 'allow_delegate',
67
+ 'max_calls_per_day',
68
+ 'suppress_setup_hint',
69
+ ]);
70
+
71
+ function _isDict(value: unknown): value is Record<string, unknown> {
72
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
73
+ }
74
+
75
+ function _isBool(value: unknown): value is boolean {
76
+ return typeof value === 'boolean';
77
+ }
78
+
79
+ function _requireBool(raw: Record<string, unknown>, key: string, fallback: boolean): boolean {
80
+ if (!(key in raw)) {
81
+ return fallback;
82
+ }
83
+ const value = raw[key];
84
+ if (!_isBool(value)) {
85
+ throw new TeamConfigError(
86
+ `\`ai_team.${key}\` must be a boolean (got ${JSON.stringify(value)}).`,
87
+ );
88
+ }
89
+ return value;
90
+ }
91
+
92
+ /**
93
+ * Validate a raw `ai_team` mapping and return the typed config.
94
+ *
95
+ * `raw = null | undefined` (block absent) returns the defaults — the
96
+ * feature is off, never an error. Every other shape is validated
97
+ * fail-closed per the module contract above.
98
+ */
99
+ export function build_ai_team_config(raw: unknown): AiTeamConfig {
100
+ if (raw === null || raw === undefined) {
101
+ return AI_TEAM_DEFAULTS;
102
+ }
103
+ if (!_isDict(raw)) {
104
+ throw new TeamConfigError('`ai_team` must be a mapping.');
105
+ }
106
+
107
+ for (const key of Object.keys(raw)) {
108
+ if (!_KNOWN_KEYS.has(key)) {
109
+ throw new TeamConfigError(
110
+ `ai_team.${key}: unknown key; valid: ` +
111
+ `[${[..._KNOWN_KEYS].map((k) => `'${k}'`).join(', ')}].`,
112
+ );
113
+ }
114
+ }
115
+
116
+ const enabled = _requireBool(raw, 'enabled', AI_TEAM_DEFAULTS.enabled);
117
+ const allow_delegate = _requireBool(raw, 'allow_delegate', AI_TEAM_DEFAULTS.allow_delegate);
118
+ const suppress_setup_hint = _requireBool(
119
+ raw,
120
+ 'suppress_setup_hint',
121
+ AI_TEAM_DEFAULTS.suppress_setup_hint,
122
+ );
123
+
124
+ let model = AI_TEAM_DEFAULTS.model;
125
+ if ('model' in raw) {
126
+ const value = raw['model'];
127
+ if (typeof value !== 'string' || value.trim() === '') {
128
+ throw new TeamConfigError(
129
+ `\`ai_team.model\` must be a non-empty string (got ${JSON.stringify(value)}).`,
130
+ );
131
+ }
132
+ model = value;
133
+ }
134
+
135
+ let max_calls_per_day = AI_TEAM_DEFAULTS.max_calls_per_day;
136
+ if ('max_calls_per_day' in raw) {
137
+ const value = raw['max_calls_per_day'];
138
+ if (_isBool(value) || typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
139
+ throw new TeamConfigError(
140
+ '`ai_team.max_calls_per_day` must be a non-negative integer ' +
141
+ `(got ${JSON.stringify(value)}).`,
142
+ );
143
+ }
144
+ max_calls_per_day = value;
145
+ }
146
+
147
+ return { enabled, model, allow_delegate, max_calls_per_day, suppress_setup_hint };
148
+ }
149
+
150
+ /**
151
+ * Load the `ai_team` config from the merged project settings cascade.
152
+ *
153
+ * `opts.settings` injects a pre-merged settings dict (test seam — no
154
+ * filesystem access); otherwise `load_agent_settings` walks the normal
155
+ * `.agent-settings.yml` cascade from `opts.cwd` (default: process cwd).
156
+ */
157
+ export function load_ai_team_config(
158
+ opts: { settings?: SettingsDict | null; cwd?: string | null } = {},
159
+ ): AiTeamConfig {
160
+ const settings = opts.settings ?? load_agent_settings({ cwd: opts.cwd ?? process.cwd() });
161
+ return build_ai_team_config((settings as Record<string, unknown>)['ai_team']);
162
+ }