Skip to content
Open
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
39 changes: 39 additions & 0 deletions .changeset/agent-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
"@fission-ai/openspec": minor
---

### Added

- **`openspec agent` command group** — namespaced helpers for AI skills and
scripts, kept separate from the human-facing top-level surface.
- **`openspec agent resolve-change [name] [--auto] [--json]`** — resolves
which active change to operate on. Lists every active change, validates a
supplied name, or auto-selects when exactly one exists. Distinct exit
codes (`0` ok, `1` none, `2` not found, `3` ambiguous) let skills branch
without parsing stderr.
- **`openspec agent next-artifact --change <name>`** — returns the next
ready artifact bundled with its full `instructions` payload. Replaces the
two-call `status --json` + `instructions <artifact> --json` pattern with
one call. JSON by default; pass `--no-json` for a human summary. Emits
`{ "done": true }` once every artifact is complete.
- **`openspec agent mark-task-done <task-id> --change <name>`** — flips a
single checkbox in the change's tracking file (typically `tasks.md`) from
`- [ ]` to `- [x]`. Idempotent on already-checked lines, preserves CRLF/LF
endings, and uses anchored matching so `1.1` does not match `1.10`.

### Changed

- `openspec instructions apply --change <n> --json` payload is now richer:
- Each task in the `tasks` array carries an optional `numericId` field
(`"1.1"`, `"2.3.4"`) extracted from the start of the task description when
the tracking file uses numbered tasks. Existing positional `id` is
unchanged.
- The top-level payload includes a new `nextPendingId` field: the first
unchecked task that has a `numericId`, or `null`. Pair with
`openspec mark-task-done` to drive an apply loop without re-parsing
`tasks.md`.
- Skill workflow templates for `openspec-propose` and `openspec-apply-change`
now use the three new commands instead of inlining multi-step CLI
orchestration. Run `openspec update` against an existing OpenSpec project to
pick up the slimmer skill bodies. Net effect: fewer tokens per propose/apply
cycle and fewer round-trips between the agent and the CLI.
167 changes: 167 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ These commands support `--json` output for programmatic use by AI agents and scr
| `openspec validate` | Check for issues | `--all --json` for bulk validation |
| `openspec status` | See artifact progress | `--json` for structured status |
| `openspec instructions` | Get next steps | `--json` for agent instructions |
| `openspec agent resolve-change` | Locate active change | `--auto`/`--json` to drive skills without prompts |
| `openspec agent next-artifact` | One-call propose loop | JSON by default; bundles status + instructions |
| `openspec agent mark-task-done` | Tick off an applied task | Anchored, idempotent flipping by `numericId` |
| `openspec templates` | Find template paths | `--json` for path resolution |
| `openspec schemas` | List available schemas | `--json` for schema discovery |
| `openspec workspace setup --no-interactive` | Create a workspace with explicit inputs | `--json` for structured setup output |
Expand Down Expand Up @@ -858,6 +861,170 @@ openspec instructions design --change add-dark-mode --json
- Content from dependency artifacts
- Per-artifact rules from config

When invoked as `instructions apply --change <name> --json`, each task in the
returned `tasks` array also carries a `numericId` field whenever the tracking
file uses numbered tasks (e.g. `- [ ] 1.1 Wire foo` → `numericId: "1.1"`). The
top-level payload also includes `nextPendingId`: the first unchecked task with a
`numericId`, or `null` when no such task exists. Use this with
`openspec agent mark-task-done` to drive an apply loop without re-parsing tasks.md.

---

### `openspec agent resolve-change`

Agent helper: resolve which active change to operate on. Lists every active
change, validates a supplied name, or auto-selects when exactly one change
exists. Useful at the top of every workflow skill where the change identity is
the first question to answer.

```bash
openspec agent resolve-change [name] [options]
```

**Arguments:**

| Argument | Required | Description |
|----------|----------|-------------|
| `name` | No | Validate that this change is active; echo it on success |

**Options:**

| Option | Description |
|--------|-------------|
| `--auto` | Succeed only when exactly one active change exists |
| `--json` | Output as JSON |

**Examples:**

```bash
# List every active change as JSON (default when no name and no --auto)
openspec agent resolve-change

# Validate a specific name (exit 2 on miss)
openspec agent resolve-change add-dark-mode

# Auto-pick the only active change
openspec agent resolve-change --auto
```

**Exit codes:**

| Code | Meaning |
|------|---------|
| 0 | Success (name echoed, or JSON list emitted) |
| 1 | `--auto` and no active changes exist |
| 2 | Named change not active (or invalid name) |
| 3 | `--auto` and multiple active changes exist |

