sdd-agent-platform 0.2.0 → 0.3.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.
@@ -50,6 +50,29 @@ export const EVIDENCE_INGESTION_CONTRACT_VERSION = 'phase-6.0-evidence-ingestion
50
50
  export const AGENT_EXECUTION_RECORD_CONTRACT_VERSION = 'phase-6.0-agent-execution-record-v1';
51
51
  export const TEAM_SESSION_RECORD_CONTRACT_VERSION = 'phase-6.0-team-session-record-v1';
52
52
  export const RESIDENT_WORKER_RUNTIME_CONTRACT_VERSION = 'phase-6.1-resident-worker-runtime-v1';
53
+ export const SDD_EVIDENCE_CONTRACT = 'sdd-evidence-v1';
54
+ export const SDD_EVIDENCE_VERSION = '1.0.0';
55
+ export const ACCEPTANCE_POLICY_RULESET_VERSION = 'acceptance-policy-v1';
56
+ export const INVOCATION_LEDGER_CONTRACT_VERSION = 'phase-6.9-invocation-ledger-v1';
57
+ export const ROUTE_CACHE_CONTRACT_VERSION = 'phase-6.9-route-cache-v1';
58
+ export const RUNTIME_PROFILE_CONTRACT_VERSION = 'phase-6.9-runtime-profile-v1';
59
+ export const CONTEXT_BUDGET_CONTRACT_VERSION = 'phase-6.10-context-budget-v1';
60
+ export const COMMAND_OUTPUT_SUMMARY_CONTRACT_VERSION = 'sdd-command-output-summary-v1';
61
+ export const EVIDENCE_SUMMARY_CONTRACT_VERSION = 'sdd-evidence-summary-v1';
62
+ export const CONTEXT_PACKAGE_CONTRACT_VERSION = 'sdd-context-package-v1';
63
+ export const LOG_WORKER_SUMMARY_CONTRACT_VERSION = 'sdd-log-worker-summary-v1';
64
+ const RUNTIME_STORE_SCHEMA_VERSION = 1;
65
+ const RUNTIME_STORE_CONTRACT_VERSION = 'phase-6.11-runtime-store-v1';
66
+ class RuntimeStoreError extends Error {
67
+ code;
68
+ constructor(code, message, options) {
69
+ super(message);
70
+ this.code = code;
71
+ this.name = 'RuntimeStoreError';
72
+ this.cause = options?.cause;
73
+ }
74
+ }
75
+ let runtimeStoreConstructorPromise = null;
53
76
  const DEFAULT_RESIDENT_WORKER_LEASE_SECONDS = 900;
