@event4u/agent-config 9.28.0 → 9.29.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 +6 -6
- package/CHANGELOG.md +66 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +13 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +18 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
- package/dist/agent-src/rules/delegation-policy.md +11 -7
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/token-budget-discipline.md +1 -1
- package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- package/dist/agent-src/skills/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +4 -4
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
- package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
- package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
- package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-NZ5N2XQL.js} +3 -3
- package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-RCIA67XN.js} +111 -90
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +6 -43
- package/dist/cli-delegate/cmd_doctor.js +9 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_settings_get.js +1 -7
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +69 -20
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -2
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +39 -2
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +12 -4
- package/dist/hooks/dispatch.js +373 -35
- package/dist/install/install.mjs +42 -17
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +651 -262
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +54 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
- package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +39 -9
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +4 -4
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_lib/agent_settings.ts +58 -0
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/host_capability.ts +63 -6
- package/src/scripts/_lib/judgment_ladder.ts +382 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +37 -0
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/self_repair.ts +160 -13
- package/src/scripts/_lib/self_repair_store.ts +28 -0
- package/src/scripts/_lib/subagent_capsule.ts +182 -0
- package/src/scripts/_lib/subagent_response.ts +19 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +76 -54
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/council_cli.ts +401 -25
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +69 -35
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/orchestration_record.ts +8 -0
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/routing_doctor.ts +21 -36
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +287 -57
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
|
@@ -38,6 +38,18 @@ export type DefectClass =
|
|
|
38
38
|
| 'council-availability-claim'
|
|
39
39
|
| 'language-mirror';
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Runtime mirror of `DefectClass`, in declaration order. The upstream issue
|
|
43
|
+
* form's dropdown (`.github/ISSUE_TEMPLATE/self_repair_report.yml`) is pinned
|
|
44
|
+
* against this list by test, so adding a detector class without extending the
|
|
45
|
+
* form is a red test, not silent drift.
|
|
46
|
+
*/
|
|
47
|
+
export const DEFECT_CLASSES: readonly DefectClass[] = [
|
|
48
|
+
'user-reported',
|
|
49
|
+
'council-availability-claim',
|
|
50
|
+
'language-mirror',
|
|
51
|
+
];
|
|
52
|
+
|
|
41
53
|
/** How far a record may travel outward. */
|
|
42
54
|
export type EgressRoute = 'pull-request' | 'issue' | 'local-only';
|
|
43
55
|
|
|
@@ -56,6 +68,12 @@ export interface DefectRecord extends DefectFinding {
|
|
|
56
68
|
last_seen: string;
|
|
57
69
|
occurrences: number;
|
|
58
70
|
status: 'open' | 'released';
|
|
71
|
+
/**
|
|
72
|
+
* Failed egress attempts from the most recent `release` call, sanitized at
|
|
73
|
+
* write time. Local-only bookkeeping: `renderReport` never includes them,
|
|
74
|
+
* so they cannot widen what leaves the machine.
|
|
75
|
+
*/
|
|
76
|
+
release_errors?: string[];
|
|
59
77
|
}
|
|
60
78
|
|
|
61
79
|
/** One finished turn, as much of it as a hook can observe. */
|
|
@@ -115,7 +133,10 @@ export function egressBlockedReason(record: DefectRecord, repoRoot?: string | nu
|
|
|
115
133
|
// An agent-directed complaint, not a complaint about the code. Deliberately
|
|
116
134
|
// narrow: a spurious record becomes a spurious PR, so precision beats recall.
|
|
117
135
|
const COMPLAINT_PATTERNS: readonly RegExp[] = [
|
|
118
|
-
|
|
136
|
+
// No \b before the alternation: JS \b is ASCII-only, so a leading umlaut
|
|
137
|
+
// ("übersehen") never sits on a word boundary — the corpus gate's
|
|
138
|
+
// near-miss class caught that as a dead alternation branch.
|
|
139
|
+
/\bdu hast\b[^.!?]{0,60}(nicht|falsch|vergessen|ignoriert|übersehen)\b/i,
|
|
119
140
|
/\bdu (ignorierst|übersiehst)\b/i,
|
|
120
141
|
/\b(das|es) (war|ist) (aber )?falsch\b/i,
|
|
121
142
|
/\bhat nicht (richtig|korrekt) (gearbeitet|funktioniert)\b/i,
|
|
@@ -125,11 +146,53 @@ const COMPLAINT_PATTERNS: readonly RegExp[] = [
|
|
|
125
146
|
/\byou (worked|did (it|that)) (wrong|incorrectly)\b/i,
|
|
126
147
|
];
|
|
127
148
|
|
|
149
|
+
// Phrasings that MATCH a complaint pattern but are not complaints — a curious
|
|
150
|
+
// question or a reassurance. Scoped to the matched span (± a small margin),
|
|
151
|
+
// never the whole prompt: a pleasantry in one clause must not mute a genuine
|
|
152
|
+
// complaint in another (R2 finding #2). These feed the must-not-fire class of
|
|
153
|
+
// the detector corpus gate.
|
|
154
|
+
const COMPLAINT_EXONERATIONS: readonly RegExp[] = [
|
|
155
|
+
/\bdu hast nicht zufällig\b/i,
|
|
156
|
+
/\bhast du nicht zufällig\b/i,
|
|
157
|
+
/\byou didn'?t (need|have) to\b/i,
|
|
158
|
+
/\bit'?s fine\b/i,
|
|
159
|
+
/\b(kein problem|passt schon|alles gut)\b/i,
|
|
160
|
+
// Explicit absolution INSIDE the complaint span, which is why it has to be
|
|
161
|
+
// its own entry rather than relying on a neighbouring pleasantry: in
|
|
162
|
+
// "das ist fine, du hast nichts falsch gemacht" the matched span is
|
|
163
|
+
// `du hast nichts falsch`, and the `it's fine` above sits outside the
|
|
164
|
+
// ± EXONERATION_MARGIN window — correctly, since that margin exists to stop
|
|
165
|
+
// a pleasantry in one clause muting a complaint in another. `nichts falsch`
|
|
166
|
+
// is not a neighbouring pleasantry: it is the negation of the complaint
|
|
167
|
+
// itself. Caught by the detector-corpus gate, which flagged this exact
|
|
168
|
+
// string as a must-not-fire fixture that fired (PR #1231).
|
|
169
|
+
/\bnichts falsch\b/i,
|
|
170
|
+
/\bkein fehler\b/i,
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
/** How far (chars) an exoneration may sit from the complaint span it excuses. */
|
|
174
|
+
const EXONERATION_MARGIN = 12;
|
|
175
|
+
|
|
176
|
+
function spanExonerated(prompt: string, start: number, end: number): boolean {
|
|
177
|
+
for (const ex of COMPLAINT_EXONERATIONS) {
|
|
178
|
+
const g = new RegExp(ex.source, ex.flags.includes('g') ? ex.flags : `${ex.flags}g`);
|
|
179
|
+
let m: RegExpExecArray | null;
|
|
180
|
+
while ((m = g.exec(prompt)) !== null) {
|
|
181
|
+
const s = m.index;
|
|
182
|
+
const e = m.index + m[0].length;
|
|
183
|
+
if (e >= start - EXONERATION_MARGIN && s <= end + EXONERATION_MARGIN) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
128
191
|
/** Intake path 1 — the user says the agent worked wrongly. */
|
|
129
192
|
export function detectUserReport(prompt: string): DefectFinding | null {
|
|
130
193
|
for (const re of COMPLAINT_PATTERNS) {
|
|
131
194
|
const m = re.exec(prompt);
|
|
132
|
-
if (m !== null) {
|
|
195
|
+
if (m !== null && !spanExonerated(prompt, m.index, m.index + m[0].length)) {
|
|
133
196
|
return {
|
|
134
197
|
defect_class: 'user-reported',
|
|
135
198
|
source: 'user-reported',
|
|
@@ -222,11 +285,38 @@ export function detectLanguageMirror(turn: TurnSnapshot): DefectFinding | null {
|
|
|
222
285
|
};
|
|
223
286
|
}
|
|
224
287
|
|
|
288
|
+
/** One registered detector — the unit the corpus gate enumerates. */
|
|
289
|
+
export interface RegisteredDetector {
|
|
290
|
+
defect_class: DefectClass;
|
|
291
|
+
source: DefectSource;
|
|
292
|
+
detect: (turn: TurnSnapshot) => DefectFinding | null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The detector registry — the single list both `runDetectors` and the corpus
|
|
297
|
+
* gate (`tests/scripts/self_repair_detector_corpus.test.ts`) iterate. A
|
|
298
|
+
* detector added here without its three fixture classes (fire /
|
|
299
|
+
* near-miss-fire / must-not-fire) is a red test, not a silently untested
|
|
300
|
+
* code path.
|
|
301
|
+
*/
|
|
302
|
+
export const DETECTOR_REGISTRY: readonly RegisteredDetector[] = [
|
|
303
|
+
{
|
|
304
|
+
defect_class: 'user-reported',
|
|
305
|
+
source: 'user-reported',
|
|
306
|
+
detect: (turn) => detectUserReport(turn.prompt),
|
|
307
|
+
},
|
|
308
|
+
{ defect_class: 'council-availability-claim', source: 'self-detected', detect: detectCouncilClaim },
|
|
309
|
+
{ defect_class: 'language-mirror', source: 'self-detected', detect: detectLanguageMirror },
|
|
310
|
+
];
|
|
311
|
+
|
|
225
312
|
/** Run every self-detected detector over a finished turn. */
|
|
226
313
|
export function runDetectors(turn: TurnSnapshot): DefectFinding[] {
|
|
227
314
|
const out: DefectFinding[] = [];
|
|
228
|
-
for (const d of
|
|
229
|
-
|
|
315
|
+
for (const d of DETECTOR_REGISTRY) {
|
|
316
|
+
if (d.source !== 'self-detected') {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const f = d.detect(turn);
|
|
230
320
|
if (f !== null) {
|
|
231
321
|
out.push(f);
|
|
232
322
|
}
|
|
@@ -286,23 +376,43 @@ export interface EgressCapability {
|
|
|
286
376
|
hasAgentConfigCheckout: boolean;
|
|
287
377
|
/** `gh` is installed AND authenticated. */
|
|
288
378
|
ghAuthenticated: boolean;
|
|
289
|
-
/**
|
|
290
|
-
|
|
379
|
+
/**
|
|
380
|
+
* The authenticated user has push permission on the UPSTREAM repo — a
|
|
381
|
+
* pre-flight probe of actual rights (`gh api repos/<repo>` →
|
|
382
|
+
* `.permissions.push`), never an inference from `git remote` existing.
|
|
383
|
+
* A consumer who cloned the public repo has a remote and no write access;
|
|
384
|
+
* the old heuristic classified exactly that machine as push-capable.
|
|
385
|
+
*/
|
|
386
|
+
canPushUpstream: boolean;
|
|
387
|
+
/** The upstream repo allows forking and the user is authenticated. */
|
|
388
|
+
canFork: boolean;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Where a pull-request route pushes its branch. */
|
|
392
|
+
export type PushVia = 'upstream' | 'fork';
|
|
393
|
+
|
|
394
|
+
export interface EgressChoice {
|
|
395
|
+
route: EgressRoute;
|
|
396
|
+
/** Set only when `route === 'pull-request'`. */
|
|
397
|
+
pushVia: PushVia | null;
|
|
291
398
|
}
|
|
292
399
|
|
|
293
400
|
/**
|
|
294
401
|
* Route selection, exactly as specified: a pull request when the fix can be
|
|
295
|
-
* authored and pushed,
|
|
296
|
-
* nothing can leave the machine.
|
|
402
|
+
* authored and pushed (directly, or via fork → cross-repo PR), an issue when
|
|
403
|
+
* it cannot, and a local record when nothing can leave the machine.
|
|
297
404
|
*/
|
|
298
|
-
export function
|
|
299
|
-
if (cap.hasAgentConfigCheckout && cap.ghAuthenticated && cap.
|
|
300
|
-
return 'pull-request';
|
|
405
|
+
export function chooseEgress(cap: EgressCapability): EgressChoice {
|
|
406
|
+
if (cap.hasAgentConfigCheckout && cap.ghAuthenticated && cap.canPushUpstream) {
|
|
407
|
+
return { route: 'pull-request', pushVia: 'upstream' };
|
|
408
|
+
}
|
|
409
|
+
if (cap.hasAgentConfigCheckout && cap.ghAuthenticated && cap.canFork) {
|
|
410
|
+
return { route: 'pull-request', pushVia: 'fork' };
|
|
301
411
|
}
|
|
302
412
|
if (cap.ghAuthenticated) {
|
|
303
|
-
return 'issue';
|
|
413
|
+
return { route: 'issue', pushVia: null };
|
|
304
414
|
}
|
|
305
|
-
return 'local-only';
|
|
415
|
+
return { route: 'local-only', pushVia: null };
|
|
306
416
|
}
|
|
307
417
|
|
|
308
418
|
/** The PR / issue body. Deterministic — same record, same bytes. */
|
|
@@ -340,3 +450,40 @@ export function renderReport(record: DefectRecord, route: EgressRoute): string {
|
|
|
340
450
|
'construction — the record type has no field that can carry them.',
|
|
341
451
|
].join('\n');
|
|
342
452
|
}
|
|
453
|
+
|
|
454
|
+
/** The structured fields the upstream intake form collects. */
|
|
455
|
+
export interface ParsedReport {
|
|
456
|
+
defect_class: string;
|
|
457
|
+
fingerprint: string;
|
|
458
|
+
occurrences: number;
|
|
459
|
+
evidence: string;
|
|
460
|
+
suggested_surface: string;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Inverse of `renderReport` for the structured fields — the round-trip that
|
|
465
|
+
* pins the rendered body to the upstream issue form: every field the form
|
|
466
|
+
* collects is recoverable from a rendered report, so a `release` body and a
|
|
467
|
+
* manually-filed form carry the same clustering keys (fingerprint above all).
|
|
468
|
+
*/
|
|
469
|
+
export function parseReport(body: string): ParsedReport | null {
|
|
470
|
+
const cls = /^## Self-repair report — `([^`]+)`$/m.exec(body)?.[1];
|
|
471
|
+
const fp = /^- \*\*Fingerprint:\*\* `([^`]+)`$/m.exec(body)?.[1];
|
|
472
|
+
const seen = /^- \*\*Seen:\*\* (.+)$/m.exec(body)?.[1];
|
|
473
|
+
const evidence = /^### Evidence\n\n> (.*)$/m.exec(body)?.[1];
|
|
474
|
+
const surface = /### What the fix should address\n\n([\s\S]*?)\n\n---/.exec(body)?.[1];
|
|
475
|
+
if (!cls || !fp || !seen || evidence === undefined || !surface) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
const occurrences = seen.startsWith('once') ? 1 : Number(/^(\d+)×/.exec(seen)?.[1] ?? Number.NaN);
|
|
479
|
+
if (!Number.isFinite(occurrences)) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
defect_class: cls,
|
|
484
|
+
fingerprint: fp,
|
|
485
|
+
occurrences,
|
|
486
|
+
evidence,
|
|
487
|
+
suggested_surface: surface.trim(),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
@@ -94,7 +94,35 @@ export function markReleased(root: string, fp: string, now: string): DefectRecor
|
|
|
94
94
|
if (rec === null) {
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
|
+
// A successful release supersedes any earlier failed attempts.
|
|
97
98
|
const next: DefectRecord = { ...rec, status: 'released', last_seen: now };
|
|
99
|
+
delete next.release_errors;
|
|
100
|
+
writeRecord(root, next);
|
|
101
|
+
return next;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A release attempt exhausted the whole egress ladder: keep the record open
|
|
106
|
+
* and attach every failed step so the next `self-repair:status` shows what
|
|
107
|
+
* went wrong. Errors arrive pre-sanitized (the CLI runs them through
|
|
108
|
+
* `sanitizeEvidence` — command output can carry local paths).
|
|
109
|
+
*/
|
|
110
|
+
export function attachReleaseErrors(
|
|
111
|
+
root: string,
|
|
112
|
+
fp: string,
|
|
113
|
+
errors: string[],
|
|
114
|
+
now: string,
|
|
115
|
+
): DefectRecord | null {
|
|
116
|
+
const rec = readRecord(root, fp);
|
|
117
|
+
if (rec === null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const next: DefectRecord = {
|
|
121
|
+
...rec,
|
|
122
|
+
status: 'open',
|
|
123
|
+
last_seen: now,
|
|
124
|
+
release_errors: errors,
|
|
125
|
+
};
|
|
98
126
|
writeRecord(root, next);
|
|
99
127
|
return next;
|
|
100
128
|
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-generation CHECKPOINT capsule (road-to-worker-generation-recycling, Phase 0).
|
|
3
|
+
*
|
|
4
|
+
* Pure, no-I/O. The structured state a worker emits BELOW its stop-loss
|
|
5
|
+
* watermark so a successor generation can continue the SAME task from the
|
|
6
|
+
* capsule alone — never from the transcript, never from an orchestrator-written
|
|
7
|
+
* re-brief. Today a worker that reaches `max_tokens_per_worker` is killed
|
|
8
|
+
* (`worker_budget.ts`) and its partial result returns as a `BLOCKED` envelope;
|
|
9
|
+
* there is no handoff. This is the handoff shape.
|
|
10
|
+
*
|
|
11
|
+
* TRANSCRIPT-EXCLUSION BY CONSTRUCTION: every field is a ref token, a single
|
|
12
|
+
* short line, or a count — there is NO field capable of holding a transcript,
|
|
13
|
+
* a file body, or prose at length, and the per-array caps mean a capsule cannot
|
|
14
|
+
* become one by accumulation either. This is not stylistic. A capsule that
|
|
15
|
+
* could smuggle raw context would make the successor's brief unmeasurable,
|
|
16
|
+
* which is the whole question the roadmap's claim asks. Do not add a free-form
|
|
17
|
+
* field. (Same principle as `orchestration_record.ts` for telemetry and
|
|
18
|
+
* `domain-safety-pii` § Surface 2 for logs.)
|
|
19
|
+
*
|
|
20
|
+
* Contract: `src/agent-src/contexts/execution/subagent-response-contract.md`
|
|
21
|
+
* Wire format: `src/skills/subagent-orchestration/schemas/subagent-status.json`
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Epistemic vocabulary for `assumptions[].epistemic_state` — pinned ONCE, here,
|
|
26
|
+
* to the three Evidence-Report buckets already in use for docs and discovery
|
|
27
|
+
* (`evidence-discipline.md` § Evidence Report). A capsule that graded its
|
|
28
|
+
* assumptions on a private scale would fork the vocabulary the rest of the
|
|
29
|
+
* suite reasons in, so it does not get one.
|
|
30
|
+
*
|
|
31
|
+
* - `verified` — confirmed against a real source during THIS worker generation.
|
|
32
|
+
* - `assumed` — taken as a premise, not confirmed; explicitly a hypothesis.
|
|
33
|
+
* - `gap` — known missing evidence the task needs.
|
|
34
|
+
*/
|
|
35
|
+
export const EPISTEMIC_STATES = ['verified', 'assumed', 'gap'] as const;
|
|
36
|
+
export type EpistemicState = (typeof EPISTEMIC_STATES)[number];
|
|
37
|
+
|
|
38
|
+
const EPISTEMIC: ReadonlySet<string> = new Set<string>(EPISTEMIC_STATES);
|
|
39
|
+
|
|
40
|
+
/** Max chars for a ref token (`file:line`, id, path). Mirrors the spawn contract. */
|
|
41
|
+
export const MAX_REF_CHARS = 200;
|
|
42
|
+
/** Max chars for one capsule sentence. A paragraph does not fit; that is the point. */
|
|
43
|
+
export const MAX_LINE_CHARS = 240;
|
|
44
|
+
/** Max entries per capsule array. A transcript cannot be reached by accumulation. */
|
|
45
|
+
export const MAX_ENTRIES = 40;
|
|
46
|
+
|
|
47
|
+
/** One stated premise the successor generation inherits instead of re-deriving. */
|
|
48
|
+
export interface CapsuleAssumption {
|
|
49
|
+
/** The premise, one line. */
|
|
50
|
+
statement: string;
|
|
51
|
+
/** What it rests on — a ref token (`file:line`, id, path, command). */
|
|
52
|
+
basis: string;
|
|
53
|
+
/** How well it is established. See {@link EPISTEMIC_STATES}. */
|
|
54
|
+
epistemic_state: EpistemicState;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The capsule a worker emits at its watermark. Rides as the body of a
|
|
59
|
+
* `CHECKPOINT` envelope (`subagent-status.json`).
|
|
60
|
+
*/
|
|
61
|
+
export interface WorkerCapsule {
|
|
62
|
+
/** One- or two-sentence outcome so far. */
|
|
63
|
+
summary: string;
|
|
64
|
+
/** Generation index in the chain; the first dispatch is 1. */
|
|
65
|
+
generation: number;
|
|
66
|
+
/** What is finished — ref tokens, never bodies. */
|
|
67
|
+
done: string[];
|
|
68
|
+
/** What is left, one short line each. */
|
|
69
|
+
remaining: string[];
|
|
70
|
+
/** Choices already made that the successor must not silently re-open. */
|
|
71
|
+
decisions: string[];
|
|
72
|
+
/** Known risks still open. */
|
|
73
|
+
open_risks: string[];
|
|
74
|
+
/** Files this generation touched — paths / `file:line`, never diffs. */
|
|
75
|
+
touched_files: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Stated premises. Unstated assumptions are the first thing compression
|
|
78
|
+
* drops, which makes them the most likely mechanism behind a degraded
|
|
79
|
+
* capsule — the field gives the successor a target list instead of
|
|
80
|
+
* implicit premises.
|
|
81
|
+
*/
|
|
82
|
+
assumptions: CapsuleAssumption[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A single-line token no longer than `max` chars. Rejects any body-shaped value. */
|
|
86
|
+
function isShortLine(value: unknown, max: number): value is string {
|
|
87
|
+
return typeof value === 'string' && value.length > 0 && value.length <= max && !value.includes('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkList(
|
|
91
|
+
errors: string[],
|
|
92
|
+
field: string,
|
|
93
|
+
value: unknown,
|
|
94
|
+
max: number,
|
|
95
|
+
required: boolean,
|
|
96
|
+
): void {
|
|
97
|
+
if (value === undefined) {
|
|
98
|
+
if (required) errors.push(`${field} is required`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!Array.isArray(value)) {
|
|
102
|
+
errors.push(`${field} must be an array`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (value.length > MAX_ENTRIES) {
|
|
106
|
+
errors.push(`${field} carries ${value.length} entries (max ${MAX_ENTRIES}) — a capsule is a handoff, not a transcript`);
|
|
107
|
+
}
|
|
108
|
+
const bad = value.filter((x) => !isShortLine(x, max));
|
|
109
|
+
if (bad.length > 0) {
|
|
110
|
+
errors.push(
|
|
111
|
+
`${field} carries ${bad.length} entr${bad.length === 1 ? 'y' : 'ies'} that are not single lines of ≤ ${max} chars (refs and short lines only, never bodies)`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validate a capsule body. Returns the list of violations (empty = valid).
|
|
118
|
+
*
|
|
119
|
+
* The orchestrator never briefs a successor from an invalid capsule — an
|
|
120
|
+
* invalid capsule falls back to today's stop-loss behaviour, loudly.
|
|
121
|
+
*/
|
|
122
|
+
export function validateCapsule(input: unknown): string[] {
|
|
123
|
+
const errors: string[] = [];
|
|
124
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
125
|
+
return ['not an object'];
|
|
126
|
+
}
|
|
127
|
+
const c = input as Record<string, unknown>;
|
|
128
|
+
|
|
129
|
+
if (!isShortLine(c['summary'], MAX_LINE_CHARS)) {
|
|
130
|
+
errors.push(`summary must be a single line of 1–${MAX_LINE_CHARS} chars`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const generation = c['generation'];
|
|
134
|
+
if (typeof generation !== 'number' || !Number.isInteger(generation) || generation < 1) {
|
|
135
|
+
errors.push('generation must be an integer ≥ 1 (the first dispatch is generation 1)');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
checkList(errors, 'done', c['done'], MAX_REF_CHARS, true);
|
|
139
|
+
checkList(errors, 'remaining', c['remaining'], MAX_LINE_CHARS, true);
|
|
140
|
+
checkList(errors, 'decisions', c['decisions'], MAX_LINE_CHARS, false);
|
|
141
|
+
checkList(errors, 'open_risks', c['open_risks'], MAX_LINE_CHARS, false);
|
|
142
|
+
checkList(errors, 'touched_files', c['touched_files'], MAX_REF_CHARS, false);
|
|
143
|
+
|
|
144
|
+
const assumptions = c['assumptions'];
|
|
145
|
+
if (assumptions !== undefined) {
|
|
146
|
+
if (!Array.isArray(assumptions)) {
|
|
147
|
+
errors.push('assumptions must be an array');
|
|
148
|
+
} else {
|
|
149
|
+
if (assumptions.length > MAX_ENTRIES) {
|
|
150
|
+
errors.push(`assumptions carries ${assumptions.length} entries (max ${MAX_ENTRIES})`);
|
|
151
|
+
}
|
|
152
|
+
for (const [i, raw] of assumptions.entries()) {
|
|
153
|
+
errors.push(...validateAssumption(raw, `assumptions[${i}]`));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return errors;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Validate one `{statement, basis, epistemic_state}` triple. Exported because
|
|
163
|
+
* the worker RESULT envelope carries the same field (`subagent_response.ts`) —
|
|
164
|
+
* one shape, one validator, so the two cannot drift.
|
|
165
|
+
*/
|
|
166
|
+
export function validateAssumption(input: unknown, label = 'assumption'): string[] {
|
|
167
|
+
const errors: string[] = [];
|
|
168
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
169
|
+
return [`${label} is not an object`];
|
|
170
|
+
}
|
|
171
|
+
const a = input as Record<string, unknown>;
|
|
172
|
+
if (!isShortLine(a['statement'], MAX_LINE_CHARS)) {
|
|
173
|
+
errors.push(`${label}.statement must be a single line of 1–${MAX_LINE_CHARS} chars`);
|
|
174
|
+
}
|
|
175
|
+
if (!isShortLine(a['basis'], MAX_REF_CHARS)) {
|
|
176
|
+
errors.push(`${label}.basis must be a ref token (file:line, id, path, command) of ≤ ${MAX_REF_CHARS} chars`);
|
|
177
|
+
}
|
|
178
|
+
if (typeof a['epistemic_state'] !== 'string' || !EPISTEMIC.has(a['epistemic_state'])) {
|
|
179
|
+
errors.push(`${label}.epistemic_state must be one of ${EPISTEMIC_STATES.join(' | ')}`);
|
|
180
|
+
}
|
|
181
|
+
return errors;
|
|
182
|
+
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* Contract: `src/agent-src/contexts/execution/subagent-response-contract.md`.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { validateAssumption, type CapsuleAssumption } from './subagent_capsule.js';
|
|
13
|
+
|
|
12
14
|
export type Confidence = 'low' | 'medium' | 'high';
|
|
13
15
|
|
|
14
16
|
export interface Finding {
|
|
@@ -25,6 +27,14 @@ export interface SubagentResponse {
|
|
|
25
27
|
risks: string[];
|
|
26
28
|
confidence: Confidence;
|
|
27
29
|
handoff: string;
|
|
30
|
+
/**
|
|
31
|
+
* Premises the worker acted on, `{statement, basis, epistemic_state}` —
|
|
32
|
+
* the same shape and the same validator as the CHECKPOINT capsule
|
|
33
|
+
* (`subagent_capsule.ts`), so the completed-result and handoff surfaces
|
|
34
|
+
* cannot drift apart. Optional: absent means not recorded, which is not
|
|
35
|
+
* the same claim as "no assumptions were made".
|
|
36
|
+
*/
|
|
37
|
+
assumptions?: CapsuleAssumption[];
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
const CONFIDENCE: ReadonlySet<string> = new Set<Confidence>(['low', 'medium', 'high']);
|
|
@@ -55,6 +65,15 @@ export function validateResponse(input: unknown): ValidationResult {
|
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
}
|
|
68
|
+
if (r.assumptions !== undefined) {
|
|
69
|
+
if (!Array.isArray(r.assumptions)) {
|
|
70
|
+
errors.push('assumptions must be an array of {statement, basis, epistemic_state}');
|
|
71
|
+
} else {
|
|
72
|
+
for (const [i, a] of (r.assumptions as unknown[]).entries()) {
|
|
73
|
+
errors.push(...validateAssumption(a, `assumptions[${i}]`));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
58
77
|
return { valid: errors.length === 0, errors };
|
|
59
78
|
}
|
|
60
79
|
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
import * as crypto from 'node:crypto';
|
|
15
15
|
|
|
16
|
+
import { CAPSULE_WATERMARK_FRACTION } from './worker_budget.js';
|
|
17
|
+
|
|
16
18
|
export type RoleMode = 'developer' | 'reviewer' | 'tester' | 'po' | 'incident' | 'planner';
|
|
17
19
|
|
|
18
20
|
const ROLE_MODES: ReadonlySet<string> = new Set<RoleMode>([
|
|
@@ -55,6 +57,12 @@ export interface SpawnBrief {
|
|
|
55
57
|
knowledge_refs: string[];
|
|
56
58
|
/** Per-worker token stop-loss; null = unset (legacy dispatch). */
|
|
57
59
|
max_tokens_per_worker: number | null;
|
|
60
|
+
/**
|
|
61
|
+
* Token count at which the worker emits a CHECKPOINT capsule — DERIVED from
|
|
62
|
+
* `max_tokens_per_worker`, never set independently, so the watermark cannot
|
|
63
|
+
* drift away from the budget it sits under. Null when no budget is set.
|
|
64
|
+
*/
|
|
65
|
+
capsule_watermark: number | null;
|
|
58
66
|
warnings: string[];
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -112,6 +120,10 @@ export function composeSpawnBrief(sel: SpawnSelection): SpawnBrief {
|
|
|
112
120
|
personas,
|
|
113
121
|
knowledge_refs,
|
|
114
122
|
max_tokens_per_worker,
|
|
123
|
+
capsule_watermark:
|
|
124
|
+
max_tokens_per_worker === null
|
|
125
|
+
? null
|
|
126
|
+
: Math.floor(max_tokens_per_worker * CAPSULE_WATERMARK_FRACTION),
|
|
115
127
|
warnings,
|
|
116
128
|
};
|
|
117
129
|
}
|
|
@@ -133,6 +145,7 @@ export function serializeSpawnPayload(brief: SpawnBrief): string {
|
|
|
133
145
|
profile: brief.profile,
|
|
134
146
|
personas: brief.personas,
|
|
135
147
|
max_tokens_per_worker: brief.max_tokens_per_worker,
|
|
148
|
+
capsule_watermark: brief.capsule_watermark,
|
|
136
149
|
});
|
|
137
150
|
const variableTail = JSON.stringify({
|
|
138
151
|
task: brief.task,
|
|
@@ -26,14 +26,19 @@ export const GUARDRAILS = {
|
|
|
26
26
|
user_override_rate: 0.3,
|
|
27
27
|
} as const;
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Always-on orchestration (road-to-always-on-orchestration Phase 1) removed
|
|
31
|
+
* the `enabled`/`auto` settings this state used to carry — the only
|
|
32
|
+
* surviving switch is the audited incident halt.
|
|
33
|
+
*/
|
|
29
34
|
export interface LayerState {
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
/** `emergency.orchestration_halt`. */
|
|
36
|
+
halted: boolean;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
/** The kill-switch: layer is disabled
|
|
39
|
+
/** The kill-switch: layer is disabled only during an active emergency halt. */
|
|
35
40
|
export function isLayerDisabled(state: LayerState): boolean {
|
|
36
|
-
return
|
|
41
|
+
return state.halted;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/** N=3 per-target budget: halt once consecutive failed attempts reach the cap. */
|
|
@@ -43,7 +43,12 @@ export interface PickTierInputs {
|
|
|
43
43
|
adequate_tier: BudgetTier;
|
|
44
44
|
/** Per-tier budget state, complete over TIER_ORDER. */
|
|
45
45
|
budgets: Record<BudgetTier, TierBudgetState>;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Caller-supplied switch. Formerly sourced from the `subagents.budget_routing`
|
|
48
|
+
* settings key, deleted by always-on orchestration (Phase 1) — this
|
|
49
|
+
* function has no production caller today, so the field stays as the
|
|
50
|
+
* DESIGNED input shape; a future wiring supplies its own source.
|
|
51
|
+
*/
|
|
47
52
|
routing_switch: BudgetRoutingSwitch;
|
|
48
53
|
/** Estimated cost of the dispatch (USD) for the availability check. */
|
|
49
54
|
estimated_cost_usd: number;
|
|
@@ -35,6 +35,24 @@ export function budgetForTier(tier: Tier, sessionTier: Exclude<Tier, 'inherit'>
|
|
|
35
35
|
return MAX_TOKENS_PER_WORKER[effective];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Fraction of the stop-loss budget at which a worker emits its CHECKPOINT
|
|
40
|
+
* capsule (road-to-worker-generation-recycling, Phase 1).
|
|
41
|
+
*
|
|
42
|
+
* The headroom IS the mechanism: a worker at 100 % of its budget cannot
|
|
43
|
+
* summarise itself, so the watermark sits below the kill line rather than on
|
|
44
|
+
* it. A start value, not a tuned one — Phase 1 measures it in shadow against a
|
|
45
|
+
* second trigger arm before anything acts on it, and `blocker:
|
|
46
|
+
* capsule-quality-near-budget` exists because a bad watermark and a bad
|
|
47
|
+
* mechanism look identical from one sample.
|
|
48
|
+
*/
|
|
49
|
+
export const CAPSULE_WATERMARK_FRACTION = 0.8;
|
|
50
|
+
|
|
51
|
+
/** Token count at which a worker of this tier emits its capsule. */
|
|
52
|
+
export function watermarkForTier(tier: Tier, sessionTier: Exclude<Tier, 'inherit'> = 'high'): number {
|
|
53
|
+
return Math.floor(budgetForTier(tier, sessionTier) * CAPSULE_WATERMARK_FRACTION);
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
/** Where the orchestrator should send the task after a budget hit. */
|
|
39
57
|
export type NextRung = 'primitive' | 'higher-tier-subagent' | 'in-session';
|
|
40
58
|
|
|
@@ -64,19 +82,36 @@ export interface BudgetEvaluation {
|
|
|
64
82
|
budget_hit: boolean;
|
|
65
83
|
consumed: number;
|
|
66
84
|
budget: number;
|
|
85
|
+
/** Token count at which the CHECKPOINT capsule is due (below the kill line). */
|
|
86
|
+
watermark: number;
|
|
87
|
+
/** Consumption reached the watermark but not yet the stop-loss. */
|
|
88
|
+
watermark_hit: boolean;
|
|
67
89
|
reason: string;
|
|
68
90
|
}
|
|
69
91
|
|
|
70
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Evaluate consumption against the worker's budget.
|
|
94
|
+
*
|
|
95
|
+
* Two lines, not one: the **watermark** (emit a capsule, still working) and the
|
|
96
|
+
* **stop-loss** (return a partial result, stop). A budget hit implies the
|
|
97
|
+
* watermark was passed, so `watermark_hit` marks the band BETWEEN them — the
|
|
98
|
+
* only window in which a capsule can still be written.
|
|
99
|
+
*/
|
|
71
100
|
export function evaluateWorkerBudget(consumed: number, budget: number): BudgetEvaluation {
|
|
72
101
|
const hit = consumed >= budget;
|
|
102
|
+
const watermark = Math.floor(budget * CAPSULE_WATERMARK_FRACTION);
|
|
103
|
+
const watermarkHit = !hit && consumed >= watermark;
|
|
73
104
|
return {
|
|
74
105
|
budget_hit: hit,
|
|
75
106
|
consumed,
|
|
76
107
|
budget,
|
|
108
|
+
watermark,
|
|
109
|
+
watermark_hit: watermarkHit,
|
|
77
110
|
reason: hit
|
|
78
111
|
? `budget hit (${consumed} >= ${budget}) — return partial result + escalation flag, stop exploring`
|
|
79
|
-
:
|
|
112
|
+
: watermarkHit
|
|
113
|
+
? `watermark reached (${consumed} >= ${watermark}) — emit a CHECKPOINT capsule while there is headroom to write one`
|
|
114
|
+
: `within budget (${consumed} < ${budget})`,
|
|
80
115
|
};
|
|
81
116
|
}
|
|
82
117
|
|