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
41 changes: 19 additions & 22 deletions docs/engineering/sdlc.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ scope, verification and, for Track C, the design. The issue is its canonical hom
`start-work` publishes it before asking for approval.

```sh
agent-workflows --config agent-workflows.json work publish WORK_ID --revision CURRENT_REVISION --project app --json
agent-workflows --config agent-workflows.json work publish WORK_ID --revision CURRENT_REVISION
```

| Work Item source | Effect |
Expand All @@ -105,28 +105,23 @@ reassessment first. Local Work Items need publication before a `github` or

## Inspect and approve work

The intake skill returns a Work Item ID and effective track. Read its current
contract and decision before approving.

```sh
agent-workflows --config agent-workflows.json work inspect WORK_ID --project app --json
```
The intake skill returns a Work Item ID, its effective track and where to approve
it. Open the Work Item in the web monitor (`agent-workflows monitor --web WORK_ID`)
or the terminal monitor (`agent-workflows monitor`, then `w`) and read its
current contract and decision before approving.

Track A can become ready after an expected outcome is recorded. Track B waits for
scope approval. Track C waits for scope approval, then a complete design and
design approval. An unresolved material uncertainty prevents admission on every
track. Read the [contract reference](task-contract.md) for field and revision rules.

For a personal pilot using `approvalSource: local-operator`, run the operator
command yourself after inspecting the exact complete revision.

```sh
agent-workflows --config agent-workflows.json work approve WORK_ID --revision CURRENT_REVISION --gate scope --project app
agent-workflows --config agent-workflows.json work approve WORK_ID --revision CURRENT_REVISION --gate design --project app
```
For a personal pilot using `approvalSource: local-operator`, approve the exact
revision you inspected with **Review scope approval** in the web monitor or `a`
in the terminal monitor. The CLI equivalent is
`work approve WORK_ID --revision CURRENT_REVISION --gate scope`.

Use only the gate currently required. Design approval requires a design. The
intake skill never runs these commands. Conversational agreement and issue labels
intake skill never approves. Conversational agreement and issue labels
do not create approval records.

The worker admits a ready Work Item on its next poll. Final approval can therefore
Expand All @@ -138,7 +133,7 @@ remains persisted. A later worker start resumes intake. Explicit submission uses
the same eligibility checks and returns an existing admission for the same revision.

```sh
agent-workflows --config agent-workflows.json work submit WORK_ID --revision CURRENT_REVISION --project app --json
agent-workflows --config agent-workflows.json work submit WORK_ID --revision CURRENT_REVISION
agent-workflows --config agent-workflows.json status --json
agent-workflows --config agent-workflows.json inspect RUN_ID
```
Expand Down Expand Up @@ -184,7 +179,7 @@ The worker imports permitted approval during polling. To import existing comment
without waiting for another poll, run the sync command.

```sh
agent-workflows --config agent-workflows.json work sync-approvals WORK_ID --project service --json
agent-workflows --config agent-workflows.json work sync-approvals WORK_ID
```

Keep approval credentials separate from implementation credentials. Configure the
Expand All @@ -206,11 +201,13 @@ source comments and credential separation before unattended company execution.

When source or scope escalation blocks a project, preserve unfinished work and
clean the checkout before starting another attempt. Reassess the Work Item and
record its required approvals. Submit the new revision explicitly with
`work submit WORK_ID --revision CURRENT_REVISION --project app --recover`.
This checks that recorded processes have stopped and the checkout is clean before
atomically clearing the block and admitting the revised work. It preserves an
operator pause. Automatic polling never clears a recovery block.
record its required approvals. The monitor then marks the ready item
recoverable: press `r` in the terminal monitor, or choose **Resubmit after
recovery** in the web monitor. The CLI equivalent is
`work submit WORK_ID --revision CURRENT_REVISION --recover`. Each checks that
recorded processes have stopped and the checkout is clean before atomically
clearing the block and admitting the revised work. It preserves an operator
pause. Automatic polling never clears a recovery block.