54
77
  export function getWorktreesDir(projectRoot) {
55
78
  return path.join(getSddDir(projectRoot), 'worktrees');
@@ -66,6 +89,9 @@ export function getRunsDir(projectRoot) {
66
89
  export function getLocalRunIndexPath(projectRoot) {
67
90
  return path.join(getSddDir(projectRoot), 'run-index.json');
68
91
  }
92
+ export function getRuntimeStorePath(projectRoot) {
93
+ return path.join(getSddDir(projectRoot), 'runtime.sqlite');
94
+ }
69
95
  export function getRunDir(projectRoot, runId) {
70
96
  assertSafePathSegment(runId, 'runId');
71
97
  return path.join(getRunsDir(projectRoot), runId);
@@ -82,6 +108,305 @@ export function getTeamSessionsDir(projectRoot, runId) {
82
108
  export function getWorkerRuntimesDir(projectRoot, runId) {
83
109
  return path.join(getRunDir(projectRoot, runId), 'worker-runtimes');
84
110
  }
111
+ export function getInvocationLedgerPath(projectRoot, runId) {
112
+ return path.join(getRunDir(projectRoot, runId), 'invocations.jsonl');
113
+ }
114
+ export function getRouteCacheDir(projectRoot) {
115
+ return path.join(getSddDir(projectRoot), 'cache', 'routes');
116
+ }
117
+ function getRouteCachePath(projectRoot, key) {
118
+ assertSafePathSegment(key, 'routeCacheKey');
119
+ return path.join(getRouteCacheDir(projectRoot), `${key}.json`);
120
+ }
121
+ async function loadRuntimeStoreConstructor() {
122
+ runtimeStoreConstructorPromise ??= importNodeSqlite()
123
+ .then((sqlite) => sqlite.DatabaseSync)
124
+ .catch((error) => {
125
+ throw new RuntimeStoreError('STORE_UNAVAILABLE', `node:sqlite is unavailable: ${messageFromError(error)}`, { cause: error });
126
+ });
127
+ return runtimeStoreConstructorPromise;
128
+ }
129
+ async function importNodeSqlite() {
130
+ const emitWarning = process.emitWarning;
131
+ const forwardWarning = emitWarning;
132
+ process.emitWarning = ((warning, ...args) => {
133
+ const message = warning instanceof Error ? warning.message : warning;
134
+ if (typeof message === 'string' && message.includes('SQLite is an experimental feature')) {
135
+ return;
136
+ }
137
+ return forwardWarning.call(process, warning, ...args);
138
+ });
139
+ try {
140
+ return await import('node:sqlite');
141
+ }
142
+ finally {
143
+ process.emitWarning = emitWarning;
144
+ }
145
+ }
146
+ async function openRuntimeStore(projectRoot) {
147
+ await mkdir(getSddDir(projectRoot), { recursive: true });
148
+ const DatabaseSync = await loadRuntimeStoreConstructor();
149
+ const storePath = getRuntimeStorePath(projectRoot);
150
+ let db;
151
+ try {
152
+ db = new DatabaseSync(storePath);
153
+ }
154
+ catch (error) {
155
+ throw new RuntimeStoreError('STORE_UNAVAILABLE', `Cannot open runtime store ${storePath}: ${messageFromError(error)}`, { cause: error });
156
+ }
157
+ try {
158
+ db.exec('PRAGMA foreign_keys = ON');
159
+ db.exec('PRAGMA busy_timeout = 5000');
160
+ db.exec('PRAGMA journal_mode = WAL');
161
+ initializeRuntimeStoreSchema(db);
162
+ return { path: storePath, db };
163
+ }
164
+ catch (error) {
165
+ db.close();
166
+ throw new RuntimeStoreError('SCHEMA_MISMATCH', `Cannot initialize runtime store ${storePath}: ${messageFromError(error)}`, { cause: error });
167
+ }
168
+ }
169
+ async function withRuntimeStore(projectRoot, fn) {
170
+ const store = await openRuntimeStore(projectRoot);
171
+ try {
172
+ return await fn(store);
173
+ }
174
+ finally {
175
+ store.db.close();
176
+ }
177
+ }
178
+ function initializeRuntimeStoreSchema(db) {
179
+ const currentVersion = readRuntimeStoreSchemaVersion(db);
180
+ if (currentVersion > RUNTIME_STORE_SCHEMA_VERSION) {
181
+ throw new RuntimeStoreError('SCHEMA_MISMATCH', `Runtime store schema ${currentVersion} is newer than supported schema ${RUNTIME_STORE_SCHEMA_VERSION}.`);
182
+ }
183
+ db.exec(`
184
+ CREATE TABLE IF NOT EXISTS runtime_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL);
185
+ CREATE TABLE IF NOT EXISTS runs (run_id TEXT PRIMARY KEY, status TEXT NOT NULL, phase TEXT, current_task TEXT, partition TEXT, git_branch TEXT, task_id TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, state_json TEXT NOT NULL, state_hash TEXT NOT NULL);
186
+ CREATE TABLE IF NOT EXISTS events (event_id INTEGER PRIMARY KEY AUTOINCREMENT, run_id TEXT NOT NULL, event_time TEXT NOT NULL, event_name TEXT NOT NULL, event_hash TEXT NOT NULL, event_json TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'runtime', FOREIGN KEY(run_id) REFERENCES runs(run_id) ON DELETE CASCADE, UNIQUE(run_id, event_hash));
187
+ CREATE TABLE IF NOT EXISTS attempts (attempt_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, task_id TEXT, status TEXT, payload_json TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(run_id) REFERENCES runs(run_id) ON DELETE CASCADE);
188
+ CREATE TABLE IF NOT EXISTS activities (activity_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, task_id TEXT, branch TEXT, kind TEXT NOT NULL, ref TEXT NOT NULL, status TEXT, created_at TEXT NOT NULL, payload_json TEXT NOT NULL, FOREIGN KEY(run_id) REFERENCES runs(run_id) ON DELETE CASCADE);
189
+ CREATE TABLE IF NOT EXISTS artifacts (artifact_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, path TEXT NOT NULL, kind TEXT NOT NULL, task_id TEXT, agent TEXT, content_hash TEXT NOT NULL, bytes INTEGER NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL, payload_json TEXT NOT NULL, FOREIGN KEY(run_id) REFERENCES runs(run_id) ON DELETE CASCADE, UNIQUE(run_id, path, content_hash));
190
+ CREATE TABLE IF NOT EXISTS artifact_ingestions (ingestion_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, delegation_id TEXT NOT NULL, task_id TEXT NOT NULL, agent TEXT NOT NULL, artifact_path TEXT NOT NULL, status TEXT NOT NULL, result_status TEXT, ingested_at TEXT NOT NULL, payload_json TEXT NOT NULL, FOREIGN KEY(run_id) REFERENCES runs(run_id) ON DELETE CASCADE);
191
+ CREATE TABLE IF NOT EXISTS policy_decisions (decision_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, task_id TEXT, acceptance_id TEXT, status TEXT NOT NULL, issue_codes TEXT NOT NULL, created_at TEXT NOT NULL, payload_json TEXT NOT NULL, FOREIGN KEY(run_id) REFERENCES runs(run_id) ON DELETE CASCADE);
192
+ CREATE TABLE IF NOT EXISTS evidence_claims (claim_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, partition TEXT, task_id TEXT NOT NULL, acceptance_id TEXT NOT NULL, coverage_status TEXT NOT NULL, source_artifact TEXT NOT NULL, is_derived INTEGER NOT NULL, created_at TEXT NOT NULL, payload_json TEXT NOT NULL, FOREIGN KEY(run_id) REFERENCES runs(run_id) ON DELETE CASCADE);
193
+ CREATE TABLE IF NOT EXISTS gaps (gap_id TEXT PRIMARY KEY, run_id TEXT, task_id TEXT, severity TEXT NOT NULL, payload_json TEXT NOT NULL, created_at TEXT NOT NULL);
194
+ CREATE TABLE IF NOT EXISTS recovery_actions (action_id TEXT PRIMARY KEY, run_id TEXT, task_id TEXT, status TEXT NOT NULL, payload_json TEXT NOT NULL, created_at TEXT NOT NULL);
195
+ CREATE TABLE IF NOT EXISTS source_snapshots (snapshot_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, partition TEXT, spec_hash TEXT, plan_hash TEXT, tasks_hash TEXT, created_at TEXT NOT NULL, payload_json TEXT NOT NULL, FOREIGN KEY(run_id) REFERENCES runs(run_id) ON DELETE CASCADE);
196
+ CREATE TABLE IF NOT EXISTS projections (projection_id TEXT PRIMARY KEY, projection_type TEXT NOT NULL, scope_key TEXT NOT NULL, generated_at TEXT NOT NULL, payload_json TEXT NOT NULL, UNIQUE(projection_type, scope_key));
197
+ CREATE TABLE IF NOT EXISTS legacy_imports (import_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, entity_type TEXT NOT NULL, content_hash TEXT NOT NULL, imported_at TEXT NOT NULL, status TEXT NOT NULL, issue TEXT, UNIQUE(run_id, entity_type));
198
+ CREATE INDEX IF NOT EXISTS idx_runs_partition_task_updated ON runs(partition, task_id, updated_at);
199
+ CREATE INDEX IF NOT EXISTS idx_events_run_time ON events(run_id, event_time, event_id);
200
+ CREATE INDEX IF NOT EXISTS idx_artifacts_run_path ON artifacts(run_id, path);
201
+ CREATE INDEX IF NOT EXISTS idx_evidence_claims_run_task ON evidence_claims(run_id, task_id, acceptance_id);
202
+ `);
203
+ db.exec(`PRAGMA user_version = ${RUNTIME_STORE_SCHEMA_VERSION}`);
204
+ const now = new Date().toISOString();
205
+ db.prepare('INSERT OR REPLACE INTO runtime_meta (key, value, updated_at) VALUES (?, ?, ?)').run('contract', RUNTIME_STORE_CONTRACT_VERSION, now);
206
+ db.prepare('INSERT OR REPLACE INTO runtime_meta (key, value, updated_at) VALUES (?, ?, ?)').run('schema_version', String(RUNTIME_STORE_SCHEMA_VERSION), now);
207
+ }
208
+ function readRuntimeStoreSchemaVersion(db) {
209
+ const row = db.prepare('PRAGMA user_version').get();
210
+ return typeof row?.user_version === 'number' ? row.user_version : 0;
211
+ }
212
+ async function upsertRuntimeRunState(projectRoot, state, serializedState) {
213
+ await withRuntimeStore(projectRoot, ({ db }) => {
214
+ db.prepare(`INSERT INTO runs (run_id, status, phase, current_task, partition, git_branch, task_id, created_at, updated_at, state_json, state_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(run_id) DO UPDATE SET status=excluded.status, phase=excluded.phase, current_task=excluded.current_task, partition=excluded.partition, git_branch=excluded.git_branch, task_id=excluded.task_id, updated_at=excluded.updated_at, state_json=excluded.state_json, state_hash=excluded.state_hash`)
215
+ .run(state.runId, state.status, state.phase, state.currentTask, state.partition, state.gitBranch, state.taskId, state.createdAt, state.updatedAt, serializedState, hashDocumentContent(serializedState));
216
+ });
217
+ }
218
+ async function readRuntimeRunState(projectRoot, runId) {
219
+ return withRuntimeStore(projectRoot, ({ db }) => {
220
+ const row = db.prepare('SELECT state_json FROM runs WHERE run_id = ?').get(runId);
221
+ return row?.state_json ? normalizeRunState(JSON.parse(row.state_json)) : null;
222
+ });
223
+ }
224
+ async function importLegacyRunStateIfNeeded(projectRoot, runId, statePath) {
225
+ if (!await exists(statePath)) {
226
+ return null;
227
+ }
228
+ try {
229
+ const raw = await readFile(statePath, 'utf8');
230
+ const contentHash = hashDocumentContent(raw);
231
+ return await withRuntimeStore(projectRoot, ({ db }) => {
232
+ const legacy = db.prepare('SELECT content_hash FROM legacy_imports WHERE run_id = ? AND entity_type = ?').get(runId, 'state');
233
+ const row = db.prepare('SELECT state_json, state_hash FROM runs WHERE run_id = ?').get(runId);
234
+ if (legacy?.content_hash === contentHash && row?.state_json && row.state_hash === contentHash) {
235
+ return normalizeRunState(JSON.parse(row.state_json));
236
+ }
237
+ const state = normalizeRunState(JSON.parse(raw));
238
+ db.prepare(`INSERT INTO runs (run_id, status, phase, current_task, partition, git_branch, task_id, created_at, updated_at, state_json, state_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(run_id) DO UPDATE SET status=excluded.status, phase=excluded.phase, current_task=excluded.current_task, partition=excluded.partition, git_branch=excluded.git_branch, task_id=excluded.task_id, created_at=excluded.created_at, updated_at=excluded.updated_at, state_json=excluded.state_json, state_hash=excluded.state_hash`)
239
+ .run(state.runId, state.status, state.phase, state.currentTask, state.partition, state.gitBranch, state.taskId, state.createdAt, state.updatedAt, raw, contentHash);
240
+ for (const artifact of state.artifacts) {
241
+ const payload = JSON.stringify({ ...artifact, status: 'legacy_imported' });
242
+ db.prepare('INSERT OR REPLACE INTO artifacts (artifact_id, run_id, path, kind, task_id, agent, content_hash, bytes, status, created_at, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
243
+ .run(runtimeScopedId(state.runId, artifact.path, hashDocumentContent(payload)), state.runId, artifact.path, artifact.kind, artifact.task, artifact.agent, hashDocumentContent(payload), Buffer.byteLength(payload, 'utf8'), 'legacy_imported', artifact.createdAt, payload);
244
+ }
245
+ for (const record of Object.values(state.artifactIngestions ?? {})) {
246
+ db.prepare('INSERT OR REPLACE INTO artifact_ingestions (ingestion_id, run_id, delegation_id, task_id, agent, artifact_path, status, result_status, ingested_at, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
247
+ .run(runtimeScopedId(record.runId, record.delegationId, record.artifactPath), record.runId, record.delegationId, record.task, record.agent, record.artifactPath, record.status, record.resultStatus, record.ingestedAt, JSON.stringify(record));
248
+ }
249
+ db.prepare('INSERT OR REPLACE INTO legacy_imports (import_id, run_id, entity_type, content_hash, imported_at, status, issue) VALUES (?, ?, ?, ?, ?, ?, ?)')
250
+ .run(legacyImportId(runId, 'state'), runId, 'state', contentHash, new Date().toISOString(), 'imported', null);
251
+ return state;
252
+ });
253
+ }
254
+ catch (error) {
255
+ await recordLegacyImportFailure(projectRoot, runId, 'state', error);
256
+ throw new RuntimeStoreError('LEGACY_IMPORT_FAILED', `Cannot import legacy state for ${runId}: ${messageFromError(error)}`, { cause: error });
257
+ }
258
+ }
259
+ async function recordRuntimeEvent(projectRoot, event, source = 'runtime') {
260
+ const eventJson = JSON.stringify(event);
261
+ await withRuntimeStore(projectRoot, ({ db }) => {
262
+ db.prepare('INSERT OR IGNORE INTO events (run_id, event_time, event_name, event_hash, event_json, source) VALUES (?, ?, ?, ?, ?, ?)')
263
+ .run(event.runId, event.time, event.event, hashDocumentContent(eventJson), eventJson, source);
264
+ });
265
+ }
266
+ async function importLegacyRunEventsIfNeeded(projectRoot, runId, eventPath) {
267
+ if (!await exists(eventPath)) {
268
+ return;
269
+ }
270
+ try {
271
+ const raw = await readFile(eventPath, 'utf8');
272
+ const contentHash = hashDocumentContent(raw);
273
+ await withRuntimeStore(projectRoot, ({ db }) => {
274
+ const legacy = db.prepare('SELECT content_hash FROM legacy_imports WHERE run_id = ? AND entity_type = ?').get(runId, 'events');
275
+ if (legacy?.content_hash === contentHash) {
276
+ return;
277
+ }
278
+ for (const line of raw.split(/\r?\n/).filter((item) => item.trim().length > 0)) {
279
+ const event = JSON.parse(line);
280
+ const eventJson = JSON.stringify(event);
281
+ db.prepare('INSERT OR IGNORE INTO events (run_id, event_time, event_name, event_hash, event_json, source) VALUES (?, ?, ?, ?, ?, ?)')
282
+ .run(event.runId, event.time, event.event, hashDocumentContent(eventJson), eventJson, 'legacy');
283
+ }
284
+ db.prepare('INSERT OR REPLACE INTO legacy_imports (import_id, run_id, entity_type, content_hash, imported_at, status, issue) VALUES (?, ?, ?, ?, ?, ?, ?)')
285
+ .run(legacyImportId(runId, 'events'), runId, 'events', contentHash, new Date().toISOString(), 'imported', null);
286
+ });
287
+ }
288
+ catch (error) {
289
+ await recordLegacyImportFailure(projectRoot, runId, 'events', error);
290
+ throw new RuntimeStoreError('LEGACY_IMPORT_FAILED', `Cannot import legacy events for ${runId}: ${messageFromError(error)}`, { cause: error });
291
+ }
292
+ }
293
+ async function readRuntimeRunEvents(projectRoot, runId) {
294
+ return withRuntimeStore(projectRoot, ({ db }) => {
295
+ const rows = db.prepare('SELECT event_json FROM events WHERE run_id = ? ORDER BY event_time ASC, event_id ASC').all(runId);
296
+ return rows.map((row) => JSON.parse(row.event_json));
297
+ });
298
+ }
299
+ async function recordRuntimeArtifact(projectRoot, input) {
300
+ const contentHash = hashDocumentContent(input.content);
301
+ const now = new Date().toISOString();
302
+ const payload = JSON.stringify({ path: input.path, status: input.status, taskId: input.taskId ?? null, agent: input.agent ?? null });
303
+ await withRuntimeStore(projectRoot, ({ db }) => {
304
+ db.prepare('INSERT OR REPLACE INTO artifacts (artifact_id, run_id, path, kind, task_id, agent, content_hash, bytes, status, created_at, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
305
+ .run(runtimeScopedId(input.runId, input.path, contentHash), input.runId, input.path, artifactKind(input.path), input.taskId ?? null, input.agent ?? null, contentHash, Buffer.byteLength(input.content, 'utf8'), input.status, now, payload);
306
+ });
307
+ }
308
+ async function recordRuntimeActivity(projectRoot, entry) {
309
+ await withRuntimeStore(projectRoot, ({ db }) => {
310
+ db.prepare('INSERT OR REPLACE INTO activities (activity_id, run_id, task_id, branch, kind, ref, status, created_at, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
311
+ .run(entry.entryId, entry.runId, entry.taskId ?? null, entry.branch ?? null, entry.kind, entry.ref, entry.status ?? null, entry.timestamp, JSON.stringify(entry));
312
+ });
313
+ }
314
+ async function recordRuntimeArtifactIngestion(projectRoot, record) {
315
+ await withRuntimeStore(projectRoot, ({ db }) => {
316
+ db.prepare('INSERT OR REPLACE INTO artifact_ingestions (ingestion_id, run_id, delegation_id, task_id, agent, artifact_path, status, result_status, ingested_at, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
317
+ .run(runtimeScopedId(record.runId, record.delegationId, record.artifactPath), record.runId, record.delegationId, record.task, record.agent, record.artifactPath, record.status, record.resultStatus, record.ingestedAt, JSON.stringify(record));
318
+ });
319
+ }
320
+ async function recordRuntimeEvidenceAdmission(projectRoot, state, ingestion, trust) {
321
+ const now = new Date().toISOString();
322
+ await withRuntimeStore(projectRoot, ({ db }) => {
323
+ for (const claim of trust?.claims ?? []) {
324
+ db.prepare('INSERT OR REPLACE INTO evidence_claims (claim_id, run_id, partition, task_id, acceptance_id, coverage_status, source_artifact, is_derived, created_at, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
325
+ .run(runtimeScopedId(ingestion.runId, claim.task, claim.acceptance, claim.sourceArtifact), ingestion.runId, state.partition, claim.task, claim.acceptance, claim.status, claim.sourceArtifact, isDerivedEvidenceRef(claim.sourceArtifact) ? 1 : 0, now, JSON.stringify(claim));
326
+ }
327
+ const issueCodes = uniqueEvidenceIssueCodes(trust?.issues ?? ingestion.issues);
328
+ if ((trust?.issues.length ?? ingestion.issues.length) > 0 || ingestion.status === 'rejected') {
329
+ db.prepare('INSERT OR REPLACE INTO policy_decisions (decision_id, run_id, task_id, acceptance_id, status, issue_codes, created_at, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)')
330
+ .run(runtimeScopedId(ingestion.runId, ingestion.delegationId, ingestion.artifactPath, 'admission'), ingestion.runId, ingestion.task, null, ingestion.status, issueCodes.join(','), now, JSON.stringify({ ingestion, trust: trust ?? null }));
331
+ }
332
+ });
333
+ }
334
+ async function readRuntimeEvidenceClaims(projectRoot, runId, taskId) {
335
+ return withRuntimeStore(projectRoot, ({ db }) => {
336
+ const rows = taskId
337
+ ? db.prepare('SELECT c.payload_json FROM evidence_claims c JOIN runs r ON r.run_id = c.run_id WHERE c.run_id = ? AND c.task_id = ? AND (c.partition IS NULL OR r.partition IS NULL OR c.partition = r.partition) ORDER BY c.created_at ASC, c.claim_id ASC').all(runId, taskId)
338
+ : db.prepare('SELECT c.payload_json FROM evidence_claims c JOIN runs r ON r.run_id = c.run_id WHERE c.run_id = ? AND (c.partition IS NULL OR r.partition IS NULL OR c.partition = r.partition) ORDER BY c.created_at ASC, c.claim_id ASC').all(runId);
339
+ return rows.map((row) => JSON.parse(row.payload_json));
340
+ });
341
+ }
342
+ async function hasRuntimeEvidenceScopeViolation(projectRoot, runId, taskId) {
343
+ return withRuntimeStore(projectRoot, ({ db }) => {
344
+ const row = taskId
345
+ ? db.prepare('SELECT COUNT(*) AS count FROM evidence_claims c JOIN runs r ON r.run_id = c.run_id WHERE c.run_id = ? AND c.task_id = ? AND c.partition IS NOT NULL AND r.partition IS NOT NULL AND c.partition <> r.partition').get(runId, taskId)
346
+ : db.prepare('SELECT COUNT(*) AS count FROM evidence_claims c JOIN runs r ON r.run_id = c.run_id WHERE c.run_id = ? AND c.partition IS NOT NULL AND r.partition IS NOT NULL AND c.partition <> r.partition').get(runId);
347
+ return (row?.count ?? 0) > 0;
348
+ });
349
+ }
350
+ async function recordRuntimeProjection(projectRoot, projectionType, scopeKey, payload) {
351
+ const now = new Date().toISOString();
352
+ await withRuntimeStore(projectRoot, ({ db }) => {
353
+ db.prepare('INSERT OR REPLACE INTO projections (projection_id, projection_type, scope_key, generated_at, payload_json) VALUES (?, ?, ?, ?, ?)')
354
+ .run(runtimeScopedId(projectionType, scopeKey), projectionType, scopeKey, now, JSON.stringify(payload));
355
+ });
356
+ }
357
+ async function recordLegacyImportFailure(projectRoot, runId, entityType, error) {
358
+ try {
359
+ await withRuntimeStore(projectRoot, ({ db }) => {
360
+ db.prepare('INSERT OR REPLACE INTO legacy_imports (import_id, run_id, entity_type, content_hash, imported_at, status, issue) VALUES (?, ?, ?, ?, ?, ?, ?)')
361
+ .run(legacyImportId(runId, entityType), runId, entityType, 'unavailable', new Date().toISOString(), 'failed', messageFromError(error));
362
+ });
363
+ }
364
+ catch {
365
+ return;
366
+ }
367
+ }
368
+ function runtimeScopedId(...parts) {
369
+ return createHash('sha256').update(parts.join('\0'), 'utf8').digest('hex').slice(0, 32);
370
+ }
371
+ function legacyImportId(runId, entityType) {
372
+ return runtimeScopedId(runId, entityType);
373
+ }
374
+ async function inspectRuntimeStoreEvidence(projectRoot) {
375
+ try {
376
+ return await withRuntimeStore(projectRoot, ({ path: storePath, db }) => {
377
+ const versionRow = db.prepare('PRAGMA user_version').get();
378
+ const integrityRow = db.prepare('PRAGMA integrity_check').get();
379
+ const schemaVersion = versionRow?.user_version ?? 0;
380
+ if (schemaVersion !== RUNTIME_STORE_SCHEMA_VERSION) {
381
+ return [{ level: 'FAIL', check: 'runtime_store', message: `Runtime store schema version ${schemaVersion} does not match expected ${RUNTIME_STORE_SCHEMA_VERSION}.`, action: 'Run a compatible sdd version or rebuild the runtime store from legacy .sdd/runs.' }];
382
+ }
383
+ if (integrityRow?.integrity_check !== 'ok') {
384
+ return [{ level: 'FAIL', check: 'runtime_store', message: `Runtime store integrity check failed: ${integrityRow?.integrity_check ?? 'unknown'}.`, action: 'Rebuild runtime projections from legacy .sdd/runs after preserving the database for debugging.' }];
385
+ }
386
+ const checks = [{ level: 'PASS', check: 'runtime_store', message: `${RUNTIME_STORE_CONTRACT_VERSION} is available at ${storePath} with schema ${schemaVersion}.` }];
387
+ const scopeLeak = db.prepare('SELECT COUNT(*) AS count FROM evidence_claims c JOIN runs r ON r.run_id = c.run_id WHERE c.partition IS NOT NULL AND r.partition IS NOT NULL AND c.partition <> r.partition').get();
388
+ checks.push(((scopeLeak?.count ?? 0) > 0)
389
+ ? { level: 'FAIL', check: 'runtime_partition_scope', message: `Runtime store has ${scopeLeak?.count ?? 0} cross-partition evidence claim(s).`, action: 'Reingest validator artifacts for the correct run partition; mismatched claims cannot satisfy PASS evidence.' }
390
+ : { level: 'PASS', check: 'runtime_partition_scope', message: 'Runtime evidence claims match their run partition scope.' });
391
+ const freshness = db.prepare('SELECT (SELECT MAX(updated_at) FROM runs) AS latest_run, (SELECT MAX(generated_at) FROM projections) AS latest_projection').get();
392
+ checks.push(freshness?.latest_run && freshness?.latest_projection && freshness.latest_projection < freshness.latest_run
393
+ ? { level: 'PASS', check: 'runtime_projection_freshness', message: `Latest runtime projection ${freshness.latest_projection} is older than latest run update ${freshness.latest_run}; views can be refreshed on demand.` }
394
+ : { level: 'PASS', check: 'runtime_projection_freshness', message: freshness?.latest_projection ? 'Runtime projections are current with known run updates.' : 'No runtime projections have been materialized yet.' });
395
+ const legacy = db.prepare("SELECT status, COUNT(*) AS count FROM legacy_imports GROUP BY status").all();
396
+ const failedLegacy = legacy.find((row) => row.status === 'failed')?.count ?? 0;
397
+ const importedLegacy = legacy.find((row) => row.status === 'imported')?.count ?? 0;
398
+ checks.push(failedLegacy > 0
399
+ ? { level: 'FAIL', check: 'runtime_legacy_import', message: `${failedLegacy} legacy import record(s) failed.`, action: 'Inspect legacy .sdd/runs files and repair malformed state/events/invocation evidence before verify or sync-back.' }
400
+ : importedLegacy > 0
401
+ ? { level: 'PASS', check: 'runtime_legacy_import', message: `${importedLegacy} legacy runtime record(s) imported non-destructively; trust debt was preserved, not upgraded.` }
402
+ : { level: 'PASS', check: 'runtime_legacy_import', message: 'No legacy runtime import debt detected.' });
403
+ return checks;
404
+ });
405
+ }
406
+ catch (error) {
407
+ return [{ level: 'FAIL', check: 'runtime_store', message: `Runtime store unavailable: ${messageFromError(error)}`, action: 'Use a Node runtime with node:sqlite support and writable .sdd/runtime.sqlite.' }];
408
+ }
409
+ }
85
410
  export function getWorkerRuntimeRecordPath(projectRoot, runId, runtimeId) {
86
411
  assertSafePathSegment(runtimeId, 'runtimeId');
87
412
  return path.join(getWorkerRuntimesDir(projectRoot, runId), `${runtimeId}.json`);
@@ -117,11 +442,116 @@ export async function writeArtifact(projectRoot, runId, artifactRootRelativePath
117
442
  const absolutePath = getArtifactPath(projectRoot, runId, normalized);
118
443
  await mkdir(path.dirname(absolutePath), { recursive: true });
119
444
  await writeFile(absolutePath, content, 'utf8');
120
- return { absolutePath, runRelativePath: getRunRelativeArtifactPath(normalized) };
445
+ const runRelativePath = getRunRelativeArtifactPath(normalized);
446
+ await appendArtifactHashLedgerEntry(projectRoot, {
447
+ runId,
448
+ artifactPath: runRelativePath,
449
+ content,
450
+ status: 'written'
451
+ });
452
+ await recordRuntimeArtifact(projectRoot, { runId, path: runRelativePath, content, status: 'written' });
453
+ return { absolutePath, runRelativePath };
121
454
  }
122
455
  export async function readArtifact(projectRoot, runId, artifactRootRelativePath) {
123
456
  return readFile(getArtifactPath(projectRoot, runId, normalizeArtifactRootRelativePath(artifactRootRelativePath)), 'utf8');
124
457
  }
458
+ export async function appendInvocationLedgerEntry(projectRoot, input) {
459
+ const timestamp = new Date().toISOString();
460
+ const entry = {
461
+ contract: 'sdd-invocation-ledger-v1',
462
+ version: INVOCATION_LEDGER_CONTRACT_VERSION,
463
+ entryId: createHash('sha256').update(`${input.runId}:${input.kind}:${input.ref}:${timestamp}`).digest('hex').slice(0, 16),
464
+ timestamp,
465
+ ...input
466
+ };
467
+ await appendFile(getInvocationLedgerPath(projectRoot, input.runId), `${JSON.stringify(entry)}\n`, 'utf8');
468
+ await recordRuntimeActivity(projectRoot, entry);
469
+ return entry;
470
+ }
471
+ async function importLegacyInvocationLedgerIfNeeded(projectRoot, runId, ledgerPath) {
472
+ if (!await exists(ledgerPath)) {
473
+ return;
474
+ }
475
+ try {
476
+ const raw = await readFile(ledgerPath, 'utf8');
477
+ const contentHash = hashDocumentContent(raw);
478
+ await withRuntimeStore(projectRoot, ({ db }) => {
479
+ const legacy = db.prepare('SELECT content_hash FROM legacy_imports WHERE run_id = ? AND entity_type = ?').get(runId, 'invocations');
480
+ if (legacy?.content_hash === contentHash) {
481
+ return;
482
+ }
483
+ for (const line of raw.split(/\r?\n/).filter(Boolean)) {
484
+ const entry = JSON.parse(line);
485
+ db.prepare('INSERT OR REPLACE INTO activities (activity_id, run_id, task_id, branch, kind, ref, status, created_at, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
486
+ .run(entry.entryId, entry.runId, entry.taskId ?? null, entry.branch ?? null, entry.kind, entry.ref, entry.status ?? null, entry.timestamp, JSON.stringify(entry));
487
+ }
488
+ db.prepare('INSERT OR REPLACE INTO legacy_imports (import_id, run_id, entity_type, content_hash, imported_at, status, issue) VALUES (?, ?, ?, ?, ?, ?, ?)')
489
+ .run(legacyImportId(runId, 'invocations'), runId, 'invocations', contentHash, new Date().toISOString(), 'imported', null);
490
+ });
491
+ }
492
+ catch (error) {
493
+ await recordLegacyImportFailure(projectRoot, runId, 'invocations', error);
494
+ throw new RuntimeStoreError('LEGACY_IMPORT_FAILED', `Cannot import legacy invocation ledger for ${runId}: ${messageFromError(error)}`, { cause: error });
495
+ }
496
+ }
497
+ export async function listInvocationLedgerEntries(projectRoot, runId) {
498
+ const ledgerPath = getInvocationLedgerPath(projectRoot, runId);
499
+ if (!await exists(ledgerPath)) {
500
+ return [];
501
+ }
502
+ await importLegacyInvocationLedgerIfNeeded(projectRoot, runId, ledgerPath);
503
+ const raw = await readFile(ledgerPath, 'utf8');
504
+ return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
505
+ }
506
+ async function appendArtifactHashLedgerEntry(projectRoot, input) {
507
+ return appendUniqueInvocationLedgerEntry(projectRoot, {
508
+ runId: input.runId,
509
+ taskId: input.taskId ?? null,
510
+ branch: input.branch ?? null,
511
+ kind: 'artifact_hash',
512
+ ref: input.artifactPath,
513
+ status: input.status ?? 'observed',
514
+ artifactPath: input.artifactPath,
515
+ outputHash: hashDocumentContent(input.content),
516
+ materialRefs: [],
517
+ metadata: { bytes: Buffer.byteLength(input.content, 'utf8') }
518
+ });
519
+ }
520
+ async function appendUniqueInvocationLedgerEntry(projectRoot, input) {
521
+ const existing = await findMatchingInvocationLedgerEntry(projectRoot, input);
522
+ if (existing) {
523
+ return existing;
524
+ }
525
+ return appendInvocationLedgerEntry(projectRoot, input);
526
+ }
527
+ async function findMatchingInvocationLedgerEntry(projectRoot, input) {
528
+ const ledgerPath = getInvocationLedgerPath(projectRoot, input.runId);
529
+ if (!await exists(ledgerPath)) {
530
+ return null;
531
+ }
532
+ const raw = await readFile(ledgerPath, 'utf8');
533
+ for (const line of raw.split(/\r?\n/).filter(Boolean)) {
534
+ const entry = JSON.parse(line);
535
+ if (entry.kind === input.kind && entry.ref === input.ref && entry.status === input.status && entry.outputHash === input.outputHash) {
536
+ return entry;
537
+ }
538
+ }
539
+ return null;
540
+ }
541
+ async function appendDeclaredCommandLedgerEntries(projectRoot, input) {
542
+ for (const command of input.commands) {
543
+ await appendUniqueInvocationLedgerEntry(projectRoot, {
544
+ runId: input.runId,
545
+ taskId: input.taskId,
546
+ branch: input.branch,
547
+ kind: 'command',
548
+ ref: command,
549
+ status: 'declared',
550
+ materialRefs: [],
551
+ metadata: { source: 'task.validation' }
552
+ });
553
+ }
554
+ }
125
555
  export async function writeAgentExecutionRecord(projectRoot, record) {
126
556
  await mkdir(getAgentExecutionsDir(projectRoot, record.runId), { recursive: true });
127
557
  await writeFile(getAgentExecutionRecordPath(projectRoot, record.runId, record.executionId), `${JSON.stringify(record, null, 2)}\n`, 'utf8');
@@ -744,8 +1174,15 @@ export async function createRun(projectRoot, options = {}) {
744
1174
  }
745
1175
  export async function readRunState(projectRoot, runId) {
746
1176
  const statePath = path.join(getRunDir(projectRoot, runId), 'state.json');
747
- const raw = await readFile(statePath, 'utf8');
748
- return normalizeRunState(JSON.parse(raw));
1177
+ const legacyState = await importLegacyRunStateIfNeeded(projectRoot, runId, statePath);
1178
+ if (legacyState) {
1179
+ return legacyState;
1180
+ }
1181
+ const storedState = await readRuntimeRunState(projectRoot, runId);
1182
+ if (storedState) {
1183
+ return storedState;
1184
+ }
1185
+ throw new Error(`Run state not found for ${runId}.`);
749
1186
  }
750
1187
  export async function writeRunState(projectRoot, state) {
751
1188
  const nextState = normalizeRunState({
@@ -753,7 +1190,9 @@ export async function writeRunState(projectRoot, state) {
753
1190
  updatedAt: new Date().toISOString()
754
1191
  });
755
1192
  const statePath = path.join(getRunDir(projectRoot, state.runId), 'state.json');
756
- await writeFile(statePath, `${JSON.stringify(nextState, null, 2)}\n`, 'utf8');
1193
+ const serializedState = `${JSON.stringify(nextState, null, 2)}\n`;
1194
+ await writeFile(statePath, serializedState, 'utf8');
1195
+ await upsertRuntimeRunState(projectRoot, nextState, serializedState);
757
1196
  }
758
1197
  function normalizeRunState(state) {
759
1198
  return {
@@ -799,6 +1238,7 @@ export async function appendEvent(projectRoot, runId, event) {
799
1238
  };
800
1239
  const eventPath = path.join(getRunDir(projectRoot, runId), 'events.jsonl');
801
1240
  await appendFile(eventPath, `${JSON.stringify(nextEvent)}\n`, 'utf8');
1241
+ await recordRuntimeEvent(projectRoot, nextEvent);
802
1242
  return nextEvent;
803
1243
  }
804
1244
  export async function archiveRun(projectRoot, runId, options = {}) {
@@ -854,6 +1294,7 @@ export async function listRuns(projectRoot) {
854
1294
  export async function rebuildLocalRunIndex(projectRoot) {
855
1295
  const index = await buildLocalRunIndexSnapshot(projectRoot);
856
1296
  await writeFile(getLocalRunIndexPath(projectRoot), `${JSON.stringify(index, null, 2)}\n`, 'utf8');
1297
+ await recordRuntimeProjection(projectRoot, 'local_run_index', 'all', index);
857
1298
  return index;
858
1299
  }
859
1300
  export async function readLocalRunIndex(projectRoot) {
@@ -868,7 +1309,7 @@ export async function queryLocalRunIndex(projectRoot, query = {}) {
868
1309
  .filter((run) => !query.taskId || run.taskIds.includes(query.taskId))
869
1310
  .filter((run) => !query.artifact || index.artifacts.some((artifact) => artifact.runId === run.runId && artifact.path === query.artifact))
870
1311
  .map((run) => run.runId));
871
- return {
1312
+ const result = {
872
1313
  ...index,
873
1314
  runs: index.runs.filter((run) => runIds.has(run.runId)),
874
1315
  tasks: index.tasks.filter((task) => runIds.has(task.runId) && (!query.taskId || task.taskId === query.taskId) && (!query.partition || task.partition === query.partition)),
@@ -878,6 +1319,8 @@ export async function queryLocalRunIndex(projectRoot, query = {}) {
878
1319
  latestByPartitionTask: index.latestByPartitionTask.filter((entry) => runIds.has(entry.runId) && (!query.partition || entry.partition === query.partition) && (!query.taskId || entry.taskId === query.taskId)),
879
1320
  activeByAffectedFile: index.activeByAffectedFile.filter((entry) => runIds.has(entry.runId) && (!query.partition || entry.partition === query.partition) && (!query.taskId || entry.taskId === query.taskId))
880
1321
  };
1322
+ await recordRuntimeProjection(projectRoot, 'local_run_index_query', hashDocumentContent(JSON.stringify(query)), result);
1323
+ return result;
881
1324
  }
882
1325
  export async function inspectLocalRunIndex(projectRoot) {
883
1326
  const indexPath = getLocalRunIndexPath(projectRoot);
@@ -1050,11 +1493,12 @@ function isActiveRunForAffectedFile(state) {
1050
1493
  }
1051
1494
  export async function inspectRun(projectRoot, runId) {
1052
1495
  const state = await readRunState(projectRoot, runId);
1053
- const [events, agentExecutions, teamSessions, workerRuntimes] = await Promise.all([
1496
+ const [events, agentExecutions, teamSessions, workerRuntimes, invocationLedger] = await Promise.all([
1054
1497
  readRunEvents(projectRoot, runId),
1055
1498
  listAgentExecutionRecords(projectRoot, runId),
1056
1499
  listTeamSessionRecords(projectRoot, runId),
1057
- listResidentWorkerRuntimeRecords(projectRoot, runId)
1500
+ listResidentWorkerRuntimeRecords(projectRoot, runId),
1501
+ listInvocationLedgerEntries(projectRoot, runId)
1058
1502
  ]);
1059
1503
  return {
1060
1504
  summary: summarizeRunState(state),
@@ -1067,14 +1511,15 @@ export async function inspectRun(projectRoot, runId) {
1067
1511
  worktrees: Object.values(state.worktrees ?? {}),
1068
1512
  validation: state.validation,
1069
1513
  syncBack: state.syncBack,
1070
- taskRunEvidence: buildTaskRunEvidence(state, events, agentExecutions, teamSessions, workerRuntimes),
1514
+ taskRunEvidence: buildTaskRunEvidence(state, events, agentExecutions, teamSessions, workerRuntimes, invocationLedger),
1071
1515
  tasks: state.tasks,
1072
1516
  agentExecutions,
1073
1517
  teamSessions,
1074
- workerRuntimes
1518
+ workerRuntimes,
1519
+ invocationLedger
1075
1520
  };
1076
1521
  }
1077
- function buildTaskRunEvidence(state, events, agentExecutions = [], teamSessions = [], workerRuntimes = []) {
1522
+ function buildTaskRunEvidence(state, events, agentExecutions = [], teamSessions = [], workerRuntimes = [], invocationLedger = []) {
1078
1523
  return {
1079
1524
  version: TASK_RUN_EVIDENCE_CONTRACT_VERSION,
1080
1525
  runId: state.runId,
@@ -1094,8 +1539,250 @@ function buildTaskRunEvidence(state, events, agentExecutions = [], teamSessions
1094
1539
  syncBackProposal: state.syncBack.proposalPath,
1095
1540
  agentExecutions,
1096
1541
  teamSessions,
1097
- workerRuntimes
1542
+ workerRuntimes,
1543
+ invocationLedger
1544
+ };
1545
+ }
1546
+ export function parseContextProfile(value) {
1547
+ if (!value || value === 'brief') {
1548
+ return 'brief';
1549
+ }
1550
+ if (value === 'normal' || value === 'forensic') {
1551
+ return value;
1552
+ }
1553
+ throw new Error(`Unsupported context profile: ${value}`);
1554
+ }
1555
+ export function contextBudgetForProfile(profile) {
1556
+ if (profile === 'forensic') {
1557
+ return { contract: CONTEXT_BUDGET_CONTRACT_VERSION, profile, maxBytes: 64 * 1024, preserve: ['blockers', 'failures', 'source_refs', 'complete_evidence'] };
1558
+ }
1559
+ if (profile === 'normal') {
1560
+ return { contract: CONTEXT_BUDGET_CONTRACT_VERSION, profile, maxBytes: 12 * 1024, preserve: ['blockers', 'failures', 'source_refs', 'next_action'] };
1561
+ }
1562
+ return { contract: CONTEXT_BUDGET_CONTRACT_VERSION, profile, maxBytes: 2 * 1024, preserve: ['blockers', 'current_task', 'next_action', 'source_refs'] };
1563
+ }
1564
+ export function summarizeCommandOutput(rawOutput, source, profile = 'brief') {
1565
+ const lines = rawOutput.split(/\r?\n/).filter((line) => line.trim().length > 0);
1566
+ const failureLines = lines.filter((line) => /\b(fail(?:ed|ure)?|error|blocked|exception|traceback|timed out|timeout)\b/i.test(line));
1567
+ const successLines = lines.filter((line) => /\b(pass(?:ed)?|success|completed|ok)\b/i.test(line));
1568
+ let maxHighlights = 5;
1569
+ if (profile === 'normal') {
1570
+ maxHighlights = 12;
1571
+ }
1572
+ else if (profile === 'forensic') {
1573
+ maxHighlights = lines.length;
1574
+ }
1575
+ const selected = [...failureLines, ...successLines, ...lines].filter((line, index, all) => all.indexOf(line) === index).slice(0, maxHighlights);
1576
+ let status = 'UNKNOWN';
1577
+ if (failureLines.some((line) => /\bblocked\b/i.test(line))) {
1578
+ status = 'BLOCKED';
1579
+ }
1580
+ else if (failureLines.length > 0) {
1581
+ status = 'FAIL';
1582
+ }
1583
+ else if (successLines.length > 0) {
1584
+ status = 'PASS';
1585
+ }
1586
+ return {
1587
+ contract: COMMAND_OUTPUT_SUMMARY_CONTRACT_VERSION,
1588
+ authoritative: false,
1589
+ usableForPass: false,
1590
+ source,
1591
+ status,
1592
+ highlights: selected,
1593
+ omittedLines: Math.max(0, lines.length - selected.length)
1594
+ };
1595
+ }
1596
+ export async function buildEvidenceSummaryProjection(projectRoot, options) {
1597
+ const state = await readRunState(projectRoot, options.runId);
1598
+ const [events, invocationLedger] = await Promise.all([
1599
+ readRunEvents(projectRoot, options.runId),
1600
+ listInvocationLedgerEntries(projectRoot, options.runId)
1601
+ ]);
1602
+ const taskId = options.taskId ?? state.currentTask ?? state.taskId;
1603
+ const admittedClaims = await readRuntimeEvidenceClaims(projectRoot, state.runId, taskId ?? null);
1604
+ const artifactIngestions = Object.values(state.artifactIngestions ?? {}).filter((record) => !taskId || record.task === taskId);
1605
+ const artifacts = state.artifacts.filter((artifact) => !taskId || artifact.task === taskId);
1606
+ const ledgerArtifactRefs = invocationLedger
1607
+ .filter((entry) => entry.kind === 'artifact_hash' && entry.artifactPath)
1608
+ .map((entry) => ({ path: entry.artifactPath, kind: 'artifact' }));
1609
+ const artifactSourceRefs = [
1610
+ ...artifacts.map((artifact) => ({ path: `.sdd/runs/${state.runId}/${artifact.path}`, kind: 'artifact' })),
1611
+ ...ledgerArtifactRefs.map((artifact) => ({ path: `.sdd/runs/${state.runId}/${artifact.path}`, kind: artifact.kind }))
1612
+ ];
1613
+ const sources = await uniqueContextSourceRefs([
1614
+ await contextSourceRefForProjectPath(projectRoot, `.sdd/runs/${state.runId}/state.json`, 'run_state'),
1615
+ await contextSourceRefForProjectPath(projectRoot, `.sdd/runs/${state.runId}/events.jsonl`, 'command_output'),
1616
+ await contextSourceRefForProjectPath(projectRoot, `.sdd/runs/${state.runId}/invocations.jsonl`, 'ledger'),
1617
+ ...(await Promise.all(artifactSourceRefs.map((artifact) => contextSourceRefForProjectPath(projectRoot, artifact.path, artifact.kind))))
1618
+ ]);
1619
+ const issueCodes = uniqueEvidenceIssueCodes(artifactIngestions.flatMap((record) => record.issues));
1620
+ const passCount = Math.max(admittedClaims.filter((claim) => claim.status === 'PASS').length, artifactIngestions.filter((record) => record.status === 'accepted' && (record.resultStatus === 'PASS' || record.resultStatus === 'PASS_WITH_GAPS')).length);
1621
+ const failCount = Math.max(admittedClaims.filter((claim) => claim.status === 'FAIL').length, artifactIngestions.filter((record) => record.status === 'rejected' || record.resultStatus === 'FAIL').length);
1622
+ const blockedCount = Math.max(admittedClaims.filter((claim) => claim.status === 'BLOCKED').length, artifactIngestions.filter((record) => record.resultStatus === 'BLOCKED' || record.resultStatus === 'TIMED_OUT' || record.resultStatus === 'CANCELLED' || record.gaps.length > 0).length);
1623
+ const highlights = [
1624
+ `run=${state.runId} status=${state.status} phase=${state.phase ?? 'none'}`,
1625
+ `task=${taskId ?? 'none'} validation=${state.validation.status} sync_back=${state.syncBack.status}`,
1626
+ `artifacts=${artifacts.length} ingestions=${artifactIngestions.length} admitted_claims=${admittedClaims.length} events=${events.length} ledger=${invocationLedger.length}`,
1627
+ `pass=${passCount} blocked=${blockedCount} fail=${failCount}`
1628
+ ];
1629
+ if (issueCodes.length > 0) {
1630
+ highlights.push(`issues=${issueCodes.join(',')}`);
1631
+ }
1632
+ const projection = {
1633
+ contract: EVIDENCE_SUMMARY_CONTRACT_VERSION,
1634
+ authoritative: false,
1635
+ usableForPass: false,
1636
+ runId: state.runId,
1637
+ taskId: taskId ?? null,
1638
+ sources,
1639
+ passCount,
1640
+ blockedCount,
1641
+ failCount,
1642
+ issueCodes,
1643
+ policyRefs: [
1644
+ `${ACCEPTANCE_POLICY_RULESET_VERSION}:require-source-evidence`,
1645
+ `${ACCEPTANCE_POLICY_RULESET_VERSION}:require-provenance`,
1646
+ `${ACCEPTANCE_POLICY_RULESET_VERSION}:reject-derived-source-evidence`
1647
+ ],
1648
+ highlights
1649
+ };
1650
+ await recordRuntimeProjection(projectRoot, 'evidence_summary', `${state.runId}:${taskId ?? 'all'}`, projection);
1651
+ return projection;
1652
+ }
1653
+ export async function buildContextBuildPackage(projectRoot, options) {
1654
+ const profile = options.profile ?? 'brief';
1655
+ const branch = options.branch ?? (await resolveSddContext(projectRoot)).partition;
1656
+ const model = await parseSddBranch(projectRoot, branch);
1657
+ const inspected = inspectSddTask(model, options.taskId);
1658
+ if (!inspected.task) {
1659
+ throw new Error(`Task not found: ${options.taskId}`);
1660
+ }
1661
+ const task = inspected.task;
1662
+ const docRefs = await Promise.all([
1663
+ contextSourceRefForAbsolutePath(projectRoot, model.tasksPath, 'document'),
1664
+ contextSourceRefForAbsolutePath(projectRoot, model.planPath, 'document'),
1665
+ contextSourceRefForAbsolutePath(projectRoot, model.specPath, 'document')
1666
+ ]);
1667
+ const affectedRefs = await Promise.all(task.affectedFiles.map((file) => contextSourceRefForProjectPath(projectRoot, file, 'document')));
1668
+ const artifactRefs = await Promise.all(task.requiredArtifacts.map((artifact) => contextSourceRefForProjectPath(projectRoot, artifact, 'artifact')));
1669
+ const routeRef = await contextSourceRefForProjectPath(projectRoot, `.sdd/cache/routes`, 'derived');
1670
+ const runIndexRef = await contextSourceRefForProjectPath(projectRoot, `.sdd/run-index.json`, 'derived');
1671
+ const mustRead = uniqueContextSourceRefs(contextMustReadRefs(options.mode, docRefs, affectedRefs, artifactRefs));
1672
+ const optionalRead = uniqueContextSourceRefs(contextOptionalRefs(options.mode, docRefs, affectedRefs, artifactRefs, routeRef, runIndexRef, options.agent ?? null));
1673
+ const doNotReadUnlessNeeded = uniqueContextSourceRefs(contextDeferredRefs(options.mode, docRefs, affectedRefs, artifactRefs, routeRef, runIndexRef));
1674
+ const contextPackage = {
1675
+ contract: CONTEXT_PACKAGE_CONTRACT_VERSION,
1676
+ profile,
1677
+ mode: options.mode,
1678
+ agent: options.agent ?? null,
1679
+ authoritative: false,
1680
+ usableForPass: false,
1681
+ taskId: task.id,
1682
+ branch,
1683
+ mustRead,
1684
+ optionalRead,
1685
+ doNotReadUnlessNeeded,
1686
+ nextCommands: contextNextCommands(task.id, branch, options.mode, options.agent),
1687
+ warnings: [
1688
+ 'Context package is derived guidance only and cannot satisfy PASS evidence.',
1689
+ ...inspected.gaps.map((gap) => `${gap.field}: ${gap.message}`)
1690
+ ]
1098
1691
  };
1692
+ await recordRuntimeProjection(projectRoot, 'context_build', `${branch}:${task.id}:${options.mode}:${options.agent ?? 'none'}:${profile}`, contextPackage);
1693
+ return contextPackage;
1694
+ }
1695
+ export function validateLogWorkerSummary(summary) {
1696
+ const issues = [];
1697
+ const candidate = summary;
1698
+ if (candidate.contract !== LOG_WORKER_SUMMARY_CONTRACT_VERSION) {
1699
+ issues.push(contractIssue('contract', 'Log worker summary contract is invalid.', `Use ${LOG_WORKER_SUMMARY_CONTRACT_VERSION}.`));
1700
+ }
1701
+ if (candidate.authoritative !== false) {
1702
+ issues.push(contractIssue('authoritative', 'Log worker summary must be non-authoritative.', 'Set authoritative=false and keep workflow decisions in core runtime.'));
1703
+ }
1704
+ if (candidate.usableForPass !== false) {
1705
+ issues.push(contractIssue('usableForPass', 'Log worker summary cannot be used for PASS evidence.', 'Set usableForPass=false and reference source artifacts for PASS evidence.'));
1706
+ }
1707
+ const forbiddenAuthority = Array.isArray(candidate.forbiddenAuthority) ? candidate.forbiddenAuthority.filter((item) => typeof item === 'string') : [];
1708
+ if (forbiddenAuthority.length > 0) {
1709
+ issues.push(contractIssue('forbiddenAuthority', `Log worker summary claims forbidden workflow authority: ${forbiddenAuthority.join(', ')}.`, 'Remove PASS/BLOCKED/route/doctor/sync-back decisions from worker summaries.'));
1710
+ }
1711
+ for (const field of ['status', 'verdict', 'routeDecision', 'doctorVerdict', 'syncBackReady']) {
1712
+ if (field in candidate) {
1713
+ issues.push(contractIssue(field, `Log worker summary must not expose workflow decision field ${field}.`, 'Keep decision fields in core runtime outputs only.'));
1714
+ }
1715
+ }
1716
+ return { valid: issues.length === 0, issues };
1717
+ }
1718
+ async function contextSourceRefForProjectPath(projectRoot, relativePath, kind) {
1719
+ const normalized = normalizePortablePath(relativePath);
1720
+ return contextSourceRefForAbsolutePath(projectRoot, path.join(projectRoot, normalized), kind, normalized);
1721
+ }
1722
+ async function contextSourceRefForAbsolutePath(projectRoot, absolutePath, kind, displayPath) {
1723
+ const relativePath = displayPath ?? normalizePortablePath(path.relative(projectRoot, absolutePath));
1724
+ if (!relativePath || relativePath.startsWith('../') || path.isAbsolute(relativePath)) {
1725
+ return { path: normalizePortablePath(absolutePath), hash: hashDocumentContent(`external:${absolutePath}`), kind };
1726
+ }
1727
+ if (!await exists(absolutePath)) {
1728
+ return { path: relativePath, hash: hashDocumentContent(`missing:${relativePath}`), kind };
1729
+ }
1730
+ const fileStat = await stat(absolutePath);
1731
+ if (fileStat.isDirectory()) {
1732
+ return { path: relativePath, hash: hashDocumentContent(`directory:${relativePath}`), kind };
1733
+ }
1734
+ return { path: relativePath, hash: hashDocumentContent(await readFile(absolutePath, 'utf8')), kind };
1735
+ }
1736
+ function uniqueContextSourceRefs(refs) {
1737
+ return [...new Map(refs.map((ref) => [`${ref.kind}:${ref.path}`, ref])).values()];
1738
+ }
1739
+ function uniqueEvidenceIssueCodes(issues) {
1740
+ const known = ['EMPTY_EVIDENCE', 'TODO_PLACEHOLDER', 'TEMPLATE_TEXT', 'MENTION_ONLY', 'UNSOURCED_PASS', 'MISSING_COMMAND_OUTPUT', 'MISSING_ARTIFACT_REFERENCE', 'MISSING_MATERIAL_REFERENCE', 'PROVENANCE_GAP', 'POLICY_RULE_FAILED', 'DERIVED_SOURCE_EVIDENCE', 'PARTITION_SCOPE_VIOLATION'];
1741
+ return known.filter((code) => issues.some((issue) => issue.message.includes(code)));
1742
+ }
1743
+ function contextMustReadRefs(mode, docs, affected, artifacts) {
1744
+ if (mode === 'do') {
1745
+ return [docs[0], docs[1], ...affected];
1746
+ }
1747
+ if (mode === 'verify') {
1748
+ return [docs[0], ...artifacts];
1749
+ }
1750
+ if (mode === 'sync-back') {
1751
+ return [docs[0], ...affected];
1752
+ }
1753
+ return [docs[0]];
1754
+ }
1755
+ function contextOptionalRefs(mode, docs, affected, artifacts, routeRef, runIndexRef, agent) {
1756
+ const refs = mode === 'doctor' ? [docs[1], docs[2], runIndexRef, routeRef, ...artifacts] : [docs[2], ...artifacts];
1757
+ if (agent === 'implementer') {
1758
+ refs.push(...affected);
1759
+ }
1760
+ if (agent === 'reviewer' || agent === 'validator') {
1761
+ refs.push(runIndexRef);
1762
+ }
1763
+ return refs;
1764
+ }
1765
+ function contextDeferredRefs(mode, docs, affected, artifacts, routeRef, runIndexRef) {
1766
+ if (mode === 'doctor') {
1767
+ return [...affected];
1768
+ }
1769
+ if (mode === 'verify') {
1770
+ return [...affected, routeRef];
1771
+ }
1772
+ return [runIndexRef, ...artifacts, docs[2]];
1773
+ }
1774
+ function contextNextCommands(taskId, branch, mode, agent) {
1775
+ if (mode === 'do') {
1776
+ return [`sdd do task ${taskId} --branch ${branch}`];
1777
+ }
1778
+ if (mode === 'verify') {
1779
+ return [`sdd verify task ${taskId} --branch ${branch}`, `sdd evidence summary <run_id> --task ${taskId} --json`];
1780
+ }
1781
+ if (mode === 'sync-back') {
1782
+ return [`sdd sync-back inspect --branch ${branch} --task ${taskId}`];
1783
+ }
1784
+ const agentSuffix = agent ? ` --agent ${agent}` : '';
1785
+ return [`sdd doctor --latest-only --branch ${branch}`, `sdd context build --task ${taskId} --branch ${branch} --mode verify${agentSuffix} --json`];
1099
1786
  }
1100
1787
  function stringData(data, key) {
1101
1788
  const value = data?.[key];
@@ -1454,13 +2141,22 @@ export async function inspectSyncBack(projectRoot, options) {
1454
2141
  reasons.push(`Affected file ${conflict.file} is active in run ${conflict.runId} for ${conflict.partition}/${conflict.taskId}.`);
1455
2142
  }
1456
2143
  const proposalPath = state.syncBack.proposalPath;
2144
+ const expectedProposalDigest = state.syncBack.proposalDigest ?? null;
1457
2145
  let proposal = null;
2146
+ let proposalDigestValid = null;
1458
2147
  if (!proposalPath) {
1459
2148
  reasons.push('Run has no sync-back proposal.');
1460
2149
  }
1461
2150
  else {
1462
2151
  try {
1463
2152
  proposal = await readArtifact(projectRoot, state.runId, toArtifactRootRelativePath(proposalPath));
2153
+ if (expectedProposalDigest) {
2154
+ const actualProposalDigest = hashDocumentContent(proposal);
2155
+ proposalDigestValid = actualProposalDigest === expectedProposalDigest;
2156
+ if (!proposalDigestValid) {
2157
+ reasons.push(`Sync-back proposal ${proposalPath} digest changed from ${expectedProposalDigest} to ${actualProposalDigest}.`);
2158
+ }
2159
+ }
1464
2160
  }
1465
2161
  catch (error) {
1466
2162
  reasons.push(`Cannot read sync-back proposal ${proposalPath}: ${messageFromError(error)}`);
@@ -1489,6 +2185,8 @@ export async function inspectSyncBack(projectRoot, options) {
1489
2185
  reasons,
1490
2186
  proposalPath,
1491
2187
  proposal,
2188
+ proposalDigest: expectedProposalDigest,
2189
+ proposalDigestValid,
1492
2190
  runTaskStatus: runtimeTaskStatus(state.tasks[taskId]),
1493
2191
  markdownTask,
1494
2192
  markdownStatus: markdownTask?.status ?? null,
@@ -1584,6 +2282,7 @@ export async function doctor(projectRoot, options = {}) {
1584
2282
  else {
1585
2283
  checks.push({ level: 'FAIL', check: 'project_config', message: '.sdd/project.yml is missing.', action: 'Run sdd init.' });
1586
2284
  }
2285
+ checks.push(...await inspectRuntimeStoreEvidence(projectRoot));
1587
2286
  const runsDir = getRunsDir(projectRoot);
1588
2287
  if (await exists(runsDir)) {
1589
2288
  try {
@@ -1603,7 +2302,7 @@ export async function doctor(projectRoot, options = {}) {
1603
2302
  checks.push(await exists(specsDir)
1604
2303
  ? { level: 'PASS', check: 'specs_dir', message: 'specs directory exists.' }
1605
2304
  : { level: 'WARN', check: 'specs_dir', message: 'specs directory is missing.', action: 'Create specs/<branch>/ documents before full SDD execution.' });
1606
- checks.push(...await inspectDocumentChainEvidence(projectRoot));
2305
+ checks.push(...await inspectDocumentChainEvidence(projectRoot, options.branch ?? undefined));
1607
2306
  if (await exists(configPath)) {
1608
2307
  checks.push(...await inspectAiToolEntryEvidence(projectRoot));
1609
2308
  }
@@ -1817,7 +2516,8 @@ export function parseSddTasksMarkdown(raw, options = {}) {
1817
2516
  return { tasks, gaps };
1818
2517
  }
1819
2518
  export async function renderSddResultArtifactTemplate(projectRoot, options) {
1820
- const branch = options.branch ?? (await resolveSddContext(projectRoot)).branch;
2519
+ const runState = options.runId ? await readRunState(projectRoot, options.runId).catch(() => null) : null;
2520
+ const branch = options.branch ?? runState?.partition ?? runState?.gitBranch ?? (await resolveSddContext(projectRoot)).branch;
1821
2521
  const status = options.status ?? 'PASS';
1822
2522
  if (!isSddResultStatus(status)) {
1823
2523
  throw new Error(`Unsupported sdd-result status ${status}.`);
@@ -1892,10 +2592,15 @@ export async function validateSddResultArtifact(projectRoot, runId, runRelativeA
1892
2592
  }
1893
2593
  const parsed = parseSddResultMarkdown(raw);
1894
2594
  issues.push(...parsed.issues);
2595
+ let trust;
1895
2596
  if (parsed.result) {
1896
2597
  issues.push(...validateSddResult(parsed.result, { ...options, runRelativeArtifactPath }));
2598
+ trust = validateArtifactTrust(raw, parsed.result, runRelativeArtifactPath, options);
2599
+ if (!trust.valid) {
2600
+ issues.push(...trust.issues);
2601
+ }
1897
2602
  }
1898
- return { valid: issues.length === 0 && parsed.result !== null, result: parsed.result, issues };
2603
+ return { valid: issues.length === 0 && parsed.result !== null, result: parsed.result, issues, trust };
1899
2604
  }
1900
2605
  export function parseSddResultMarkdown(raw) {
1901
2606
  const matches = Array.from(raw.matchAll(/^\s*```sdd-result\s*\r?\n([\s\S]*?)\r?^\s*```\s*$/gm));
@@ -1924,6 +2629,177 @@ export function validateSddResult(result, options = {}) {
1924
2629
  }
1925
2630
  return issues;
1926
2631
  }
2632
+ export function parseSddEvidenceMarkdown(raw, options = {}) {
2633
+ const matches = Array.from(raw.matchAll(/^\s*```sdd-evidence\s*\r?\n([\s\S]*?)\r?^\s*```\s*$/gm));
2634
+ const claims = [];
2635
+ const issues = [];
2636
+ for (const match of matches) {
2637
+ const metadata = parseSimpleYamlBlock(match[1] ?? '');
2638
+ const built = buildEvidenceClaim(metadata, options.sourceArtifact);
2639
+ issues.push(...built.issues);
2640
+ if (built.claim) {
2641
+ claims.push(built.claim);
2642
+ issues.push(...validateEvidenceClaim(built.claim, options));
2643
+ }
2644
+ }
2645
+ return { valid: claims.length > 0 && issues.length === 0, claims, issues };
2646
+ }
2647
+ function validateArtifactTrust(raw, result, runRelativeArtifactPath, options = {}) {
2648
+ const expectedAgent = options.expectedAgent ?? result.agent;
2649
+ const expectedTask = options.expectedTask ?? result.task;
2650
+ const requiresTrust = expectedAgent === 'validator' && (result.status === 'PASS' || result.status === 'PASS_WITH_GAPS');
2651
+ const parsed = parseSddEvidenceMarkdown(raw, { expectedTask, sourceArtifact: runRelativeArtifactPath });
2652
+ const issues = requiresTrust || parsed.claims.length > 0 ? [...parsed.issues] : [];
2653
+ if (requiresTrust) {
2654
+ issues.push(...validateArtifactBodyTrust(raw));
2655
+ if (parsed.claims.length === 0) {
2656
+ issues.push(evidenceIssue('UNSOURCED_PASS', 'sdd-evidence', `Validator ${result.status} artifact ${runRelativeArtifactPath} has no structured ${SDD_EVIDENCE_CONTRACT} evidence block.`, 'Add policy-backed sdd-evidence with acceptance, claim, source artifact, evidence refs, provenance refs, and policy refs.'));
2657
+ }
2658
+ }
2659
+ return { valid: issues.length === 0 && (!requiresTrust || parsed.claims.length > 0), claims: parsed.claims, issues };
2660
+ }
2661
+ function buildEvidenceClaim(metadata, sourceArtifact) {
2662
+ const issues = [];
2663
+ const contract = scalarValue(metadata.contract);
2664
+ const version = scalarValue(metadata.version);
2665
+ const task = scalarValue(metadata.task);
2666
+ const acceptance = scalarValue(metadata.acceptance);
2667
+ const status = scalarValue(metadata.status);
2668
+ const claimText = scalarValue(metadata.claim);
2669
+ const claimSourceArtifact = scalarValue(metadata.source_artifact) ?? scalarValue(metadata.sourceArtifact) ?? sourceArtifact ?? null;
2670
+ if (!contract) {
2671
+ issues.push(evidenceIssue('POLICY_RULE_FAILED', 'sdd-evidence.contract', 'sdd-evidence contract is missing.', `Set contract: ${SDD_EVIDENCE_CONTRACT}.`));
2672
+ }
2673
+ if (!version) {
2674
+ issues.push(evidenceIssue('POLICY_RULE_FAILED', 'sdd-evidence.version', 'sdd-evidence version is missing.', `Set version: ${SDD_EVIDENCE_VERSION}.`));
2675
+ }
2676
+ if (!task) {
2677
+ issues.push(evidenceIssue('POLICY_RULE_FAILED', 'sdd-evidence.task', 'sdd-evidence task is missing.', 'Set task to the delegated task id.'));
2678
+ }
2679
+ if (!acceptance) {
2680
+ issues.push(evidenceIssue('POLICY_RULE_FAILED', 'sdd-evidence.acceptance', 'sdd-evidence acceptance is missing.', 'Set acceptance to the AC id or acceptance text being proven.'));
2681
+ }
2682
+ if (!status || !isEvidenceCoverageStatus(status)) {
2683
+ issues.push(evidenceIssue('POLICY_RULE_FAILED', 'sdd-evidence.status', `sdd-evidence status ${status ?? 'missing'} is not supported.`, 'Use PASS, FAIL, BLOCKED, REFERENCED_ONLY, or MISSING.'));
2684
+ }
2685
+ if (!claimText) {
2686
+ issues.push(evidenceIssue('UNSOURCED_PASS', 'sdd-evidence.claim', 'sdd-evidence claim is missing.', 'Add a concise claim that states what acceptance is proven.'));
2687
+ }
2688
+ if (!claimSourceArtifact) {
2689
+ issues.push(evidenceIssue('MISSING_ARTIFACT_REFERENCE', 'sdd-evidence.source_artifact', 'sdd-evidence source artifact is missing.', 'Set source_artifact to the run-relative artifact path.'));
2690
+ }
2691
+ if (!task || !acceptance || !status || !isEvidenceCoverageStatus(status) || !claimText || !claimSourceArtifact || contract !== SDD_EVIDENCE_CONTRACT || version !== SDD_EVIDENCE_VERSION) {
2692
+ return { claim: null, issues };
2693
+ }
2694
+ const evidence = listValue(metadata.evidence_refs ?? metadata.evidence_ref ?? metadata.evidence).map(parseEvidenceItem);
2695
+ const provenance = listValue(metadata.provenance_refs ?? metadata.provenance_ref ?? metadata.provenance);
2696
+ const policy = listValue(metadata.policy_refs ?? metadata.policy_ref ?? metadata.policy);
2697
+ return {
2698
+ claim: {
2699
+ contract: SDD_EVIDENCE_CONTRACT,
2700
+ version: SDD_EVIDENCE_VERSION,
2701
+ task,
2702
+ acceptance,
2703
+ status,
2704
+ claim: claimText,
2705
+ sourceArtifact: claimSourceArtifact,
2706
+ evidence,
2707
+ provenance,
2708
+ policy,
2709
+ rawMetadata: metadata
2710
+ },
2711
+ issues
2712
+ };
2713
+ }
2714
+ function validateEvidenceClaim(claim, options) {
2715
+ const issues = [];
2716
+ if (options.expectedTask && claim.task !== options.expectedTask) {
2717
+ issues.push(evidenceIssue('POLICY_RULE_FAILED', 'sdd-evidence.task', `sdd-evidence task ${claim.task} does not match expected task ${options.expectedTask}.`, 'Use the same task id as the delegated artifact.'));
2718
+ }
2719
+ if (containsTemplatePlaceholder(claim.claim)) {
2720
+ issues.push(evidenceIssue('TODO_PLACEHOLDER', 'sdd-evidence.claim', 'sdd-evidence claim contains placeholder text.', 'Replace TODO/template text with actual validation evidence.'));
2721
+ }
2722
+ if (isDerivedEvidenceRef(claim.sourceArtifact)) {
2723
+ issues.push(evidenceIssue('DERIVED_SOURCE_EVIDENCE', 'sdd-evidence.source_artifact', `sdd-evidence source artifact ${claim.sourceArtifact} is derived output, not source evidence.`, 'Reference the validator/reviewer/source artifact that contains the evidence, not coverage, cache, proposal, or summary output.'));
2724
+ }
2725
+ if (claim.status === 'PASS') {
2726
+ if (claim.evidence.length === 0) {
2727
+ issues.push(evidenceIssue('UNSOURCED_PASS', 'sdd-evidence.evidence', `PASS claim ${claim.acceptance} has no evidence refs.`, 'Add at least one command, artifact, file, test, or review evidence ref.'));
2728
+ }
2729
+ if (claim.provenance.length === 0) {
2730
+ issues.push(evidenceIssue('PROVENANCE_GAP', 'sdd-evidence.provenance', `PASS claim ${claim.acceptance} has no provenance refs.`, 'Add provenance refs for the artifact, command, run state, or material used by this claim.'));
2731
+ }
2732
+ if (claim.policy.length === 0) {
2733
+ issues.push(evidenceIssue('POLICY_RULE_FAILED', 'sdd-evidence.policy', `PASS claim ${claim.acceptance} has no policy refs.`, `Add ${ACCEPTANCE_POLICY_RULESET_VERSION}:<rule-id> policy refs.`));
2734
+ }
2735
+ }
2736
+ for (const item of claim.evidence) {
2737
+ if (!item.ref) {
2738
+ issues.push(evidenceIssue('MISSING_ARTIFACT_REFERENCE', 'sdd-evidence.evidence', `Evidence item for ${claim.acceptance} has an empty ref.`, `Provide a non-empty evidence ref.`));
2739
+ }
2740
+ if (containsTemplatePlaceholder(item.ref) || containsTemplatePlaceholder(item.summary ?? '')) {
2741
+ issues.push(evidenceIssue('TODO_PLACEHOLDER', 'sdd-evidence.evidence', `Evidence item for ${claim.acceptance} contains placeholder text.`, `Replace TODO/template text with real evidence.`));
2742
+ }
2743
+ if (isDerivedEvidenceRef(item.ref)) {
2744
+ issues.push(evidenceIssue('DERIVED_SOURCE_EVIDENCE', 'sdd-evidence.evidence', `Evidence ref ${item.ref} is derived output, not source evidence.`, `Use source artifacts, commands, files, tests, or material refs.`));
2745
+ }
2746
+ }
2747
+ for (const ref of claim.provenance) {
2748
+ if (isDerivedEvidenceRef(ref)) {
2749
+ issues.push(evidenceIssue('DERIVED_SOURCE_EVIDENCE', 'sdd-evidence.provenance', `Provenance ref ${ref} is derived output, not source evidence.`, `Use source artifact, command, run-state, or material provenance refs.`));
2750
+ }
2751
+ }
2752
+ return issues;
2753
+ }
2754
+ function validateArtifactBodyTrust(raw) {
2755
+ const issues = [];
2756
+ if (raw.trim().length === 0) {
2757
+ issues.push(evidenceIssue('EMPTY_EVIDENCE', 'evidence', 'Artifact body is empty.', 'Write non-empty evidence and an sdd-result block.'));
2758
+ }
2759
+ if (containsTemplatePlaceholder(raw)) {
2760
+ issues.push(evidenceIssue('TODO_PLACEHOLDER', 'evidence', 'Artifact still contains TODO/template placeholder text.', 'Replace scaffold text with real evidence before claiming PASS.'));
2761
+ }
2762
+ if (/^\s*-\s*\[PASS\]\s*(?:Acceptance\s+)?(?:AC[-\w.]+|[^\r\n]+)\s*$/im.test(raw) && !/^\s*```sdd-evidence\s*$/im.test(raw)) {
2763
+ issues.push(evidenceIssue('MENTION_ONLY', 'acceptance_coverage', 'Validator PASS artifact only mentions an acceptance target without structured source evidence.', `Add ${SDD_EVIDENCE_CONTRACT} claim/evidence/provenance/policy records.`));
2764
+ }
2765
+ if (/Mentioned in artifacts\//i.test(raw)) {
2766
+ issues.push(evidenceIssue('MENTION_ONLY', 'acceptance_coverage', 'Artifact cites generated mention-only coverage text.', 'Use source evidence, not generated coverage summaries.'));
2767
+ }
2768
+ return issues;
2769
+ }
2770
+ function parseEvidenceItem(value) {
2771
+ const separator = value.indexOf(':');
2772
+ if (separator > 0) {
2773
+ const kind = value.slice(0, separator).trim();
2774
+ const ref = value.slice(separator + 1).trim();
2775
+ if (/^[A-Za-z0-9_-]+$/.test(kind)) {
2776
+ return { kind, ref, summary: null };
2777
+ }
2778
+ }
2779
+ return { kind: value.startsWith('artifacts/') ? 'artifact' : 'text', ref: value, summary: null };
2780
+ }
2781
+ function isEvidenceCoverageStatus(value) {
2782
+ return value === 'PASS' || value === 'FAIL' || value === 'BLOCKED' || value === 'REFERENCED_ONLY' || value === 'MISSING';
2783
+ }
2784
+ function containsTemplatePlaceholder(value) {
2785
+ return /\bTODO\b|template placeholder|TODO\.|Add validation evidence|TODO run validation command|TODO cite files/i.test(value);
2786
+ }
2787
+ function isDerivedEvidenceRef(value) {
2788
+ const normalized = normalizePortablePath(value).toLowerCase();
2789
+ return normalized.includes('acceptance-coverage-')
2790
+ || normalized.endsWith('sync-back-proposal.md')
2791
+ || normalized.includes('/cache/')
2792
+ || normalized.includes('/profile')
2793
+ || normalized.endsWith('run-index.json')
2794
+ || normalized.includes('command-output-summary')
2795
+ || normalized.includes('evidence-summary')
2796
+ || normalized.includes('context-package')
2797
+ || normalized.includes('context-build')
2798
+ || normalized.includes('log-worker-summary');
2799
+ }
2800
+ function evidenceIssue(code, field, message, recommendation) {
2801
+ return contractIssue(field, `${code}: ${message}`, recommendation);
2802
+ }
1927
2803
  export function createDelegationRecord(input) {
1928
2804
  return {
1929
2805
  contract: DELEGATION_LIVENESS_CONTRACT,
@@ -2034,6 +2910,8 @@ export async function ingestArtifactResult(projectRoot, runId, input) {
2034
2910
  [key]: record
2035
2911
  }
2036
2912
  });
2913
+ await recordRuntimeArtifactIngestion(projectRoot, record);
2914
+ await recordRuntimeEvidenceAdmission(projectRoot, state, record, report.trust);
2037
2915
  if (!accepted && delegation.status === 'RUNNING' && !report.valid) {
2038
2916
  await appendEvent(projectRoot, runId, { event: 'artifact_invalid', runId, summary: `Artifact ingestion rejected for ${delegation.delegationId}`, data: { delegationId: delegation.delegationId, artifact: artifactPath, issues } });
2039
2917
  }
@@ -2318,6 +3196,7 @@ export async function runSingleTaskLoop(projectRoot, options) {
2318
3196
  taskState: { status: 'blocked', gaps: allGaps, artifacts: [gapArtifact.runRelativePath] },
2319
3197
  validationStatus: 'blocked',
2320
3198
  syncBackProposalPath: proposal.runRelativePath,
3199
+ syncBackProposalDigest: proposal.digest,
2321
3200
  artifacts: [{ path: gapArtifact.runRelativePath, kind: 'gap-report', task: options.taskId, agent: 'runtime' }]
2322
3201
  });
2323
3202
  await appendEvent(projectRoot, runId, {
@@ -2361,6 +3240,7 @@ export async function runSingleTaskLoop(projectRoot, options) {
2361
3240
  let terminalStatus = 'completed';
2362
3241
  let validationStatus = 'pass';
2363
3242
  for (const step of steps) {
3243
+ const stepRouteDecision = await routeSddTask(projectRoot, { taskId: options.taskId, branch, agent: step.agent, teamModeEnabled: options.teamModeEnabled, teamModeActivation: options.teamModeActivation });
2364
3244
  if (!step.suppliedArtifact) {
2365
3245
  if (!step.required) {
2366
3246
  await appendEvent(projectRoot, runId, {
@@ -2373,7 +3253,7 @@ export async function runSingleTaskLoop(projectRoot, options) {
2373
3253
  runId,
2374
3254
  taskId: options.taskId,
2375
3255
  agent: step.agent,
2376
- route: routeDecision,
3256
+ route: stepRouteDecision,
2377
3257
  status: 'skipped',
2378
3258
  delegationId: `B-${options.taskId}-${step.agent}-001`,
2379
3259
  evidenceSummary: `${step.agent} artifact was not supplied and the step is optional.`
@@ -2392,7 +3272,7 @@ export async function runSingleTaskLoop(projectRoot, options) {
2392
3272
  runId,
2393
3273
  taskId: options.taskId,
2394
3274
  agent: step.agent,
2395
- route: routeDecision,
3275
+ route: stepRouteDecision,
2396
3276
  status: 'blocked',
2397
3277
  delegationId: `B-${options.taskId}-${step.agent}-001`,
2398
3278
  evidenceSummary: `${step.agent} artifact was not supplied; execution is blocked before host invocation.`
@@ -2471,6 +3351,7 @@ export async function runSingleTaskLoop(projectRoot, options) {
2471
3351
  taskState: { status: terminalStatus, gaps, artifacts: acceptedArtifacts },
2472
3352
  validationStatus,
2473
3353
  syncBackProposalPath: proposal.runRelativePath,
3354
+ syncBackProposalDigest: proposal.digest,
2474
3355
  artifacts: acceptedArtifacts.map((artifactPath) => ({ path: artifactPath, kind: artifactKind(artifactPath), task: options.taskId, agent: agentFromArtifactPath(artifactPath) }))
2475
3356
  });
2476
3357
  await appendEvent(projectRoot, runId, {
@@ -2528,6 +3409,7 @@ export async function runGoalVerify(projectRoot, options) {
2528
3409
  const acceptedArtifacts = [];
2529
3410
  let reviewStatus = null;
2530
3411
  let validationStatus = null;
3412
+ let validationTrust = null;
2531
3413
  await appendEvent(projectRoot, runId, {
2532
3414
  event: 'phase_started',
2533
3415
  runId,
@@ -2558,6 +3440,7 @@ export async function runGoalVerify(projectRoot, options) {
2558
3440
  }
2559
3441
  else {
2560
3442
  const validationReport = await validateSddResultArtifact(projectRoot, runId, validationArtifact, { expectedTask: options.taskId, expectedAgent: 'validator' });
3443
+ validationTrust = validationReport.trust ?? null;
2561
3444
  if (!validationReport.valid || !validationReport.result) {
2562
3445
  gaps.push(taskGap(options.taskId, 'validation_artifact', `Validator artifact ${validationArtifact} is invalid: ${validationReport.issues.map((issue) => issue.message).join('; ')}`, 'Fix validator artifact contract before goal-level verify.'));
2563
3446
  }
@@ -2571,15 +3454,50 @@ export async function runGoalVerify(projectRoot, options) {
2571
3454
  }
2572
3455
  if (inspected.task) {
2573
3456
  const validationRaw = validationArtifact ? await readArtifactIfExists(projectRoot, runId, validationArtifact) : '';
3457
+ const reviewRaw = reviewArtifact ? await readArtifactIfExists(projectRoot, runId, reviewArtifact) : '';
3458
+ if (reviewArtifact && reviewRaw.length > 0) {
3459
+ await appendArtifactHashLedgerEntry(projectRoot, { runId, taskId: options.taskId, branch, artifactPath: reviewArtifact, content: reviewRaw });
3460
+ }
3461
+ if (validationArtifact && validationRaw.length > 0) {
3462
+ await appendArtifactHashLedgerEntry(projectRoot, { runId, taskId: options.taskId, branch, artifactPath: validationArtifact, content: validationRaw });
3463
+ }
3464
+ await appendDeclaredCommandLedgerEntries(projectRoot, { runId, taskId: options.taskId, branch, commands: inspected.task.validation });
3465
+ const invocationLedger = await listInvocationLedgerEntries(projectRoot, runId);
3466
+ const admittedClaims = await readRuntimeEvidenceClaims(projectRoot, runId, options.taskId);
3467
+ const scopeViolation = await hasRuntimeEvidenceScopeViolation(projectRoot, runId, options.taskId);
3468
+ if (scopeViolation) {
3469
+ gaps.push(taskGap(options.taskId, 'runtime_scope', 'PARTITION_SCOPE_VIOLATION: Runtime evidence claims do not match the run partition.', 'Reingest validator evidence in the correct branch/partition before verify.'));
3470
+ }
2574
3471
  for (const target of taskAcceptanceCoverageTargets(inspected.task)) {
2575
- const covered = target.matchTexts.some((text) => validationRaw.toLowerCase().includes(text.toLowerCase()));
2576
- acceptanceCoverage.push({
2577
- acceptance: target.label,
2578
- status: covered ? statusFromValidation(validationStatus) : 'GAP',
2579
- evidence: covered ? `Mentioned in ${validationArtifact}.` : 'No matching acceptance evidence found in validator artifact.'
3472
+ const coverage = scopeViolation
3473
+ ? acceptanceCoverageDecision(target.label, 'BLOCKED', 'Runtime evidence scope violation blocks acceptance coverage.', ['PARTITION_SCOPE_VIOLATION'], [], ['require-partition-scope'])
3474
+ : evaluateAcceptanceCoverageTarget(target, {
3475
+ taskId: options.taskId,
3476
+ validationArtifact,
3477
+ validationRaw,
3478
+ claims: admittedClaims.length > 0 ? admittedClaims : validationTrust?.claims ?? [],
3479
+ validationStatus,
3480
+ invocationLedger
3481
+ });
3482
+ await appendInvocationLedgerEntry(projectRoot, {
3483
+ runId,
3484
+ taskId: options.taskId,
3485
+ branch,
3486
+ kind: 'policy_evaluation',
3487
+ ref: `${ACCEPTANCE_POLICY_RULESET_VERSION}:${target.label}`,
3488
+ status: coverage.status,
3489
+ artifactPath: validationArtifact,
3490
+ inputHash: validationRaw.length > 0 ? hashDocumentContent(validationRaw) : null,
3491
+ materialRefs: [],
3492
+ metadata: {
3493
+ passedRules: coverage.policyDecision?.passedRules.join(',') ?? '',
3494
+ failedRules: coverage.policyDecision?.failedRules.join(',') ?? '',
3495
+ issueCodes: coverage.issueCodes?.join(',') ?? ''
3496
+ }
2580
3497
  });
2581
- if (!covered) {
2582
- gaps.push(taskGap(options.taskId, 'acceptance_coverage', `Acceptance target is not covered by validator evidence: ${target.label}`, 'Update the validator artifact so it includes the acceptance ref or exact Acceptance text, preferably under ## Acceptance Mapping; use sdd artifact template to generate the mapping skeleton.'));
3498
+ acceptanceCoverage.push(coverage);
3499
+ if (coverage.status !== 'PASS') {
3500
+ gaps.push(taskGap(options.taskId, 'acceptance_coverage', `Acceptance target ${target.label} is ${coverage.status}: ${coverage.evidence}`, `Add ${SDD_EVIDENCE_CONTRACT} claim/evidence/provenance/policy records for ${target.label}; mention-only acceptance refs cannot pass.`));
2583
3501
  }
2584
3502
  }
2585
3503
  }
@@ -2595,6 +3513,7 @@ export async function runGoalVerify(projectRoot, options) {
2595
3513
  commands: inspected.task?.validation ?? [],
2596
3514
  evidence: allArtifacts,
2597
3515
  syncBackProposalPath: proposal.runRelativePath,
3516
+ syncBackProposalDigest: proposal.digest,
2598
3517
  artifacts: allArtifacts.map((artifactPath) => ({ path: artifactPath, kind: artifactKind(artifactPath), task: options.taskId, agent: agentFromArtifactPath(artifactPath) }))
2599
3518
  });
2600
3519
  await appendEvent(projectRoot, runId, {
@@ -3045,7 +3964,7 @@ export async function runBackgroundExecutor(projectRoot, options) {
3045
3964
  if (worker?.kind === 'manual_handoff') {
3046
3965
  issues.push(contractIssue('workerAdapterId', `Worker adapter ${workerAdapterId} is manual handoff only.`, 'Use a runnable worker adapter for background executor claim/run/ingest.'));
3047
3966
  }
3048
- const route = await routeSddTask(projectRoot, { taskId: options.taskId, branch });
3967
+ const route = await routeSddTask(projectRoot, { taskId: options.taskId, branch, agent });
3049
3968
  if (!inspected.task || inspected.gaps.some((gap) => gap.severity === 'blocking')) {
3050
3969
  issues.push(...inspected.gaps.map((gap) => contractIssue(gap.field, gap.message, gap.recommendation)));
3051
3970
  }
@@ -3745,7 +4664,9 @@ async function persistLoopState(projectRoot, runId, input) {
3745
4664
  syncBack: {
3746
4665
  mode: 'proposal',
3747
4666
  proposalPath: input.syncBackProposalPath,
3748
- status: 'proposed'
4667
+ proposalDigest: input.syncBackProposalDigest,
4668
+ sourceVerifyStatus: input.status,
4669
+ status: state.syncBack.status === 'applied' ? 'applied' : 'proposed'
3749
4670
  }
3750
4671
  });
3751
4672
  }
@@ -3774,13 +4695,16 @@ async function persistVerifyState(projectRoot, runId, input) {
3774
4695
  syncBack: {
3775
4696
  mode: 'proposal',
3776
4697
  proposalPath: input.syncBackProposalPath,
3777
- status: 'proposed'
4698
+ proposalDigest: input.syncBackProposalDigest,
4699
+ sourceVerifyStatus: input.status,
4700
+ status: state.syncBack.status === 'applied' ? 'applied' : 'proposed'
3778
4701
  }
3779
4702
  });
3780
4703
  }
3781
4704
  async function writeSyncBackProposal(projectRoot, runId, taskId, status, artifacts, gaps, summary) {
3782
4705
  const content = `# Sync-back Proposal\n\n## ${taskId}\n\n- status: ${status}\n- summary: ${summary}\n- artifacts:\n${artifacts.length > 0 ? artifacts.map((artifact) => ` - ${artifact}`).join('\n') : ' - none'}\n- gaps:\n${gaps.length > 0 ? gaps.map((gap) => ` - [${gap.severity}] ${gap.type} ${gap.field}: ${gap.message}`).join('\n') : ' - none'}\n\n## Boundaries\n\n- Proposal only; tasks.md/spec.md/plan.md were not modified by runtime.\n- Runtime modeled agent/verify steps through supplied artifacts and contract validation; no external agent API was invoked.\n`;
3783
- return writeArtifact(projectRoot, runId, 'sync-back-proposal.md', content);
4706
+ const written = await writeArtifact(projectRoot, runId, 'sync-back-proposal.md', content);
4707
+ return { ...written, digest: hashDocumentContent(content) };
3784
4708
  }
3785
4709
  function toSafeRecordId(value) {
3786
4710
  const sanitized = value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
@@ -3796,6 +4720,22 @@ function routeRecordSnapshot(route) {
3796
4720
  blockedReason: route.blockedReason
3797
4721
  };
3798
4722
  }
4723
+ function routeRecordId(route, profile) {
4724
+ const content = JSON.stringify({
4725
+ version: route.version,
4726
+ taskId: route.taskId,
4727
+ branch: route.branch,
4728
+ category: route.category,
4729
+ profile,
4730
+ recommendedProfile: route.recommendedProfile,
4731
+ autonomyCeiling: route.autonomyCeiling,
4732
+ requiredCapabilities: route.requiredCapabilities,
4733
+ toolPermissionProfile: route.toolPermission?.profile ?? null,
4734
+ toolPermissionPolicy: route.toolPermission?.policy ?? null,
4735
+ blockedReason: route.blockedReason
4736
+ });
4737
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
4738
+ }
3799
4739
  function sourceAttributionForCapabilities(capabilityIds, route) {
3800
4740
  return capabilityIds.map((capabilityId) => {
3801
4741
  const routeSource = route?.registrySources?.find((source) => source.kind === 'skill_capability' && source.id === capabilityId);
@@ -3833,6 +4773,7 @@ function buildAgentExecutionRecord(input) {
3833
4773
  queueItemId: input.queueItemId ?? null,
3834
4774
  ingestionStatus: input.ingestion?.status ?? null,
3835
4775
  resultStatus: input.ingestion?.resultStatus ?? null,
4776
+ routeId: routeRecordId(input.route, profile),
3836
4777
  routeDecision: routeRecordSnapshot(input.route),
3837
4778
  evidenceSummary: input.evidenceSummary,
3838
4779
  createdAt: now,
@@ -3871,7 +4812,170 @@ function renderLoopGapReport(taskId, gaps) {
3871
4812
  return `# Gap Report ${taskId}\n\n\`\`\`sdd-result\ncontract: ${SDD_RESULT_CONTRACT}\nversion: ${SDD_RESULT_VERSION}\nagent: runtime\ntask: ${taskId}\nstatus: BLOCKED\nartifacts:\n - artifacts/gap-report-${taskId}.md\n\`\`\`\n\n## Gaps\n\n${gaps.length > 0 ? gaps.map((gap) => `- [${gap.severity}] ${gap.type} ${gap.field}: ${gap.message} Recommendation: ${gap.recommendation}`).join('\n') : '- No structured gaps were provided; inspect task selection and supplied artifacts.'}\n`;
3872
4813
  }
3873
4814
  function renderAcceptanceCoverageArtifact(taskId, status, task, reviewArtifact, validationArtifact, coverage, gaps) {
3874
- return `# Acceptance Coverage ${taskId}\n\n\`\`\`sdd-result\ncontract: ${SDD_RESULT_CONTRACT}\nversion: ${SDD_RESULT_VERSION}\nagent: validator\ntask: ${taskId}\nstatus: ${status}\nartifacts:\n - artifacts/acceptance-coverage-${taskId}.md\n\`\`\`\n\n## Source Evidence\n\n- review_artifact: ${reviewArtifact ?? 'missing'}\n- validation_artifact: ${validationArtifact ?? 'missing'}\n- task_source: ${task ? sourceLocationEvidence(task.source) : 'missing'}\n\n## Commands Declared\n\n${task && task.validation.length > 0 ? task.validation.map((command) => `- ${command}`).join('\n') : '- none'}\n\n## Acceptance Mapping\n\n${coverage.length > 0 ? coverage.map((item) => `- [${item.status}] ${item.acceptance} Evidence: ${item.evidence}`).join('\n') : '- No acceptance items available.'}\n\n## Gaps\n\n${gaps.length > 0 ? gaps.map((gap) => `- [${gap.severity}] ${gap.type} ${gap.field}: ${gap.message} Recommendation: ${gap.recommendation}`).join('\n') : '- none'}\n`;
4815
+ return `# Acceptance Coverage ${taskId}\n\n\`\`\`sdd-result\ncontract: ${SDD_RESULT_CONTRACT}\nversion: ${SDD_RESULT_VERSION}\nagent: validator\ntask: ${taskId}\nstatus: ${status}\nartifacts:\n - artifacts/acceptance-coverage-${taskId}.md\n\`\`\`\n\n## Source Evidence\n\n- review_artifact: ${reviewArtifact ?? 'missing'}\n- validation_artifact: ${validationArtifact ?? 'missing'}\n- task_source: ${task ? sourceLocationEvidence(task.source) : 'missing'}\n\n## Commands Declared\n\n${task && task.validation.length > 0 ? task.validation.map((command) => `- ${command}`).join('\n') : '- none'}\n\n## Acceptance Mapping\n\n${coverage.length > 0 ? coverage.map((item) => `- [${item.status}] ${item.acceptance} Evidence: ${item.evidence}`).join('\n') : '- No acceptance items available.'}\n\n## Policy Decisions\n\n${coverage.length > 0 ? coverage.map((item) => `- ${item.acceptance}: status=${item.policyDecision?.status ?? item.status}; ruleset=${item.policyDecision?.ruleSet.id ?? ACCEPTANCE_POLICY_RULESET_VERSION}; passed=${item.policyDecision?.passedRules.join(',') || 'none'}; failed=${item.policyDecision?.failedRules.join(',') || 'none'}; issues=${item.issueCodes?.join(',') || 'none'}`).join('\n') : '- none'}\n\n## Gaps\n\n${gaps.length > 0 ? gaps.map((gap) => `- [${gap.severity}] ${gap.type} ${gap.field}: ${gap.message} Recommendation: ${gap.recommendation}`).join('\n') : '- none'}\n`;
4816
+ }
4817
+ function evaluateAcceptanceCoverageTarget(target, input) {
4818
+ const matchingClaims = input.claims.filter((claim) => claim.task === input.taskId && acceptanceMatchesTarget(target, claim.acceptance));
4819
+ const issueCodes = [];
4820
+ const failedRules = [];
4821
+ if (matchingClaims.length === 0) {
4822
+ if (target.matchTexts.some((text) => text.length > 0 && input.validationRaw.toLowerCase().includes(text.toLowerCase()))) {
4823
+ issueCodes.push('MENTION_ONLY');
4824
+ failedRules.push('require-structured-evidence');
4825
+ return acceptanceCoverageDecision(target.label, 'REFERENCED_ONLY', `Referenced ${target.label} in ${input.validationArtifact ?? 'validator artifact'} without policy-backed evidence.`, issueCodes, [], failedRules);
4826
+ }
4827
+ issueCodes.push('MISSING_ARTIFACT_REFERENCE');
4828
+ failedRules.push('require-acceptance-claim');
4829
+ return acceptanceCoverageDecision(target.label, 'MISSING', 'No policy-backed acceptance evidence found in validator artifact.', issueCodes, [], failedRules);
4830
+ }
4831
+ let best = matchingClaims[0];
4832
+ for (const claim of matchingClaims.slice(1)) {
4833
+ if (coverageRank(claim.status) > coverageRank(best.status)) {
4834
+ best = claim;
4835
+ }
4836
+ }
4837
+ if (best.status === 'PASS') {
4838
+ if (best.evidence.length === 0) {
4839
+ issueCodes.push('UNSOURCED_PASS');
4840
+ failedRules.push('require-source-evidence');
4841
+ }
4842
+ if (best.provenance.length === 0) {
4843
+ issueCodes.push('PROVENANCE_GAP');
4844
+ failedRules.push('require-provenance');
4845
+ }
4846
+ if (best.policy.length === 0) {
4847
+ issueCodes.push('POLICY_RULE_FAILED');
4848
+ failedRules.push('require-policy-rule');
4849
+ }
4850
+ if (input.validationStatus !== 'PASS') {
4851
+ issueCodes.push('POLICY_RULE_FAILED');
4852
+ failedRules.push('require-validator-pass');
4853
+ }
4854
+ const ledgerDecision = evaluateClaimLedgerCorroboration(best, input.invocationLedger);
4855
+ issueCodes.push(...ledgerDecision.issueCodes);
4856
+ failedRules.push(...ledgerDecision.failedRules);
4857
+ if (issueCodes.length === 0) {
4858
+ return acceptanceCoverageDecision(target.label, 'PASS', `Policy-proven by ${SDD_EVIDENCE_CONTRACT} claim for ${best.acceptance} in ${best.sourceArtifact}; evidence=${best.evidence.map((item) => `${item.kind}:${item.ref}`).join(', ')}; provenance=${best.provenance.join(', ')}; policy=${best.policy.join(', ')}.`, [], ['require-source-evidence', 'require-provenance', 'require-policy-rule', 'require-validator-pass', 'require-ledger-corroboration'], []);
4859
+ }
4860
+ return acceptanceCoverageDecision(target.label, 'BLOCKED', `PASS claim for ${best.acceptance} is missing required policy/provenance corroboration.`, issueCodes, [], failedRules);
4861
+ }
4862
+ if (best.status === 'FAIL') {
4863
+ return acceptanceCoverageDecision(target.label, 'FAIL', `Explicit FAIL claim for ${best.acceptance}: ${best.claim}`, [], ['explicit-fail-overrides-pass'], []);
4864
+ }
4865
+ if (best.status === 'BLOCKED') {
4866
+ return acceptanceCoverageDecision(target.label, 'BLOCKED', `Explicit BLOCKED claim for ${best.acceptance}: ${best.claim}`, [], ['explicit-blocked-overrides-pass'], []);
4867
+ }
4868
+ if (best.status === 'REFERENCED_ONLY') {
4869
+ return acceptanceCoverageDecision(target.label, 'REFERENCED_ONLY', `Structured evidence references ${best.acceptance} but does not prove PASS.`, ['MENTION_ONLY'], [], ['require-pass-claim']);
4870
+ }
4871
+ return acceptanceCoverageDecision(target.label, 'MISSING', `Structured evidence marks ${best.acceptance} missing.`, ['MISSING_ARTIFACT_REFERENCE'], [], ['require-pass-claim']);
4872
+ }
4873
+ function evaluateClaimLedgerCorroboration(claim, entries) {
4874
+ const issueCodes = [];
4875
+ const failedRules = [];
4876
+ const commandRefs = invocationLedgerRefs(entries, 'command');
4877
+ const artifactRefs = invocationLedgerRefs(entries, 'artifact_hash');
4878
+ const materialRefs = ledgerMaterialRefs(entries);
4879
+ const addIssue = (code, rule) => {
4880
+ if (!issueCodes.includes(code)) {
4881
+ issueCodes.push(code);
4882
+ }
4883
+ if (!failedRules.includes(rule)) {
4884
+ failedRules.push(rule);
4885
+ }
4886
+ };
4887
+ if (!artifactRefs.has(claim.sourceArtifact)) {
4888
+ addIssue('MISSING_ARTIFACT_REFERENCE', 'require-source-artifact-hash');
4889
+ }
4890
+ for (const item of claim.evidence) {
4891
+ if (item.kind === 'command' && !commandRefs.has(item.ref)) {
4892
+ addIssue('MISSING_COMMAND_OUTPUT', 'require-command-ledger');
4893
+ }
4894
+ if (item.kind === 'artifact' && !artifactRefs.has(item.ref)) {
4895
+ addIssue('MISSING_ARTIFACT_REFERENCE', 'require-artifact-hash');
4896
+ }
4897
+ if (item.kind === 'material' && !materialRefs.has(item.ref)) {
4898
+ addIssue('MISSING_MATERIAL_REFERENCE', 'require-material-ledger');
4899
+ }
4900
+ }
4901
+ for (const ref of claim.provenance) {
4902
+ const typed = parseTypedLedgerRef(ref);
4903
+ if (!typed) {
4904
+ continue;
4905
+ }
4906
+ if (typed.kind === 'command' && !commandRefs.has(typed.ref)) {
4907
+ addIssue('PROVENANCE_GAP', 'require-command-provenance-ledger');
4908
+ }
4909
+ if (typed.kind === 'artifact' && !artifactRefs.has(typed.ref)) {
4910
+ addIssue('PROVENANCE_GAP', 'require-artifact-provenance-ledger');
4911
+ }
4912
+ if (typed.kind === 'material' && !materialRefs.has(typed.ref)) {
4913
+ addIssue('PROVENANCE_GAP', 'require-material-provenance-ledger');
4914
+ }
4915
+ }
4916
+ return { issueCodes, failedRules };
4917
+ }
4918
+ function invocationLedgerRefs(entries, kind) {
4919
+ const refs = new Set();
4920
+ for (const entry of entries) {
4921
+ if (entry.kind === kind) {
4922
+ refs.add(entry.ref);
4923
+ if (entry.artifactPath) {
4924
+ refs.add(entry.artifactPath);
4925
+ }
4926
+ }
4927
+ }
4928
+ return refs;
4929
+ }
4930
+ function parseTypedLedgerRef(value) {
4931
+ const separator = value.indexOf(':');
4932
+ if (separator <= 0) {
4933
+ return null;
4934
+ }
4935
+ const kind = value.slice(0, separator).trim();
4936
+ const ref = value.slice(separator + 1).trim();
4937
+ if (!/^[A-Za-z0-9_-]+$/.test(kind) || !ref) {
4938
+ return null;
4939
+ }
4940
+ return { kind, ref };
4941
+ }
4942
+ function acceptanceMatchesTarget(target, acceptance) {
4943
+ const normalizedAcceptance = acceptance.toLowerCase();
4944
+ return target.matchTexts.some((text) => text.toLowerCase() === normalizedAcceptance) || target.label.toLowerCase() === normalizedAcceptance;
4945
+ }
4946
+ function coverageRank(status) {
4947
+ if (status === 'FAIL') {
4948
+ return 5;
4949
+ }
4950
+ if (status === 'BLOCKED') {
4951
+ return 4;
4952
+ }
4953
+ if (status === 'PASS') {
4954
+ return 3;
4955
+ }
4956
+ if (status === 'REFERENCED_ONLY') {
4957
+ return 2;
4958
+ }
4959
+ return 1;
4960
+ }
4961
+ function acceptanceCoverageDecision(acceptance, status, evidence, issueCodes, passedRules, failedRules) {
4962
+ return {
4963
+ acceptance,
4964
+ status,
4965
+ evidence,
4966
+ issueCodes,
4967
+ policyDecision: {
4968
+ status,
4969
+ ruleSet: {
4970
+ id: ACCEPTANCE_POLICY_RULESET_VERSION,
4971
+ version: SDD_EVIDENCE_VERSION,
4972
+ ruleIds: ['require-structured-evidence', 'require-source-evidence', 'require-provenance', 'require-policy-rule']
4973
+ },
4974
+ passedRules,
4975
+ failedRules,
4976
+ issueCodes
4977
+ }
4978
+ };
3875
4979
  }
3876
4980
  function taskAcceptanceCoverageTargets(task) {
3877
4981
  if (task.acceptanceRefs.length > 0) {
@@ -3890,21 +4994,6 @@ function taskAcceptanceCoverageTargets(task) {
3890
4994
  matchTexts: [acceptance]
3891
4995
  }));
3892
4996
  }
3893
- function statusFromValidation(status) {
3894
- if (status === 'PASS') {
3895
- return 'PASS';
3896
- }
3897
- if (status === 'PASS_WITH_GAPS') {
3898
- return 'PASS_WITH_GAPS';
3899
- }
3900
- if (status === 'FAIL') {
3901
- return 'FAIL';
3902
- }
3903
- if (status === 'BLOCKED' || status === 'TIMED_OUT' || status === 'CANCELLED') {
3904
- return 'BLOCKED';
3905
- }
3906
- return 'GAP';
3907
- }
3908
4997
  function deriveGoalVerifyStatus(reviewStatus, validationStatus, gaps) {
3909
4998
  if (gaps.length > 0) {
3910
4999
  return validationStatus === 'PASS_WITH_GAPS' ? 'PASS_WITH_GAPS' : 'BLOCKED';
@@ -5316,17 +6405,35 @@ export async function inspectTeamModePolicy(projectRoot, options = {}) {
5316
6405
  const autonomyCeiling = task ? taskAutonomyCeiling(task) : 'research_before_implementation';
5317
6406
  return buildTeamModePolicy({ activation, task, category, risk: task?.risk ?? [], autonomyCeiling, blockedReason: blockingGap?.message ?? null });
5318
6407
  }
5319
- export async function routeSddTask(projectRoot, options) {
6408
+ export async function routeSddTask(projectRoot, options = { taskId: '' }) {
6409
+ const profileSpans = [];
6410
+ const routeStart = Date.now();
6411
+ const routeStartedAt = new Date(routeStart).toISOString();
5320
6412
  const registry = await buildAgentSkillRuntimeRegistry(projectRoot);
6413
+ profileSpans.push(runtimeProfileSpan('agent_runtime_registry', routeStart));
6414
+ const branchStart = Date.now();
5321
6415
  const branch = options.branch ?? (await resolveSddContext(projectRoot)).branch;
5322
6416
  const model = await parseSddBranch(projectRoot, branch);
6417
+ profileSpans.push(runtimeProfileSpan('document_parse', branchStart));
6418
+ const cacheKey = routeCacheKey({ taskId: options.taskId, branch, agent: options.agent ?? null, teamModeActivation: resolveTeamModeActivation(options, 'auto'), documents: model.documents });
6419
+ const cachedDecision = options.cache ? await readRouteCache(projectRoot, cacheKey) : null;
6420
+ if (cachedDecision) {
6421
+ return {
6422
+ ...cachedDecision,
6423
+ cache: { contract: ROUTE_CACHE_CONTRACT_VERSION, key: cacheKey, status: 'hit', source: 'content_addressed_derived_route', authoritative: false },
6424
+ profile: options.profile ? [...(cachedDecision.profile ?? []), runtimeProfileSpan('route_total', routeStart, routeStartedAt)] : undefined
6425
+ };
6426
+ }
6427
+ const computeStart = Date.now();
5323
6428
  const inspected = inspectSddTask(model, options.taskId);
5324
6429
  const task = inspected.task;
5325
6430
  const blockingGap = inspected.gaps.find((gap) => gap.severity === 'blocking');
5326
6431
  const matchedRules = task && !blockingGap ? matchRoutingRules(task, registry) : [];
5327
6432
  const profileSelection = task && !blockingGap ? deriveAllowedProfiles(task, registry, matchedRules) : { profiles: [], resolvedAliases: [] };
5328
- const allowedProfiles = profileSelection.profiles;
5329
- const recommendedProfile = task && !blockingGap ? chooseRecommendedProfile(task, allowedProfiles, registry, matchedRules) : null;
6433
+ const delegatedProfile = task && !blockingGap && options.agent ? toAgentProfileId(options.agent, registry) : null;
6434
+ const allowedProfiles = delegatedProfile && !profileSelection.profiles.includes(delegatedProfile) ? [...profileSelection.profiles, delegatedProfile] : profileSelection.profiles;
6435
+ const taskRecommendedProfile = task && !blockingGap ? chooseRecommendedProfile(task, allowedProfiles, registry, matchedRules) : null;
6436
+ const recommendedProfile = delegatedProfile ?? taskRecommendedProfile;
5330
6437
  const category = task ? routeCategory(task, blockingGap, allowedProfiles, matchedRules) : 'blocked';
5331
6438
  const autonomyCeiling = task ? taskAutonomyCeiling(task) : 'research_before_implementation';
5332
6439
  const requiredCapabilities = task && recommendedProfile ? selectRequiredSkillCapabilities(task, recommendedProfile, registry, matchedRules) : [];
@@ -5336,7 +6443,7 @@ export async function routeSddTask(projectRoot, options) {
5336
6443
  const routedCategory = blockedReason ? 'blocked' : category;
5337
6444
  const registrySources = routeRegistrySources(registry, recommendedProfile, requiredCapabilities);
5338
6445
  const adapterMapping = recommendedProfile ? adapterMappingForProfile(registry, recommendedProfile) : null;
5339
- return {
6446
+ const decision = {
5340
6447
  version: AGENT_ROUTER_CONTRACT_VERSION,
5341
6448
  taskId: options.taskId,
5342
6449
  branch,
@@ -5365,8 +6472,66 @@ export async function routeSddTask(projectRoot, options) {
5365
6472
  resolvedAliases: profileSelection.resolvedAliases,
5366
6473
  routingRuleHits: matchedRules.map((rule) => rule.id),
5367
6474
  quarantineWarnings: quarantineWarningsForSources(registrySources),
5368
- adapterMapping
6475
+ adapterMapping,
6476
+ cache: options.cache ? { contract: ROUTE_CACHE_CONTRACT_VERSION, key: cacheKey, status: 'miss', source: 'content_addressed_derived_route', authoritative: false } : undefined
5369
6477
  };
6478
+ profileSpans.push(runtimeProfileSpan('route_compute', computeStart));
6479
+ if (options.cache) {
6480
+ await writeRouteCache(projectRoot, cacheKey, decision);
6481
+ decision.cache = { contract: ROUTE_CACHE_CONTRACT_VERSION, key: cacheKey, status: 'stored', source: 'content_addressed_derived_route', authoritative: false };
6482
+ }
6483
+ if (options.profile) {
6484
+ decision.profile = [...profileSpans, runtimeProfileSpan('route_total', routeStart, routeStartedAt)];
6485
+ }
6486
+ return decision;
6487
+ }
6488
+ function runtimeProfileSpan(name, startedAtMs, startedAt = new Date(startedAtMs).toISOString()) {
6489
+ const endedAtMs = Date.now();
6490
+ return {
6491
+ contract: RUNTIME_PROFILE_CONTRACT_VERSION,
6492
+ name,
6493
+ startedAt,
6494
+ endedAt: new Date(endedAtMs).toISOString(),
6495
+ durationMs: Math.max(0, endedAtMs - startedAtMs)
6496
+ };
6497
+ }
6498
+ function routeCacheKey(input) {
6499
+ return createHash('sha256')
6500
+ .update(JSON.stringify({
6501
+ contract: ROUTE_CACHE_CONTRACT_VERSION,
6502
+ router: AGENT_ROUTER_CONTRACT_VERSION,
6503
+ teamMode: TEAM_MODE_POLICY_VERSION,
6504
+ policy: ACCEPTANCE_POLICY_RULESET_VERSION,
6505
+ runtime: AGENT_SKILL_TEAM_RUNTIME_CONTRACT_VERSION,
6506
+ capabilities: TOOL_CAPABILITY_REGISTRY_VERSION,
6507
+ plugins: TOOL_PLUGIN_CONTRACT_REGISTRY_VERSION,
6508
+ workers: WORKER_ADAPTER_CONTRACT_REGISTRY_VERSION,
6509
+ taskId: input.taskId,
6510
+ branch: input.branch,
6511
+ agent: input.agent,
6512
+ teamModeActivation: input.teamModeActivation,
6513
+ specHash: input.documents.specHash ?? null,
6514
+ planHash: input.documents.planHash ?? null,
6515
+ tasksHash: input.documents.tasksHash ?? null,
6516
+ planBasedOnSpecHash: input.documents.planBasedOnSpecHash ?? null,
6517
+ tasksBasedOnPlanHash: input.documents.tasksBasedOnPlanHash ?? null
6518
+ }))
6519
+ .digest('hex')
6520
+ .slice(0, 32);
6521
+ }
6522
+ async function readRouteCache(projectRoot, key) {
6523
+ const cachePath = getRouteCachePath(projectRoot, key);
6524
+ if (!await exists(cachePath)) {
6525
+ return null;
6526
+ }
6527
+ const parsed = JSON.parse(await readFile(cachePath, 'utf8'));
6528
+ return parsed.version === AGENT_ROUTER_CONTRACT_VERSION ? parsed : null;
6529
+ }
6530
+ async function writeRouteCache(projectRoot, key, decision) {
6531
+ const cachePath = getRouteCachePath(projectRoot, key);
6532
+ await mkdir(path.dirname(cachePath), { recursive: true });
6533
+ const { cache: _cache, profile: _profile, ...cacheable } = decision;
6534
+ await writeFile(cachePath, `${JSON.stringify(cacheable, null, 2)}\n`, 'utf8');
5370
6535
  }
5371
6536
  export async function inspectQueryStatusContract(projectRoot) {
5372
6537
  await readProjectConfig(projectRoot);
@@ -5993,6 +7158,9 @@ function baseTeamModePolicy(input) {
5993
7158
  activation: input.activation,
5994
7159
  costClass: input.costClass,
5995
7160
  reason: input.reason,
7161
+ costRoute: input.costRoute ?? 'not_applicable',
7162
+ downgradeReason: input.downgradeReason ?? null,
7163
+ trustPolicyEnforced: input.trustPolicyEnforced ?? true,
5996
7164
  chiefProfile: 'orchestrator',
5997
7165
  memberProfiles,
5998
7166
  allowedWaves,
@@ -6018,7 +7186,9 @@ function buildTeamModePolicy(options) {
6018
7186
  decision: 'blocked',
6019
7187
  costClass: 'none',
6020
7188
  reason: options.blockedReason,
6021
- blockedReason: options.blockedReason
7189
+ blockedReason: options.blockedReason,
7190
+ costRoute: 'blocked',
7191
+ trustPolicyEnforced: true
6022
7192
  });
6023
7193
  }
6024
7194
  if (activation === 'off') {
@@ -6028,7 +7198,9 @@ function buildTeamModePolicy(options) {
6028
7198
  enabled: false,
6029
7199
  decision: 'disabled',
6030
7200
  costClass: 'none',
6031
- reason: 'Team-mode automation disabled for this route.'
7201
+ reason: 'Team-mode automation disabled for this route.',
7202
+ costRoute: 'not_applicable',
7203
+ trustPolicyEnforced: true
6032
7204
  });
6033
7205
  }
6034
7206
  const task = options.task ?? null;
@@ -6045,6 +7217,8 @@ function buildTeamModePolicy(options) {
6045
7217
  decision: 'enabled',
6046
7218
  costClass: 'high',
6047
7219
  reason: 'Security-sensitive task automatically requires bounded security-research team evidence.',
7220
+ costRoute: 'no_downgrade',
7221
+ trustPolicyEnforced: true,
6048
7222
  allowedWaves: selectTeamWaves(['security_research', 'validation']),
6049
7223
  memberProfiles: ['security', 'reviewer', 'validator'],
6050
7224
  maxMembers: 3
@@ -6058,6 +7232,8 @@ function buildTeamModePolicy(options) {
6058
7232
  decision: 'enabled',
6059
7233
  costClass: highRisk ? 'high' : 'medium',
6060
7234
  reason: 'High-risk or research-before-implementation task automatically requires adversarial planning/review evidence.',
7235
+ costRoute: 'no_downgrade',
7236
+ trustPolicyEnforced: true,
6061
7237
  allowedWaves: selectTeamWaves(['hyperplan', 'implementation_review', 'validation']),
6062
7238
  memberProfiles: ['architect', 'reviewer', 'security', 'validator'],
6063
7239
  maxMembers: 4
@@ -6071,6 +7247,9 @@ function buildTeamModePolicy(options) {
6071
7247
  decision: 'enabled',
6072
7248
  costClass: 'low',
6073
7249
  reason: activation === 'force' ? 'Team-mode was forced, so router selects the lowest-cost review-lite team.' : 'Task metadata indicates review or validation evidence is useful, so router selects review-lite.',
7250
+ costRoute: activation === 'force' ? 'no_downgrade' : 'downgraded',
7251
+ downgradeReason: activation === 'force' ? null : 'Low-cost review-lite route keeps reviewer/validator artifacts and policy-backed verify mandatory.',
7252
+ trustPolicyEnforced: true,
6074
7253
  allowedWaves: selectTeamWaves(['implementation_review', 'validation']),
6075
7254
  memberProfiles: ['reviewer', 'validator'],
6076
7255
  maxMembers: 2
@@ -6082,7 +7261,10 @@ function buildTeamModePolicy(options) {
6082
7261
  enabled: false,
6083
7262
  decision: 'disabled',
6084
7263
  costClass: 'none',
6085
- reason: 'Low-risk task does not need an agent team.'
7264
+ reason: 'Low-risk task does not need an agent team.',
7265
+ costRoute: 'downgraded',
7266
+ downgradeReason: 'Low-risk task uses no team automation, but artifact trust policy and policy-backed verify remain mandatory.',
7267
+ trustPolicyEnforced: true
6086
7268
  });
6087
7269
  }
6088
7270
  function modelPolicyForProfile(profile, registry) {
@@ -7151,9 +8333,9 @@ function isTaskGap(value) {
7151
8333
  function isRecord(value) {
7152
8334
  return typeof value === 'object' && value !== null;
7153
8335
  }
7154
- async function inspectDocumentChainEvidence(projectRoot) {
8336
+ async function inspectDocumentChainEvidence(projectRoot, branch) {
7155
8337
  try {
7156
- const context = await resolveSddContext(projectRoot);
8338
+ const context = await resolveSddContext(projectRoot, branch ? { branch, branchSource: 'cli_option' } : {});
7157
8339
  const model = await parseSddBranch(projectRoot, context.branch);
7158
8340
  if (!model.documents.specExists || !model.documents.tasksExists) {
7159
8341
  return [{
@@ -7307,6 +8489,119 @@ function routePreflightNeedsTeamSession(event) {
7307
8489
  const teamMode = decision.teamMode;
7308
8490
  return isRecord(teamMode) && (teamMode.decision === 'enabled' || teamMode.decision === 'blocked');
7309
8491
  }
8492
+ function acceptanceCoverageEntries(taskState) {
8493
+ if (!isRecord(taskState) || !Array.isArray(taskState.acceptanceCoverage)) {
8494
+ return [];
8495
+ }
8496
+ return taskState.acceptanceCoverage.filter(isRecord);
8497
+ }
8498
+ function ledgerMaterialRefs(entries) {
8499
+ const refs = new Set();
8500
+ for (const entry of entries) {
8501
+ if (entry.kind === 'material') {
8502
+ refs.add(entry.ref);
8503
+ }
8504
+ for (const ref of entry.materialRefs) {
8505
+ refs.add(ref);
8506
+ }
8507
+ }
8508
+ return refs;
8509
+ }
8510
+ function claimMaterialRefs(claim) {
8511
+ const refs = [];
8512
+ for (const item of claim.evidence) {
8513
+ if (item.kind === 'material') {
8514
+ refs.push(item.ref);
8515
+ }
8516
+ }
8517
+ for (const ref of claim.provenance) {
8518
+ if (ref.startsWith('material:')) {
8519
+ refs.push(ref.slice('material:'.length));
8520
+ }
8521
+ }
8522
+ return refs;
8523
+ }
8524
+ async function inspectRunTrustEvidence(projectRoot, state, invocationLedger) {
8525
+ const checks = [];
8526
+ const runId = state.runId;
8527
+ const materials = ledgerMaterialRefs(invocationLedger);
8528
+ let inspectedTrustArtifacts = 0;
8529
+ if (state.syncBack.status !== 'not_created') {
8530
+ if (!state.syncBack.proposalPath) {
8531
+ checks.push({ level: 'FAIL', check: 'sync_back_monotonicity', message: `${runId}: sync-back status is ${state.syncBack.status} but proposalPath is missing.`, action: 'Keep sync-back transitions monotonic and preserve the proposal path from verify/apply.' });
8532
+ }
8533
+ else if (!state.syncBack.proposalDigest) {
8534
+ checks.push({ level: 'WARN', check: 'sync_back_proposal_digest', message: `${runId}: sync-back proposal ${state.syncBack.proposalPath} has no recorded digest.`, action: 'Re-run verify with the Phase 6.9 runtime so inspect/apply can detect proposal drift.' });
8535
+ }
8536
+ else {
8537
+ try {
8538
+ const proposal = await readArtifact(projectRoot, runId, toArtifactRootRelativePath(state.syncBack.proposalPath));
8539
+ const digest = hashDocumentContent(proposal);
8540
+ if (digest !== state.syncBack.proposalDigest) {
8541
+ checks.push({ level: 'FAIL', check: 'sync_back_proposal_digest', message: `${runId}: sync-back proposal ${state.syncBack.proposalPath} digest changed from ${state.syncBack.proposalDigest} to ${digest}.`, action: 'Restore the verified proposal or re-run verify before sync-back apply.' });
8542
+ }
8543
+ }
8544
+ catch (error) {
8545
+ checks.push({ level: 'FAIL', check: 'sync_back_proposal_digest', message: `${runId}: cannot read sync-back proposal ${state.syncBack.proposalPath}: ${messageFromError(error)}`, action: 'Restore the proposal artifact before sync-back inspect/apply.' });
8546
+ }
8547
+ }
8548
+ }
8549
+ for (const [taskId, taskState] of Object.entries(state.tasks)) {
8550
+ for (const coverage of acceptanceCoverageEntries(taskState)) {
8551
+ if (coverage.status === 'PASS') {
8552
+ const evidence = typeof coverage.evidence === 'string' ? coverage.evidence : '';
8553
+ const issueCodes = Array.isArray(coverage.issueCodes) ? coverage.issueCodes.join(',') : '';
8554
+ if (/Mentioned in artifacts\//i.test(evidence) || !isRecord(coverage.policyDecision) || issueCodes.includes('MENTION_ONLY')) {
8555
+ checks.push({ level: 'FAIL', check: 'acceptance_trust', message: `${runId}/${taskId}: acceptance ${String(coverage.acceptance ?? 'unknown')} is PASS without policy-backed source evidence.`, action: `Use ${SDD_EVIDENCE_CONTRACT} claims; mention-only acceptance coverage cannot satisfy PASS.` });
8556
+ }
8557
+ if (containsTemplatePlaceholder(evidence)) {
8558
+ checks.push({ level: 'FAIL', check: 'acceptance_trust', message: `${runId}/${taskId}: acceptance ${String(coverage.acceptance ?? 'unknown')} PASS evidence contains template placeholder text.`, action: 'Replace generated TODO/template acceptance text with real source evidence.' });
8559
+ }
8560
+ }
8561
+ }
8562
+ }
8563
+ for (const artifact of state.artifacts) {
8564
+ const looksLikeValidatorArtifact = artifact.agent === 'validator' || /(?:^|\/)validation-[^/]+\.md$/i.test(artifact.path);
8565
+ if (!looksLikeValidatorArtifact) {
8566
+ continue;
8567
+ }
8568
+ try {
8569
+ const artifactRootRelativePath = toArtifactRootRelativePath(artifact.path);
8570
+ const raw = await readArtifact(projectRoot, runId, artifactRootRelativePath);
8571
+ const parsed = parseSddResultMarkdown(raw);
8572
+ if (!parsed.result || parsed.result.agent !== 'validator' || (parsed.result.status !== 'PASS' && parsed.result.status !== 'PASS_WITH_GAPS')) {
8573
+ continue;
8574
+ }
8575
+ inspectedTrustArtifacts += 1;
8576
+ const trust = validateArtifactTrust(raw, parsed.result, artifact.path, { expectedTask: artifact.task ?? parsed.result.task, expectedAgent: 'validator' });
8577
+ if (!trust.valid) {
8578
+ for (const issue of trust.issues) {
8579
+ checks.push({ level: 'FAIL', check: 'artifact_trust', message: `${runId}/${artifact.path}: ${issue.message}`, action: issue.recommendation });
8580
+ }
8581
+ }
8582
+ for (const claim of trust.claims) {
8583
+ for (const ref of claimMaterialRefs(claim)) {
8584
+ if (!materials.has(ref)) {
8585
+ checks.push({ level: 'FAIL', check: 'material_provenance', message: `${runId}/${artifact.path}: material evidence ${ref} has no matching invocation ledger entry.`, action: 'Record material/tool/command provenance in invocations.jsonl before using the material as source evidence.' });
8586
+ }
8587
+ }
8588
+ }
8589
+ }
8590
+ catch (error) {
8591
+ checks.push({ level: 'FAIL', check: 'artifact_trust', message: `${runId}/${artifact.path}: cannot inspect validator artifact: ${messageFromError(error)}`, action: 'Restore the validator artifact or remove it from accepted run evidence.' });
8592
+ }
8593
+ }
8594
+ if (inspectedTrustArtifacts > 0 && !checks.some((check) => check.check === 'artifact_trust' || check.check === 'material_provenance')) {
8595
+ checks.push({ level: 'PASS', check: 'artifact_trust', message: `${runId}: inspected ${inspectedTrustArtifacts} validator trust artifact(s).` });
8596
+ }
8597
+ return checks;
8598
+ }
8599
+ function runStateMatchesPartition(state, partition) {
8600
+ if (state.partition === partition) {
8601
+ return true;
8602
+ }
8603
+ return state.gitBranch ? branchToSafePartition(state.gitBranch) === partition : false;
8604
+ }
7310
8605
  async function inspectRunEvidence(projectRoot, options = {}) {
7311
8606
  const runsDir = getRunsDir(projectRoot);
7312
8607
  const entries = await readdir(runsDir, { withFileTypes: true });
@@ -7323,14 +8618,18 @@ async function inspectRunEvidence(projectRoot, options = {}) {
7323
8618
  unreadableRunIds.push(runId);
7324
8619
  }
7325
8620
  }
7326
- const nonArchived = states.filter((entry) => entry.state.status !== 'archived');
7327
- let inspected = options.allRuns ? states : nonArchived;
8621
+ const branchPartition = options.branch ? branchToSafePartition(options.branch) : null;
8622
+ const scopedStates = branchPartition
8623
+ ? states.filter((entry) => runStateMatchesPartition(entry.state, branchPartition))
8624
+ : states;
8625
+ const nonArchived = scopedStates.filter((entry) => entry.state.status !== 'archived');
8626
+ let inspected = options.allRuns ? scopedStates : nonArchived;
7328
8627
  if (!options.allRuns && options.latestOnly && inspected.length > 0) {
7329
8628
  inspected = [inspected.slice().sort((left, right) => Date.parse(right.state.updatedAt) - Date.parse(left.state.updatedAt))[0]];
7330
8629
  }
7331
8630
  const inspectedRunIds = new Set(inspected.map((entry) => entry.runId));
7332
- const skippedArchived = states.length - nonArchived.length;
7333
- const skippedByScope = states.filter((entry) => !inspectedRunIds.has(entry.runId) && entry.state.status !== 'archived').length;
8631
+ const skippedArchived = scopedStates.length - nonArchived.length;
8632
+ const skippedByScope = scopedStates.filter((entry) => !inspectedRunIds.has(entry.runId) && entry.state.status !== 'archived').length;
7334
8633
  if (skippedArchived > 0 && !options.allRuns) {
7335
8634
  checks.push({ level: 'PASS', check: 'run_evidence_scope', message: `Skipped ${skippedArchived} archived run(s); use sdd doctor --all-runs for historical audit.` });
7336
8635
  }
@@ -7362,11 +8661,24 @@ async function inspectRunEvidence(projectRoot, options = {}) {
7362
8661
  const teamSessionRecords = await listTeamSessionRecords(projectRoot, runId);
7363
8662
  const workerRuntimeList = await listResidentWorkerRuntimes(projectRoot, { runId });
7364
8663
  const routePreflightEvents = events.filter((event) => event.event === 'agent_router_preflight');
8664
+ const invocationLedger = await listInvocationLedgerEntries(projectRoot, runId);
7365
8665
  for (const record of agentExecutionRecords) {
7366
8666
  for (const issue of validateAgentExecutionRecordShape(runId, record)) {
7367
8667
  issueCount += 1;
7368
8668
  checks.push({ level: 'FAIL', check: 'agent_execution_record', message: `${runId}/${record.executionId ?? 'unknown'}: ${issue.message}`, action: issue.recommendation });
7369
8669
  }
8670
+ const delegation = record.delegationId ? state.delegations[record.delegationId] : null;
8671
+ if (delegation) {
8672
+ const expectedProfile = toAgentProfileId(delegation.agent);
8673
+ if (record.taskId !== delegation.task) {
8674
+ issueCount += 1;
8675
+ checks.push({ level: 'FAIL', check: 'agent_route_consistency', message: `${runId}/${record.executionId}: execution task ${record.taskId} does not match delegation task ${delegation.task}.`, action: 'Persist per-delegation execution records from the matching route decision.' });
8676
+ }
8677
+ if (expectedProfile && (record.profile !== expectedProfile || record.toolPermission?.profile !== expectedProfile || record.routeDecision.recommendedProfile !== expectedProfile)) {
8678
+ issueCount += 1;
8679
+ checks.push({ level: 'FAIL', check: 'agent_route_consistency', message: `${runId}/${record.executionId}: execution profile/tool-permission/route does not match delegation agent ${delegation.agent}.`, action: 'Route each delegation independently and persist matching profile, tool permission, and route decision.' });
8680
+ }
8681
+ }
7370
8682
  }
7371
8683
  for (const record of teamSessionRecords) {
7372
8684
  for (const issue of validateTeamSessionRecordShape(runId, record)) {
@@ -7398,6 +8710,10 @@ async function inspectRunEvidence(projectRoot, options = {}) {
7398
8710
  if (agentExecutionRecords.length > 0 || teamSessionRecords.length > 0 || routePreflightEvents.length > 0) {
7399
8711
  checks.push({ level: 'PASS', check: 'agent_team_execution_records', message: `${runId}: inspected ${agentExecutionRecords.length} agent execution record(s), ${teamSessionRecords.length} team session record(s), and ${routePreflightEvents.length} router preflight event(s).` });
7400
8712
  }
8713
+ for (const check of await inspectRunTrustEvidence(projectRoot, state, invocationLedger)) {
8714
+ issueCount += check.level === 'PASS' ? 0 : 1;
8715
+ checks.push(check);
8716
+ }
7401
8717
  for (const delegation of Object.values(state.delegations)) {
7402
8718
  const report = await validateDelegationRecord(projectRoot, runId, delegation);
7403
8719
  if (report.stale) {
@@ -7445,10 +8761,10 @@ async function inspectRunEvidence(projectRoot, options = {}) {
7445
8761
  checks.push({ level: 'WARN', check: 'run_evidence', message: 'No runs found under .sdd/runs.', action: 'Create a run before /sdd-do or /sdd-verify.' });
7446
8762
  }
7447
8763
  else if (inspected.length === 0 && issueCount === 0) {
7448
- checks.push({ level: 'WARN', check: 'run_evidence', message: 'No non-archived runs were inspected.', action: 'Use sdd doctor --all-runs to audit archived history or create a new run.' });
8764
+ checks.push({ level: 'WARN', check: 'run_evidence', message: branchPartition ? `No non-archived runs were inspected for branch ${branchPartition}.` : 'No non-archived runs were inspected.', action: 'Use sdd doctor --all-runs to audit archived history or create a new run.' });
7449
8765
  }
7450
8766
  else if (issueCount === 0) {
7451
- checks.push({ level: 'PASS', check: 'run_evidence', message: `Inspected ${inspected.length} run(s); no stale delegation, invalid artifact, terminal event gap, or resident worker runtime issue found.` });
8767
+ checks.push({ level: 'PASS', check: 'run_evidence', message: `Inspected ${inspected.length} run(s); no stale delegation, invalid artifact, terminal event gap, trust evidence, or resident worker runtime issue found.` });
7452
8768
  }
7453
8769
  return checks;
7454
8770
  }
@@ -7929,8 +9245,10 @@ async function inspectProjectContextPackDoctorContract(projectRoot) {
7929
9245
  }
7930
9246
  export async function readRunEvents(projectRoot, runId) {
7931
9247
  const eventPath = path.join(getRunDir(projectRoot, runId), 'events.jsonl');
7932
- if (!await exists(eventPath)) {
7933
- return [];
9248
+ await importLegacyRunEventsIfNeeded(projectRoot, runId, eventPath);
9249
+ const storedEvents = await readRuntimeRunEvents(projectRoot, runId);
9250
+ if (storedEvents.length > 0 || !await exists(eventPath)) {
9251
+ return storedEvents;
7934
9252
  }
7935
9253
  const raw = await readFile(eventPath, 'utf8');
7936
9254
  return raw.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));