@event4u/agent-config 8.10.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 +67 -149
- package/README.md +6 -6
- package/dist/agent-src/commands/challenge-me/vision.md +121 -32
- package/dist/agent-src/commands/challenge-me/with-docs.md +21 -8
- package/dist/agent-src/commands/commit/in-chunks.md +1 -1
- package/dist/agent-src/commands/commit.md +2 -1
- package/dist/agent-src/commands/cost/report.md +2 -1
- package/dist/agent-src/commands/feature/dev.md +4 -4
- package/dist/agent-src/commands/humanize.md +101 -0
- package/dist/agent-src/commands/jira-ticket.md +2 -2
- package/dist/agent-src/commands/memory/load.md +15 -0
- package/dist/agent-src/commands/optimize/project.md +12 -0
- package/dist/agent-src/commands/roadmap/create.md +12 -11
- package/dist/agent-src/commands/tests/create.md +1 -1
- package/dist/agent-src/commands/worktree/cleanup.md +22 -17
- package/dist/agent-src/commands/worktree/create.md +3 -1
- package/dist/agent-src/commands/worktree/status.md +12 -0
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +66 -0
- package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +1 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +47 -3
- package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +111 -85
- package/dist/agent-src/contexts/execution/roadmap-writing-source-derived.md +65 -0
- package/dist/agent-src/contexts/execution/subagent-modes-detail.md +84 -0
- package/dist/agent-src/rules/architecture.md +6 -51
- package/dist/agent-src/rules/autonomous-execution.md +7 -37
- package/dist/agent-src/rules/broken-access-control.md +7 -45
- package/dist/agent-src/rules/code-comment-discipline.md +64 -0
- package/dist/agent-src/rules/context-hygiene.md +14 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
- package/dist/agent-src/rules/delegation-policy.md +10 -8
- package/dist/agent-src/rules/design-fidelity.md +4 -0
- package/dist/agent-src/rules/git-history-discipline.md +4 -79
- package/dist/agent-src/rules/improve-before-implement.md +7 -66
- package/dist/agent-src/rules/legal-safety-floor.md +11 -55
- 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/roadmap-progress-sync.md +5 -56
- package/dist/agent-src/rules/think-before-action.md +10 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
- package/dist/agent-src/rules/verify-before-complete.md +12 -0
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +96 -12
- package/dist/agent-src/skills/ai-council/SKILL.md +19 -9
- package/dist/agent-src/skills/api-design/SKILL.md +7 -5
- package/dist/agent-src/skills/api-testing/SKILL.md +1 -1
- package/dist/agent-src/skills/authz-review/SKILL.md +59 -2
- package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/bug-analyzer/SKILL.md +19 -16
- package/dist/agent-src/skills/code-refactoring/SKILL.md +9 -9
- package/dist/agent-src/skills/content-funnel-design/SKILL.md +2 -0
- package/dist/agent-src/skills/data-flow-mapper/SKILL.md +1 -1
- package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
- package/dist/agent-src/skills/defense-in-depth/SKILL.md +2 -2
- package/dist/agent-src/skills/dependency-upgrade/SKILL.md +7 -7
- package/dist/agent-src/skills/design-intelligence/SKILL.md +8 -8
- package/dist/agent-src/skills/design-review/SKILL.md +23 -2
- package/dist/agent-src/skills/doc-coauthoring/SKILL.md +2 -0
- package/dist/agent-src/skills/editorial-calendar/SKILL.md +2 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +7 -9
- package/dist/agent-src/skills/git-workflow/SKILL.md +86 -0
- package/dist/agent-src/skills/humanizer/SKILL.md +147 -0
- package/dist/agent-src/skills/humanizer/data/patterns.md +190 -0
- package/dist/agent-src/skills/humanizer/evals/triggers.json +17 -0
- package/dist/agent-src/skills/laravel/SKILL.md +14 -1
- package/dist/agent-src/skills/launch-readiness/SKILL.md +20 -19
- package/dist/agent-src/skills/legal-practice-profile/SKILL.md +126 -0
- package/dist/agent-src/skills/logging-monitoring/SKILL.md +1 -1
- package/dist/agent-src/skills/mcp/SKILL.md +1 -1
- package/dist/agent-src/skills/memory-consolidation/SKILL.md +14 -1
- package/dist/agent-src/skills/migration-architect/SKILL.md +2 -1
- package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
- package/dist/agent-src/skills/performance/SKILL.md +1 -1
- package/dist/agent-src/skills/php-debugging/SKILL.md +17 -0
- package/dist/agent-src/skills/readme-writing/SKILL.md +2 -0
- package/dist/agent-src/skills/readme-writing-package/SKILL.md +2 -0
- package/dist/agent-src/skills/release-comms/SKILL.md +2 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +22 -51
- package/dist/agent-src/skills/security-audit/SKILL.md +6 -9
- package/dist/agent-src/skills/sentry-integration/SKILL.md +2 -2
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +8 -0
- package/dist/agent-src/skills/source-discovery/SKILL.md +26 -24
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +55 -46
- package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +42 -0
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +10 -0
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +10 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +27 -20
- package/dist/agent-src/skills/worktree-lifecycle/evals/evals.json +38 -0
- package/dist/agent-src/templates/agent-settings.md +3 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +15 -1
- package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
- package/dist/agent-src/templates/roadmaps.md +14 -0
- package/dist/cli/commands/recordTriggerEval.js +4 -14
- package/dist/cli/commands/recordTriggerEval.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +157 -88
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +6 -6
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +13 -10
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +14 -9
- package/dist/install/install.mjs +12 -7
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_settings_hooks.js +8 -0
- package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
- package/dist/scripts/_lib/trigger_eval_floors.js +24 -0
- package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -0
- package/dist/scripts/condense.js +12 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +5 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +54 -5
- package/docs/architecture.md +4 -4
- package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
- package/docs/benchmark.md +90 -0
- package/docs/catalog.md +8 -5
- package/docs/command-flows.md +2 -1
- package/docs/contracts/ai-council-config.md +97 -0
- package/docs/contracts/command-clusters.md +1 -0
- package/docs/contracts/evidence-based-pruning.md +1 -0
- package/docs/contracts/hook-architecture-v1.md +25 -0
- package/docs/contracts/kernel-membership.md +35 -0
- package/docs/contracts/measurement-baseline.md +1 -0
- package/docs/contracts/persona-schema.md +49 -0
- package/docs/contracts/provider-lifecycle.md +29 -4
- package/docs/contracts/release-sizing.md +22 -0
- package/docs/contracts/write-engine.md +38 -1
- package/docs/customization.md +12 -0
- package/docs/decisions/ADR-002-kernel-bucket-overrides.md +11 -0
- package/docs/decisions/ADR-003-flat-cluster-subs-and-colon-syntax.md +11 -0
- package/docs/decisions/ADR-006-skill-tools-python-pilot.md +1 -1
- package/docs/decisions/ADR-028-root-layout.md +1 -1
- package/docs/decisions/ADR-038-canonical-settings-path.md +10 -0
- package/docs/decisions/ADR-092-defer-command-tier-alias-removal.md +29 -5
- package/docs/decisions/ADR-093-ai-council-config-user-global.md +1 -1
- package/docs/decisions/ADR-100-global-knowledge-card-sharing.md +7 -0
- package/docs/decisions/ADR-103-global-knowledge-default-off-until-measured.md +2 -2
- package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +1 -1
- package/docs/decisions/ADR-114-commit-policy-iron-law-override.md +11 -0
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -1
- package/docs/decisions/ADR-117-subagents-auto-default-on.md +2 -1
- package/docs/decisions/ADR-118-loop-engineering-boundaries.md +162 -0
- package/docs/decisions/ADR-119-global-knowledge-default-on.md +104 -0
- 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/ADR-200-python-to-typescript-migration.md +11 -1
- package/docs/decisions/ADR-rule-kernel-and-router.md +11 -0
- package/docs/decisions/INDEX.md +13 -9
- package/docs/design/council-vs-solo-baseline.md +92 -0
- package/docs/design/cross-model-parity-eval.md +82 -0
- package/docs/design/length-neutral-judge-rerun.md +92 -0
- package/docs/design/utilization-window-criteria.md +51 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +80 -5
- 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 +191 -1
- package/docs/guidelines/design-antipatterns.md +20 -0
- package/docs/guidelines/php/php-coding-patterns.md +12 -2
- package/docs/maintainers/branch-protection.md +87 -0
- package/docs/maintainers/system-map.md +85 -0
- package/docs/proof.md +32 -15
- package/docs/wedge/production-validator/production-validator.md +27 -0
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +38 -9
- package/src/scripts/_cli/cmd_doctor.ts +129 -1
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +87 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +8 -0
- package/src/scripts/_lib/knowledge_global.ts +50 -2
- package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
- package/src/scripts/_lib/knowledge_global_redaction.ts +42 -4
- package/src/scripts/_lib/lexical_index.ts +15 -2
- package/src/scripts/_lib/orchestration_record.ts +14 -0
- package/src/scripts/_lib/trigger_eval_floors.ts +31 -0
- package/src/scripts/ai_council/chairman.ts +90 -0
- package/src/scripts/ai_council/clients.ts +18 -3
- package/src/scripts/ai_council/config.ts +157 -0
- package/src/scripts/ai_council/debate_gates.ts +89 -0
- package/src/scripts/ai_council/orchestrator.ts +252 -2
- package/src/scripts/ai_council/prompts.ts +121 -11
- package/src/scripts/ai_council/replay.ts +17 -0
- package/src/scripts/ai_council/stance_tally.ts +184 -0
- package/src/scripts/ai_tells_rules.ts +433 -0
- package/src/scripts/bench_humanizer_eval.ts +240 -0
- package/src/scripts/bench_persona_placebo.ts +308 -0
- package/src/scripts/bench_quality_rerun.ts +357 -0
- package/src/scripts/build_discovery_manifest.ts +6 -0
- package/src/scripts/build_proof.ts +30 -9
- package/src/scripts/check_artefact_count_messaging.ts +56 -0
- package/src/scripts/check_claims.ts +40 -0
- package/src/scripts/check_knowledge_cards.ts +33 -4
- package/src/scripts/check_portability.ts +1 -0
- package/src/scripts/check_rule_invariants.ts +10 -3
- package/src/scripts/complexity_report.ts +776 -0
- package/src/scripts/condense.ts +16 -2
- package/src/scripts/consumer_matrix.ts +292 -14
- package/src/scripts/council_cli.ts +203 -1
- package/src/scripts/detect_ai_tells.ts +312 -0
- package/src/scripts/explain_run.ts +593 -0
- package/src/scripts/install.ts +12 -0
- package/src/scripts/knowledge_global_cli.ts +235 -46
- package/src/scripts/lint_framework_leakage.ts +88 -7
- package/src/scripts/lint_framework_leakage_allowlist.json +59 -572
- package/src/scripts/lint_knowledge_scale.ts +30 -0
- package/src/scripts/lint_originality_shingles.ts +210 -0
- package/src/scripts/orchestration_record.ts +1 -0
- package/src/scripts/rule_backlinks.ts +0 -0
- package/src/scripts/schemas/agent-settings.schema.json +11 -0
- package/src/scripts/self_review_gate.ts +41 -15
- package/src/scripts/skill_eval_coverage.ts +71 -3
- package/src/scripts/skill_linter.ts +1 -0
- package/src/scripts/trigger_eval_rotation.ts +344 -0
- package/src/scripts/update_counts.ts +5 -17
- package/src/scripts/worktree_cleanup_check.ts +263 -0
- package/src/server/schemas/settings.ts +8 -3
- package/src/scripts/analysis_freshness.ts +0 -362
- package/src/scripts/export_replay_corpus.ts +0 -208
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic `parallelizable:` classifier recall + false-positive measurement
|
|
3
|
+
* against the orchestration corpus (road-to-orchestration-scope-decision Phase 2
|
|
4
|
+
* / road-to-subagent-value-realization-followup Phase 1 Step 3).
|
|
5
|
+
*
|
|
6
|
+
* The corpus tasks (`internal/bench/orchestration/corpus/*.md`) each document a
|
|
7
|
+
* "Classification signal" and an expected dispatch mode. This encodes those
|
|
8
|
+
* documented signals as `TaskSignals` and asserts `classifyTask` (the pure v1
|
|
9
|
+
* classifier) fires the expected mode — recall on the modes v1 covers
|
|
10
|
+
* (`do-in-parallel`, `do-in-steps`), FP=0 on the negative control, and the
|
|
11
|
+
* documented scope gap (v1 has no `do-competitively` / verdict mode).
|
|
12
|
+
*
|
|
13
|
+
* This is the durable, reproducible "record actual hit/miss counts" deliverable
|
|
14
|
+
* + a regression guard for the classifier. Deterministic — no spend, no agents.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, it } from 'vitest';
|
|
17
|
+
|
|
18
|
+
import { classifyTask, type ActivationInputs, type TaskSignals } from './auto_dispatch.js';
|
|
19
|
+
|
|
20
|
+
const ON: ActivationInputs = { enabled: true, auto: 'on', subagent_spawn: true };
|
|
21
|
+
|
|
22
|
+
/** Corpus signals transcribed from each task's documented "Classification signal". */
|
|
23
|
+
const CORPUS: Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
signals: TaskSignals;
|
|
26
|
+
expectMode: 'do-in-parallel' | 'do-in-steps' | null;
|
|
27
|
+
note: string;
|
|
28
|
+
}> = [
|
|
29
|
+
{
|
|
30
|
+
id: 'orch-01',
|
|
31
|
+
signals: { parallelizable: 'files', independent_slices: 4, size_estimate: 4, ordered_plan: false },
|
|
32
|
+
expectMode: 'do-in-parallel',
|
|
33
|
+
note: 'N independent files, same analysis shape',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'orch-02',
|
|
37
|
+
signals: { ordered_plan: true, size_estimate: 5, independent_slices: 0 },
|
|
38
|
+
expectMode: 'do-in-steps',
|
|
39
|
+
note: 'explicit ordered numbered plan',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'orch-03',
|
|
43
|
+
signals: { size_estimate: 3, independent_slices: 0, ordered_plan: false },
|
|
44
|
+
expectMode: null,
|
|
45
|
+
note: 'do-competitively — NOT a v1 DispatchMode (documented scope gap)',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'pv-01',
|
|
49
|
+
signals: { size_estimate: 2, independent_slices: 0, ordered_plan: false },
|
|
50
|
+
expectMode: null,
|
|
51
|
+
note: 'verdict dispatch — out of the v1 parallel/steps scope',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'pv-02',
|
|
55
|
+
signals: { size_estimate: 1, independent_slices: 0, ordered_plan: false },
|
|
56
|
+
expectMode: null,
|
|
57
|
+
note: 'negative control — a clean one-file task MUST NOT dispatch',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
describe('parallelizable classifier — corpus recall', () => {
|
|
62
|
+
for (const c of CORPUS) {
|
|
63
|
+
it(`${c.id}: ${c.note}`, () => {
|
|
64
|
+
const r = classifyTask(c.signals, ON);
|
|
65
|
+
expect(r.mode).toBe(c.expectMode);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
it('recall on v1-covered modes is 2/2 (orch-01 parallel, orch-02 steps)', () => {
|
|
70
|
+
const covered = CORPUS.filter((c) => c.expectMode !== null);
|
|
71
|
+
const hits = covered.filter((c) => classifyTask(c.signals, ON).mode === c.expectMode);
|
|
72
|
+
expect(covered).toHaveLength(2);
|
|
73
|
+
expect(hits).toHaveLength(2);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('false-positive rate is 0 — the negative control (pv-02) never dispatches', () => {
|
|
77
|
+
const pv02 = CORPUS.find((c) => c.id === 'pv-02')!;
|
|
78
|
+
expect(classifyTask(pv02.signals, ON).action).not.toBe('dispatch');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('documents the v1 scope gap: do-competitively (orch-03) + verdict (pv-01) are not classified', () => {
|
|
82
|
+
for (const id of ['orch-03', 'pv-01']) {
|
|
83
|
+
const c = CORPUS.find((x) => x.id === id)!;
|
|
84
|
+
expect(classifyTask(c.signals, ON).mode).toBeNull();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -108,8 +108,16 @@ export function build_claude_hook_matrix(manifest_path: string): ClaudeHookMatri
|
|
|
108
108
|
if (!concerns || (Array.isArray(concerns) && concerns.length === 0)) continue;
|
|
109
109
|
const native = ac_to_native[ac_event];
|
|
110
110
|
if (native === undefined) continue;
|
|
111
|
+
// Hook-resilience shim (road-to-opt-subagent-harvest P1.4):
|
|
112
|
+
// local binary → PATH fallback → SILENT exit 0 when neither exists
|
|
113
|
+
// (a hook must never error-spam the loop on an uninstalled repo).
|
|
114
|
+
// The dispatcher's own exit code PROPAGATES when it runs — a
|
|
115
|
+
// fail_closed concern (e.g. block-no-verify) still denies; only the
|
|
116
|
+
// missing-binary case degrades open, which is the honest degrade
|
|
117
|
+
// (an absent dispatcher cannot evaluate anything).
|
|
111
118
|
matrix[native] =
|
|
112
119
|
'BIN="$CLAUDE_PROJECT_DIR/agent-config"; [ -x "$BIN" ] || BIN=agent-config; ' +
|
|
120
|
+
'command -v "$BIN" >/dev/null 2>&1 || exit 0; ' +
|
|
113
121
|
`"$BIN" dispatch:hook --platform claude --event ${ac_event} ` +
|
|
114
122
|
`--native-event ${native} --project-dir "$CLAUDE_PROJECT_DIR" ` +
|
|
115
123
|
`--min-version ${String(hook_spec)}`;
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* Python→TS migration). The public API and CLI contract mirror the Python
|
|
7
7
|
* original EXACTLY — same exported snake_case names, same semantics, same
|
|
8
8
|
* tier-classification, provenance footer, and config-merge behavior, same
|
|
9
|
-
* exit codes and byte-identical stdout. No behaviour changes
|
|
9
|
+
* exit codes and byte-identical stdout. No behaviour changes at port time;
|
|
10
|
+
* one deliberate post-port change: DEFAULT_ALLOWED_TIERS narrowed to
|
|
11
|
+
* ['public'] per ADR-119 (2026-07-12).
|
|
10
12
|
*
|
|
11
13
|
* Structure-grounding v2 (ADR-100 / road-to-structure-grounding-v2). Promotes
|
|
12
14
|
* *expensive* (remote) project-local knowledge cards to a per-user, file-first
|
|
@@ -51,8 +53,33 @@ export const TIERS: readonly string[] = ['public', 'vendor', 'proprietary'];
|
|
|
51
53
|
/**
|
|
52
54
|
* Default tiers eligible to auto-cross a project boundary under default-on.
|
|
53
55
|
* `proprietary` is excluded by design — manual-only regardless of `enabled`.
|
|
56
|
+
* Narrowed to `public` only per ADR-119 (2026-07-12, council-validated
|
|
57
|
+
* flip): `vendor` is a deliberate opt-in widening, not the shipped default.
|
|
54
58
|
*/
|
|
55
|
-
export const DEFAULT_ALLOWED_TIERS: readonly string[] = ['public'
|
|
59
|
+
export const DEFAULT_ALLOWED_TIERS: readonly string[] = ['public'];
|
|
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';
|
|
56
83
|
|
|
57
84
|
/**
|
|
58
85
|
* Hard defaults applied when the setting block is absent. Default is ON for
|
|
@@ -380,6 +407,14 @@ const _PROVENANCE_FIELDS: readonly string[] = [
|
|
|
380
407
|
'last_verified',
|
|
381
408
|
'tier',
|
|
382
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',
|
|
383
418
|
];
|
|
384
419
|
|
|
385
420
|
/** Render the global-card provenance footer (the unversioned-store audit trail). */
|
|
@@ -390,6 +425,10 @@ export function render_provenance_footer(args: {
|
|
|
390
425
|
last_verified: string;
|
|
391
426
|
tier: string;
|
|
392
427
|
seen_in: string[];
|
|
428
|
+
source_repo: string;
|
|
429
|
+
owner: string;
|
|
430
|
+
review_after: string;
|
|
431
|
+
promotion_reason: string;
|
|
393
432
|
}): string {
|
|
394
433
|
return [
|
|
395
434
|
PROVENANCE_START,
|
|
@@ -399,11 +438,20 @@ export function render_provenance_footer(args: {
|
|
|
399
438
|
`- last_verified: ${args.last_verified}`,
|
|
400
439
|
`- tier: ${args.tier}`,
|
|
401
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)}`,
|
|
402
445
|
PROVENANCE_END,
|
|
403
446
|
'',
|
|
404
447
|
].join('\n');
|
|
405
448
|
}
|
|
406
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
|
+
|
|
407
455
|
/** Extract the provenance footer fields as a flat dict (empty if absent). */
|
|
408
456
|
export function parse_provenance_footer(text: string): Record<string, string> {
|
|
409
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
|
*
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* the tier gate are matched byte-for-byte against the retired Python implementation. The
|
|
8
8
|
* public API and CLI contract mirror Python exactly — same exported
|
|
9
9
|
* snake_case names, same `GateResult` shape + `summary()` text, same exit
|
|
10
|
-
* codes and stdout. No behaviour changes
|
|
10
|
+
* codes and stdout. No behaviour changes at port time; one ADDITIVE
|
|
11
|
+
* hardening landed after the port (2026-07-12, ADR-103 flip validation):
|
|
12
|
+
* hidden-unicode detection + strip-then-rescan in `redaction_scan` — inputs
|
|
13
|
+
* without invisible characters are unaffected.
|
|
11
14
|
*
|
|
12
15
|
* Structure-grounding v2, Phase 1 — the privacy crux (ADR-100 /
|
|
13
16
|
* road-to-structure-grounding-v2). Before any project-local card may cross a
|
|
@@ -121,11 +124,26 @@ function _compileIgnoreCase(pattern: string): RegExp {
|
|
|
121
124
|
return new RegExp(pattern, 'i');
|
|
122
125
|
}
|
|
123
126
|
|
|
127
|
+
// Invisible / control characters usable to smuggle an identifier past the
|
|
128
|
+
// pattern scan (zero-width class, bidi controls, soft hyphen, BOM, Unicode
|
|
129
|
+
// Tag block) — the encoding-smuggling attack class from the ADR-103
|
|
130
|
+
// council re-evaluation (2026-07-11). Presence is itself a violation:
|
|
131
|
+
// a knowledge card has no legitimate need for invisible characters, and
|
|
132
|
+
// halt-and-surface (never silent-rewrite) is this gate's contract.
|
|
133
|
+
const _HIDDEN_UNICODE_RE =
|
|
134
|
+
/[----\u{E0000}-\u{E007F}]/gu;
|
|
135
|
+
|
|
124
136
|
/**
|
|
125
137
|
* Combined privacy-floor + source-confidentiality scan over `text`.
|
|
126
138
|
*
|
|
127
139
|
* Returns the list of violations (empty = clean). Reused by the Phase-4
|
|
128
140
|
* linter as the CI net that redaction actually fired on committed cards.
|
|
141
|
+
*
|
|
142
|
+
* Hidden-unicode hardening (additive, 2026-07-12): invisible characters are
|
|
143
|
+
* flagged as their own violation class AND the pattern scan runs on the
|
|
144
|
+
* stripped text, so a zero-width-broken key/email is still caught by its
|
|
145
|
+
* own category. Inputs without invisible characters behave byte-identically
|
|
146
|
+
* to the pre-hardening scan.
|
|
129
147
|
*/
|
|
130
148
|
export function redaction_scan(
|
|
131
149
|
text: string,
|
|
@@ -136,16 +154,36 @@ export function redaction_scan(
|
|
|
136
154
|
sql_identifiers?: readonly string[];
|
|
137
155
|
} = {},
|
|
138
156
|
): RedactionViolation[] {
|
|
139
|
-
const
|
|
157
|
+
const violations: RedactionViolation[] = [];
|
|
158
|
+
_HIDDEN_UNICODE_RE.lastIndex = 0;
|
|
159
|
+
const hidden = text.match(_HIDDEN_UNICODE_RE);
|
|
160
|
+
let scanText = text;
|
|
161
|
+
if (hidden !== null && hidden.length > 0) {
|
|
162
|
+
const counts = new Map<string, number>();
|
|
163
|
+
for (const ch of hidden) {
|
|
164
|
+
const cp = `U+${(ch.codePointAt(0) as number).toString(16).toUpperCase().padStart(4, '0')}`;
|
|
165
|
+
counts.set(cp, (counts.get(cp) ?? 0) + 1);
|
|
166
|
+
}
|
|
167
|
+
const summary = [...counts.entries()]
|
|
168
|
+
.map(([cp, n]) => (n > 1 ? `${cp}×${n}` : cp))
|
|
169
|
+
.join(' ');
|
|
170
|
+
violations.push({
|
|
171
|
+
category: 'hidden_unicode',
|
|
172
|
+
snippet: _pySliceHead(summary, 40),
|
|
173
|
+
note: 'invisible characters — possible identifier smuggling; pattern scan ran on the stripped text',
|
|
174
|
+
});
|
|
175
|
+
scanText = text.replace(_HIDDEN_UNICODE_RE, '');
|
|
176
|
+
}
|
|
177
|
+
const floor = redact_low_impact_entry(scanText, {
|
|
140
178
|
repoRoot: options.repo_root ?? null,
|
|
141
179
|
privateDomains: options.private_domains ?? [],
|
|
142
180
|
customerNames: options.customer_names ?? [],
|
|
143
181
|
sqlIdentifiers: options.sql_identifiers ?? [],
|
|
144
182
|
});
|
|
145
|
-
|
|
183
|
+
violations.push(...floor.violations);
|
|
146
184
|
for (const rx of _load_denylist_patterns()) {
|
|
147
185
|
rx.lastIndex = 0;
|
|
148
|
-
const m = rx.exec(
|
|
186
|
+
const m = rx.exec(scanText);
|
|
149
187
|
if (m) {
|
|
150
188
|
violations.push({
|
|
151
189
|
category: 'external_source',
|
|
@@ -90,11 +90,20 @@ export class LexicalIndex {
|
|
|
90
90
|
return Math.max(0, Math.log(1 + (this.n - df + 0.5) / (df + 0.5)));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* BM25 score of one document against the tokenised query terms, weighted by
|
|
95
|
+
* **term coverage**: the raw BM25 sum is multiplied by `coverage²`, where
|
|
96
|
+
* `coverage = distinct-query-terms-matched / distinct-query-terms`. This
|
|
97
|
+
* stops a single generic, high-TF term match from outranking a document that
|
|
98
|
+
* matches several rarer query terms once each (Phase 1 refinement). A
|
|
99
|
+
* single-term query has `coverage == 1`, so its score is unchanged — the
|
|
100
|
+
* weighting only re-orders genuinely multi-term queries.
|
|
101
|
+
*/
|
|
94
102
|
score(queryTerms: readonly string[], docId: string): number {
|
|
95
103
|
const doc = this.docs.get(docId);
|
|
96
104
|
if (doc === undefined || doc.len === 0) return 0;
|
|
97
105
|
let s = 0;
|
|
106
|
+
const matched = new Set<string>();
|
|
98
107
|
for (const term of queryTerms) {
|
|
99
108
|
const tf = doc.tf.get(term);
|
|
100
109
|
if (tf === undefined) continue;
|
|
@@ -102,8 +111,12 @@ export class LexicalIndex {
|
|
|
102
111
|
if (idf === 0) continue;
|
|
103
112
|
const denom = tf + BM25_K1 * (1 - BM25_B + (BM25_B * doc.len) / (this.avgdl || 1));
|
|
104
113
|
s += (idf * (tf * (BM25_K1 + 1))) / denom;
|
|
114
|
+
matched.add(term);
|
|
105
115
|
}
|
|
106
|
-
return
|
|
116
|
+
if (s === 0) return 0;
|
|
117
|
+
const distinctQuery = new Set(queryTerms).size;
|
|
118
|
+
const coverage = distinctQuery > 0 ? matched.size / distinctQuery : 1;
|
|
119
|
+
return s * coverage * coverage;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
/**
|
|
@@ -24,6 +24,17 @@ export type TierSource = 'static' | 'inferred' | 'inherit';
|
|
|
24
24
|
export type Band = 'low' | 'medium' | 'high';
|
|
25
25
|
export type LinePhase = 'refine' | 'memory' | 'analyze' | 'plan' | 'implement' | 'test' | 'verify' | 'report';
|
|
26
26
|
export type LineOutcome = 'success' | 'blocked' | 'skipped' | 'error';
|
|
27
|
+
/** The orchestration form the form-gate selected (road-to-opt-subagent-harvest P2). */
|
|
28
|
+
export type DispatchModeId =
|
|
29
|
+
| 'do-and-judge'
|
|
30
|
+
| 'do-and-judge-two-stage'
|
|
31
|
+
| 'do-in-steps'
|
|
32
|
+
| 'do-in-parallel'
|
|
33
|
+
| 'do-competitively'
|
|
34
|
+
| 'judge-with-debate'
|
|
35
|
+
| 'do-in-worktrees'
|
|
36
|
+
| 'do-with-live-app-judge'
|
|
37
|
+
| 'none';
|
|
27
38
|
|
|
28
39
|
/** Inputs the agent supplies about one auto-dispatch. */
|
|
29
40
|
export interface RecordInput {
|
|
@@ -37,6 +48,8 @@ export interface RecordInput {
|
|
|
37
48
|
tier_chosen?: TierChosen | null | undefined;
|
|
38
49
|
tier_source?: TierSource | null | undefined;
|
|
39
50
|
task_class?: string | null | undefined;
|
|
51
|
+
/** Form-gate outcome: which of the seven modes (or 'none') the gate selected. */
|
|
52
|
+
dispatch_mode?: DispatchModeId | null | undefined;
|
|
40
53
|
task_size_estimate?: number | undefined;
|
|
41
54
|
wall_clock_ms?: number | undefined;
|
|
42
55
|
/** Absolute measured tokens the dispatched slice consumed (feeds the modeled cost-%). */
|
|
@@ -153,6 +166,7 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
|
|
|
153
166
|
outcome: dOutcome,
|
|
154
167
|
verify_mode: vMode,
|
|
155
168
|
task_class: input.task_class ?? null,
|
|
169
|
+
dispatch_mode: input.dispatch_mode ?? null,
|
|
156
170
|
tier_chosen: input.tier_chosen ?? null,
|
|
157
171
|
tier_source: input.tier_source ?? null,
|
|
158
172
|
dispatch_tokens: input.dispatch_tokens ?? null,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared trigger-eval precision/recall floors — single source of truth.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `src/cli/commands/recordTriggerEval.ts` so the weekly
|
|
5
|
+
* canary rotation (`src/scripts/trigger_eval_rotation.ts`, ADR-118 §4) and
|
|
6
|
+
* the `eval:record` provenance recorder enforce the SAME per-domain floors.
|
|
7
|
+
* Values are unchanged from the recorder's originals (Phase D council
|
|
8
|
+
* verdict — tuned to the trigger pattern each skill shows, not a single
|
|
9
|
+
* global pair): a reference task wants near-perfect precision; a judgment
|
|
10
|
+
* task tolerates a looser one.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface TriggerEvalFloor {
|
|
14
|
+
minRecall: number;
|
|
15
|
+
minPrecision: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Universal fallback floor when a skill carries no domain-specific entry. */
|
|
19
|
+
export const DEFAULT_FLOOR: TriggerEvalFloor = { minRecall: 1.0, minPrecision: 0.8 };
|
|
20
|
+
|
|
21
|
+
/** Domain-specific floors; skills absent here fall back to {@link DEFAULT_FLOOR}. */
|
|
22
|
+
export const DOMAIN_FLOORS: Readonly<Record<string, TriggerEvalFloor>> = {
|
|
23
|
+
'image-generation': { minRecall: 1.0, minPrecision: 0.85 },
|
|
24
|
+
iconography: { minRecall: 1.0, minPrecision: 0.9 },
|
|
25
|
+
'brand-strategy': { minRecall: 0.9, minPrecision: 0.7 },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Resolve the effective floor for a skill (domain entry, else default). */
|
|
29
|
+
export function floor_for(skill: string | null | undefined): TriggerEvalFloor {
|
|
30
|
+
return (skill !== null && skill !== undefined && DOMAIN_FLOORS[skill]) || DEFAULT_FLOOR;
|
|
31
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Chairman selection (road-to-opt-council-deliberation Phase 2).
|
|
2
|
+
//
|
|
3
|
+
// The council's Iron Law argues the host, having framed the artefact, cannot
|
|
4
|
+
// independently judge it — so synthesis by a *non-deliberating* member removes
|
|
5
|
+
// that bias. This module is the pure SELECTION decision (who chairs, and the
|
|
6
|
+
// visible verdict annotation); the billable dispatch of the chairman's synthesis
|
|
7
|
+
// call lives in the CLI run path, gated behind the same config.
|
|
8
|
+
//
|
|
9
|
+
// `auto` policy — decided by the contested-design council pass (anthropic/
|
|
10
|
+
// claude-sonnet-4-5 + openai/gpt-4o, 2-round debate, 2026-07-12):
|
|
11
|
+
// **provider-family difference is primary** — independence from the
|
|
12
|
+
// deliberators' priors is the binding constraint for a judge, and provider
|
|
13
|
+
// diversity is the one structural guarantee available without an LLM call.
|
|
14
|
+
// The **tie-break is the capability signal**: an explicit optional
|
|
15
|
+
// `members.<name>.tier` when present (higher = stronger), else deterministic
|
|
16
|
+
// config order. In this engine each member IS one provider (the
|
|
17
|
+
// one-advisor-per-provider invariant), so every enabled member that did not
|
|
18
|
+
// deliberate is provider-different from every deliberator by construction.
|
|
19
|
+
|
|
20
|
+
export interface ChairmanCandidate {
|
|
21
|
+
/** Member name (== provider key in this engine). */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Optional capability rank from `members.<name>.tier`; higher = stronger. */
|
|
24
|
+
tier: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ChairmanSelection {
|
|
28
|
+
/** The chosen chairman member name, or `null` → host synthesis (today's path). */
|
|
29
|
+
member: string | null;
|
|
30
|
+
/** Visible annotation for the synthesis verdict — never a silent substitution. */
|
|
31
|
+
annotation: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pure chairman selection.
|
|
36
|
+
*
|
|
37
|
+
* @param mode one of `host` | `member` | `auto` (validated at config load)
|
|
38
|
+
* @param member the configured member name when `mode === 'member'`
|
|
39
|
+
* @param deliberated member names that produced a real (non-error, non-empty) response this session
|
|
40
|
+
* @param candidates enabled members in **config order** (the engine's only trusted ordering)
|
|
41
|
+
*/
|
|
42
|
+
export function select_chairman(
|
|
43
|
+
mode: string,
|
|
44
|
+
member: string | null,
|
|
45
|
+
deliberated: ReadonlySet<string>,
|
|
46
|
+
candidates: readonly ChairmanCandidate[],
|
|
47
|
+
): ChairmanSelection {
|
|
48
|
+
if (mode === 'host') {
|
|
49
|
+
return { member: null, annotation: 'Chairman: host' };
|
|
50
|
+
}
|
|
51
|
+
const enabledNames = new Set(candidates.map((c) => c.name));
|
|
52
|
+
if (mode === 'member') {
|
|
53
|
+
if (member === null || !enabledNames.has(member)) {
|
|
54
|
+
return {
|
|
55
|
+
member: null,
|
|
56
|
+
annotation: `Chairman: host (member ${_q(member)} unavailable — host fallback)`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (deliberated.has(member)) {
|
|
60
|
+
// A member that argued in the debate cannot independently judge it.
|
|
61
|
+
return {
|
|
62
|
+
member: null,
|
|
63
|
+
annotation: `Chairman: host (member ${_q(member)} deliberated — cannot self-judge, host fallback)`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { member, annotation: `Chairman: ${member}` };
|
|
67
|
+
}
|
|
68
|
+
// mode === 'auto' — provider-family difference primary (non-deliberating ⇒
|
|
69
|
+
// provider-different by construction here); tier tie-break; config-order final.
|
|
70
|
+
const pool = candidates.filter((c) => !deliberated.has(c.name));
|
|
71
|
+
if (pool.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
member: null,
|
|
74
|
+
annotation: 'Chairman: host (no non-panel member available)',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
let best = pool[0] as ChairmanCandidate;
|
|
78
|
+
for (const c of pool.slice(1)) {
|
|
79
|
+
const bestTier = best.tier ?? Number.NEGATIVE_INFINITY;
|
|
80
|
+
const cTier = c.tier ?? Number.NEGATIVE_INFINITY;
|
|
81
|
+
if (cTier > bestTier) {
|
|
82
|
+
best = c; // strictly higher tier wins; equal/lower keeps config order
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { member: best.name, annotation: `Chairman: ${best.name} (auto)` };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _q(v: string | null): string {
|
|
89
|
+
return v === null ? 'null' : `'${v}'`;
|
|
90
|
+
}
|
|
@@ -71,6 +71,21 @@ export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
|
|
|
71
71
|
export const DEFAULT_XAI_MODEL = 'grok-4';
|
|
72
72
|
export const DEFAULT_PERPLEXITY_MODEL = 'sonar-pro';
|
|
73
73
|
|
|
74
|
+
// Vendor CLI (subscription transport) default models. Omitting `model` in a
|
|
75
|
+
// CLI client's options pins THESE values — a pin, not "latest"; the vendor
|
|
76
|
+
// CLI's own default may be newer. Bump deliberately; never assume drift.
|
|
77
|
+
//
|
|
78
|
+
// These are SEPARATE from the API-transport `DEFAULT_*_MODEL` constants above
|
|
79
|
+
// because API and CLI defaults can legitimately diverge — openai proves it
|
|
80
|
+
// (API `gpt-4o` vs CLI `gpt-5`). The anthropic/gemini CLI values currently
|
|
81
|
+
// coincide with their API defaults, but stay pinned independently so a future
|
|
82
|
+
// divergence is a one-line change, not a hunt through inline literals.
|
|
83
|
+
// (`xai` / `perplexity` CLI reuse their API constants: their CLIs are
|
|
84
|
+
// community wrappers around the same paid API, so the values do not diverge.)
|
|
85
|
+
export const DEFAULT_OPENAI_CLI_MODEL = 'gpt-5';
|
|
86
|
+
export const DEFAULT_ANTHROPIC_CLI_MODEL = 'claude-sonnet-4-5';
|
|
87
|
+
export const DEFAULT_GEMINI_CLI_MODEL = 'gemini-2.5-pro';
|
|
88
|
+
|
|
74
89
|
// OpenAI-API-compatible endpoints. xAI and Perplexity both expose the
|
|
75
90
|
// `/v1/chat/completions` shape, so their clients reuse the `openai` SDK with a
|
|
76
91
|
// custom `base_url`. Gemini has its own SDK (`google-genai`).
|
|
@@ -1171,7 +1186,7 @@ export class AnthropicCliClient extends CliClient {
|
|
|
1171
1186
|
|
|
1172
1187
|
constructor(opts: CliClientOptions = {}) {
|
|
1173
1188
|
super({
|
|
1174
|
-
model: opts.model ??
|
|
1189
|
+
model: opts.model ?? DEFAULT_ANTHROPIC_CLI_MODEL,
|
|
1175
1190
|
binary: opts.binary,
|
|
1176
1191
|
timeout_seconds: opts.timeout_seconds,
|
|
1177
1192
|
max_calls_per_day: opts.max_calls_per_day,
|
|
@@ -1271,7 +1286,7 @@ export class OpenAICliClient extends CliClient {
|
|
|
1271
1286
|
|
|
1272
1287
|
constructor(opts: CliClientOptions = {}) {
|
|
1273
1288
|
super({
|
|
1274
|
-
model: opts.model ??
|
|
1289
|
+
model: opts.model ?? DEFAULT_OPENAI_CLI_MODEL,
|
|
1275
1290
|
binary: opts.binary,
|
|
1276
1291
|
timeout_seconds: opts.timeout_seconds,
|
|
1277
1292
|
max_calls_per_day: opts.max_calls_per_day,
|
|
@@ -1394,7 +1409,7 @@ export class GeminiCliClient extends CliClient {
|
|
|
1394
1409
|
|
|
1395
1410
|
constructor(opts: CliClientOptions = {}) {
|
|
1396
1411
|
super({
|
|
1397
|
-
model: opts.model ??
|
|
1412
|
+
model: opts.model ?? DEFAULT_GEMINI_CLI_MODEL,
|
|
1398
1413
|
binary: opts.binary,
|
|
1399
1414
|
timeout_seconds: opts.timeout_seconds,
|
|
1400
1415
|
max_calls_per_day: opts.max_calls_per_day,
|