@event4u/agent-config 9.28.0 → 9.30.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 +172 -5
- package/README.md +2 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/inbox.md +22 -1
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/explain-run.md +31 -1
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +14 -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 +22 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +65 -1
- 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/preservation-guard.md +9 -0
- 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/scripts/roadmap_gates.ts +312 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
- 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 +17 -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/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
- package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-BTJNRUX3.js} +22 -35
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-DDUNNFYX.js} +24 -17
- package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-EQY4FLJB.js} +3 -3
- package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-IYTGIG2Y.js} +155 -91
- 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-MMJL4TIX.js +18 -0
- package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
- package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
- package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
- package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
- 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 +2803 -137
- package/dist/cli-delegate/cmd_doctor.js +10 -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 +88 -8
- 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 +3 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +177 -0
- package/dist/cli-delegate/cmd_settings_get.js +2 -8
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- 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 +72 -23
- 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 +2278 -611
- package/dist/install/install.mjs +56 -17
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +4122 -699
- 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 +12 -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-B5qGYOJo.js +26 -0
- package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +58 -12
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +312 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/src/agent-src/templates/AGENTS.md +4 -4
- package/src/config/agent-settings.template.yml +88 -66
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/dispatch-economy-metrics.json +61 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/hook-token-budget.json +91 -0
- package/src/config/recycle-threshold-budget.json +12 -0
- package/src/config/rule-enforcement-baseline.json +86 -0
- package/src/scripts/_cli/cmd_conformance.ts +31 -44
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
- package/src/scripts/_dispatch.bash +23 -0
- 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 +634 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +78 -10
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
- package/src/scripts/_lib/self_repair.ts +354 -13
- package/src/scripts/_lib/self_repair_store.ts +28 -0
- package/src/scripts/_lib/session_eol.ts +230 -0
- package/src/scripts/_lib/session_role.ts +42 -0
- package/src/scripts/_lib/subagent_capsule.ts +341 -0
- package/src/scripts/_lib/subagent_response.ts +94 -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 +126 -55
- 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/ask_transport.ts +221 -0
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/bench_hook_injection.ts +234 -0
- package/src/scripts/check_adr_frontmatter.ts +84 -40
- package/src/scripts/check_branch_freshness.ts +1 -0
- package/src/scripts/check_corpus_staleness.ts +18 -4
- package/src/scripts/check_detector_corpus.ts +219 -0
- package/src/scripts/check_gitignore_freshness.ts +42 -8
- package/src/scripts/check_hook_bundle_freshness.ts +24 -1
- package/src/scripts/check_kernel_prefix_stability.ts +1 -0
- package/src/scripts/check_standing_rule_delivery.ts +73 -32
- package/src/scripts/check_static_layer_stability.ts +154 -0
- package/src/scripts/check_suppression_hygiene.ts +48 -0
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/council_cli.ts +401 -25
- package/src/scripts/dispatch_economy_report.ts +448 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -2
- package/src/scripts/explain_run.ts +206 -4
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/generate_settings_reference.ts +1 -0
- package/src/scripts/handoff_context_hook.ts +112 -4
- package/src/scripts/hook_manifest.yaml +117 -10
- package/src/scripts/hooks/concern_registry.ts +8 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +106 -35
- package/src/scripts/hooks/dispatch_hook.ts +39 -2
- package/src/scripts/hooks/edit_shape_hook.ts +303 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
- package/src/scripts/hooks/reread_guard_hook.ts +314 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
- package/src/scripts/hooks/session_eol_hook.ts +268 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +507 -0
- package/src/scripts/install-hooks.sh +1 -1
- package/src/scripts/language_mirror_hook.ts +89 -0
- package/src/scripts/lint_decision_impact.ts +14 -1
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +82 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/lint_readme_serial_comma.ts +1 -0
- package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
- package/src/scripts/measure_turn_end_gate.ts +194 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/rebuild_hook_bundle.ts +178 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/report_conformance_funnel.ts +284 -0
- package/src/scripts/report_imperative_density.ts +1 -0
- package/src/scripts/routing_doctor.ts +118 -36
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +337 -59
- package/src/scripts/session_eol_report.ts +261 -0
- 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 +24 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
- package/src/templates/compact-instructions.md +40 -0
- package/dist/ui/assets/index-DmR0mbk4.js +0 -26
- package/dist/ui/assets/index-DmR0mbk4.js.map +0 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session end-of-life scanner (road-to-token-economy-recycling Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Pure counting over Claude Code transcript JSONL: per session, the FINAL
|
|
5
|
+
* main-chain context size in parsed tokens, the turn count, and every
|
|
6
|
+
* compaction event — the three numbers the recycle roadmap's baseline and
|
|
7
|
+
* threshold are built from. Token math delegates to `cc_transcript.ts`
|
|
8
|
+
* (`billableInputTokens` — input + cache_read + cache_creation); this module
|
|
9
|
+
* adds no second token definition ("no new parser", roadmap prerequisite).
|
|
10
|
+
*
|
|
11
|
+
* Compaction detector — pinned to OBSERVED reality (blocker
|
|
12
|
+
* `compaction-marker-shape`): a real auto-compaction recorded 2026-08-06 on
|
|
13
|
+
* host v2.1.222 writes TWO markers into the transcript:
|
|
14
|
+
*
|
|
15
|
+
* 1. `{"type":"system","subtype":"compact_boundary","compactMetadata":
|
|
16
|
+
* {"trigger":"auto","preTokens":1000410,"postTokens":15870,...}}`
|
|
17
|
+
* 2. a `{"type":"user","isCompactSummary":true,...}` record carrying the
|
|
18
|
+
* summary the session was restarted from.
|
|
19
|
+
*
|
|
20
|
+
* The fixture in `tests/scripts/_lib_session_eol.test.ts` freezes both
|
|
21
|
+
* shapes. Never-silent discipline: the scanner counts BOTH markers
|
|
22
|
+
* independently — `compactions[]` from the boundary records,
|
|
23
|
+
* `compact_summaries` from the summary records. A host update that changes
|
|
24
|
+
* one shape but not the other makes the two counters diverge, which the
|
|
25
|
+
* report surfaces as marker drift instead of silently zeroing the metric.
|
|
26
|
+
*
|
|
27
|
+
* Incremental by design: the Stop-slot carrier re-runs every turn, so
|
|
28
|
+
* `scanEolSlice` accepts prior counters and only the newly appended complete
|
|
29
|
+
* lines (`readNewCompleteLines`). A full rescan is only ever the bounded
|
|
30
|
+
* 50 MB `isSafeTranscriptPath` cap away.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import * as fs from 'node:fs';
|
|
34
|
+
|
|
35
|
+
import { billableInputTokens } from './cc_transcript.js';
|
|
36
|
+
|
|
37
|
+
/** The observed compaction boundary marker (host v2.1.222, 2026-08-06). */
|
|
38
|
+
export const COMPACT_BOUNDARY_SUBTYPE = 'compact_boundary';
|
|
39
|
+
|
|
40
|
+
/** One compaction event, read from a `compact_boundary` system record. */
|
|
41
|
+
export interface CompactionEvent {
|
|
42
|
+
/** `"auto"` observed; the field is host territory — kept as an open string. */
|
|
43
|
+
trigger: string;
|
|
44
|
+
/** Context tokens immediately before compaction (`compactMetadata.preTokens`). */
|
|
45
|
+
pre_tokens: number | null;
|
|
46
|
+
/** Context tokens immediately after (`compactMetadata.postTokens`). */
|
|
47
|
+
post_tokens: number | null;
|
|
48
|
+
timestamp: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Accumulated per-session end-of-life counters. Counts and refs only — never content. */
|
|
52
|
+
export interface EolCounters {
|
|
53
|
+
schema_version: 1;
|
|
54
|
+
/** Byte offset up to which the transcript has been consumed (end of last complete line). */
|
|
55
|
+
scanned_bytes: number;
|
|
56
|
+
/** External user prompts on the main chain (excl. meta / compact-summary records). */
|
|
57
|
+
turns: number;
|
|
58
|
+
/** Main-chain assistant records carrying a `usage` block. */
|
|
59
|
+
assistant_records: number;
|
|
60
|
+
/** Billable input tokens of the LAST main-chain assistant record — the fill level. */
|
|
61
|
+
final_context_tokens: number | null;
|
|
62
|
+
/** `timestamp` of that record. */
|
|
63
|
+
final_context_at: string | null;
|
|
64
|
+
/** Every `compact_boundary` event seen, in order. */
|
|
65
|
+
compactions: CompactionEvent[];
|
|
66
|
+
/** `isCompactSummary` user records seen — the drift twin of `compactions`. */
|
|
67
|
+
compact_summaries: number;
|
|
68
|
+
/** Lines that failed `JSON.parse`. */
|
|
69
|
+
bad_lines: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function emptyCounters(): EolCounters {
|
|
73
|
+
return {
|
|
74
|
+
schema_version: 1,
|
|
75
|
+
scanned_bytes: 0,
|
|
76
|
+
turns: 0,
|
|
77
|
+
assistant_records: 0,
|
|
78
|
+
final_context_tokens: null,
|
|
79
|
+
final_context_at: null,
|
|
80
|
+
compactions: [],
|
|
81
|
+
compact_summaries: 0,
|
|
82
|
+
bad_lines: 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function numOrNull(value: unknown): number | null {
|
|
87
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function numOr0(value: unknown): number {
|
|
91
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Detect the observed compaction boundary marker on one parsed record.
|
|
96
|
+
* Returns the event, or `null` for every other record shape.
|
|
97
|
+
*/
|
|
98
|
+
export function detectCompaction(record: Record<string, unknown>): CompactionEvent | null {
|
|
99
|
+
if (record['type'] !== 'system' || record['subtype'] !== COMPACT_BOUNDARY_SUBTYPE) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const md = (record['compactMetadata'] ?? {}) as Record<string, unknown>;
|
|
103
|
+
return {
|
|
104
|
+
trigger: typeof md['trigger'] === 'string' ? md['trigger'] : 'unknown',
|
|
105
|
+
pre_tokens: numOrNull(md['preTokens']),
|
|
106
|
+
post_tokens: numOrNull(md['postTokens']),
|
|
107
|
+
timestamp: typeof record['timestamp'] === 'string' ? record['timestamp'] : null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** True for a main-chain external user PROMPT record (a turn), per the baseline heuristic. */
|
|
112
|
+
function isTurnRecord(record: Record<string, unknown>): boolean {
|
|
113
|
+
if (record['type'] !== 'user') return false;
|
|
114
|
+
if (record['isCompactSummary'] === true || record['isMeta'] === true) return false;
|
|
115
|
+
const message = record['message'];
|
|
116
|
+
if (typeof message !== 'object' || message === null) return false;
|
|
117
|
+
const content = (message as Record<string, unknown>)['content'];
|
|
118
|
+
if (typeof content === 'string') return content.trim().length > 0;
|
|
119
|
+
if (Array.isArray(content)) {
|
|
120
|
+
return content.some(
|
|
121
|
+
(b) => typeof b === 'object' && b !== null && (b as Record<string, unknown>)['type'] === 'text',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fold newly appended COMPLETE transcript lines into the counters. Pure —
|
|
129
|
+
* returns a new counters object; `scanned_bytes` is the caller's business
|
|
130
|
+
* (it knows the byte offsets, this function only sees text).
|
|
131
|
+
*/
|
|
132
|
+
export function scanEolSlice(text: string, prior: EolCounters): EolCounters {
|
|
133
|
+
const next: EolCounters = {
|
|
134
|
+
...prior,
|
|
135
|
+
compactions: [...prior.compactions],
|
|
136
|
+
};
|
|
137
|
+
for (const line of text.split('\n')) {
|
|
138
|
+
if (line.trim().length === 0) continue;
|
|
139
|
+
let record: Record<string, unknown>;
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(line) as unknown;
|
|
142
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
143
|
+
next.bad_lines += 1;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
record = parsed as Record<string, unknown>;
|
|
147
|
+
} catch {
|
|
148
|
+
next.bad_lines += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (record['isSidechain'] === true) continue; // subagent legs are not this session's context
|
|
152
|
+
|
|
153
|
+
const compaction = detectCompaction(record);
|
|
154
|
+
if (compaction) {
|
|
155
|
+
next.compactions.push(compaction);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (record['type'] === 'user' && record['isCompactSummary'] === true) {
|
|
159
|
+
next.compact_summaries += 1;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (isTurnRecord(record)) {
|
|
163
|
+
next.turns += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (record['type'] === 'assistant') {
|
|
167
|
+
const message = record['message'];
|
|
168
|
+
const usage =
|
|
169
|
+
typeof message === 'object' && message !== null
|
|
170
|
+
? ((message as Record<string, unknown>)['usage'] as Record<string, unknown> | undefined)
|
|
171
|
+
: undefined;
|
|
172
|
+
if (typeof usage === 'object' && usage !== null && usage['input_tokens'] !== undefined) {
|
|
173
|
+
next.assistant_records += 1;
|
|
174
|
+
next.final_context_tokens = billableInputTokens({
|
|
175
|
+
input_tokens: numOr0(usage['input_tokens']),
|
|
176
|
+
cache_read_input_tokens: numOr0(usage['cache_read_input_tokens']),
|
|
177
|
+
cache_creation_input_tokens: numOr0(usage['cache_creation_input_tokens']),
|
|
178
|
+
});
|
|
179
|
+
next.final_context_at =
|
|
180
|
+
typeof record['timestamp'] === 'string' ? record['timestamp'] : null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return next;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface NewLinesRead {
|
|
188
|
+
/** The complete lines appended since `fromByte` (possibly empty). */
|
|
189
|
+
text: string;
|
|
190
|
+
/** The offset to persist — end of the last complete line consumed. */
|
|
191
|
+
nextOffset: number;
|
|
192
|
+
/** True when the file shrank below `fromByte` (rotation/truncation) — caller rescans from 0. */
|
|
193
|
+
truncated: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Read the bytes appended to `filePath` since `fromByte`, trimmed to the
|
|
198
|
+
* last complete line (a JSONL writer may be mid-line). Never throws — any
|
|
199
|
+
* fs error reads as "nothing new".
|
|
200
|
+
*/
|
|
201
|
+
export function readNewCompleteLines(filePath: string, fromByte: number): NewLinesRead {
|
|
202
|
+
try {
|
|
203
|
+
const { size } = fs.statSync(filePath);
|
|
204
|
+
if (size < fromByte) {
|
|
205
|
+
return { text: '', nextOffset: 0, truncated: true };
|
|
206
|
+
}
|
|
207
|
+
if (size === fromByte) {
|
|
208
|
+
return { text: '', nextOffset: fromByte, truncated: false };
|
|
209
|
+
}
|
|
210
|
+
const length = size - fromByte;
|
|
211
|
+
const buffer = Buffer.alloc(length);
|
|
212
|
+
const fd = fs.openSync(filePath, 'r');
|
|
213
|
+
try {
|
|
214
|
+
fs.readSync(fd, buffer, 0, length, fromByte);
|
|
215
|
+
} finally {
|
|
216
|
+
fs.closeSync(fd);
|
|
217
|
+
}
|
|
218
|
+
const lastNewline = buffer.lastIndexOf(0x0a);
|
|
219
|
+
if (lastNewline === -1) {
|
|
220
|
+
return { text: '', nextOffset: fromByte, truncated: false };
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
text: buffer.subarray(0, lastNewline + 1).toString('utf-8'),
|
|
224
|
+
nextOffset: fromByte + lastNewline + 1,
|
|
225
|
+
truncated: false,
|
|
226
|
+
};
|
|
227
|
+
} catch {
|
|
228
|
+
return { text: '', nextOffset: fromByte, truncated: false };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-role detection — the ONE shared detector (road-to-token-economy-
|
|
3
|
+
* dispatch Phase 2.2: "one detector, two consumers, test-pinned equal").
|
|
4
|
+
*
|
|
5
|
+
* Consumers:
|
|
6
|
+
* - `hooks/dispatch_hook.ts` — applies the manifest's role axis (a marked
|
|
7
|
+
* worker session runs a shorter concern chain).
|
|
8
|
+
* - `hooks/delegation_nudge_hook.ts` — feeds the judgment ladder's
|
|
9
|
+
* recursive-dispatch guard (`insideSubagentSession`), whose own header
|
|
10
|
+
* documents the fact as CALLER-SUPPLIED: no verified host discriminator
|
|
11
|
+
* exists (upstream agent-identity request closed NOT_PLANNED), and
|
|
12
|
+
* probing for invented host variable names is the hallucinated-field
|
|
13
|
+
* failure `source-discovery-gate` exists to stop.
|
|
14
|
+
*
|
|
15
|
+
* `AGENT_CONFIG_SESSION_ROLE` is therefore this suite's OWN variable, set
|
|
16
|
+
* only by suite-owned spawn wrappers that launch a separate CLI session
|
|
17
|
+
* (council transports, bench runners, future worker wrappers). Host
|
|
18
|
+
* limitation, pre-registered in the roadmap's `worker-chain-host-delivery`
|
|
19
|
+
* blocker: an in-process Agent-tool subagent shares the host process env
|
|
20
|
+
* and CANNOT be marked per-spawn — on that path the role resolves
|
|
21
|
+
* `orchestrator` and the full chain runs (fail-open, Phase 2.4).
|
|
22
|
+
*
|
|
23
|
+
* Fail-open is the contract: unset, empty, or unknown values resolve to
|
|
24
|
+
* `orchestrator` — the thin path is the opt-in of a marked spawn, never
|
|
25
|
+
* the accident of a missing or typo'd variable.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export const SESSION_ROLE_ENV = 'AGENT_CONFIG_SESSION_ROLE';
|
|
29
|
+
|
|
30
|
+
/** `reviewer` is enum-reserved for Phase 3.2; until a `roles.reviewer`
|
|
31
|
+
* manifest entry exists it resolves to the full chain like any role
|
|
32
|
+
* without a manifest entry. */
|
|
33
|
+
export type SessionRole = 'orchestrator' | 'worker' | 'reviewer';
|
|
34
|
+
|
|
35
|
+
const KNOWN_ROLES: ReadonlySet<string> = new Set(['orchestrator', 'worker', 'reviewer']);
|
|
36
|
+
|
|
37
|
+
export function resolveSessionRole(
|
|
38
|
+
env: Record<string, string | undefined> = process.env,
|
|
39
|
+
): SessionRole {
|
|
40
|
+
const raw = (env[SESSION_ROLE_ENV] ?? '').trim().toLowerCase();
|
|
41
|
+
return (KNOWN_ROLES.has(raw) ? raw : 'orchestrator') as SessionRole;
|
|
42
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
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
|
+
/**
|
|
86
|
+
* Capsule schema version (road-to-token-economy-recycling Phase 2.1).
|
|
87
|
+
* Versioning is INTRODUCED here — the Phase-0 worker capsule shipped
|
|
88
|
+
* unversioned and stays valid without a version field (implicit v1, wire
|
|
89
|
+
* shape unchanged, `subagent-status.json` untouched). Version 2 adds the
|
|
90
|
+
* explicit `capsule_version` + `variant` discriminator that the
|
|
91
|
+
* main-session recycle envelope requires. Policy: additive only — a new
|
|
92
|
+
* version may add fields or variants, never repurpose or remove one; both
|
|
93
|
+
* variants validate through THIS module (one schema file, one validator
|
|
94
|
+
* family — roadmap 5.6's anti-fork rule).
|
|
95
|
+
*/
|
|
96
|
+
export const CAPSULE_SCHEMA_VERSION = 2;
|
|
97
|
+
|
|
98
|
+
/** The two CHECKPOINT variants sharing this schema. */
|
|
99
|
+
export const CAPSULE_VARIANTS = ['worker', 'main_session'] as const;
|
|
100
|
+
export type CapsuleVariant = (typeof CAPSULE_VARIANTS)[number];
|
|
101
|
+
|
|
102
|
+
/** A single-line token no longer than `max` chars. Rejects any body-shaped value. */
|
|
103
|
+
function isShortLine(value: unknown, max: number): value is string {
|
|
104
|
+
return typeof value === 'string' && value.length > 0 && value.length <= max && !value.includes('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function checkList(
|
|
108
|
+
errors: string[],
|
|
109
|
+
field: string,
|
|
110
|
+
value: unknown,
|
|
111
|
+
max: number,
|
|
112
|
+
required: boolean,
|
|
113
|
+
): void {
|
|
114
|
+
if (value === undefined) {
|
|
115
|
+
if (required) errors.push(`${field} is required`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!Array.isArray(value)) {
|
|
119
|
+
errors.push(`${field} must be an array`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (value.length > MAX_ENTRIES) {
|
|
123
|
+
errors.push(`${field} carries ${value.length} entries (max ${MAX_ENTRIES}) — a capsule is a handoff, not a transcript`);
|
|
124
|
+
}
|
|
125
|
+
const bad = value.filter((x) => !isShortLine(x, max));
|
|
126
|
+
if (bad.length > 0) {
|
|
127
|
+
errors.push(
|
|
128
|
+
`${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)`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Validate a capsule body. Returns the list of violations (empty = valid).
|
|
135
|
+
*
|
|
136
|
+
* The orchestrator never briefs a successor from an invalid capsule — an
|
|
137
|
+
* invalid capsule falls back to today's stop-loss behaviour, loudly.
|
|
138
|
+
*/
|
|
139
|
+
export function validateCapsule(input: unknown): string[] {
|
|
140
|
+
const errors: string[] = [];
|
|
141
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
142
|
+
return ['not an object'];
|
|
143
|
+
}
|
|
144
|
+
const c = input as Record<string, unknown>;
|
|
145
|
+
|
|
146
|
+
if (!isShortLine(c['summary'], MAX_LINE_CHARS)) {
|
|
147
|
+
errors.push(`summary must be a single line of 1–${MAX_LINE_CHARS} chars`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const generation = c['generation'];
|
|
151
|
+
if (typeof generation !== 'number' || !Number.isInteger(generation) || generation < 1) {
|
|
152
|
+
errors.push('generation must be an integer ≥ 1 (the first dispatch is generation 1)');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
checkList(errors, 'done', c['done'], MAX_REF_CHARS, true);
|
|
156
|
+
checkList(errors, 'remaining', c['remaining'], MAX_LINE_CHARS, true);
|
|
157
|
+
checkList(errors, 'decisions', c['decisions'], MAX_LINE_CHARS, false);
|
|
158
|
+
checkList(errors, 'open_risks', c['open_risks'], MAX_LINE_CHARS, false);
|
|
159
|
+
checkList(errors, 'touched_files', c['touched_files'], MAX_REF_CHARS, false);
|
|
160
|
+
|
|
161
|
+
const assumptions = c['assumptions'];
|
|
162
|
+
if (assumptions !== undefined) {
|
|
163
|
+
if (!Array.isArray(assumptions)) {
|
|
164
|
+
errors.push('assumptions must be an array');
|
|
165
|
+
} else {
|
|
166
|
+
if (assumptions.length > MAX_ENTRIES) {
|
|
167
|
+
errors.push(`assumptions carries ${assumptions.length} entries (max ${MAX_ENTRIES})`);
|
|
168
|
+
}
|
|
169
|
+
for (const [i, raw] of assumptions.entries()) {
|
|
170
|
+
errors.push(...validateAssumption(raw, `assumptions[${i}]`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return errors;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The main-session recycle envelope (road-to-token-economy-recycling
|
|
180
|
+
* Phase 2.1) — the `main_session` CHECKPOINT variant. Unlike the worker
|
|
181
|
+
* capsule there is NO surviving orchestrator to receive it: the successor
|
|
182
|
+
* session bootstraps from the envelope ALONE, so the required set is
|
|
183
|
+
* stricter — the active task, its acceptance criteria, the open work, and
|
|
184
|
+
* the explicit NOT-carried-forward list (what the successor must re-derive
|
|
185
|
+
* from source rather than trust) are all mandatory.
|
|
186
|
+
*
|
|
187
|
+
* Same transcript-exclusion-by-construction as the worker capsule: every
|
|
188
|
+
* field is a ref token or a single short line, validated by the SAME
|
|
189
|
+
* primitives. A prose transcript summary is schema-invalid twice over —
|
|
190
|
+
* any multi-line value fails `isShortLine`, and any field not listed below
|
|
191
|
+
* (e.g. `transcript_summary`) fails the unknown-key sweep. The strict
|
|
192
|
+
* sweep is deliberate where the worker validator stays open: the worker
|
|
193
|
+
* side has `subagent-status.json` (`additionalProperties: false`) as its
|
|
194
|
+
* wire gate; this variant is consumed from a FILE by
|
|
195
|
+
* `handoff_context_hook`, so the module itself must be the strict gate.
|
|
196
|
+
*/
|
|
197
|
+
export interface MainSessionRecycleEnvelope {
|
|
198
|
+
/** Must be {@link CAPSULE_SCHEMA_VERSION}. */
|
|
199
|
+
capsule_version: number;
|
|
200
|
+
variant: 'main_session';
|
|
201
|
+
/** One- or two-sentence outcome so far. */
|
|
202
|
+
summary: string;
|
|
203
|
+
/** The active task, one line. */
|
|
204
|
+
task: string;
|
|
205
|
+
/** Workspace root this envelope belongs to — the consumer's identity check (Risk 4). */
|
|
206
|
+
workspace: string;
|
|
207
|
+
/** ISO-8601 stamp of the write — the consumer's staleness guard. */
|
|
208
|
+
written_at: string;
|
|
209
|
+
/** What "done" means — one short line each. Required, ≥ 1. */
|
|
210
|
+
acceptance_criteria: string[];
|
|
211
|
+
/** Open work, one short line each. Required (may be empty when only verification remains). */
|
|
212
|
+
remaining: string[];
|
|
213
|
+
/**
|
|
214
|
+
* What the successor must RE-DERIVE from source instead of trusting —
|
|
215
|
+
* the explicit omission list that makes envelope loss visible instead
|
|
216
|
+
* of silent. Required; an empty list is a claim, not a default.
|
|
217
|
+
*/
|
|
218
|
+
not_carried_forward: string[];
|
|
219
|
+
/** Choices already made, each with its one-line rationale in the same line. */
|
|
220
|
+
decisions?: string[];
|
|
221
|
+
/** Binding constraints the successor must not silently re-open. */
|
|
222
|
+
constraints?: string[];
|
|
223
|
+
/** Open worker CHECKPOINT envelopes by path — never inlined bodies. */
|
|
224
|
+
open_worker_envelopes?: string[];
|
|
225
|
+
/** Artifact paths (deliverables, notes, evidence) — refs, never bodies. */
|
|
226
|
+
artifact_paths?: string[];
|
|
227
|
+
/** Stated premises — the shared `{statement, basis, epistemic_state}` shape. */
|
|
228
|
+
assumptions?: CapsuleAssumption[];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Every key `validateRecycleEnvelope` accepts — anything else is schema-invalid. */
|
|
232
|
+
const RECYCLE_ENVELOPE_KEYS: ReadonlySet<string> = new Set([
|
|
233
|
+
'capsule_version',
|
|
234
|
+
'variant',
|
|
235
|
+
'summary',
|
|
236
|
+
'task',
|
|
237
|
+
'workspace',
|
|
238
|
+
'written_at',
|
|
239
|
+
'acceptance_criteria',
|
|
240
|
+
'remaining',
|
|
241
|
+
'not_carried_forward',
|
|
242
|
+
'decisions',
|
|
243
|
+
'constraints',
|
|
244
|
+
'open_worker_envelopes',
|
|
245
|
+
'artifact_paths',
|
|
246
|
+
'assumptions',
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Validate a main-session recycle envelope. Returns the list of violations
|
|
251
|
+
* (empty = valid). The consumer never injects an invalid envelope — it is
|
|
252
|
+
* consumed and discarded loudly instead (`handoff_context_hook`).
|
|
253
|
+
*/
|
|
254
|
+
export function validateRecycleEnvelope(input: unknown): string[] {
|
|
255
|
+
const errors: string[] = [];
|
|
256
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
257
|
+
return ['not an object'];
|
|
258
|
+
}
|
|
259
|
+
const e = input as Record<string, unknown>;
|
|
260
|
+
|
|
261
|
+
for (const key of Object.keys(e)) {
|
|
262
|
+
if (!RECYCLE_ENVELOPE_KEYS.has(key)) {
|
|
263
|
+
errors.push(
|
|
264
|
+
`unknown field "${key}" — a recycle envelope carries selection and pointers only; ` +
|
|
265
|
+
'free-form additions (prose summaries included) are schema-invalid by construction',
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (e['capsule_version'] !== CAPSULE_SCHEMA_VERSION) {
|
|
271
|
+
errors.push(`capsule_version must be ${CAPSULE_SCHEMA_VERSION}`);
|
|
272
|
+
}
|
|
273
|
+
if (e['variant'] !== 'main_session') {
|
|
274
|
+
errors.push("variant must be 'main_session'");
|
|
275
|
+
}
|
|
276
|
+
if (!isShortLine(e['summary'], MAX_LINE_CHARS)) {
|
|
277
|
+
errors.push(`summary must be a single line of 1–${MAX_LINE_CHARS} chars`);
|
|
278
|
+
}
|
|
279
|
+
if (!isShortLine(e['task'], MAX_LINE_CHARS)) {
|
|
280
|
+
errors.push(`task must be a single line of 1–${MAX_LINE_CHARS} chars`);
|
|
281
|
+
}
|
|
282
|
+
if (!isShortLine(e['workspace'], MAX_REF_CHARS)) {
|
|
283
|
+
errors.push(`workspace must be a path ref of ≤ ${MAX_REF_CHARS} chars`);
|
|
284
|
+
}
|
|
285
|
+
const writtenAt = e['written_at'];
|
|
286
|
+
if (typeof writtenAt !== 'string' || Number.isNaN(Date.parse(writtenAt))) {
|
|
287
|
+
errors.push('written_at must be a parseable ISO-8601 timestamp');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
checkList(errors, 'acceptance_criteria', e['acceptance_criteria'], MAX_LINE_CHARS, true);
|
|
291
|
+
if (Array.isArray(e['acceptance_criteria']) && e['acceptance_criteria'].length === 0) {
|
|
292
|
+
errors.push('acceptance_criteria must carry at least one entry — the successor cannot know "done" without it');
|
|
293
|
+
}
|
|
294
|
+
checkList(errors, 'remaining', e['remaining'], MAX_LINE_CHARS, true);
|
|
295
|
+
checkList(errors, 'not_carried_forward', e['not_carried_forward'], MAX_LINE_CHARS, true);
|
|
296
|
+
checkList(errors, 'decisions', e['decisions'], MAX_LINE_CHARS, false);
|
|
297
|
+
checkList(errors, 'constraints', e['constraints'], MAX_LINE_CHARS, false);
|
|
298
|
+
checkList(errors, 'open_worker_envelopes', e['open_worker_envelopes'], MAX_REF_CHARS, false);
|
|
299
|
+
checkList(errors, 'artifact_paths', e['artifact_paths'], MAX_REF_CHARS, false);
|
|
300
|
+
|
|
301
|
+
// `remaining` and `not_carried_forward` must be PRESENT (checkList already
|
|
302
|
+
// enforces that) — empty arrays are legal, explicit claims.
|
|
303
|
+
const assumptions = e['assumptions'];
|
|
304
|
+
if (assumptions !== undefined) {
|
|
305
|
+
if (!Array.isArray(assumptions)) {
|
|
306
|
+
errors.push('assumptions must be an array');
|
|
307
|
+
} else {
|
|
308
|
+
if (assumptions.length > MAX_ENTRIES) {
|
|
309
|
+
errors.push(`assumptions carries ${assumptions.length} entries (max ${MAX_ENTRIES})`);
|
|
310
|
+
}
|
|
311
|
+
for (const [i, raw] of assumptions.entries()) {
|
|
312
|
+
errors.push(...validateAssumption(raw, `assumptions[${i}]`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return errors;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Validate one `{statement, basis, epistemic_state}` triple. Exported because
|
|
322
|
+
* the worker RESULT envelope carries the same field (`subagent_response.ts`) —
|
|
323
|
+
* one shape, one validator, so the two cannot drift.
|
|
324
|
+
*/
|
|
325
|
+
export function validateAssumption(input: unknown, label = 'assumption'): string[] {
|
|
326
|
+
const errors: string[] = [];
|
|
327
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
328
|
+
return [`${label} is not an object`];
|
|
329
|
+
}
|
|
330
|
+
const a = input as Record<string, unknown>;
|
|
331
|
+
if (!isShortLine(a['statement'], MAX_LINE_CHARS)) {
|
|
332
|
+
errors.push(`${label}.statement must be a single line of 1–${MAX_LINE_CHARS} chars`);
|
|
333
|
+
}
|
|
334
|
+
if (!isShortLine(a['basis'], MAX_REF_CHARS)) {
|
|
335
|
+
errors.push(`${label}.basis must be a ref token (file:line, id, path, command) of ≤ ${MAX_REF_CHARS} chars`);
|
|
336
|
+
}
|
|
337
|
+
if (typeof a['epistemic_state'] !== 'string' || !EPISTEMIC.has(a['epistemic_state'])) {
|
|
338
|
+
errors.push(`${label}.epistemic_state must be one of ${EPISTEMIC_STATES.join(' | ')}`);
|
|
339
|
+
}
|
|
340
|
+
return errors;
|
|
341
|
+
}
|