---

### `openspec agent next-artifact`

Agent helper: return the next ready artifact for a change, bundled with the
full instructions payload for that artifact. Replaces the
`status --json` + `instructions <artifact> --json` two-call dance with a single
call. JSON is the default since agents are the primary consumers; pass
`--no-json` for a human summary.

```bash
openspec agent next-artifact --change <name> [options]
```

**Options:**

| Option | Description |
|--------|-------------|
| `--change <id>` | Change name (required) |
| `--schema <name>` | Schema override (auto-detected from change config) |
| `--no-json` | Print a human-readable summary instead of JSON |

**Examples:**

```bash
# Get the next ready artifact and its instructions (JSON, default)
openspec agent next-artifact --change add-dark-mode

# Compact human summary
openspec agent next-artifact --change add-dark-mode --no-json
```

**Output (JSON):**

```json
// When work remains:
{
"done": false,
"artifactId": "design",
"resolvedOutputPath": "/abs/path/openspec/changes/add-dark-mode/design.md",
"template": "## Overview\n...",
"instruction": "Create the design document...",
"context": "...", // optional
"rules": ["..."], // optional
"dependencies": [ // completed artifacts to read first
{ "id": "proposal", "path": "proposal.md", "done": true, "description": "..." }
],
"unlocks": ["specs"]
}

// When every artifact is complete:
{ "done": true }
```

**Exit codes:**

| Code | Meaning |
|------|---------|
| 0 | Success (next artifact emitted, or `{done: true}`) |
| 1 | Missing `--change` or change not found |
| 3 | No runnable artifact — every pending artifact is dependency-blocked |

---

### `openspec agent mark-task-done`

Agent helper: flip a single checkbox in the change's tracking file (usually
`tasks.md`) from `- [ ]` to `- [x]`. The target line is identified by the
leading numeric task id (`1`, `1.1`, `2.3.4`) captured by
`instructions apply --json`. Idempotent on already-checked lines; preserves
CRLF vs LF line endings; anchored matching ensures `1.1` does not match
`1.10`.

```bash
openspec agent mark-task-done <task-id> --change <name> [options]
```

**Arguments:**

| Argument | Required | Description |
|----------|----------|-------------|
| `task-id` | Yes | The hierarchical task id (matches `numericId` from `instructions apply`) |

**Options:**

| Option | Description |
|--------|-------------|
| `--change <id>` | Change name (required) |
| `--schema <name>` | Schema override (auto-detected from change config) |
| `--json` | Output as JSON |

**Examples:**

```bash
# Mark task 1.1 done
openspec agent mark-task-done --change add-dark-mode 1.1

# Inside an apply loop, driven by nextPendingId from `instructions apply --json`
TASK=$(openspec instructions apply --change add-dark-mode --json | jq -r .nextPendingId)
openspec agent mark-task-done --change add-dark-mode "$TASK"
```

**Exit codes:**

| Code | Meaning |
|------|---------|
| 0 | Success (flipped, or already-done — both are no-op safe) |
| 2 | Bad input — missing change, schema lacks `apply.tracks`, tracking file missing, or no matching unchecked task line |

---

### `openspec templates`
Expand Down
63 changes: 63 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,19 @@ import {
schemasCommand,
newChangeCommand,
setChangeCommand,
resolveChangeCommand,
nextArtifactCommand,
markTaskDoneCommand,
DEFAULT_SCHEMA,
type StatusOptions,
type InstructionsOptions,
type TemplatesOptions,
type SchemasOptions,
type NewChangeOptions,
type SetChangeOptions,
type ResolveChangeOptions,
type NextArtifactOptions,
type MarkTaskDoneOptions,
} from '../commands/workflow/index.js';
import { maybeShowTelemetryNotice, trackCommand, shutdown } from '../telemetry/index.js';

Expand Down Expand Up @@ -532,6 +538,63 @@ newCmd
}
});

// Agent command group (subcommands intended for AI skills and scripts, not
// direct human use). Grouping them under a dedicated namespace keeps the
// top-level CLI surface focused on human workflows; agents call
// `openspec agent <subcommand>` explicitly.
const agentCmd = program
.command('agent')
.description('Helpers for AI skills and scripts (not intended for direct human use)');

agentCmd
.command('resolve-change [name]')
.description('Resolve an active change by name, list active changes, or auto-select the only one')
.option('--auto', 'Succeed only when exactly one active change exists')
.option('--json', 'Output as JSON')
.action(async (name: string | undefined, options: ResolveChangeOptions) => {
try {
await resolveChangeCommand(name, options);
} catch (error) {
console.log();
ora().fail(`Error: ${(error as Error).message}`);
process.exit(1);
}
});

