Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/concepts/execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ Git is deterministic and owned by the engine — **the agent never runs git and

For PR-per-unit delivery, DynamoDB is scheduling truth. A unit is complete only after every changed repository confirms the exact ready head is on the intent branch. A partial multi-repository merge preserves merged work, blocks dependents, and enters halt-and-ask.

The lane board calls this outcome **Integrated**. Its stored identifier remains
`MERGED`; it does not mean that the final intent-to-base PR has merged. Counts
use the active unit plan; retained rows from earlier plans remain history.

A walking skeleton must be a dependency-free unit in the current DAG. A missing
choice receives a dependency-free default before approval. Re-promotion retains
valid captured choices. If the regenerated DAG renames that unit or gives it
dependencies, promotion records the change and offers a new dependency-free
default at the normal fan-out approval. Original answers remain in history.
Execution still rejects invalid choices in an already approved plan. A workflow with `skeleton: off`
still runs all units, without the solo-first ceremony.

Shared stages and successful completion require every active-plan lane to be
integrated or explicitly omitted by a recorded human skip. Blocked-only work
still enters halt-and-ask. Retry preserves integration; skip also omits the
unintegrated dependent closure; abort fails the run. Partial delivery is presented
separately from complete delivery.

Provider comments never launch agents by themselves. A project member selects comments in the intent's review drawer; the API refetches those comments with fresh credentials, rejects bot/system comments, and queues a bounded, audited feedback batch. The lane revisits its last successful stage, runs its normal sensors, commits and pushes the revision, and posts a summary without resolving provider threads.

## Human gates, park and resume
Expand Down
40 changes: 40 additions & 0 deletions docs/development/recovery-ownership.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,43 @@ Storage must accept a write to persist FAILED state. During a complete outage th
worker preserves its structured failure, while durable dispatch and callback
reconciliation remain the recovery backstop. These guards protect process state;
they do not undo provider requests or Git operations already in flight.

## Completion boundaries

New histories capture completion policy 1 in the existing `load-meta` result.
The shared unit-completion evaluator validates the pinned, resolved workflow's
expected sections, current-plan units, repository integration evidence and scoped
human omissions. Section fan-in and final success use the same evaluator against
persisted process state. Missing rows, unfinished work and partial integrations
cannot pass; missing provenance, read failures and ambiguous legacy omissions
remain unverified. Serial workflows and sections excluded by approved workflow
scope/composition remain valid. Pre-deploy `MERGED` rows without an
`integrationResults` attribute retain their supported integrated meaning, including
multi-repository retries and rewinds past completed sections. New integrations
always write the attribute; explicit empty, invalid or incomplete evidence never
receives this legacy exemption. PR-per-unit integration still requires its
per-repository PR records. Unpinned historical serial intents without unit work
keep their success presentation without unit-completion diagnostics. Cancelled
executions and recorded aborts from the current run are classified as aborted
before workflow-pin resolution, without implying that their work was integrated.
Missing or truncated execution reads remain unverified.

Required integration writes are not best-effort. A lost response is reconciled
from persisted state; an unrecorded integration stops the lane before dependent
work can proceed. Final status and its completion receipt commit together under
run, status, META revision and unit-plan conditions. Initial publication rechecks
completion before opening a PR. These checks never rerun models or repair rows.
Marker-absent histories (policy 0) retain their saved operation sequence and
terminal step. The intermediate completion implementation was not released and
has no separate replay path. Saved historical successes are never rewritten.

A terminal success with unfinished or unverified work is shown honestly in the
intent detail. Accepted omissions remain partial delivery, not execution defects.
This release does not add automatic preservation-and-continuation recovery for
finished incomplete intents. Existing orphan-lane/PR-review repair and ordinary
rewind remain supported; rewind resets affected lanes, including integrated work,
and clears omission receipts for the work explicitly being retried. Original
answers and events remain historical. Preserve runtime and methodology pins.

