diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 5b7f49b..43ab3c6 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -1,5 +1,33 @@ # Test Integrity Notes +## 2026-10-03 — Visible automatic-work stops + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-runtime-continuation-health + given: "An enabled pilot exhausts its daily attempt allowance" + when: "Observation reads the ledger, next UTC day, pause or malformed state" + then: "The exact stop and remaining allowance are visible without resetting or modifying state, and no timer/token qualification is fabricated" + - id: SCN-console-continuation-stop + given: "A daily cap stop and a specialist no-progress hold" + when: "Attention items are projected" + then: "The budget decision belongs to the owner, coordination belongs to the manager, and no fake approval action or automatic budget increase is offered" + observable_outcomes: + - "Live snapshot health, byte-identical ledger and owner/manager attention routing" + determinism_controls: + - "Hermetic repository, fixed UTC clock, injected executor and plain snapshot fixtures" + anti_cheat_rationale: + prevents: + - "Silent budget exhaustion disguised as healthy idle" + - "Resetting a ledger to make work appear available" + - "Treating declared policy as a verified running timer or measured usage" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + ## 2026-10-03 — Rolling continuation and bounded no-progress escalation ```yaml diff --git a/docs/TODO.md b/docs/TODO.md index 371be8c..e7a205d 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -2,6 +2,17 @@ ## Current owner priority: reliability first (2026-10-03 UTC) +End-to-end repair qualification is still open. Runtime reported exact converged +candidate `d2d9845` with four fresh native check PASS receipts; QA independent +acceptance is not yet recorded. Owner source fixes cover guarded presence, +rolling/fair dispatch, no-progress escalation and visible daily-cap stops. The +first automatic allowance of 12 turns was exhausted; any increase needs an +explicit owner choice, not a manual-start bypass. Next dispatch should prioritize +QA's exact verdict/support decisions, Release's declared-inventory consumer +decision, safe Kernel/Work composition and actual Console implementation. +Require evidence of specialist work, independent review/checks, serialized +integration and next-work continuation before calling the autonomous loop ready. + Live all-fleet resume exposed a new urgent proposal: `TASK-d95e72d0-816d-4fc2-acaf-b73642fd4881` — refill continuation capacity when individual turns exit instead of waiting for the longest batch member. diff --git a/docs/spec_changes/2026-10-03-rolling-continuation.md b/docs/spec_changes/2026-10-03-rolling-continuation.md index 3f48e19..e495d46 100644 --- a/docs/spec_changes/2026-10-03-rolling-continuation.md +++ b/docs/spec_changes/2026-10-03-rolling-continuation.md @@ -14,4 +14,10 @@ Status exposes occupied guard identities and no-progress holds. Installed releas Eligible identities are ordered least-recently-dispatched within the current UTC day, with the Manager first only on ties. A repeatedly revised specialist task must not consume every freed slot while an eligible peer has never started. +## Visible operating stops + +The read-only project observation includes the automatic-turn budget, remaining attempts, exact pause/cap stop, retained capacity guards and current no-progress holds. It never resets the ledger, changes authority or treats declared policy as verified timer installation. No-progress holds are displayed only while the same active task IDs/revisions remain; resolved or revised tasks do not retain stale alerts. + +An exhausted automatic allowance appears as an owner policy decision in Overview and as an explicit Fleet status line. Manager-owned no-progress waits remain with the Manager, rather than being falsely routed to the owner. There is no fabricated approval button or silent allowance increase. Manual owner turns are not included in this ledger; token/cost governance remains unknown. + Deterministic acceptance scenarios and anti-cheat notes are in docs/TEST_INTEGRITY.md. Independent live qualification remains necessary; do not close broader event-driven or persistent-operations umbrellas based on this source fix alone. diff --git a/site/attention-projection.js b/site/attention-projection.js index 1808af1..5d32ad2 100644 --- a/site/attention-projection.js +++ b/site/attention-projection.js @@ -16,6 +16,18 @@ function attentionGroups(snapshot) { const agentName = (id) => agents.get(id)?.title ?? id ?? 'Owner not recorded'; const add = (group, item) => groups[group].push(item); const approvals = (snapshot.approvalRequests?.items ?? []).filter((approval) => approval.status === 'pending'); + const continuation = snapshot.continuation; + if (continuation?.available && continuation.stopReason === 'daily-turn-cap') add('owner', { + tone: 'decision', title: 'Automatic fleet work stopped at its daily limit', owner: 'Project owner', + detail: 'No additional automatic agent turns can start today unless you explicitly change the limit. Existing turns may finish; idle does not mean all work is done.', + evidence: `${continuation.attempts}/${continuation.maxTurnsPerDay} automatic attempts · UTC day ${continuation.day} · not measured token/cost usage`, + href: '#fleet', action: 'Review continuation policy', decisionApprovalId: null, + }); + for (const hold of continuation?.held ?? []) add('fleet', { + tone: 'review', title: `${agentName(hold.areaId)} needs a concrete coordination decision`, owner: agentName('session-manager'), + detail: 'Two completed turns left the assigned task state unchanged. Unchanged-work retries are held; inspect evidence and resolve the actual blocker rather than restarting blindly.', + evidence: hold.areaId, href: '#communications', action: 'Open coordination messages', requestOwner: 'session-manager', + }); const advisory = []; const recheckCodes = new Set(['BACKLOG_OBSERVED_COMMIT_STALE', 'BACKLOG_OBSERVED_COMMIT_MISSING']); diff --git a/site/console.html b/site/console.html index 4af0f6e..23cd5b8 100644 --- a/site/console.html +++ b/site/console.html @@ -183,6 +183,7 @@