// JSON is the default for next-artifact since agents are the primary
// consumers; pass --no-json for the human summary.
agentCmd
.command('next-artifact')
.description('Return the next ready artifact for a change, bundled with its instructions (JSON by default)')
.option('--change <id>', 'Change name')
.option('--schema <name>', 'Schema override (auto-detected from config.yaml)')
.option('--no-json', 'Print a human-readable summary instead of JSON')
.action(async (options: NextArtifactOptions) => {
try {
await nextArtifactCommand(options);
} catch (error) {
console.log();
ora().fail(`Error: ${(error as Error).message}`);
process.exit(1);
}
});

agentCmd
.command('mark-task-done <task-id>')
.description('Mark a task complete in the change\'s tracking file (idempotent)')
.option('--change <id>', 'Change name')
.option('--schema <name>', 'Schema override (auto-detected from config.yaml)')
.option('--json', 'Output as JSON')
.action(async (taskId: string, options: MarkTaskDoneOptions) => {
try {
await markTaskDoneCommand(taskId, options);
} catch (error) {
console.log();
ora().fail(`Error: ${(error as Error).message}`);
process.exit(1);
}
});

// Set command group
const setCmd = program.command('set').description('Set checked-in OpenSpec metadata');

Expand Down
9 changes: 9 additions & 0 deletions src/commands/workflow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,13 @@ export type { NewChangeOptions } from './new-change.js';
export { setChangeCommand } from './set-change.js';
export type { SetChangeOptions } from './set-change.js';

export { resolveChangeCommand } from './resolve-change.js';
export type { ResolveChangeOptions } from './resolve-change.js';

export { nextArtifactCommand } from './next-artifact.js';
export type { NextArtifactOptions } from './next-artifact.js';

export { markTaskDoneCommand } from './mark-task-done.js';
export type { MarkTaskDoneOptions } from './mark-task-done.js';

export { DEFAULT_SCHEMA } from './shared.js';
37 changes: 33 additions & 4 deletions src/commands/workflow/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,30 +229,58 @@ export function printInstructionsText(instructions: ArtifactInstructions, isBloc

/**
* Parses tasks.md content and extracts task items with their completion status.
*
* When a line begins with a `N` or `N.N(.N)*` token (e.g. `1`, `2.3`, `1.10.4`),
* that token is captured as `numericId` and the leading whitespace/punctuation
* after it is stripped from `description`. Lines without such a token still
* parse, and only get a positional `id`. The positional `id` is always set so
* existing callers keep working.
*/
function parseTasksFile(content: string): TaskItem[] {
const tasks: TaskItem[] = [];
const lines = content.split('\n');
let taskIndex = 0;

// Capture leading task numbers like `1`, `1.1`, `1.10.4`. Followed by a
// separator (space, dot, colon, paren, dash) to anchor against descriptions
// that just happen to start with a digit.
const numericIdRe = /^(\d+(?:\.\d+)*)(?:[.:)\]-]?\s+|\s+)(.*)$/;

for (const line of lines) {
// Match checkbox patterns: - [ ] or - [x] or - [X]
const checkboxMatch = line.match(/^[-*]\s*\[([ xX])\]\s*(.+)\s*$/);
if (checkboxMatch) {
taskIndex++;
const done = checkboxMatch[1].toLowerCase() === 'x';
const description = checkboxMatch[2].trim();
tasks.push({
const rawDescription = checkboxMatch[2].trim();
const numericMatch = rawDescription.match(numericIdRe);
const task: TaskItem = {
id: `${taskIndex}`,
description,
description: numericMatch ? numericMatch[2].trim() : rawDescription,
done,
});
};
if (numericMatch) {
task.numericId = numericMatch[1];
}
tasks.push(task);
}
}

return tasks;
}

/**
* Picks the first unchecked task with a `numericId` (document order). Returns
* `null` if no task qualifies. Mirrors what agent skills need to drive
* `openspec mark-task-done` without re-parsing the list.
*/
function pickNextPendingId(tasks: TaskItem[]): string | null {
for (const t of tasks) {
if (!t.done && t.numericId) return t.numericId;
}
return null;
}

/**
* Generates apply instructions for implementing tasks from a change.
* Schema-aware: reads apply phase configuration from schema to determine
Expand Down Expand Up @@ -356,6 +384,7 @@ export async function generateApplyInstructions(
state,
missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
instruction,
nextPendingId: pickNextPendingId(tasks),
};
}

Expand Down
Loading