Deploy the runtime, orchestrator, intents API and frontend from the same release.
No automatic production audit, status migration or runtime repinning is performed.
6 changes: 3 additions & 3 deletions frontend/src/components/intent/UnitLaneBoard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe('UnitLaneBoardView', () => {
// (>=1) rather than uniqueness; state labels are unique per fixture.
expect(screen.getAllByText('auth').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('reporting')).toBeInTheDocument();
expect(screen.getByText('Merged')).toBeInTheDocument();
expect(screen.getByText('Integrated')).toBeInTheDocument();
expect(screen.getByText('Building')).toBeInTheDocument();
expect(screen.getByText('Blocked')).toBeInTheDocument();
expect(screen.getByText('Pending')).toBeInTheDocument();
Expand All @@ -115,7 +115,7 @@ describe('UnitLaneBoardView', () => {

it('summarizes only the states that are present', () => {
renderView();
expect(screen.getByText('1 merged')).toBeInTheDocument();
expect(screen.getByText('1 integrated')).toBeInTheDocument();
expect(screen.getByText('1 building')).toBeInTheDocument();
expect(screen.getByText('1 blocked')).toBeInTheDocument();
expect(screen.getByText('1 pending')).toBeInTheDocument();
Expand All @@ -137,7 +137,7 @@ describe('UnitLaneBoardView', () => {
expect(screen.queryByText(/failed/)).not.toBeInTheDocument();
expect(screen.queryByText('legacy')).not.toBeInTheDocument();
// real units still counted
expect(screen.getByText('1 merged')).toBeInTheDocument();
expect(screen.getByText('1 integrated')).toBeInTheDocument();
});

it('renders the live stream only when a running stage instance is provided', () => {
Expand Down
21 changes: 16 additions & 5 deletions frontend/src/components/intent/UnitLaneBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const STATE_LABEL: Record<DisplayState, string> = {
PR_READY: 'Ready to merge',
ADDRESSING_FEEDBACK: 'Fixing feedback',
MERGING: 'Merging',
MERGED: 'Merged',
MERGED: 'Integrated',
BLOCKED: 'Blocked',
FAILED: 'Failed',
WAITING_INPUT: 'Waiting for input',
Expand Down Expand Up @@ -402,6 +402,7 @@ function UnitCard({

// ── Presentational board (pure — tested in isolation) ────────────────────────
export interface UnitLaneBoardViewProps {
intentBranch?: string | null;
unitPlan: IntentUnitPlan;
units: IntentUnit[];
/** Live transcript tail per unit slug (only running units need an entry). */
Expand All @@ -418,6 +419,7 @@ export interface UnitLaneBoardViewProps {
}

export function UnitLaneBoardView({
intentBranch,
unitPlan,
units,
streamsBySlug,
Expand Down Expand Up @@ -449,10 +451,8 @@ export function UnitLaneBoardView({
// lambda/shared/v2-process-store.js:1000), so counting over all detail.units
// would report states for units that aren't on the board.
const planSlugs = useMemo(() => {
const fromBatches = (unitPlan.batches ?? []).flat();
const slugs = fromBatches.length > 0 ? fromBatches : (unitPlan.units ?? []).map((u) => u.slug);
return new Set(slugs);
}, [unitPlan.batches, unitPlan.units]);
return new Set((unitPlan.units ?? []).map((u) => u.slug));
}, [unitPlan.units]);

// State-count chips — only states actually present among plan units, in a
// stable order.
Expand Down Expand Up @@ -480,6 +480,16 @@ export function UnitLaneBoardView({
return (
<Card>
<CardHeader className="pb-3">
<p className="text-xs text-muted-foreground">
Integrated units are on the intent branch
{intentBranch ? (
<>
{' '}
<code>{intentBranch}</code>
</>
) : null}
. The final pull request is reviewed and merged separately.
</p>
<div className="flex flex-wrap items-center gap-2">
<CardTitle className="text-sm">Units</CardTitle>
<Badge variant="outline" className="text-[10px] text-muted-foreground">
Expand Down Expand Up @@ -708,6 +718,7 @@ export function UnitLaneBoard({
return (
<>
<UnitLaneBoardView
intentBranch={detail?.intent.branch}
unitPlan={unitPlan}
units={units}
streamsBySlug={streamsBySlug}
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/pages/IntentView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2397,3 +2397,46 @@ describe('IntentView — resume after a failed gate callback', () => {
).not.toBeInTheDocument();
});
});

describe('terminal completion diagnostics without terminal recovery', () => {
it.each(['incomplete', 'unknown', 'partial'] as const)(
'shows %s evidence without offering preservation recovery',
async (state) => {
get.mockResolvedValue({
...baseDetail({ status: 'SUCCEEDED' }),
unitCompletion: {
state,
total: 2,
integrated: 1,
skipped: state === 'partial' ? 1 : 0,
unresolved:
state === 'partial'
? []
: [
{
sectionIndex: 1,
slug: 'remaining',
state: 'BLOCKED',
reason: 'unit_unfinished',
},
],
legacyPartial: false,
},
});
renderAt();
expect(
await screen.findByText(
state === 'incomplete'
? 'Incomplete work'
: state === 'unknown'
? 'Completion unverified'
: 'Partial delivery',
),
).toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /Preview recovery|Confirm and continue/ }),
).not.toBeInTheDocument();
expect(repair).not.toHaveBeenCalled();
},
);
});
37 changes: 36 additions & 1 deletion frontend/src/pages/IntentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ export default function IntentView() {
}

const intent = detail.intent;
const completion = detail.unitCompletion;
const terminalCompletion = intent.status === 'SUCCEEDED' ? completion?.state : null;
const laneWaits = deriveLaneWaits(detail.stages, gates);
const recoveryWaits = Object.values(laneWaits).filter((wait) => wait.kind === 'recovery');
const needsLaneRepair =
Expand Down Expand Up @@ -417,7 +419,13 @@ export default function IntentView() {
</h1>
{TERMINAL_STATUSES.has(intent.status) && (
<Badge variant="outline" className="text-[10px] shrink-0">
{intent.status}
{terminalCompletion === 'incomplete'
? 'Incomplete work'
: terminalCompletion === 'unknown'
? 'Completion unverified'
: terminalCompletion === 'partial'
? 'Partial delivery'
: intent.status}
</Badge>
)}
<MethodologyReleaseBadge release={intent.methodologyRelease ?? null} />
Expand Down Expand Up @@ -588,6 +596,33 @@ export default function IntentView() {
</div>
)}

{terminalCompletion && terminalCompletion !== 'complete' && (
<div className="rounded border border-agent-error/30 px-3 py-3 text-sm" role="status">
<p>
{terminalCompletion === 'partial'
? 'Partial delivery was explicitly accepted. Integrated work and the recorded omissions are preserved.'
: terminalCompletion === 'unknown'
? 'Completion could not be verified. Review the recorded work and available evidence before publishing.'
: 'The recorded success has unfinished current-plan work. Review the remaining units before publishing.'}
</p>
<p>
{completion?.integrated} of {completion?.total} units verified integrated;{' '}
{completion?.skipped} explicitly omitted.
</p>
{completion?.unresolved.slice(0, 12).map((unit) => (
<p key={`${unit.sectionIndex}/${unit.slug}`}>
Section {unit.sectionIndex}/{unit.slug}: {unit.reason ?? unit.state}
</p>
))}
{terminalCompletion !== 'partial' && (
<p>
Automatic continuation of finished incomplete intents is not supported. Ordinary
rewind reruns the affected work.
</p>
)}
</div>
)}

{needsLaneRepair && (
<div className="rounded border border-agent-error/30 bg-agent-error/[0.06] px-3 py-3 text-sm">
<div className="flex items-start justify-between gap-3">
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/services/intents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,20 @@ export interface IntentDetail {
/** Composer sessions (scope/grid proposals + authoritative validation). */
composes?: ComposeSession[];
unitPlan?: IntentUnitPlan | null;
unitCompletion?: {
state: 'complete' | 'partial' | 'incomplete' | 'unknown' | 'aborted';
reason?: string | null;
total: number;
integrated: number;
skipped: number;
unresolved: {
sectionIndex: number;
slug: string;
state: UnitState | 'UNKNOWN' | 'MISSING';
reason?: string;
}[];
legacyPartial: boolean;
} | null;
units?: IntentUnit[];
unitPrs?: IntentUnitPr[];
feedbackBatches?: IntentFeedbackBatch[];
Expand Down
31 changes: 26 additions & 5 deletions lambda/agentcore/commands/promote-units.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

import { createGraphWriter, closeGraphSource } from '../mcp/graph-writer.js';
import { parseBoltDag } from '../../shared/v2-sensor-contract.js';
import { effectiveSkeleton, skeletonProblem } from '../../shared/unit-completion.js';

const DAG_ARTIFACT_TYPE = 'unit-of-work-dependency';

Expand Down Expand Up @@ -122,20 +123,39 @@ export const promoteUnits = async (payload, deps) => {
// batch (batches are sorted). The fan-out approval can override.
// Consistent: a stale read would drop decisions the fan-out approval saved.
const existingPlan = await store.getUnitPlan(executionId, { consistentRead: true });
const priorSkeleton = existingPlan?.walkingSkeleton;
const priorProblem =
priorSkeleton == null ? null : skeletonProblem({ units, batches }, priorSkeleton);
const walkingSkeleton = priorProblem
? effectiveSkeleton({ units, batches })
: effectiveSkeleton({ units, batches, walkingSkeleton: priorSkeleton });
const plan = await store.putUnitPlan({
executionId,
units,
batches,
sourceArtifactId: artifact.id ?? null,
producedByStageInstanceId: stageInstanceId,
// Preserve previously captured human decisions across a re-promotion —
// a rewind re-produces the DAG, not the humans' skip/skeleton/autonomy
// answers (the unit-DAG stage's gate re-confirms them on re-approval).
// Keep valid choices across re-promotion. A regenerated DAG can rename
// or add dependencies to the old skeleton; choose a valid default BEFORE
// the normal fan-out approval, preserving the original answer in history.
skipMatrix: existingPlan?.skipMatrix ?? {},
walkingSkeleton: existingPlan?.walkingSkeleton ?? batches[0]?.[0] ?? null,
walkingSkeleton,
autonomyMode: existingPlan?.autonomyMode ?? null,
});
const sync = await store.syncUnitRows({ executionId, units, batches, sectionIndexes });
const sync = await store.syncUnitRows({
executionId,
units,
batches,
sectionIndexes,
});
if (priorProblem)
await event(
'v2.units.skeleton_reselected',
`${priorProblem} Regenerated DAG default: ${walkingSkeleton}; review it at fan-out approval.`,
);
// Execution still rejects invalid choices in an already approved plan.
const selectionProblem = skeletonProblem(plan);
if (selectionProblem) await event('v2.units.decisions_invalid', selectionProblem);

// 4. Traceability mirror (never blocks promotion outcome — the DDB truth
// is already written; a mirror failure is recorded and visible).
Expand Down Expand Up @@ -164,6 +184,7 @@ export const promoteUnits = async (payload, deps) => {
unitCount: units.length,
batchCount: batches.length,
walkingSkeleton: plan.walkingSkeleton,
selectionProblem,
sync,
mirror,
};
Expand Down
Loading
Loading