@event4u/agent-config 8.11.0 → 8.12.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +49 -279
- package/README.md +2 -2
- package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +106 -94
- package/dist/agent-src/rules/architecture.md +6 -51
- package/dist/agent-src/rules/code-comment-discipline.md +10 -94
- package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
- package/dist/agent-src/rules/improve-before-implement.md +7 -66
- package/dist/agent-src/rules/no-roadmap-references.md +13 -77
- package/dist/agent-src/rules/persona-governance.md +5 -47
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
- package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +83 -0
- package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
- package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +14 -14
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/router.json +1 -1
- package/docs/CLAIMS.md +30 -2
- package/docs/architecture.md +1 -1
- package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
- package/docs/benchmark.md +39 -0
- package/docs/contracts/ai-council-config.md +9 -4
- package/docs/contracts/persona-schema.md +49 -0
- package/docs/contracts/provider-lifecycle.md +29 -4
- package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
- package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
- package/docs/decisions/INDEX.md +2 -0
- package/docs/design/council-vs-solo-baseline.md +92 -0
- package/docs/design/utilization-window-criteria.md +51 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
- package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
- package/docs/guidelines/code-clarity.md +86 -1
- package/docs/maintainers/branch-protection.md +87 -0
- package/docs/maintainers/system-map.md +85 -0
- package/docs/proof.md +14 -3
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +9 -0
- package/src/scripts/_cli/cmd_doctor.ts +95 -0
- package/src/scripts/_lib/knowledge_global.ts +44 -0
- package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
- package/src/scripts/ai_council/orchestrator.ts +188 -1
- package/src/scripts/bench_persona_placebo.ts +308 -0
- package/src/scripts/bench_quality_rerun.ts +357 -0
- package/src/scripts/build_proof.ts +9 -0
- package/src/scripts/check_artefact_count_messaging.ts +56 -0
- package/src/scripts/check_knowledge_cards.ts +33 -4
- package/src/scripts/check_portability.ts +1 -0
- package/src/scripts/complexity_report.ts +776 -0
- package/src/scripts/consumer_matrix.ts +251 -12
- package/src/scripts/council_cli.ts +56 -1
- package/src/scripts/explain_run.ts +593 -0
- package/src/scripts/knowledge_global_cli.ts +234 -45
- package/src/scripts/rule_backlinks.ts +0 -0
|
@@ -58,6 +58,29 @@ export const TIERS: readonly string[] = ['public', 'vendor', 'proprietary'];
|
|
|
58
58
|
*/
|
|
59
59
|
export const DEFAULT_ALLOWED_TIERS: readonly string[] = ['public'];
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Sensitivity axis (Phase 1, road-to-feedback-8.11 / successor note to
|
|
63
|
+
* ADR-119) — orthogonal to `TIERS` (origin) and layered on top of the
|
|
64
|
+
* existing tier + redaction gate. `tier` classifies WHERE a card came from;
|
|
65
|
+
* `sensitivity` classifies WHETHER it may ever leave the project at all.
|
|
66
|
+
*
|
|
67
|
+
* * `prohibited` — contains redaction-class content; never leaves the
|
|
68
|
+
* repo. Machine-derived from the redaction scan (a card is never
|
|
69
|
+
* hand-labelled into this class — a positive hit forces it).
|
|
70
|
+
* * `project` — DEFAULT. Stays project-local; promotion is refused
|
|
71
|
+
* unless a human explicitly reclassifies the card to `shareable`.
|
|
72
|
+
* * `shareable` — eligible for the global store; must pass redaction
|
|
73
|
+
* AND carry full promotion provenance (see the G5 footer fields).
|
|
74
|
+
*
|
|
75
|
+
* Deliberately kept to three machine-anchored classes — a wider
|
|
76
|
+
* team/organization taxonomy is phantom for a single-maintainer install and
|
|
77
|
+
* would over-fit; revisit only once a real multi-tenant consumer exists.
|
|
78
|
+
*/
|
|
79
|
+
export const SENSITIVITIES: readonly string[] = ['prohibited', 'project', 'shareable'];
|
|
80
|
+
|
|
81
|
+
/** Default sensitivity for a card with no explicit `sensitivity:` field. */
|
|
82
|
+
export const DEFAULT_SENSITIVITY = 'project';
|
|
83
|
+
|
|
61
84
|
/**
|
|
62
85
|
* Hard defaults applied when the setting block is absent. Default is ON for
|
|
63
86
|
* the safe tiers; turning sharing on never makes promotion automatic.
|
|
@@ -384,6 +407,14 @@ const _PROVENANCE_FIELDS: readonly string[] = [
|
|
|
384
407
|
'last_verified',
|
|
385
408
|
'tier',
|
|
386
409
|
'seen_in',
|
|
410
|
+
// G5 promotion audit-trail fields (Phase 1, road-to-feedback-8.11):
|
|
411
|
+
// the more consistent home for these than frontmatter, since they sit
|
|
412
|
+
// alongside the other promotion-time facts above, not the card's own
|
|
413
|
+
// origin-tier/sensitivity classification (which lives in frontmatter).
|
|
414
|
+
'source_repo',
|
|
415
|
+
'owner',
|
|
416
|
+
'review_after',
|
|
417
|
+
'promotion_reason',
|
|
387
418
|
];
|
|
388
419
|
|
|
389
420
|
/** Render the global-card provenance footer (the unversioned-store audit trail). */
|
|
@@ -394,6 +425,10 @@ export function render_provenance_footer(args: {
|
|
|
394
425
|
last_verified: string;
|
|
395
426
|
tier: string;
|
|
396
427
|
seen_in: string[];
|
|
428
|
+
source_repo: string;
|
|
429
|
+
owner: string;
|
|
430
|
+
review_after: string;
|
|
431
|
+
promotion_reason: string;
|
|
397
432
|
}): string {
|
|
398
433
|
return [
|
|
399
434
|
PROVENANCE_START,
|
|
@@ -403,11 +438,20 @@ export function render_provenance_footer(args: {
|
|
|
403
438
|
`- last_verified: ${args.last_verified}`,
|
|
404
439
|
`- tier: ${args.tier}`,
|
|
405
440
|
`- seen_in: ${args.seen_in.join(', ')}`,
|
|
441
|
+
`- source_repo: ${args.source_repo}`,
|
|
442
|
+
`- owner: ${args.owner}`,
|
|
443
|
+
`- review_after: ${args.review_after}`,
|
|
444
|
+
`- promotion_reason: ${_singleLine(args.promotion_reason)}`,
|
|
406
445
|
PROVENANCE_END,
|
|
407
446
|
'',
|
|
408
447
|
].join('\n');
|
|
409
448
|
}
|
|
410
449
|
|
|
450
|
+
/** Collapse a human-entered reason to one line — footer fields are line-based. */
|
|
451
|
+
function _singleLine(s: string): string {
|
|
452
|
+
return s.replace(/\r\n|\r|\n/g, ' ').trim();
|
|
453
|
+
}
|
|
454
|
+
|
|
411
455
|
/** Extract the provenance footer fields as a flat dict (empty if absent). */
|
|
412
456
|
export function parse_provenance_footer(text: string): Record<string, string> {
|
|
413
457
|
if (!text.includes(PROVENANCE_START) || !text.includes(PROVENANCE_END)) {
|
|
@@ -187,6 +187,156 @@ export function should_suggest(
|
|
|
187
187
|
return len >= options.threshold;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Sensitivity gate (Phase 1, road-to-feedback-8.11 — successor note to
|
|
192
|
+
// ADR-119). Layered ON TOP of the existing tier/redaction gate in
|
|
193
|
+
// `knowledge_global_redaction.ts`: a card must clear tier + redaction FIRST,
|
|
194
|
+
// then clear this gate before its write is allowed to proceed.
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/** Outcome of the sensitivity gate for one promotion attempt. */
|
|
198
|
+
export interface SensitivityGateResult {
|
|
199
|
+
readonly eligible: boolean;
|
|
200
|
+
readonly reason: string;
|
|
201
|
+
/** The EFFECTIVE sensitivity used for the decision (post auto-derivation). */
|
|
202
|
+
readonly sensitivity: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolve the effective sensitivity used for a promotion decision.
|
|
207
|
+
*
|
|
208
|
+
* A redaction hit always forces `prohibited`, overriding any declared value —
|
|
209
|
+
* this is independent of the tier gate's own `halt_on_trigger` setting, so a
|
|
210
|
+
* `prohibited` override never depends on that unrelated config flag. An
|
|
211
|
+
* unset / invalid declared value defaults to `project` (never `shareable` —
|
|
212
|
+
* `shareable` is never auto-assigned, only explicitly declared by a human).
|
|
213
|
+
*/
|
|
214
|
+
export function resolve_effective_sensitivity(declared: string, violations_present: boolean): string {
|
|
215
|
+
if (violations_present) {
|
|
216
|
+
return 'prohibited';
|
|
217
|
+
}
|
|
218
|
+
const d = (declared ?? '').trim();
|
|
219
|
+
return knowledge_global.SENSITIVITIES.includes(d) ? d : knowledge_global.DEFAULT_SENSITIVITY;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Gate a card for promotion on the sensitivity axis.
|
|
224
|
+
*
|
|
225
|
+
* Refuses anything whose effective sensitivity is not `shareable` (including
|
|
226
|
+
* the machine-derived `prohibited` override), and refuses a `shareable` card
|
|
227
|
+
* that has no human-entered `promotion_reason` — the auto-promote
|
|
228
|
+
* *suggestion* only ever leads to a write once a human states why the card
|
|
229
|
+
* is safe to share.
|
|
230
|
+
*/
|
|
231
|
+
export function gate_sensitivity_for_promotion(
|
|
232
|
+
declared_sensitivity: string,
|
|
233
|
+
options: { violations_present?: boolean; promotion_reason?: string } = {},
|
|
234
|
+
): SensitivityGateResult {
|
|
235
|
+
const violations_present = options.violations_present ?? false;
|
|
236
|
+
const effective = resolve_effective_sensitivity(declared_sensitivity, violations_present);
|
|
237
|
+
|
|
238
|
+
if (effective === 'prohibited') {
|
|
239
|
+
return {
|
|
240
|
+
eligible: false,
|
|
241
|
+
sensitivity: effective,
|
|
242
|
+
reason:
|
|
243
|
+
'sensitivity: prohibited — redaction-class content, never leaves the repo',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (effective !== 'shareable') {
|
|
247
|
+
return {
|
|
248
|
+
eligible: false,
|
|
249
|
+
sensitivity: effective,
|
|
250
|
+
reason:
|
|
251
|
+
`sensitivity '${effective}' — promotion refused unless a human explicitly ` +
|
|
252
|
+
"reclassifies the card to 'shareable'",
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const reason = (options.promotion_reason ?? '').trim();
|
|
256
|
+
if (!reason) {
|
|
257
|
+
return {
|
|
258
|
+
eligible: false,
|
|
259
|
+
sensitivity: effective,
|
|
260
|
+
reason:
|
|
261
|
+
"sensitivity 'shareable' but no promotion_reason given — a human must state " +
|
|
262
|
+
'why this card is safe to share before it may be promoted',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return { eligible: true, sensitivity: effective, reason: 'sensitivity gate passed' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Revocation ledger — append-only tombstone trail (Phase 1, road-to-feedback-8.11)
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
export const REVOCATIONS_FILENAME = '.revocations.jsonl';
|
|
273
|
+
|
|
274
|
+
/** One append-only tombstone line: what was revoked, when, and why. */
|
|
275
|
+
export interface RevocationEntry {
|
|
276
|
+
readonly revoked_at: string;
|
|
277
|
+
readonly card_id: string;
|
|
278
|
+
readonly reason: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function _revocations_path(env: knowledge_global.EnvMapType): string {
|
|
282
|
+
return path.join(knowledge_global.global_store_dir(env, { create: true }), REVOCATIONS_FILENAME);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Append one tombstone line to the revocation ledger — the caller MUST call
|
|
287
|
+
* this BEFORE deleting the card/usage entry it documents. Append-only (a
|
|
288
|
+
* single `fs.appendFileSync`, never rewritten): no `forget` / `forget --tier`
|
|
289
|
+
* / `purge` call ever removes a prior tombstone, so the ledger stays a
|
|
290
|
+
* durable audit trail across every deletion path — including `purge`, which
|
|
291
|
+
* deliberately spares this one file while wiping everything else.
|
|
292
|
+
*/
|
|
293
|
+
export function append_tombstone(
|
|
294
|
+
card_id: string,
|
|
295
|
+
reason: string,
|
|
296
|
+
options: { today?: string; env?: knowledge_global.EnvMapType } = {},
|
|
297
|
+
): RevocationEntry {
|
|
298
|
+
const entry: RevocationEntry = {
|
|
299
|
+
revoked_at: options.today ?? '',
|
|
300
|
+
card_id,
|
|
301
|
+
reason: reason || 'no reason given',
|
|
302
|
+
};
|
|
303
|
+
fs.appendFileSync(_revocations_path(options.env ?? null), JSON.stringify(entry) + '\n', 'utf-8');
|
|
304
|
+
return entry;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Read the revocation ledger. Tolerant: missing file → []; malformed lines are skipped. */
|
|
308
|
+
export function load_tombstones(env: knowledge_global.EnvMapType = null): RevocationEntry[] {
|
|
309
|
+
let text: string;
|
|
310
|
+
try {
|
|
311
|
+
text = fs.readFileSync(
|
|
312
|
+
path.join(knowledge_global.global_store_dir(env), REVOCATIONS_FILENAME),
|
|
313
|
+
'utf-8',
|
|
314
|
+
);
|
|
315
|
+
} catch {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
const out: RevocationEntry[] = [];
|
|
319
|
+
for (const raw of text.split(/\r\n|\r|\n/)) {
|
|
320
|
+
const line = raw.trim();
|
|
321
|
+
if (!line) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const obj: unknown = JSON.parse(line);
|
|
326
|
+
if (_isPlainObject(obj) && typeof obj['card_id'] === 'string') {
|
|
327
|
+
out.push({
|
|
328
|
+
revoked_at: String(obj['revoked_at'] ?? ''),
|
|
329
|
+
card_id: obj['card_id'],
|
|
330
|
+
reason: String(obj['reason'] ?? ''),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// corrupt line — skip; never let one bad line crash the ledger read
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
|
|
190
340
|
/**
|
|
191
341
|
* List cards that warrant a promotion suggestion under the current config.
|
|
192
342
|
*
|
|
@@ -65,7 +65,8 @@ import {
|
|
|
65
65
|
synthesis_template,
|
|
66
66
|
system_prompt_for,
|
|
67
67
|
} from './prompts.js';
|
|
68
|
-
import {
|
|
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);
|