Inspect workflow-executed validation, independent review, limitations, and final
readiness. Outstanding manual checks, missing configured validation, or incomplete
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
} from "./cli-runner-control.js";
import { resolveConfigPath } from "./config-discovery.js";
import { registerDoctor } from "./doctor.js";
import { applyCliTokens } from "./hosting-token.js";
import { inferProject } from "./repository-inference.js";
import { createTerminalNotificationWriter, Monitor } from "./tui/index.js";
import { defaultWebPort } from "./web/contracts.js";
Expand Down Expand Up @@ -474,6 +475,7 @@ program
}
if (control?.envFile) await loadEnvironmentFile(control.envFile);
const config = control?.configuration ?? (await configuration());
applyCliTokens(config.projects);
if (control && sha256(databaseUrl(config)) !== control.databaseUrlHash)
throw new Error(
"Database connection differs from the running server; use its startup environment",
Expand All @@ -485,6 +487,7 @@ program
await withConfiguredStore(config, async (store) => {
const work = workOperator({
store,
stateDirectory: resolve(configBaseDirectory(), config.stateDirectory),
projects: async () =>
(
await readActiveConfiguration(configPath(), config.id)
Expand All @@ -497,6 +500,7 @@ program
source: {
workItems: () => work.list(),
approveWork: (id, approval) => work.approve(id, approval),
resubmitWork: (id, revision) => work.resubmit(id, revision),
projects: async () =>
(await store.projects()).filter((project) =>
config.projects.some((active) => active.id === project.id),
Expand All @@ -522,6 +526,7 @@ program
});
});
registerWorkCommands(program, {
launchDirectory,
pathBaseDirectory: configBaseDirectory,
configuration,
monitorUrl: async () => {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/tui/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
export interface MonitorSource {
workItems?: () => Promise<WorkItemView[]>;
approveWork?: (id: string, approval: WorkApproval) => Promise<WorkItemView>;
resubmitWork?: (id: string, revision: string) => Promise<WorkItemView>;
configuration?: () => Promise<Configuration>;
projects: Store["projects"];
runs: Store["runs"];
Expand Down
64 changes: 62 additions & 2 deletions packages/cli/src/tui/work-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export function WorkItemsView({
title: string;
approval: WorkApproval;
}>();
const [resubmission, setResubmission] = useState<{
id: string;
title: string;
revision: string;
}>();
const refresh = useCallback(async () => {
if (!source.workItems)
throw new Error(
Expand Down Expand Up @@ -81,6 +86,10 @@ export function WorkItemsView({
!pending &&
selected?.approvalSource === "local-operator" &&
Boolean(gate && source.approveWork);
const recoverable =
connected &&
!pending &&
Boolean(selected?.recoverable && source.resubmitWork);
const height = rows - 8;
const lines = selected
? [
Expand All @@ -91,6 +100,11 @@ export function WorkItemsView({
selected.work.revision,
"",
...selected.decision.reasons.map((reason) => reason.message),
...(selected.recoverable
? [
"The project is blocked by an earlier attempt. Preserve and clean the checkout, then press r to resubmit.",
]
: []),
...(selected.approvalSource !== "local-operator"
? [
`Approval requires an authenticated allowed human through ${selected.approvalSource}.`,
Expand All @@ -115,7 +129,13 @@ export function WorkItemsView({
const maximum = Math.max(0, wrapLines(lines, columns - 4).length - height);
const currentOffset = Math.min(offset, maximum);
useInput((input, key) => {
if (key.ctrl || key.meta || key.eventType === "release" || confirmation)
if (
key.ctrl ||
key.meta ||
key.eventType === "release" ||
confirmation ||
resubmission
)
return;
if (key.escape || input === "w" || input === "q") onClose();
else if (key.tab && visible.length) {
Expand All @@ -135,6 +155,12 @@ export function WorkItemsView({
title: selected.work.contract.title,
approval: { revision: selected.work.revision, gate },
});
else if (input === "r" && recoverable && selected)
setResubmission({
id: selected.work.id,
title: selected.work.contract.title,
revision: selected.work.revision,
});
else if (key.home) setOffset(0);
else if (key.end) setOffset(maximum);
else if (key.upArrow || key.pageUp)
Expand Down Expand Up @@ -164,6 +190,40 @@ export function WorkItemsView({
setPending(false);
}
};
const resubmit = async () => {
if (!resubmission || !source.resubmitWork || pendingRef.current) return;
pendingRef.current = true;
setPending(true);
setResubmission(undefined);
setMessage("Checking the checkout and resubmitting...");
try {
await source.resubmitWork(resubmission.id, resubmission.revision);
setMessage("Resubmitted; the runner starts it on its next scan.");
setItems(await refresh());
} catch (error) {
setMessage(error instanceof Error ? error.message : String(error));
} finally {
pendingRef.current = false;
setPending(false);
}
};
if (resubmission)
return (
<ConfirmDialog
columns={columns}
rows={rows}
title="Resubmit after recovery"
subject={resubmission.title}
description={`Clear the project block and run this revision once earlier processes stopped and the checkout is clean.\n\n${resubmission.revision}`}
available={
recoverable &&
selected?.work.id === resubmission.id &&
selected.work.revision === resubmission.revision
}
onCancel={() => setResubmission(undefined)}
onConfirm={() => void resubmit()}
/>
);
if (confirmation)
return (
<ConfirmDialog
Expand Down Expand Up @@ -201,7 +261,7 @@ export function WorkItemsView({
<Text>{chromeCells(message, columns - 4)}</Text>
<Text>
{chromeCells(
`${allowed ? `a approve ${gate} / ` : ""}Tab select / Up/Down scroll / PgUp/PgDn / Esc close`,
`${allowed ? `a approve ${gate} / ` : ""}${recoverable ? "r resubmit / " : ""}Tab select / Up/Down scroll / PgUp/PgDn / Esc close`,
columns - 4,
)}
</Text>
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/web/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export const workApprovalSchema = z.strictObject({
gate: z.enum(["scope", "design"]),
});
export type WorkApproval = z.infer<typeof workApprovalSchema>;
export const workResubmissionSchema = workApprovalSchema.pick({
revision: true,
});
export type { WorkItemView };
export interface WorkItemsSnapshot {
items: WorkItemView[];
Expand Down
27 changes: 26 additions & 1 deletion packages/cli/src/web/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import {
} from "../cli-runner-control.js";
import { discoverRuns, runFilters } from "../monitor/discovery.js";
import { webAssetDirectory } from "./assets.js";
import { monitorCommandSchema, workApprovalSchema } from "./contracts.js";
import {
monitorCommandSchema,
workApprovalSchema,
workResubmissionSchema,
} from "./contracts.js";
import { MonitorEvidence } from "./evidence.js";
import type { WebStartup } from "./lifecycle.js";

Expand Down Expand Up @@ -171,6 +175,7 @@ export function monitorServer(store: Store, startup: WebStartup) {
);
const work = workOperator({
store,
stateDirectory: startup.stateDirectory,
projects: async () =>
(
await readActiveConfiguration(startup.configPath, startup.scope)
Expand All @@ -196,6 +201,26 @@ export function monitorServer(store: Store, startup: WebStartup) {
await work.approve(context.req.param("id"), parsed.data),
);
});
app.use("/api/work-items/:id/resubmit", bodyLimit({ maxSize: 4096 }));
app.post("/api/work-items/:id/resubmit", async (context) => {
const parsed = workResubmissionSchema.safeParse(
await context.req.json().catch(() => null),
);
if (!parsed.success)
return context.json({ error: "Provide the inspected revision" }, 400);
try {
return context.json(
await work.resubmit(context.req.param("id"), parsed.data.revision),
);
} catch (error) {
if (error instanceof WorkItemNotFoundError) throw error;
// Safety and policy refusals tell the operator what to fix first.
return context.json(
{ error: error instanceof Error ? error.message : String(error) },
409,
);
}
});
app.get("/api/runs/:id/log", async (context) => {
const parsed = logQuerySchema.safeParse(context.req.query());
if (!parsed.success)
Expand Down
Loading
Loading