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
35 changes: 35 additions & 0 deletions agents/BADGER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Badger Book initialized

Version: 0.0.1
Date: 2026-06-07
Initialized: 2026-06-09
Updated: 2026-06-09

## Issue Tracker

- Type: GitHub Issues + GitHub Project
- Repository: https://github.com/modoterra/stasium
- Project: https://github.com/orgs/modoterra/projects/1

## Labels And Status

Labels are for triage/readiness:

- `needs-triage`
- `needs-info`
- `ready-for-agent`
- `ready-for-human`
- `wontfix`

GitHub Project `Status` is for delivery progress:

- `Todo`
- `In Progress`
- `In Review`
- `Ready to Merge`
- `Done`
- `Blocked`

Do not duplicate delivery progress as labels while the GitHub Project is configured.

Rerun `/initialize` when workflow labels, Project fields, or `docs/agents` drift.
21 changes: 10 additions & 11 deletions docs/agents/issue-tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,16 @@ For multiple issues, repeat both steps for each issue. Do not leave newly create

Use the `Status` field in the `Stasium` Project to show workflow state:

| Status | Use when |
| ----------------- | ---------------------------------------------------- |
| `Needs triage` | New or unclassified item |
| `Needs info` | Waiting on reporter, owner, or product clarification |
| `Ready for agent` | Fully specified and safe for an AFK coding agent |
| `Ready for human` | Requires human judgment or implementation |
| `In progress` | Actively owned work |
| `Done` | Completed or merged work |
| `Won't fix` | Closed as intentionally not actioned |

When creating issues from a PRD or plan, set `Ready for agent` only if the issue is independently grabbable and has enough acceptance criteria. Otherwise set `Needs triage` or `Needs info`.
| Status | Use when |
| ---------------- | ------------------------------------------------------ |
| `Todo` | Queued for delivery |
| `In Progress` | Actively owned work |
| `In Review` | PR exists and is awaiting review or checks |
| `Ready to Merge` | PR is self-reviewed and all agent-runnable checks pass |
| `Done` | Completed or merged work |
| `Blocked` | Delivery cannot continue until a blocker is resolved |

When creating issues from a PRD or plan, apply the `ready-for-agent` label only if the issue is independently grabbable and has enough acceptance criteria. Otherwise apply `needs-triage` or `needs-info`. Use Project `Status` for delivery progress only.

To set a Project item status, fetch the Project item ID and option IDs, then update the `Status` field:

Expand Down
20 changes: 10 additions & 10 deletions docs/agents/triage-labels.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Triage Labels

The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. Delivery progress lives in the GitHub Project `Status` field, not in labels.

| Label in mattpocock/skills | Label in our tracker | Project `Status` | Meaning |
| -------------------------- | -------------------- | ----------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | `Needs triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | `Needs info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | `Ready for agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | `Ready for human` | Requires human implementation |
| `wontfix` | `wontfix` | `Won't fix` | Will not be actioned |
| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |

When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table and set the matching Project `Status` when the issue is in the `Stasium` Project.
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.