Persistent identities

Fleet and ownership

Session state from latest observation
+

Reading automatic continuation policy…

Sessions

One context per identity
diff --git a/site/console.js b/site/console.js index c82d85d..7835193 100644 --- a/site/console.js +++ b/site/console.js @@ -1039,6 +1039,12 @@ function renderAttention(groups) { } function render(snapshot) { + const continuation = snapshot.continuation; + setText('#continuation-status', continuation?.available + ? `Automatic turns: ${continuation.attempts}/${continuation.maxTurnsPerDay} used today (UTC); ${continuation.remainingTurns} left. ${continuation.stopReason === 'daily-turn-cap' + ? 'No further automatic starts: the owner-controlled daily cap is reached. Running turns may finish.' + : continuation.stopReason === 'paused' ? 'Owner policy is paused; no new automatic starts.' : 'Policy permits new eligible turns; this does not verify timer installation.'} ${continuation.held?.length ?? 0} unchanged-work hold(s) need Manager coordination. This is not measured token or cost usage.` + : 'Automatic continuation policy is unavailable; do not assume the fleet can start more work.'); const project = snapshot.project ?? snapshot.repository; const agents = snapshot.agents ?? []; const tasks = snapshot.backlog ?? []; diff --git a/src/observability/snapshot.mjs b/src/observability/snapshot.mjs index a8d9879..19b6156 100644 --- a/src/observability/snapshot.mjs +++ b/src/observability/snapshot.mjs @@ -17,6 +17,7 @@ import { observeTaskActivity } from '../backlog/activity.mjs'; import { ScheduleLauncherService } from '../schedules/launcher.mjs'; import { hierarchyOrder } from '../runtime/hierarchy-order.mjs'; import { observeRuntimeTurn } from '../runtime/turn-guard.mjs'; +import { observeContinuation } from '../runtime/continuation.mjs'; function json(path) { return JSON.parse(readFileSync(path, 'utf8')); @@ -404,6 +405,7 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => metrics: ['cachedInput', 'uncachedInput', 'cacheCreation', 'cacheRead', 'compactions', 'resumedPromptBytes', 'costPerVerifiedItem'], }, }; + result.continuation = observeContinuation({ stateRoot, tasks: result.backlog, now }); result.backlogHealth = backlogHealth(repository.root, result.backlog, result.agents, now); result.backlogActivity = observeBacklogActivity({ repositoryRoot: repository.root, tasks: result.backlog, config, manifest, now: now() }); diff --git a/src/runtime/continuation.mjs b/src/runtime/continuation.mjs index 14f945e..376767a 100644 --- a/src/runtime/continuation.mjs +++ b/src/runtime/continuation.mjs @@ -43,6 +43,27 @@ export function configureContinuation(control, { actorId, enabled, maxTurnsPerDa return { enabled, maxTurnsPerDay, areas: state.areas, sessionsStarted: false }; } +export function observeContinuation({ stateRoot, tasks = [], now = () => new Date() } = {}) { + try { + const state = read({ stateRoot }); + const day = now().toISOString().slice(0, 10); + const attempts = state.day === day ? state.attempts : 0; + const remainingTurns = Math.max(0, state.maxTurnsPerDay - attempts); + const held = Object.entries(state.progress).filter(([area, progress]) => { + const active = tasks.filter(t => t.owner === area && ['assigned', 'in_progress'].includes(t.state)); + return active.length && progress?.status === 0 && progress.noProgress >= 2 && progress.taskKey === taskKey(active); + }).map(([areaId]) => ({ areaId, reason: 'no-progress-needs-coordination' })); + return { available: existsSync(join(stateRoot, 'continuation-policy.json')), enabled: state.enabled, + day, dayTimezone: 'UTC', attempts, maxTurnsPerDay: state.maxTurnsPerDay, remainingTurns, + stopReason: !state.enabled ? 'paused' : remainingTurns === 0 ? 'daily-turn-cap' : null, + capacityHeldBy: occupiedAreas({ stateRoot }), held, + budgetKind: 'automatic-turn-cap', manualTurnsIncluded: false, tokenOrCostGovernance: 'unknown', + timerInstallationVerified: false, mutationPerformed: false }; + } catch { + return { available: false, stopReason: 'policy-or-ledger-unreadable', mutationPerformed: false }; + } +} + export function planContinuation(control, backlog, { at = new Date(), limit = 3 } = {}) { const state = read(control); const day = at.toISOString().slice(0, 10); diff --git a/test/fleet/console-attention.test.mjs b/test/fleet/console-attention.test.mjs index 35b0377..2330d41 100644 --- a/test/fleet/console-attention.test.mjs +++ b/test/fleet/console-attention.test.mjs @@ -6,6 +6,18 @@ import '../../site/live-refresh.js'; const project = (snapshot) => globalThis.TorchAttentionProjection.groups(snapshot); +test('SCN-console-continuation-stop: exhausted budget is an owner policy decision; no-progress remains manager-owned', () => { + const groups = project({ continuation: { available: true, stopReason: 'daily-turn-cap', attempts: 12, + maxTurnsPerDay: 12, day: '2026-10-03', held: [{ areaId: 'owner-console' }] } }); + assert.equal(groups.owner.length, 1); + assert.match(groups.owner[0].detail, /explicitly change the limit/); + assert.equal(groups.owner[0].decisionApprovalId, null, 'No fake approval or automatic budget expansion'); + assert.equal(groups.fleet.length, 1); + assert.equal(groups.fleet[0].owner, 'session-manager'); + assert.equal(groups.fleet[0].requestOwner, 'session-manager'); + assert.equal(project({ continuation: { available: true, stopReason: 'paused', held: [] } }).owner.length, 0); +}); + test('SCN-console-attention-ownership: only owner-addressed pending decisions enter Waiting on you', () => { const groups = project({ agents: [ diff --git a/test/fleet/legacy-runtime-recovery.test.mjs b/test/fleet/legacy-runtime-recovery.test.mjs index ede4a1c..125f133 100644 --- a/test/fleet/legacy-runtime-recovery.test.mjs +++ b/test/fleet/legacy-runtime-recovery.test.mjs @@ -12,7 +12,7 @@ import { openControlPlane } from '../../src/control-plane/service.mjs'; import { issueStoppedExecutorEvidence } from '../../src/runtime/stopped-executor-evidence.mjs'; import { observeRuntimeTurn, withRuntimeTurnGuard } from '../../src/runtime/turn-guard.mjs'; import { observeProject } from '../../src/observability/snapshot.mjs'; -import { configureContinuation, planContinuation, runContinuation } from '../../src/runtime/continuation.mjs'; +import { configureContinuation, observeContinuation, planContinuation, runContinuation } from '../../src/runtime/continuation.mjs'; function fixture({ recoveryWorktree = true, extraWorker = false } = {}) { const root = mkdtempSync(join(tmpdir(), 'torch-legacy-recovery-')); @@ -332,6 +332,33 @@ test('SCN-runtime-continuation-fairness: a fresh task revision does not let one } finally { f.control.close(); } }); +test('SCN-runtime-continuation-health: budget stops, day reset, pause and unreadable ledgers are visible without mutation', async () => { + const f = fixture({ recoveryWorktree: false }); + const at = new Date('2026-10-03T00:00:00Z'); + try { + f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Idle' }); + f.control.sendOwnerRequest({ actorId: 'owner', recipient: f.areaId, body: 'Work' }); + configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 1 }); + await runContinuation(f.control, { list: () => [] }, { actorId: 'owner', at, launch: () => 0 }); + const path = join(f.control.stateRoot, 'continuation.json'); + const before = readFileSync(path, 'utf8'); + const health = observeContinuation({ stateRoot: f.control.stateRoot, now: () => at }); + assert.equal(health.stopReason, 'daily-turn-cap'); + assert.equal(health.remainingTurns, 0); + assert.equal(health.manualTurnsIncluded, false); + assert.equal(health.timerInstallationVerified, false); + const snapshot = observeProject({ repositoryRoot: f.root, env: f.env, now: () => at }); + assert.equal(snapshot.continuation.stopReason, 'daily-turn-cap'); + assert.equal(readFileSync(path, 'utf8'), before); + assert.equal(observeContinuation({ stateRoot: f.control.stateRoot, now: () => new Date('2026-10-04T00:00:00Z') }).remainingTurns, 1); + configureContinuation(f.control, { actorId: 'owner', enabled: false, maxTurnsPerDay: 1 }); + assert.equal(observeContinuation({ stateRoot: f.control.stateRoot, now: () => at }).stopReason, 'paused'); + writeFileSync(path, 'not-json'); + assert.equal(observeContinuation({ stateRoot: f.control.stateRoot, now: () => at }).stopReason, 'policy-or-ledger-unreadable'); + assert.equal(readFileSync(path, 'utf8'), 'not-json', 'Observation must not repair or reset the attempt ledger'); + } finally { f.control.close(); } +}); + test('SCN-legacy-runtime-recovery-project: evidence cannot be applied to a different checkout with cloned project/session fields', () => { const first = fixture(); const second = fixture();