---
title: Schedule updates and reports
description: Fetch remote changes, report tracked work, and keep on-call attention visible.
---

Once one issue has completed the cycle, add a patrol. Every 15 minutes it
fetches remote refs, lists labeled issues, investigates one new or changed
incident, checks tracked PRs, and publishes a report.

The patrol imports the shared `record.ts`, the
[on-call modules](/marbles/use-cases/software-factory/maintenance/on-call),
and `status.ts` from [delivery](/marbles/use-cases/software-factory/development/approve#observe-merge-and-closure).
Create those helpers before adding it.

Keep delivery and maintenance as separately launchable workflows. This
patrol gathers status. A [PR review](/marbles/use-cases/software-factory/maintenance/pull-requests)
or [inspection](/marbles/use-cases/software-factory/improvement/inspector) is additional
work with its own inputs and limits. Schedule those only after a manual run
produces evidence you can use.

Create `.foundry/marbles/_factory/patrol.ts`:

<details>
<summary>Complete patrol.ts</summary>

```ts
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { artifact, schedule, step } from "@foundry.sh/marbles";
import { z } from "zod";
import { coverageSchema, onCall } from "./on-call";
import { baseBranch, readRecord, repository } from "./record";
import { status } from "./status";

const reports = artifact({ name: "Factory patrol", type: "text/markdown" });
const issuesSchema = z.array(
  z.object({
    labels: z.array(z.object({ name: z.string() })),
    number: z.number(),
    state: z.string(),
    title: z.string(),
    updatedAt: z.string(),
    url: z.string(),
  })
);
const patrol = step("factory-patrol").do(
  async ({ exec, workspaces, report, run }) => {
    await exec(["git", "fetch", "origin"]);
    const revision = (
      await exec(["git", "rev-parse", `origin/${baseBranch}`])
    ).stdout.trim();
    const previous = await (await reports.latest())?.text("revision.txt");
    const range = previous ? `${previous.trim()}..${revision}` : revision;
    const changes = await exec(["git", "log", "-10", "--oneline", range]);
    const response = await exec([
      "gh",
      "issue",
      "list",
      "--repo",
      repository,
      "--state",
      "all",
      "--search",
      "label:factory-ready,incident",
      "--limit",
      "100",
      "--json",
      "number,title,url,state,updatedAt,labels",
    ]);
    const issues = issuesSchema.parse(JSON.parse(response.stdout));
    const coverage = coverageSchema.parse(
      JSON.parse(
        await readFile(
          join(workspaces.current.root, ".foundry/on-call.json"),
          "utf8"
        )
      )
    );
    const rows: string[] = [];
    let investigated = false;
    for (const issue of issues) {
      let work = await readRecord(issue.number);
      const marker = JSON.stringify({ coverage, updatedAt: issue.updatedAt });
      const observed = work?.events.some(
        (event) => event.stage === "on-call observed" && event.detail === marker
      );
      if (
        !investigated &&
        issue.state === "OPEN" &&
        !observed &&
        issue.labels.some((label) => label.name === "incident")
      ) {
        work = await onCall.run({ coverage, issue: issue.number });
        investigated = true;
      }
      if (work?.pullRequest) {
        work = await status.run({ issue: issue.number, quiet: true });
      }
      const assessment = work?.events.findLast(
        (event) => event.stage === "on-call assessment"
      );
      const handoff = work?.events.findLast(
        (event) => event.stage === "on-call handoff"
      );
      let attention = "";
      if (
        issue.state === "OPEN" &&
        assessment &&
        (!handoff || handoff.at < assessment.at)
      ) {
        const due = z
          .object({ acknowledgeBy: z.string(), backup: z.string() })
          .parse(JSON.parse(assessment.detail));
        if (Date.now() > Date.parse(due.acknowledgeBy)) {
          attention = `; acknowledgement overdue, ${due.backup} needs to take over`;
        }
      }
      rows.push(
        `- [#${issue.number}](${issue.url}) ${issue.title}: ${work?.status ?? issue.state}${attention}`
      );
    }
    const body = [
      `Run: ${run.id}`,
      `Fetched origin/${baseBranch}: ${revision}`,
      "## Latest commits",
      changes.stdout || "No new commits since the last report.",
      "## Issues and incidents",
      ...rows,
      "This report covers up to 100 labeled issues and 10 commits. Check the tracker and Git history for the rest.",
    ].join("\n\n");
    const version = await report.artifact(
      reports,
      {
        "patrol.md": `# Factory patrol\n\n${body}`,
        "revision.txt": revision,
      },
      { body }
    );
    return { issues: issues.length, report: version.id, revision };
  }
);

schedule(patrol).every("15m");
```

</details>

Create `.foundry/marbles/patrol.ts`:

```ts
import "./_factory/patrol";
```

The query includes issues labeled `factory-ready` or `incident`, including
closed issues so delivery can be observed. It is bounded to 100 issues and
10 recent commits. Increase coverage or paginate before using it for a larger
queue. Keep a tracked issue's label until its delivery has been recorded.

## Run once before scheduling

```sh
marbles roll factory-patrol
marbles list
marbles
```

Inspect the report in the Feed, then start triggers in the dashboard.
For unattended operation, use `marbles run </dev/null`. Keep the process
running; schedules stop when it exits.

## What the report tells you

The versioned `Factory patrol` artifact records the fetched revision,
recent commits, issue progress, unstarted work, open incidents, and overdue
acknowledgements. It keeps the last reported revision for the next comparison.
Use `factory-status` to read one issue's full chronicle.

`report.artifact(reports, files, { body })` also puts the readable patrol
report in the Feed. People read that entry; the next patrol reads
`revision.txt` from the artifact. The
[artifact and Feed roles](/marbles/foundation/artifacts-and-results#artifacts-and-the-feed)
stay separate even when one call creates both.

Fetching brings in new remote changes without merging them into the active
checkout. New issues pin that fetched revision. Existing candidates retain
their recorded base until you deliberately update and reverify them.

The patrol's calls to `factory-on-call` and `factory-status` use `.run()`.
They belong to the patrol's execution rather than appearing as separate
dashboard runs. Their issue logs still record the results. Run those steps
by name when investigating one issue directly.

## Advance ready work

Use the report to select the next issue, then run
`marbles roll factory --input '{"issue":42}'`. Keep publication attended.
The patrol does not interpret every observed issue as permission to edit code.

This sample assumes one writer per issue. A schedule's local overlap lock
does not protect against a second manual launch or another machine.
Before adding automatic claims, define ownership, duplicate handling, and
recovery of partially completed work. [Remote execution](/marbles/reference/remote)
and the [roadmap](/marbles/roadmap) describe the current deployment limits
and future webhook support.