Issues with no triage label should usually be placed in `Needs triage`. Use `In progress` for actively owned work and `Done` for completed Project items.
Issues with no triage label should usually be placed in `needs-triage`. Use the GitHub Project `Status` field for active delivery states such as `In Progress`, `In Review`, `Ready to Merge`, `Done`, and `Blocked`.
16 changes: 16 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ const setupKeybindings = (
case "i":
await openDiscovery();
break;
case "space":
if (manager.getSelectedView()) {
manager.deselect();
} else if (manager.getViews().length > 0) {
manager.setSelectedIndex(0);
}
break;
case "d": {
const view = manager.getSelectedView();
if (view) {
Expand Down Expand Up @@ -419,6 +426,15 @@ const setupKeybindings = (
case "select":
manager.moveSelection(1);
return;
case "scope":
case "all logs":
case "service logs":
if (manager.getSelectedView()) {
manager.deselect();
} else if (manager.getViews().length > 0) {
manager.setSelectedIndex(0);
}
return;
default:
return;
}
Expand Down
2 changes: 2 additions & 0 deletions src/cross-runtime-startup-dependencies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LaunchInstructionExecutionAdapter } from "./launch-execution";
import { addProcessDefinition } from "./manifest-editing";
import { loadManifest, saveManifest } from "./manifest";
import { ProcessClaimStore } from "./process-claim";
import { getExternalRuntimeStatus } from "./runtime-status";
import { ServiceManager } from "./service-manager";
import { normalizeProcessDefinition } from "./process-definition";
import type { ExternalManagedProcess, LogEntry, ServicePid } from "./types";
Expand All @@ -19,6 +20,7 @@ const externalProcess = (
runtimeName: "Docker Compose",
name,
state,
runtimeStatus: getExternalRuntimeStatus(state),
status: state,
ports: "",
});
Expand Down
2 changes: 2 additions & 0 deletions src/docker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ describe("createDockerComposeExternalRuntimeAdapter", () => {
runtimeName: "Docker Compose",
name: "api",
state: "exited",
runtimeStatus: "off",
status: "Exited",
ports: "",
},
Expand All @@ -109,6 +110,7 @@ describe("createDockerComposeExternalRuntimeAdapter", () => {
runtimeName: "Docker Compose",
name: "db",
state: "running",
runtimeStatus: "running",
status: "Up",
ports: "5432",
},
Expand Down
6 changes: 5 additions & 1 deletion src/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ExternalRuntimeAdapter,
ExternalRuntimeOutputStream,
} from "./external-runtime";
import { getExternalRuntimeStatus } from "./runtime-status";
import { fileExists } from "./shared";
import type { ExternalManagedProcess, ExternalManagedProcessState, LogEntry } from "./types";

Expand Down Expand Up @@ -194,11 +195,13 @@ class DockerComposeExternalRuntime implements ExternalRuntime {
return getStableDockerServiceNames(configServices, entryOrder).map((name) => {
const list = entriesByService.get(name) ?? [];
if (list.length === 0) {
const state = "created";
return {
runtimeId: this.id,
runtimeName: this.name,
name,
state: "created",
state,
runtimeStatus: getExternalRuntimeStatus(state),
status: "",
ports: "",
};
Expand All @@ -213,6 +216,7 @@ class DockerComposeExternalRuntime implements ExternalRuntime {
runtimeName: this.name,
name,
state,
runtimeStatus: getExternalRuntimeStatus(state),
status: representative?.Status ?? "",
ports: representative?.Ports ?? "",
};
Expand Down
2 changes: 2 additions & 0 deletions src/external-runtime-session.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { ExternalRuntimeSession } from "./external-runtime";
import { getExternalRuntimeStatus } from "./runtime-status";
import type { ExternalManagedProcess } from "./types";

describe("External Runtime Session", () => {
Expand Down Expand Up @@ -28,6 +29,7 @@ const process = (runtimeId: string, name: string): ExternalManagedProcess => ({
runtimeName: runtimeId,
name,
state: "running",
runtimeStatus: getExternalRuntimeStatus("running"),
status: "Up",
ports: "",
});
2 changes: 2 additions & 0 deletions src/external-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type ExternalRuntime,
type ExternalRuntimeAdapter,
} from "./external-runtime";
import { getExternalRuntimeStatus } from "./runtime-status";
import type { ExternalManagedProcess, LogEntry } from "./types";

describe("detectExternalRuntimes", () => {
Expand Down Expand Up @@ -236,6 +237,7 @@ const process = (
runtimeName: runtimeId,
name,
state,
runtimeStatus: getExternalRuntimeStatus(state),
status: "Up",
ports: "",
});
1 change: 1 addition & 0 deletions src/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const MANIFEST_SHORTCUTS: Shortcut[] = [
{ key: "i", label: "discover" },
{ key: "d", label: "delete" },
{ key: "e", label: "edit" },
{ key: "space", label: "scope" },
{ key: "up/down", label: "select" },
];

Expand Down
2 changes: 2 additions & 0 deletions src/manifest-editing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { loadManifest, renderServiceBlock, saveManifest } from "./manifest";
import { normalizeProcessDefinition } from "./process-definition";
import { ProcessClaimStore } from "./process-claim";
import { getExternalRuntimeStatus } from "./runtime-status";
import { ServiceManager } from "./service-manager";
import type { ExternalManagedProcess } from "./types";

Expand Down Expand Up @@ -53,6 +54,7 @@ const externalProcess = (name = "db"): ExternalManagedProcess => ({
runtimeName: "Docker Compose",
name,
state: "running",
runtimeStatus: getExternalRuntimeStatus("running"),
status: "running",
ports: "",
});
Expand Down
37 changes: 37 additions & 0 deletions src/runtime-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, test } from "bun:test";
import {
getDirectManagedRuntimeStatus,
getExternalRuntimeStatus,
getRuntimeStatusView,
} from "./runtime-status";

describe("runtime status", () => {
test("uses user-facing codes for canonical statuses", () => {
expect(getRuntimeStatusView("running")).toMatchObject({ code: "Run", label: "running" });
expect(getRuntimeStatusView("retrying")).toMatchObject({ code: "Rty", label: "retrying" });
expect(getRuntimeStatusView("errored")).toMatchObject({ code: "Err", label: "errored" });
expect(getRuntimeStatusView("blocked")).toMatchObject({ code: "Blk", label: "blocked" });
expect(getRuntimeStatusView("paused")).toMatchObject({ code: "Pau", label: "paused" });
expect(getRuntimeStatusView("unknown")).toMatchObject({ code: "Unk", label: "unknown" });
});

test("normalizes direct-managed service states", () => {
expect(getDirectManagedRuntimeStatus("RUNNING", null)).toBe("running");
expect(getDirectManagedRuntimeStatus("STARTING", null)).toBe("starting");
expect(getDirectManagedRuntimeStatus("STOPPING", null)).toBe("stopping");
expect(getDirectManagedRuntimeStatus("FAILED", null)).toBe("errored");
expect(getDirectManagedRuntimeStatus("BLOCKED", null)).toBe("blocked");
expect(getDirectManagedRuntimeStatus("STOPPED", null)).toBe("off");
expect(getDirectManagedRuntimeStatus("FAILED", 250)).toBe("retrying");
});

test("normalizes external runtime states", () => {
expect(getExternalRuntimeStatus("running")).toBe("running");
expect(getExternalRuntimeStatus("restarting")).toBe("retrying");
expect(getExternalRuntimeStatus("dead")).toBe("errored");
expect(getExternalRuntimeStatus("paused")).toBe("paused");
expect(getExternalRuntimeStatus("created")).toBe("off");
expect(getExternalRuntimeStatus("exited")).toBe("off");
expect(getExternalRuntimeStatus("unknown")).toBe("unknown");
});
});
74 changes: 74 additions & 0 deletions src/runtime-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { ExternalManagedProcessState, RuntimeStatus, ServiceState } from "./types";

export interface RuntimeStatusView {
status: RuntimeStatus;
code: string;
label: string;
severity: "good" | "attention" | "bad" | "muted";
}

export const getRuntimeStatusView = (status: RuntimeStatus): RuntimeStatusView => {
switch (status) {
case "running":
return { status, code: "Run", label: "running", severity: "good" };
case "retrying":
return { status, code: "Rty", label: "retrying", severity: "attention" };
case "errored":
return { status, code: "Err", label: "errored", severity: "bad" };
case "blocked":
return { status, code: "Blk", label: "blocked", severity: "attention" };
case "starting":
return { status, code: "Str", label: "starting", severity: "attention" };
case "stopping":
return { status, code: "Stp", label: "stopping", severity: "attention" };
case "off":
return { status, code: "Off", label: "off", severity: "muted" };
case "paused":
return { status, code: "Pau", label: "paused", severity: "attention" };
case "unknown":
return { status, code: "Unk", label: "unknown", severity: "muted" };
}
};

export const getDirectManagedRuntimeStatus = (
state: ServiceState,
restartInMs: number | null,
): RuntimeStatus => {
if (restartInMs !== null) return "retrying";

switch (state) {
case "RUNNING":
return "running";
case "STARTING":
return "starting";
case "STOPPING":
return "stopping";
case "FAILED":
return "errored";
case "BLOCKED":
return "blocked";
case "STOPPED":
return "off";
}
};

export const getExternalRuntimeStatus = (state: ExternalManagedProcessState): RuntimeStatus => {
switch (state) {
case "running":
return "running";
case "restarting":
return "retrying";
case "paused":
return "paused";
case "dead":
return "errored";
case "created":
return "off";
case "removing":
return "starting";
Comment on lines +65 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat created containers as off

When Docker Compose reports a service as created (and this adapter also assigns created to configured services with no ps entry in src/docker.ts), the container is not running rather than actively starting; Docker's ps docs list created separately from running, where running is the state after docker start/docker run (https://docs.docker.com/reference/cli/docker/container/ls/). Mapping it to starting makes never-started Compose services show Str indefinitely and skews panel summaries/colors until the user starts them.

Useful? React with 👍 / 👎.

case "exited":
return "off";
case "unknown":
return "unknown";
}
};
Loading
Loading