diff --git a/LOGIC.md b/LOGIC.md index e0530e6f2..7f74d9925 100644 --- a/LOGIC.md +++ b/LOGIC.md @@ -51,7 +51,7 @@ The Framework: autonomous AI programming. The user registers repositories as pro - `packages/agent-driver` — the driver [2] seam: one contract for driving a coding agent as a black box, with the Claude Code, Codex, GitHub Actions and fake implementations; the product adds its own cloud-session implementation behind the same contract. The Framework prompts one turn at a time, lets the coding agent's own loop run to completion, and learns everything from the turn's final message: it never gates on the agent's individual tool calls, holds no model key, and runs on the user's own subscription. - `packages/agent-data` — a branch of the project's repository used as a file store: the `agent-data` branch [6], checked out under `.branches/`, written through one sync → commit → push cycle that re-applies when a push loses a race. A library, not a skill: read by code, never by an agent. Every skill depends on it; nothing else does. - `packages/skill-branches`, `packages/skill-tickets`, `packages/skill-queue`, `packages/skill-logs` — the four skills [8]: an agent's own checkout [3] and branch; the tickets with their plans and claims; the agent queue [7]; the record of every run [18]. Tickets, queue, runs and routine locks live on the `agent-data` branch, never on a code branch, so the default branch stays code only. No skill depends on another. -- `packages/routines` — the routines the daemon fires on its own, one skill file each and no code: jobs that compose the four skills [8], starting with the queued work; The Framework depends on it and fires a routine by its slash command. +- `packages/skill-work-queue` — the first command skill: a job for an agent, one skill file and no code, composing the four skills [8] without naming one; the daemon fires it by its slash command, `/work-queue`. One package per command, `@gemstack/skill-`; The Framework depends on it for now. - `packages/chrome-extension` — the far end of the Claude web bridge [14], a Chrome extension reading claude.ai in a signed-in browser; it talks to the daemon over HTTP only. - `packages/the-framework.ai` — the marketing site at https://the-framework.ai; it presents the product and shares no code with it. - `.github/workflows` — every push builds, type-checks and tests the monorepo; the website deploys itself from the default branch; and one workflow is the far end of the `github-actions` driver implementation, running one turn per workflow run. diff --git a/packages/LOGIC.md b/packages/LOGIC.md index ff1f9fab2..11cf12622 100644 --- a/packages/LOGIC.md +++ b/packages/LOGIC.md @@ -17,7 +17,7 @@ The deliverables The Framework ships, one directory each. The product is the `fr - **The `tickets` skill** (`skill-tickets/`) - the `@gemstack/skill-tickets` npm package: the project's tickets with their plans and claims on the `agent-data` branch, the `tickets` command that reads, writes, claims and closes them, and the skill text. Depends on `agent-data`. - **The `queue` skill** (`skill-queue/`) - the `@gemstack/skill-queue` npm package: the agent queue on the `agent-data` branch, the `queue` command that reads it, adds an entry at a priority and takes one off, and the skill text. Depends on `agent-data`. - **The `logs` skill** (`skill-logs/`) - the `@gemstack/skill-logs` npm package: the record of every run agents made on a project, on the `agent-data` branch, and the read-only `logs` command; the product writes every run through it. Depends on `agent-data`. -- **The routines** (`routines/`) - the `@gemstack/routines` npm package: the jobs a daemon fires on its own, one skill file each under `skills//` and no code, each composing the capability skills of a project; the queued work first. +- **The `work-queue` command** (`skill-work-queue/`) - the `@gemstack/skill-work-queue` npm package: a command skill, one `SKILL.md` and no code, the job of working one queued task off the agent queue; it composes the capability skills of a project without naming one. One package per command; the daemon fires it by its slash command. - **The Claude web bridge's extension** (`chrome-extension/`) - a Chrome extension, not an npm package: the far end of the Claude web bridge [5], reading claude.ai in a signed-in browser. Talks to the product over HTTP only. - **The website** (`the-framework.ai/`) - the marketing site at https://the-framework.ai. Presents the product; shares no code with it. diff --git a/packages/framework/package.json b/packages/framework/package.json index 0672f058d..db79d85bc 100644 --- a/packages/framework/package.json +++ b/packages/framework/package.json @@ -47,7 +47,7 @@ "dependencies": { "@base-ui-components/react": "1.0.0-rc.0", "@gemstack/agent-data": "workspace:*", - "@gemstack/routines": "workspace:*", + "@gemstack/skill-work-queue": "workspace:*", "@gemstack/skill-branches": "workspace:*", "@gemstack/skill-logs": "workspace:*", "@gemstack/skill-queue": "workspace:*", diff --git a/packages/framework/prompts/presets/LOGIC.md b/packages/framework/prompts/presets/LOGIC.md index 3681f5276..9c82d8813 100644 --- a/packages/framework/prompts/presets/LOGIC.md +++ b/packages/framework/prompts/presets/LOGIC.md @@ -1,4 +1,4 @@ -The fourteen presets: the canned prompts behind the launcher's buttons and the daemon's rotation routines [2], one markdown file each. Five of them are quality passes over a target the user names; one sweeps the code and queues quality work; four are product management, proposing or picking tickets; four are the routines that keep the tickets and the agent queue [4] fresh. The routine that works the queue is not a preset: it is the skill file `skills/work-queue/SKILL.md` of the `@gemstack/routines` package, which only the daemon fires. The table in `src/preset-catalog.ts` says which button or routine each backs, in which order the launcher shows them, and appends the queue-only rule of `../triage_scope.md` to the two triage presets. +The fourteen presets: the canned prompts behind the launcher's buttons and the daemon's rotation routines [2], one markdown file each. Five of them are quality passes over a target the user names; one sweeps the code and queues quality work; four are product management, proposing or picking tickets; four are the routines that keep the tickets and the agent queue [4] fresh. The routine that works the queue is not a preset: it is the `SKILL.md` of the `@gemstack/skill-work-queue` package, a command skill only a person or the daemon fires. The table in `src/preset-catalog.ts` says which button or routine each backs, in which order the launcher shows them, and appends the queue-only rule of `../triage_scope.md` to the two triage presets. ## Context diff --git a/packages/framework/src/auto-pm.LOGIC.md b/packages/framework/src/auto-pm.LOGIC.md index 41d89e12a..95ac5957b 100644 --- a/packages/framework/src/auto-pm.LOGIC.md +++ b/packages/framework/src/auto-pm.LOGIC.md @@ -1,4 +1,4 @@ -Decides, once per look [1] and per project, whether the daemon may spend the account's quota [2] on work nobody asked for, and on what. Auto PM [3] either starts an agent [6] on the queued work [4] because the `agent-data` branch [11] moved, or, once a run found nothing queued, refills the agent queue [5] by firing the next routine [7] of a fixed rotation. The daemon reads no queue and names no skill: it reads the head of the branch, and it starts the queued work with one slash command, `/work-queue`, the routine skill [22] shipped with The Framework. Every reason not to start is a sentence the daemon logs and the dashboard shows. +Decides, once per look [1] and per project, whether the daemon may spend the account's quota [2] on work nobody asked for, and on what. Auto PM [3] either starts an agent [6] on the queued work [4] because the `agent-data` branch [11] moved, or, once a run found nothing queued, refills the agent queue [5] by firing the next routine [7] of a fixed rotation. The daemon reads no queue and names no skill: it reads the head of the branch, and it starts the queued work with one slash command, `/work-queue`, the command skill [22] shipped with The Framework. Every reason not to start is a sentence the daemon logs and the dashboard shows. ## Context @@ -29,7 +29,7 @@ Decides, once per look [1] and per project, whether the daemon may spend the acc [19] checkout: an agent's own working copy of the project: a git worktree under the project's `.branches/` directory, named as its branch. [20] agent id: an agent's stable id, derived from the moment it started; it names the agent's checkout directory, its branch until the agent names it, and its run. [21] holder: who a claim names: the agent's id when the daemon started the agent, else the branch the `tickets` command ran on. -[22] routine skill: a skill file (`SKILL.md`) marked so that only a person or the daemon invokes it, whose body is the job's prompt; the daemon starts the agent with the skill's slash command and the coding agent's harness expands it. +[22] command skill: a skill file (`SKILL.md`) marked so that only a person or the daemon invokes it, whose body is the job's prompt, shipped as its own package; the daemon starts the agent with the skill's slash command and the coding agent's harness expands it. [23] the heartbeat: one run on the queued work a day when nothing moved, the belt for a move the daemon missed. [24] the chain: the queued work firing again as its run ends, because the run's own commits moved the branch; it ends with a run that moves nothing. @@ -53,7 +53,7 @@ Decides, once per look [1] and per project, whether the daemon may spend the acc - **Starting, and stopping** - the first refused start ends the batch, claims of agents that never started are released, and a stopped daemon spawns nothing more. - **"Run now": a look a person asked for** - runs with the preference off and without the cooldown, scoped to one project or one routine; the queued work's row starts an agent on the queue whether or not the branch moved. - **What the last look reports** - the dashboard shows whether the preference was on, when the look ran, when the next is due, and one sentence per project; the log gets a stand-down only when it is news. -- **The routines** - the queued work is the routine skill `work-queue`; the rest are built from the presets: name, prompt, label and tooltip come off the preset; what a routine does (works the queue, fans out, takes a lock, auto-merges) is declared on the routine, never matched by name. +- **The routines** - the queued work is the command skill `work-queue`; the rest are built from the presets: name, prompt, label and tooltip come off the preset; what a routine does (works the queue, fans out, takes a lock, auto-merges) is declared on the routine, never matched by name. ## Business logic @@ -75,7 +75,7 @@ See `## Context`. #### Business logic -A project whose branch moved [15] starts one agent [6] told `/work-queue`, the routine skill [22] shipped in the `@gemstack/routines` package and linked into every checkout the daemon makes (`daemon-runtime.ts`), unattended [16], with its handoff [18] at `merge`, since what it implements has already been triaged onto the queue where a human could have vetoed it. One agent per move, however high the concurrency cap: the next start needs the branch to move again, which the agent's own commits do. The start spends the move and the rotation's turn alike: whether the queue wants refilling is for this run to find out. A commit the daemon wrote itself is not a move: the run's record the daemon writes at teardown, a routine lock [10] it takes or drops, a claim [9] it mints for a plan agent all carry the trailer, on this machine and on every other machine running the daemon, so a record never starts the next run and two daemons on one branch never fire empty runs at each other's records. A person's writes from the dashboard — queue an entry, release a claim — carry no trailer and are moves: a person asking for work is exactly what should start a run. +A project whose branch moved [15] starts one agent [6] told `/work-queue`, the command skill [22] shipped as the `@gemstack/skill-work-queue` package and linked into every checkout the daemon makes (`daemon-runtime.ts`), unattended [16], with its handoff [18] at `merge`, since what it implements has already been triaged onto the queue where a human could have vetoed it. One agent per move, however high the concurrency cap: the next start needs the branch to move again, which the agent's own commits do. The start spends the move and the rotation's turn alike: whether the queue wants refilling is for this run to find out. A commit the daemon wrote itself is not a move: the run's record the daemon writes at teardown, a routine lock [10] it takes or drops, a claim [9] it mints for a plan agent all carry the trailer, on this machine and on every other machine running the daemon, so a record never starts the next run and two daemons on one branch never fire empty runs at each other's records. A person's writes from the dashboard — queue an entry, release a claim — carry no trailer and are moves: a person asking for work is exactly what should start a run. ### The chain, and after an empty run the rotation @@ -280,7 +280,7 @@ Before deciding, every agent this loop started on the project and has not yet be #### Business logic -- The queued work [4] is the routine skill [22] `work-queue`: its name is the skill's, its prompt is the slash command `/work-queue`, its label "Work the queue" and its tooltip "Work one queued task off the agent queue, unattended." are written here, and it is declared as working the queue and as auto-merging its pull request. The skill file itself is `skills/work-queue/SKILL.md` in the `@gemstack/routines` package, which The Framework depends on. +- The queued work [4] is the command skill [22] `work-queue`: its name is the skill's, its prompt is the slash command `/work-queue`, its label "Work the queue" and its tooltip "Work one queued task off the agent queue, unattended." are written here, and it is declared as working the queue and as auto-merging its pull request. The skill file itself is the `SKILL.md` of the `@gemstack/skill-work-queue` package, which The Framework depends on. - Each preset-backed routine carries the preset's stable name, which is what the rotation's position and the switched-off list key on; the prompt rendered from the preset; and the preset's label and one-line tooltip, read off the preset so a relabeled preset relabels its routine and the sentence the launcher shows for a preset and the sentence the routines list shows for its routine are the same sentence. - Only the maintenance routine carries a separate description line, "sweeping the codebase for maintenance work", because "Maintenance" names its preset rather than the work; the other routines' labels read as what they do, so their rows stay one line and their log lines say the label itself. - The planning routine is declared as fanning out [14]; the triage routines each declare their routine lock [10]. diff --git a/packages/framework/src/auto-pm.test.ts b/packages/framework/src/auto-pm.test.ts index 85c83c75b..51c195478 100644 --- a/packages/framework/src/auto-pm.test.ts +++ b/packages/framework/src/auto-pm.test.ts @@ -544,13 +544,13 @@ test('a stand-down is logged when it is news, not once a minute (#1774)', async assert.equal(loop.report().outcomes[0]?.message, 'there is no job to run') }) -test('AUTO_PM_WORK_JOB fires the routine skill by its slash command, and lands its own PRs (#1216/#1774)', () => { +test('AUTO_PM_WORK_JOB fires the command skill by its slash command, and lands its own PRs (#1216/#1774)', () => { // The prompt is the skill's name as a slash command; the agent's harness expands it. The skill - // file ships in the routines package, and only a person or the daemon may invoke it. + // file ships as its own package, and only a person or the daemon may invoke it. assert.equal(AUTO_PM_WORK_JOB.prompt, `/${WORK_QUEUE_SKILL_NAME}`) assert.equal(AUTO_PM_WORK_JOB.works, true) - const routines = dirname(createRequire(import.meta.url).resolve('@gemstack/routines/package.json')) - const skill = readFileSync(join(routines, 'skills', WORK_QUEUE_SKILL_NAME, 'SKILL.md'), 'utf8') + const pkg = dirname(createRequire(import.meta.url).resolve('@gemstack/skill-work-queue/package.json')) + const skill = readFileSync(join(pkg, 'SKILL.md'), 'utf8') assert.match(skill, new RegExp(`^---\\nname: ${WORK_QUEUE_SKILL_NAME}\\n`)) assert.match(skill, /\ndisable-model-invocation: true\n/) // What the agent is told: one task, commit but do not push, committed counts as published, diff --git a/packages/framework/src/auto-pm.ts b/packages/framework/src/auto-pm.ts index 0d1644e50..5f9c572ae 100644 --- a/packages/framework/src/auto-pm.ts +++ b/packages/framework/src/auto-pm.ts @@ -344,10 +344,10 @@ export const AUTO_PM_JOBS: readonly AutoPmJob[] = [ ] /** - * The routine skill the daemon fires on the queued work (#1774): `skills/work-queue/SKILL.md` in - * the `@gemstack/routines` package, linked into every checkout the daemon makes, marked so that - * only a person or the daemon invokes it. Its prompt is the slash command; the agent's harness - * expands it. + * The command skill the daemon fires on the queued work (#1774): the `SKILL.md` of the + * `@gemstack/skill-work-queue` package, linked into every checkout the daemon makes, marked so + * that only a person or the daemon invokes it. Its prompt is the slash command; the agent's + * harness expands it. */ export const WORK_QUEUE_SKILL_NAME = 'work-queue' diff --git a/packages/framework/src/daemon-runtime.LOGIC.md b/packages/framework/src/daemon-runtime.LOGIC.md index 5cee92ce7..9afc84227 100644 --- a/packages/framework/src/daemon-runtime.LOGIC.md +++ b/packages/framework/src/daemon-runtime.LOGIC.md @@ -42,7 +42,7 @@ What the daemon does for a project: starting an agent [1] (the project it is for - **The device side of a relay** - a daemon that relayed an agent here may read, steer and hand off that agent against this daemon's home project only, through a fixed list of actions that never includes starting, previewing or deleting. - **Preflight before a checkout is spent** - an agent whose turns run on this machine starts only if its driver's [12] coding agent is installed and logged in; a pass is trusted for 30 seconds, a failure never. - **A continuation reopens the agent's own checkout** - Resume starts from the project's own options, waits out the previous leg's exit, reuses or re-attaches the agent's checkout on its recorded branch, and restores its history so it stays one agent. -- **Every agent gets its own checkout and branch** - a checkout under `.branches/` on branch `agent-` with the `branches` skill [11] and the routine skill `work-queue` linked in; a directory that is not a repository gets no checkout and runs in the project's checkout one agent at a time; a repository whose checkout cannot be created refuses the start rather than borrowing the user's checkout. +- **Every agent gets its own checkout and branch** - a checkout under `.branches/` on branch `agent-` with the `branches` skill [11] and the command skill `work-queue` linked in; a directory that is not a repository gets no checkout and runs in the project's checkout one agent at a time; a repository whose checkout cannot be created refuses the start rather than borrowing the user's checkout. - **One agent per checkout** - a second start aimed at a checkout whose agent is alive or mid-spawn is refused as busy. - **The agent spec, the spawned process and its environment** - the process is handed one spec [4] with its prompt verbatim, runs detached with its stderr in its checkout, sees nothing added to its PATH, its id as `AGENT_ID` and the daemon's address, and is refused and undone if the daemon began closing meanwhile. - **When the process ends** - the spec is removed, the slot freed, and for an agent with its own checkout a chain runs: a failed-start marker if it never reported anything, the teardown, then the transient-death retry. @@ -122,7 +122,7 @@ After the preflight, the agent's checkout is reused when it still exists; otherw #### Business logic -A new agent's id is derived from the moment of the start, unless the caller minted the id first: a sweep [15] that claimed a ticket for the agent wrote the claim [24] under the id it now starts the agent with. The checkout is created by the `branches` skill [11] in one sequence: a git worktree under the project's `.branches/` directory on a fresh branch `agent-`, `.branches/` hidden from git, the project's dependency trees linked in, and the routine skill `work-queue` — the skill file from the `@gemstack/routines` package that the daemon starts the queued work with — linked in beside `branches` where the coding agent's [18] harness looks for skills. The `tickets`, `queue` and `logs` skills are the project's own tracked files, not the daemon's to link. +A new agent's id is derived from the moment of the start, unless the caller minted the id first: a sweep [15] that claimed a ticket for the agent wrote the claim [24] under the id it now starts the agent with. The checkout is created by the `branches` skill [11] in one sequence: a git worktree under the project's `.branches/` directory on a fresh branch `agent-`, `.branches/` hidden from git, the project's dependency trees linked in, and the command skill `work-queue` — the skill file of the `@gemstack/skill-work-queue` package that the daemon starts the queued work with — linked in beside `branches` where the coding agent's [18] harness looks for skills. The `tickets`, `queue` and `logs` skills are the project's own tracked files, not the daemon's to link. A project that is not a git repository cannot be given a checkout: its agents run in the project's own checkout, one at a time since they would collide, and the daemon logs "[framework] is not a git repository, so it gets no worktree; running in the main checkout". A project that is a repository but whose checkout could not be created does not fall back to the user's checkout: the start is refused with "could not create a worktree for this run: ", the dashboard shows it, and starting again is the retry, because a failed agent is recoverable and a user's checkout with an agent's edits mixed in is not. When the creation was cut short by its time budget, the partial directory it left is removed; a failure of any other kind leaves the directory alone, since it may be a path that was on disk before this agent asked for it. diff --git a/packages/framework/src/daemon-runtime.test.LOGIC.md b/packages/framework/src/daemon-runtime.test.LOGIC.md index 56889f436..719f366e6 100644 --- a/packages/framework/src/daemon-runtime.test.LOGIC.md +++ b/packages/framework/src/daemon-runtime.test.LOGIC.md @@ -4,4 +4,4 @@ What the tests cover, driving the runtime's waits and its process termination di - **A leg whose state cannot be read** - an unreadable state is asked again rather than taken for a live leg, so the exit is waited out instead of refused; an unreadable leg that then reports running still short-circuits to the busy guard, the asking having continued until the leg committed; a leg that once reported ended is not read again. - **Letting go of the repository at shutdown** - slots nothing has touched return at once; a slot whose process is gone but whose exit has not landed yet is waited for, together with the teardown that appears mid-wait; an agent with no checkout of its own parks no teardown and holds nothing up; a teardown that throws does not fail the shutdown; the wait is bounded, so a wedged teardown costs the grace period and not the exit. - **Terminating an agent's process** - an agent that ignores the graceful stop is ended forcibly together with its whole process group, so the browser it launched dies with it instead of living on unowned. -- **What a spawned agent is handed** - its id as `AGENT_ID`, the daemon's address, and a PATH that is the daemon's own with nothing added; the routine skill the daemon links into every checkout is `work-queue`, a skill file whose front matter names it and marks it as invocable only by a person or the daemon. +- **What a spawned agent is handed** - its id as `AGENT_ID`, the daemon's address, and a PATH that is the daemon's own with nothing added; the command skill the daemon links into every checkout is `work-queue`, a skill file whose front matter names it and marks it as invocable only by a person or the daemon. diff --git a/packages/framework/src/daemon-runtime.test.ts b/packages/framework/src/daemon-runtime.test.ts index 192e521bb..283abbc02 100644 --- a/packages/framework/src/daemon-runtime.test.ts +++ b/packages/framework/src/daemon-runtime.test.ts @@ -3,7 +3,7 @@ import { test } from 'node:test' import { spawn } from 'node:child_process' import { readFileSync } from 'node:fs' import { join } from 'node:path' -import { childEnv, ROUTINE_SKILLS, terminate, waitOutFinishedLeg, waitOutSlots, type FinishedLegState } from './daemon-runtime.js' +import { childEnv, COMMAND_SKILLS, terminate, waitOutFinishedLeg, waitOutSlots, type FinishedLegState } from './daemon-runtime.js' import { isPidAlive } from './store/index.js' import { AGENT_ID_ENV } from './agent-id.js' import { WORK_QUEUE_SKILL_NAME } from './auto-pm.js' @@ -238,9 +238,9 @@ test('a spawned agent gets its id as AGENT_ID and no PATH entries of the daemon assert.equal(childEnv(undefined, undefined, base)[AGENT_ID_ENV], undefined) }) -test('the routine skill the daemon links into every checkout is the work-queue skill file (#1774)', () => { - assert.deepEqual(ROUTINE_SKILLS.map(s => s.name), [WORK_QUEUE_SKILL_NAME]) - const skill = readFileSync(join(ROUTINE_SKILLS[0]!.dir, 'SKILL.md'), 'utf8') +test('the command skill the daemon links into every checkout is the work-queue skill file (#1774)', () => { + assert.deepEqual(COMMAND_SKILLS.map(s => s.name), [WORK_QUEUE_SKILL_NAME]) + const skill = readFileSync(join(COMMAND_SKILLS[0]!.dir, 'SKILL.md'), 'utf8') assert.match(skill, /^---\nname: work-queue\n/, 'the front matter names the skill the daemon fires') assert.match(skill, /\ndisable-model-invocation: true\n/, 'only a person or the daemon invokes it') }) diff --git a/packages/framework/src/daemon-runtime.ts b/packages/framework/src/daemon-runtime.ts index 2f78e303a..132230409 100644 --- a/packages/framework/src/daemon-runtime.ts +++ b/packages/framework/src/daemon-runtime.ts @@ -121,14 +121,14 @@ export function childEnv(daemonUrl: string | undefined, agentId: string | undefi } /** - * The routine skills the daemon fires (#1774), linked into every checkout it makes beside the - * `branches` skill, through the branches package's caller-given list. They ship in the - * `@gemstack/routines` package, one `skills//SKILL.md` each, no code; the daemon starts the - * queued work with `/work-queue`. The skills an agent composes — tickets, queue, logs — are the + * The command skills the daemon fires (#1774), linked into every checkout it makes beside the + * `branches` skill, through the branches package's caller-given list. Each ships as its own + * package, `@gemstack/skill-`, a `SKILL.md` and no code; the daemon starts the queued + * work with `/work-queue`. The skills an agent composes — tickets, queue, logs — are the * project's own tracked files, not the daemon's to link. */ -const ROUTINES_DIR = join(dirname(createRequire(import.meta.url).resolve('@gemstack/routines/package.json')), 'skills') -export const ROUTINE_SKILLS: readonly SkillLink[] = [{ name: WORK_QUEUE_SKILL_NAME, dir: join(ROUTINES_DIR, WORK_QUEUE_SKILL_NAME) }] +const WORK_QUEUE_SKILL_DIR = dirname(createRequire(import.meta.url).resolve('@gemstack/skill-work-queue/package.json')) +export const COMMAND_SKILLS: readonly SkillLink[] = [{ name: WORK_QUEUE_SKILL_NAME, dir: WORK_QUEUE_SKILL_DIR }] /** The daemon's signed write funnel to the data branch (`daemon-writes.ts`): the run's record is its own commit. */ const funnel = daemonFunnel() @@ -498,7 +498,7 @@ export function createProjectRuntime({ cwd, env, binPath, retryDelayMs, driverPr // says which name it got; re-attaching by the birth branch would continue the agent on a // branch without its previous commits. const branch = agentBranchFor(archived ?? { id: agentId }) - await attachCheckout(projectCwd, { agentId, branch, skills: ROUTINE_SKILLS }) + await attachCheckout(projectCwd, { agentId, branch, skills: COMMAND_SKILLS }) } await restoreArchivedAgent(projectCwd, path, agentId).catch(() => false) return { cwd: path, agentId } @@ -560,7 +560,7 @@ export function createProjectRuntime({ cwd, env, binPath, retryDelayMs, driverPr try { // The package's one sequence (#1725): the worktree, the parent's dependencies linked in, the // routine skill linked in, the branches view (#1580) told now rather than at the next tick. - const worktree = await createCheckout(projectCwd, { agentId, skills: ROUTINE_SKILLS }) + const worktree = await createCheckout(projectCwd, { agentId, skills: COMMAND_SKILLS }) return { ok: true, workspace: { cwd: worktree.path, agentId } } } catch (err) { if (await isGitRepo(projectCwd)) { diff --git a/packages/framework/src/daemon-workspace.test.LOGIC.md b/packages/framework/src/daemon-workspace.test.LOGIC.md index 1c431c150..5e3aff358 100644 --- a/packages/framework/src/daemon-workspace.test.LOGIC.md +++ b/packages/framework/src/daemon-workspace.test.LOGIC.md @@ -8,4 +8,4 @@ What the tests cover, starting agents through the runtime against real git repos - **What counts as a transient death** - a connection closed mid-response, a reset connection and an overloaded API count as transient; a boot death, an assertion failure and an absent detail do not; the detail is read from the agent's own final end event and only from a failed one, skipping malformed lines, with an empty stream yielding none and a later successful end canceling an earlier failure. - **Retrying a transient death** - an agent that dies to a transient error is continued as the same agent in its retained checkout, at most twice, after which it stays failed; an agent that fails on its own terms is not retried. - **Preflight before anything is spent** - a start on a logged-out coding agent is refused with the fix named ("not logged in", "auth login") and creates no checkout, no branch and no process; an `actions` agent is not probed at all; a passing preflight is probed once for a burst of starts; a failing one is probed again on every start, so logging in is picked up by the very next start. -- **What a spawned agent finds** - its PATH is the daemon's own, with nothing added: a skill's command resolves from the project's dependencies; its checkout links exactly two skills wherever a coding agent's harness looks, the `branches` skill and the daemon's routine skill `work-queue`, each pointing at the directory holding its `SKILL.md`. +- **What a spawned agent finds** - its PATH is the daemon's own, with nothing added: a skill's command resolves from the project's dependencies; its checkout links exactly two skills wherever a coding agent's harness looks, the `branches` skill and the daemon's command skill `work-queue`, each pointing at the directory holding its `SKILL.md`. diff --git a/packages/framework/src/daemon-workspace.test.ts b/packages/framework/src/daemon-workspace.test.ts index 677e9e6b0..f10a70788 100644 --- a/packages/framework/src/daemon-workspace.test.ts +++ b/packages/framework/src/daemon-workspace.test.ts @@ -4,7 +4,7 @@ import { mkdtemp, mkdir, writeFile, readdir, readFile, readlink, rm, stat, realp import { join, resolve } from 'node:path' import { execFile } from 'node:child_process' import { tmpdir } from 'node:os' -import { createProjectRuntime, cleanupTimedOutWorktree, markFailedStart, agentStderrPath, isTransientAgentFailure, lastAgentFailureDetail, MAX_TRANSIENT_RETRIES, ROUTINE_SKILLS } from './daemon-runtime.js' +import { createProjectRuntime, cleanupTimedOutWorktree, markFailedStart, agentStderrPath, isTransientAgentFailure, lastAgentFailureDetail, MAX_TRANSIENT_RETRIES, COMMAND_SKILLS } from './daemon-runtime.js' import type { PreflightResult } from './preflight.js' /** @@ -666,11 +666,11 @@ test("a spawned agent gets the daemon's PATH untouched, and its checkout links t // project's own dependencies, `npx tickets`, as its SKILL.md says. assert.equal(recorded.trim(), process.env['PATH'], "the agent's PATH is the daemon's own") // Two skills are linked into the checkout, where each harness looks for them: the branches - // package's own (#1739), and the routine skill the daemon fires (#1774), from the routines package. The skills an agent + // package's own (#1739), and the command skill the daemon fires (#1774), from its own package. The skills an agent // composes — tickets, queue, logs — are the project's tracked files, not links. const checkout = worktreePath(cwd, result.agentId!) for (const harnessDir of HARNESS_SKILL_DIRS) { - for (const [name, dir] of [['branches', BRANCHES_SKILL_DIR], ...ROUTINE_SKILLS.map(s => [s.name, s.dir] as const)] as const) { + for (const [name, dir] of [['branches', BRANCHES_SKILL_DIR], ...COMMAND_SKILLS.map(s => [s.name, s.dir] as const)] as const) { const target = await readlink(join(checkout, harnessDir, name)) assert.equal(await realpath(resolve(join(checkout, harnessDir), target)), await realpath(dir), `${harnessDir}/${name} links the directory holding its SKILL.md`) } diff --git a/packages/framework/src/preset-catalog.LOGIC.md b/packages/framework/src/preset-catalog.LOGIC.md index 15921b520..586bb9490 100644 --- a/packages/framework/src/preset-catalog.LOGIC.md +++ b/packages/framework/src/preset-catalog.LOGIC.md @@ -100,7 +100,7 @@ The "Update from GitHub" row is marked as always running in an agent of its own. #### Business logic -The launcher [1] shows fourteen buttons, in this order: "Research", "Readability", "Maintainability", "Security audit", "UX (auto)", "Suggest new tickets", "Suggest new features", "Suggest tickets to work on", "Plan tickets (aka spike)", "Market research", "Update from GitHub", "Maintenance", "Add quick-win work to AI Queue", "Add consensual work to AI Queue": every preset. The routine that works the agent queue [4] is not a preset but the skill file `skills/work-queue/SKILL.md` of the `@gemstack/routines` package, which only the daemon fires (the rules in `auto-pm.ts`). +The launcher [1] shows fourteen buttons, in this order: "Research", "Readability", "Maintainability", "Security audit", "UX (auto)", "Suggest new tickets", "Suggest new features", "Suggest tickets to work on", "Plan tickets (aka spike)", "Market research", "Update from GitHub", "Maintenance", "Add quick-win work to AI Queue", "Add consensual work to AI Queue": every preset. The routine that works the agent queue [4] is not a preset but the `SKILL.md` of the `@gemstack/skill-work-queue` package, a command skill only a person or the daemon fires (the rules in `auto-pm.ts`). ### Which presets may run unattended diff --git a/packages/routines/DECISIONS.md b/packages/routines/DECISIONS.md deleted file mode 100644 index ecd1431a0..000000000 --- a/packages/routines/DECISIONS.md +++ /dev/null @@ -1,25 +0,0 @@ -Non-obvious decisions only, grouped by business-logic flow. Anything not listed is left -to the implementer's judgment. Flag conflicts instead of silently deviating. Keep -outdated decisions (no history). - -A bullet is a person's pick, and says what it was picked over. What the code does belongs -in LOGIC.md; a choice made while implementing is the implementer's judgment, not a -decision. An AI proposes a bullet and asks; it never adds or rewrites one. - -## Routines -- Two kinds of skill: capability skills (`branches`, `tickets`, `queue`, `logs`), each a - SKILL.md and a command, each saying how to do one thing; and routine skills, each a - SKILL.md only, each a job that composes capabilities. A routine names the capabilities - it uses; a capability never names another skill. Picked over shipping a routine inside - the capability package it mostly uses (the queue's), which would make that package know - about tickets. -- Routines are their own package, SKILL.md files only, under `skills//`. Picked over - the framework package's own `skills/` folder, so a project can install the routines - without the daemon and can replace one with its own; and over one package per routine, - which gains nothing while every routine is a single file. -- A routine is marked `disable-model-invocation`: a person or a daemon fires it, the - agent never picks it on its own. -- The daemon fires a routine by its name, `/work-queue`; the agent's harness expands the - skill. The routine's words are the rules of the job — one task, commit but do not push, - committed counts as published, say so and stop when nothing is queued — never a skill's - name or command. diff --git a/packages/routines/LOGIC.md b/packages/routines/LOGIC.md deleted file mode 100644 index 70d17ed90..000000000 --- a/packages/routines/LOGIC.md +++ /dev/null @@ -1,12 +0,0 @@ -The `@gemstack/routines` npm package: the routines [1] a daemon fires on its own, one skill file each under `skills//SKILL.md`, no code. A routine is a job for a coding agent [2] that composes the capability skills [3] of a project; it names no command and no package, and it is marked so that only a person or a daemon invokes it, never the agent by itself. The Framework's daemon depends on this package and starts an agent with a routine's slash command, `/work-queue`; a project that installs the skills without The Framework can fire the same routine by hand or from another runner. - -## Glossary - -[1] routine: a job the daemon fires on its own — the queued work, and later update tickets, triage quick, triage consensual, plan tickets, maintenance — each switchable off and runnable on demand. -[2] agent: the unit of work: one task worked by a coding agent under a runner's control, in its own checkout, on its own branch. -[3] capability skill: a skill that says how to do one thing and ships its command — `branches`, `tickets`, `queue`, `logs`; a routine composes them and none of them knows another. - -## Business logic — TL;DR - -- **One folder per routine** (`skills/`) - the skills-npm layout, so a tool that scans `node_modules/**/skills/*/SKILL.md` finds them and `$ skills` can copy them into a project. -- **Working the queue** (`skills/work-queue/`) - one queued task off the agent queue, unattended. diff --git a/packages/routines/package.json b/packages/routines/package.json deleted file mode 100644 index 5db7cb312..000000000 --- a/packages/routines/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@gemstack/routines", - "version": "0.0.1", - "description": "The routines a daemon fires on its own: jobs for coding agents, one skill file each, that compose the tickets, queue and other capability skills of a project.", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/framework/the-framework.git", - "directory": "packages/routines" - }, - "publishConfig": { - "access": "public" - }, - "files": [ - "skills" - ] -} diff --git a/packages/routines/skills/LOGIC.md b/packages/routines/skills/LOGIC.md deleted file mode 100644 index b1bafc402..000000000 --- a/packages/routines/skills/LOGIC.md +++ /dev/null @@ -1,3 +0,0 @@ -The routine skills: one folder per routine, each holding the `SKILL.md` the daemon fires, marked so that only a person or the daemon invokes it. Its body is the job's prompt: the daemon starts the agent with the skill's slash command and the coding agent's harness expands it. - -- **Working the queue** (`work-queue/`) - one queued task off the agent queue, unattended; the routine The Framework's daemon fires when the `agent-data` branch moved (the rules in `packages/framework/src/auto-pm.ts`). diff --git a/packages/skill-work-queue/DECISIONS.md b/packages/skill-work-queue/DECISIONS.md new file mode 100644 index 000000000..e9a0e2839 --- /dev/null +++ b/packages/skill-work-queue/DECISIONS.md @@ -0,0 +1,25 @@ +Non-obvious decisions only, grouped by business-logic flow. Anything not listed is left +to the implementer's judgment. Flag conflicts instead of silently deviating. Keep +outdated decisions (no history). + +A bullet is a person's pick, and says what it was picked over. What the code does belongs +in LOGIC.md; a choice made while implementing is the implementer's judgment, not a +decision. An AI proposes a bullet and asks; it never adds or rewrites one. + +## Commands +- Two kinds of skill: capability skills (`branches`, `tickets`, `queue`, `logs`), each a + SKILL.md and a command, each saying how to do one thing; and command skills, each a + SKILL.md only, each a job that composes capabilities. A command assumes no capability + and names no skill; where the job is broken without a capability, it says so in + capability words and stops. Picked over a command naming the capabilities it uses, + which would tie the job to a package. A capability never names another skill. +- One package per command, `@gemstack/skill-`, the SKILL.md at the package root + like the capability skills. Picked over one package holding every command, so a project + installs the commands it wants and nothing else; the name follows Claude Code's own + convention, a command is a skill. +- A command is marked `disable-model-invocation`: a person or a runner fires it, the + agent never picks it on its own. +- A runner fires a command by its name, `/work-queue`; the agent's harness expands the + skill. The command's words are the rules of the job — one task, commit but do not push, + committed counts as published, say so and stop when nothing is queued — never a skill's + name or command. diff --git a/packages/skill-work-queue/LOGIC.md b/packages/skill-work-queue/LOGIC.md new file mode 100644 index 000000000..fccf130f2 --- /dev/null +++ b/packages/skill-work-queue/LOGIC.md @@ -0,0 +1,11 @@ +The `@gemstack/skill-work-queue` npm package: the `work-queue` command skill [1], one `SKILL.md` and no code. A command skill is a job for a coding agent [2] that composes the capability skills [3] of a project; it names no skill, no command and no package, and it is marked so that only a person or a runner invokes it, never the agent by itself. One package per command, named `@gemstack/skill-`. The Framework's daemon depends on this package and starts an agent with the skill's slash command, `/work-queue`; a project that installs the skill without The Framework fires it by hand or from another runner. + +## Glossary + +[1] command skill: a skill file (`SKILL.md`) whose body is a job's prompt, marked so that only a person or a runner invokes it; a runner starts the agent with the skill's slash command and the coding agent's harness expands it. +[2] agent: the unit of work: one task worked by a coding agent under a runner's control, in its own checkout, on its own branch. +[3] capability skill: a skill that says how to do one thing and ships its command — `branches`, `tickets`, `queue`, `logs`; a command skill composes them and none of them knows another. + +## Business logic — TL;DR + +- **Working the queue** (`SKILL.md`) - one queued task off the agent queue, unattended; the job The Framework's daemon fires when the `agent-data` branch moved (the rules in `packages/framework/src/auto-pm.ts`). diff --git a/packages/routines/skills/work-queue/SKILL.LOGIC.md b/packages/skill-work-queue/SKILL.LOGIC.md similarity index 66% rename from packages/routines/skills/work-queue/SKILL.LOGIC.md rename to packages/skill-work-queue/SKILL.LOGIC.md index 8247ff305..90c3319a2 100644 --- a/packages/routines/skills/work-queue/SKILL.LOGIC.md +++ b/packages/skill-work-queue/SKILL.LOGIC.md @@ -1,10 +1,10 @@ -The `work-queue` routine skill: the prompt of the agent the daemon starts on the queued work, as a skill file the coding agent's harness expands from `/work-queue`. Marked so that only a person or the daemon invokes it; the agent never chooses it by itself. +The `work-queue` command skill: the prompt of the agent a runner starts on the queued work, as a skill file the coding agent's harness expands from `/work-queue`. Marked so that only a person or a runner invokes it; the agent never chooses it by itself. ## Context **User story**: the daemon starts an agent with `/work-queue`. The agent reads the skills in its checkout, takes one queued task, works it in its own branch, commits, closes the task's ticket, marks the queue entry done, and stops; the run that started it publishes the branch. An agent that finds nothing queued says so and stops. -**Business logic story**: the skill names no skill and no command. The agent composes the `tickets` and `queue` skills tracked in the project on its own; what this file carries is the rules of the job, which an unattended agent cannot infer from the skills alone. +**Business logic story**: the skill names no skill and no command, and assumes no capability. The agent composes the `tickets` and `queue` skills tracked in the project on its own; what this file carries is the rules of the job, which an unattended agent cannot infer from the skills alone. Where the job is broken without a capability, the skill says so in capability words and stops. ## Business logic — TL;DR diff --git a/packages/routines/skills/work-queue/SKILL.md b/packages/skill-work-queue/SKILL.md similarity index 100% rename from packages/routines/skills/work-queue/SKILL.md rename to packages/skill-work-queue/SKILL.md diff --git a/packages/skill-work-queue/package.json b/packages/skill-work-queue/package.json new file mode 100644 index 000000000..d69856d84 --- /dev/null +++ b/packages/skill-work-queue/package.json @@ -0,0 +1,17 @@ +{ + "name": "@gemstack/skill-work-queue", + "version": "0.0.1", + "description": "The work-queue command for coding agents: work one queued task off the project's agent queue, unattended. A skill file only, no code.", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/framework/the-framework.git", + "directory": "packages/skill-work-queue" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "SKILL.md" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0a36df2e..ba1758160 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,9 +50,6 @@ importers: '@gemstack/agent-data': specifier: workspace:* version: link:../agent-data - '@gemstack/routines': - specifier: workspace:* - version: link:../routines '@gemstack/skill-branches': specifier: workspace:* version: link:../skill-branches @@ -65,6 +62,9 @@ importers: '@gemstack/skill-tickets': specifier: workspace:* version: link:../skill-tickets + '@gemstack/skill-work-queue': + specifier: workspace:* + version: link:../skill-work-queue '@puppeteer/browsers': specifier: 3.2.1 version: 3.2.1 @@ -154,8 +154,6 @@ importers: specifier: ^4.1.10 version: 4.1.10(@types/node@20.19.43)(jsdom@29.1.1)(vite@8.1.4(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) - packages/routines: {} - packages/skill-branches: dependencies: '@gemstack/agent-data': @@ -208,6 +206,8 @@ importers: specifier: ^7.0.2 version: 7.0.2 + packages/skill-work-queue: {} + packages/the-framework.ai: dependencies: react: