@open-agent-toolkit/cli 0.1.40 → 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/docs/cli-utilities/backlog-lifecycle.md +61 -0
- package/assets/docs/cli-utilities/config-and-local-state.md +47 -0
- package/assets/docs/cli-utilities/index.md +1 -0
- package/assets/docs/provider-sync/instruction-sync.md +1 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-pjm-review-backlog/SKILL.md +29 -7
- package/assets/skills/oat-pjm-update-repo-reference/SKILL.md +25 -5
- package/assets/templates/pjm-agents.md +80 -1
- package/assets/templates/pjm-handoffs-readme.md +21 -0
- package/assets/templates/reference-agents.md +13 -0
- package/assets/templates/repo-agents.md +4 -0
- package/assets/templates/repo-readme.md +35 -0
- package/dist/commands/backlog/archive.d.ts +33 -0
- package/dist/commands/backlog/archive.d.ts.map +1 -0
- package/dist/commands/backlog/archive.js +229 -0
- package/dist/commands/backlog/index.d.ts +2 -0
- package/dist/commands/backlog/index.d.ts.map +1 -1
- package/dist/commands/backlog/index.js +56 -2
- package/dist/commands/backlog/regenerate-index.d.ts +5 -1
- package/dist/commands/backlog/regenerate-index.d.ts.map +1 -1
- package/dist/commands/backlog/regenerate-index.js +7 -1
- package/dist/commands/backlog/shared/item-status.d.ts +13 -0
- package/dist/commands/backlog/shared/item-status.d.ts.map +1 -0
- package/dist/commands/backlog/shared/item-status.js +34 -0
- package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
- package/dist/commands/instructions/instructions.utils.js +24 -0
- package/dist/commands/pjm/doctor.d.ts.map +1 -1
- package/dist/commands/pjm/doctor.js +161 -0
- package/dist/commands/pjm/index.d.ts.map +1 -1
- package/dist/commands/pjm/index.js +2 -1
- package/dist/commands/pjm/init.d.ts +2 -1
- package/dist/commands/pjm/init.d.ts.map +1 -1
- package/dist/commands/pjm/init.js +7 -0
- package/package.json +2 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Backlog Lifecycle
|
|
3
|
+
description: The states a file-backed backlog item moves through, how oat backlog archive closes it out atomically, and how oat pjm doctor catches lifecycle drift.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Backlog Lifecycle
|
|
7
|
+
|
|
8
|
+
The file-backed backlog under `.oat/repo/pjm/backlog/` tracks work as one Markdown file per item. This page describes the lifecycle those items move through, the atomic close-out command that keeps the pieces in sync, and the diagnostics that flag drift when a close-out is done by hand and something is missed.
|
|
9
|
+
|
|
10
|
+
For the flag-by-flag command reference, see [`oat backlog archive`](config-and-local-state.md#oat-backlog-archive). For the two-layer PJM surface that hosts the backlog, see [Tool Packs](tool-packs.md#install-vs-initialize).
|
|
11
|
+
|
|
12
|
+
## Where a backlog item lives
|
|
13
|
+
|
|
14
|
+
- **`items/<id>.md`** - active, file-backed records. Each carries frontmatter with a `status` and an `updated` timestamp.
|
|
15
|
+
- **`archived/<id>.md`** - the resting place for closed-out items, preserving the full item file as history.
|
|
16
|
+
- **`completed.md`** - a newest-first summary log of completed work, one line per entry (`YYYY-MM-DD — BL-YYMMDD-slug — Title — one-line outcome`).
|
|
17
|
+
- **`index.md`** - the human-facing curated overview plus a managed, generated index table rebuilt from item frontmatter.
|
|
18
|
+
|
|
19
|
+
## Item statuses
|
|
20
|
+
|
|
21
|
+
An item's `status` frontmatter field is one of exactly four values:
|
|
22
|
+
|
|
23
|
+
- `open` - captured, not yet started.
|
|
24
|
+
- `in_progress` - actively being worked.
|
|
25
|
+
- `closed` - completed.
|
|
26
|
+
- `wont_do` - abandoned or intentionally declined.
|
|
27
|
+
|
|
28
|
+
`closed` and `wont_do` are the two **terminal** statuses. These are the only valid values — never invent variants like `done`. A terminal item belongs in `archived/`, not `items/`.
|
|
29
|
+
|
|
30
|
+
## Closing out an item
|
|
31
|
+
|
|
32
|
+
When an item reaches a terminal state, close it out with the atomic command rather than editing files by hand:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# completed work
|
|
36
|
+
oat backlog archive BL-260705-example --summary "shipped the thing"
|
|
37
|
+
|
|
38
|
+
# abandoned work
|
|
39
|
+
oat backlog archive BL-260705-example --wont-do --summary "superseded by BL-260706-other"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A single `oat backlog archive` run performs the whole close-out so its parts cannot drift apart:
|
|
43
|
+
|
|
44
|
+
1. Sets the terminal `status` (`closed` by default, `wont_do` with `--wont-do`) and stamps `updated`.
|
|
45
|
+
2. Appends a canonical newest-first entry to `completed.md`. `closed` items always get an entry (with a visible `TODO: summarize outcome` placeholder when `--summary` is omitted, so the gap stays visible); `wont_do` items get one only when you pass `--summary`.
|
|
46
|
+
3. Moves `items/<id>.md` into `archived/` — with `git mv` inside a work tree, or a plain rename outside git.
|
|
47
|
+
4. Regenerates the managed backlog index.
|
|
48
|
+
|
|
49
|
+
The command is safe to re-run: an item already in `archived/` produces a no-op warning with no writes. An out-of-enum current status (for example a hand-set `done`) is a hard error that names the file, lists the valid statuses, and tells you how to recover. See the [command reference](config-and-local-state.md#oat-backlog-archive) for exit codes and the `--json` payload.
|
|
50
|
+
|
|
51
|
+
## Catching lifecycle drift
|
|
52
|
+
|
|
53
|
+
The manual close-out this command replaces is exactly where the two motivating repos drifted — an item was marked closed in frontmatter and summarized in `completed.md` but never moved to `archived/`, and one shipped with the invalid status `done`. `oat pjm doctor` (and therefore `oat doctor`, which aggregates the `pjm:*` checks) now surfaces that drift:
|
|
54
|
+
|
|
55
|
+
- **`pjm:backlog_terminal_in_items`** (fail) - a `closed` or `wont_do` item is still sitting in `items/`. Fix: run `oat backlog archive <id>` to finish the move.
|
|
56
|
+
- **`pjm:backlog_invalid_status`** (fail) - an item carries an out-of-enum or missing `status`. The message lists the offending file paths and the valid statuses.
|
|
57
|
+
- **`pjm:backlog_archived_open`** (warn) - an `open` or `in_progress` item is in `archived/`, which usually means it was archived prematurely.
|
|
58
|
+
- **`pjm:backlog_completed_unarchived`** (warn) - `completed.md` references an item whose file still lives in `items/`.
|
|
59
|
+
- **`pjm:backlog_duplicate_id`** (fail) - the same `<id>.md` exists in both `items/` and `archived/`. `oat backlog archive` refuses to auto-resolve this (it would clobber the archived record), so the duplicate must be reconciled by hand.
|
|
60
|
+
|
|
61
|
+
Doctor reports drift; it never auto-fixes. A human or agent runs `oat backlog archive` (or corrects the status) and re-runs doctor to confirm the backlog is clean again.
|
|
@@ -16,12 +16,59 @@ Use the `oat backlog` group when you want direct CLI support for the file-backed
|
|
|
16
16
|
- `oat backlog init` - scaffold `.oat/repo/pjm/backlog/` with starter files and directories for a fresh repo
|
|
17
17
|
- `oat backlog generate-id <title>` - generate a deterministic `BL-YYMMDD-slug` backlog ID from a title
|
|
18
18
|
- `oat backlog generate-id <title> --created-at <timestamp>` - generate a reproducible ID for a known creation timestamp
|
|
19
|
+
- `oat backlog archive <id>` - atomic close-out: set a terminal status, record the completion in `completed.md`, move the item into `archived/`, and regenerate the index in one step
|
|
19
20
|
- `oat backlog regenerate-index` - rebuild the managed backlog index table from item frontmatter
|
|
20
21
|
|
|
21
22
|
Backlog IDs are deterministic date+slug identifiers (`BL-YYMMDD-slug`) derived from the creation date and title, so two machines or worktrees produce the same ID for the same record without scanning the local checkout. The slug is capped at 30 characters at the last whole-word boundary (with trailing stop-words trimmed), so prefer concise, meaningful titles. Index regeneration is deterministic and safe to re-run when resolving an index merge conflict.
|
|
22
23
|
|
|
23
24
|
Run `oat backlog init` first when the local backlog scaffold does not exist yet in a fresh repo. This command group is primarily used by the `oat-pjm-*` project-management skills, but it is also available directly when you need to inspect or repair backlog metadata by hand.
|
|
24
25
|
|
|
26
|
+
For the end-to-end states an item moves through — and how `oat backlog archive` and `oat pjm doctor` keep the backlog honest — see [Backlog Lifecycle](backlog-lifecycle.md).
|
|
27
|
+
|
|
28
|
+
### `oat backlog archive`
|
|
29
|
+
|
|
30
|
+
`oat backlog archive <id> [--wont-do] [--summary <text>] [--json] [--backlog-root <path>]` performs the full close-out for a backlog item so status flip, completed-log entry, file move, and index regeneration never drift apart.
|
|
31
|
+
|
|
32
|
+
**Arguments and flags:**
|
|
33
|
+
|
|
34
|
+
- `<id>` (required) - the backlog item id (`BL-YYMMDD-slug`); the item file must live under `items/`.
|
|
35
|
+
- `--wont-do` - close the item as `wont_do` instead of the default terminal status `closed`.
|
|
36
|
+
- `--summary <text>` - one-line outcome summary recorded in `completed.md`.
|
|
37
|
+
- `--backlog-root <path>` - override the backlog root (defaults to `.oat/repo/pjm/backlog`).
|
|
38
|
+
- `--json` - emit the machine-readable result payload instead of human log lines.
|
|
39
|
+
|
|
40
|
+
**Behavior:**
|
|
41
|
+
|
|
42
|
+
- Validates the item's current `status` against the enum (`open | in_progress | closed | wont_do`); an out-of-enum value such as `done` is a hard error with a fix hint. Archiving is legal from any valid status — a `closed` item still in `items/` just gets its move finished.
|
|
43
|
+
- Rewrites only the `status:` and `updated:` frontmatter lines (preserving any inline enum comment), then moves the item from `items/` to `archived/` with `git mv` inside a work tree, falling back to a plain rename (with a warning) outside git or if `git mv` fails.
|
|
44
|
+
- `closed` archives always append a canonical newest-first `completed.md` entry (`YYYY-MM-DD — <id> — Title — summary`); when `--summary` is omitted the entry carries a visible `TODO: summarize outcome` placeholder. `wont_do` archives append an entry only when `--summary` is provided. A missing `completed.md` is scaffolded from the starter template; a missing `## Completed Items` heading is scaffolded with a warning.
|
|
45
|
+
- Regenerates the managed backlog index after the move.
|
|
46
|
+
- Idempotent: re-running on an item already in `archived/` is a no-op warning with no writes.
|
|
47
|
+
|
|
48
|
+
**Exit codes:**
|
|
49
|
+
|
|
50
|
+
- `0` - item archived, or already-archived no-op.
|
|
51
|
+
- `1` - actionable error: unknown id (no file under `items/`) or an out-of-enum current status. The message names the file path, the valid statuses, and the fix.
|
|
52
|
+
- `2` - reserved for unexpected system/runtime failures.
|
|
53
|
+
|
|
54
|
+
**JSON payload (`--json`):**
|
|
55
|
+
|
|
56
|
+
On success the payload is the archive result object:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"id": "BL-260705-example",
|
|
61
|
+
"result": "archived",
|
|
62
|
+
"status": "closed",
|
|
63
|
+
"completedEntry": "written",
|
|
64
|
+
"movedTo": ".oat/repo/pjm/backlog/archived/BL-260705-example.md",
|
|
65
|
+
"indexRegenerated": true,
|
|
66
|
+
"warnings": []
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`result` is `archived` or `noop` (already archived); `completedEntry` is `written`, `scaffolded`, or `skipped` (e.g. a `wont_do` archive without `--summary`); `movedTo` is the destination path or `null`. On an actionable failure the payload is `{ "result": "error", "id": "<id>", "message": "<why + fix>" }`.
|
|
71
|
+
|
|
25
72
|
For full project-management repo-reference setup, use [`oat pjm init`](tool-packs.md#install-vs-initialize). It scaffolds the two-layer PJM surface (`pjm/current-state.md`, `pjm/roadmap.md`, `reference/decisions/`, and AGENTS guides) and delegates the backlog sub-surface to `oat backlog init`.
|
|
26
73
|
|
|
27
74
|
## `oat decision ...`
|
|
@@ -15,6 +15,7 @@ Use this section when you want bootstrap guidance, tool-pack lifecycle details,
|
|
|
15
15
|
- [Tool Packs and Installed Assets](tool-packs.md) - Bundled packs and `oat tools` lifecycle commands.
|
|
16
16
|
- [Configuration](configuration.md) - OAT configuration guidance across shared, local, user, and provider-sync surfaces.
|
|
17
17
|
- [Config and Local State](config-and-local-state.md) - Utility command groups for config, local state, diagnostics, and related inspection flows.
|
|
18
|
+
- [Backlog Lifecycle](backlog-lifecycle.md) - Backlog item states, atomic close-out with `oat backlog archive`, and lifecycle drift detection in `oat pjm doctor`.
|
|
18
19
|
- [Workflow Gates](workflow-gates.md) - Per-skill final commands and cross-runtime review dispatch with `oat gate`.
|
|
19
20
|
|
|
20
21
|
## What Lives Here
|
|
@@ -22,6 +22,7 @@ Instruction sync is currently project-only.
|
|
|
22
22
|
- It scans the current repository recursively.
|
|
23
23
|
- It supports nested directories all the way down the tree.
|
|
24
24
|
- It skips provider-irrelevant or local-only roots such as `.git`, `.oat`, `.worktrees`, and `node_modules`.
|
|
25
|
+
- Exception: `.oat/repo/**` is scanned even though the rest of `.oat/` is skipped, so the curated `AGENTS.md` files there (repo root guidance, `pjm/`, `reference/`) get their sibling `CLAUDE.md` shims managed and validated like any other directory. The rest of `.oat/` (`templates/`, `projects/`, `sync/`) stays excluded.
|
|
25
26
|
- It does not scan user-level provider roots such as `~/.claude` in this release.
|
|
26
27
|
|
|
27
28
|
## Canonical Model
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oat-pjm-review-backlog
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.4.0
|
|
4
4
|
description: Use when prioritizing the file-backed repo backlog or evaluating roadmap alignment. Produces value-effort ratings, dependency mapping, and execution recommendations.
|
|
5
5
|
argument-hint: '[backlog-root] [--roadmap=<path>] [--output=<path>]'
|
|
6
6
|
disable-model-invocation: true
|
|
@@ -48,12 +48,13 @@ When executing this skill, provide lightweight progress feedback so the user can
|
|
|
48
48
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
49
49
|
|
|
50
50
|
- Before multi-step work, print short step indicators, e.g.:
|
|
51
|
-
- `[1/
|
|
52
|
-
- `[2/
|
|
53
|
-
- `[3/
|
|
54
|
-
- `[4/
|
|
55
|
-
- `[5/
|
|
56
|
-
- `[6/
|
|
51
|
+
- `[1/7] Resolving backlog inputs…`
|
|
52
|
+
- `[2/7] Cataloging backlog items…`
|
|
53
|
+
- `[3/7] Reading codebase context…`
|
|
54
|
+
- `[4/7] Writing review document…`
|
|
55
|
+
- `[5/7] Summarizing recommendations…`
|
|
56
|
+
- `[6/7] (Optional) Priority-alignment walkthrough…` — only print after the operator accepts the offer in Step 9
|
|
57
|
+
- `[7/7] (Optional) Refreshing project kickoff handoffs…` — only print after the operator agrees a kickoff stack in Step 10
|
|
57
58
|
|
|
58
59
|
## Arguments
|
|
59
60
|
|
|
@@ -225,6 +226,26 @@ If the operator declines, stop after the summary. Do not silently write or modif
|
|
|
225
226
|
|
|
226
227
|
When referencing backlog items inside the priority-alignment doc, the **Reference Format Convention** still applies — link to the item file and pair the ID with a human-readable title.
|
|
227
228
|
|
|
229
|
+
### Step 10: Refresh Project Kickoff Handoffs (Optional)
|
|
230
|
+
|
|
231
|
+
When a priority-alignment pass concludes (Step 9), generate or refresh one **kickoff handoff** per item in the agreed kickoff stack. A handoff is a one-shot kickoff prompt — consumable context for turning a backlog item into a project, not durable documentation. The item file and `reference/` remain the source of truth. Handoffs live under `.oat/repo/pjm/handoffs/`; see that directory's `README.md` for the convention (generated by `oat pjm init`).
|
|
232
|
+
|
|
233
|
+
This step mirrors the **Project Kickoff Handoffs** section of the pjm instruction template (`pjm/AGENTS.md`) — follow that section's semantics; do not invent divergent wording. It is **collaborative and human-gated**: kickoff-stack membership, lane count, and ordering are the operator's decisions from Step 9. Present them; do not choose them. Do not generate handoffs for parked or queued items until they are actually next.
|
|
234
|
+
|
|
235
|
+
**When to run:** only after the operator has agreed a kickoff stack in the Step 9 walkthrough. If Step 9 was declined or produced no kickoff stack, skip this step.
|
|
236
|
+
|
|
237
|
+
**For each item in the agreed kickoff stack**, write or refresh `.oat/repo/pjm/handoffs/<BL-id>.md` with:
|
|
238
|
+
|
|
239
|
+
- the backlog item reference — its ID **and** human-readable title **and** path (never a bare ID);
|
|
240
|
+
- the recommended project mode (`oat-project-quick-start` vs `oat-project-new`), including which artifacts (spec/design/plan) to pre-populate from existing research when it exists;
|
|
241
|
+
- authoritative input pointers (research directories, decision records, code paths);
|
|
242
|
+
- repo conventions and verification gates the item file does not restate;
|
|
243
|
+
- a close-out section requiring (a) the **Backlog Lifecycle** — `oat backlog archive <id>` — executed in the same PR that ships the item, and (b) deletion of the handoff file (`git rm`) in that same PR.
|
|
244
|
+
|
|
245
|
+
**Staleness:** if this alignment pass drops an item from the kickoff stack, delete its handoff (`git rm .oat/repo/pjm/handoffs/<BL-id>.md`) in the same pass rather than letting it drift.
|
|
246
|
+
|
|
247
|
+
Every backlog item reference in a handoff — like every reference in review output and alignment docs — pairs the ID with its human-readable title per the **Reference Format Convention**. No bare IDs.
|
|
248
|
+
|
|
228
249
|
## Success Criteria
|
|
229
250
|
|
|
230
251
|
- Every active backlog item file has a value-effort rating with rationale
|
|
@@ -234,4 +255,5 @@ When referencing backlog items inside the priority-alignment doc, the **Referenc
|
|
|
234
255
|
- Output document follows the review template structure
|
|
235
256
|
- Living review is written to `.oat/repo/pjm/backlog/reviews/backlog-and-roadmap-review.md` (unless `--output` is explicitly overridden); dated snapshots, when emitted, live in the same `backlog/reviews/` directory and never under `.oat/repo/reviews/`
|
|
236
257
|
- The operator is offered (but never forced into) a collaborative walkthrough that produces or updates `backlog/reviews/priority-alignment.md`; if the operator accepts, the file is written using the priority-alignment template and includes a Changelog entry for this pass; if the operator declines, no file is created or modified
|
|
258
|
+
- When a priority-alignment pass produces an agreed kickoff stack, one handoff per kickoff-stack item is written or refreshed under `.oat/repo/pjm/handoffs/` (with item reference, recommended mode, input pointers, repo conventions/gates, and a close-out requiring the Backlog Lifecycle executed and the handoff deleted in the same shipping PR); handoffs for reprioritized-out items are deleted in the same pass; kickoff-stack membership, lane count, and ordering remain human decisions the skill presents rather than chooses
|
|
237
259
|
- Every user-facing reference to a backlog item pairs the ID with a human-readable title (per the Reference Format Convention)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oat-pjm-update-repo-reference
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.3.0
|
|
4
4
|
description: Use when repo reference artifacts need updating — roadmap, decision records, backlog status, or completed history. Frequently invoked at project completion, often chained from `oat-project-document`, to ensure active `.oat/repo/pjm/` state and durable `.oat/repo/reference/` records reflect what shipped.
|
|
5
5
|
disable-model-invocation: false
|
|
6
6
|
user-invocable: true
|
|
@@ -69,7 +69,11 @@ For recently completed or in-progress projects, read `discovery.md`, `spec.md`,
|
|
|
69
69
|
Promote notable findings into one of:
|
|
70
70
|
|
|
71
71
|
- Backlog item files under `.oat/repo/pjm/backlog/items/`
|
|
72
|
-
- Completed
|
|
72
|
+
- Completed close-outs via `oat backlog archive <id>` (see Step 4) — the atomic
|
|
73
|
+
command flips the item's `status`, appends the canonical
|
|
74
|
+
`.oat/repo/pjm/backlog/completed.md` entry, moves the item file into
|
|
75
|
+
`archived/`, and regenerates the index in one step. Reserve hand-editing
|
|
76
|
+
`completed.md` for narrative touch-ups the command does not own.
|
|
73
77
|
- Decision records under `.oat/repo/reference/decisions/`, created with
|
|
74
78
|
`oat decision new "<title>"` (delegate to `oat-pjm-decision` for a guided
|
|
75
79
|
capture). Do not hand-author decision files or write into a legacy
|
|
@@ -89,16 +93,32 @@ Update these files as applicable:
|
|
|
89
93
|
4. `.oat/repo/pjm/backlog/items/*.md`
|
|
90
94
|
- Add or update active backlog items as file-backed records.
|
|
91
95
|
5. `.oat/repo/pjm/backlog/completed.md`
|
|
92
|
-
- Keep newest completed summaries first.
|
|
96
|
+
- Keep newest completed summaries first. Prefer letting `oat backlog archive <id>`
|
|
97
|
+
append the canonical entry (see below) over hand-editing.
|
|
93
98
|
6. `.oat/repo/pjm/backlog/archived/*.md`
|
|
94
|
-
-
|
|
99
|
+
- Item files land here automatically when you run `oat backlog archive <id>`.
|
|
100
|
+
Only hand-add or enrich a file here when a completed item needs preserved
|
|
101
|
+
detail the command did not capture.
|
|
95
102
|
7. `.oat/repo/reference/decisions/`
|
|
96
103
|
- Create new decisions with `oat decision new` (see `oat-pjm-decision`); the
|
|
97
104
|
command writes one `DR-YYMMDD-slug` record and regenerates the managed
|
|
98
105
|
decision index. Do not hand-edit `reference/decisions/index.md` inside its
|
|
99
106
|
managed markers.
|
|
100
107
|
|
|
101
|
-
|
|
108
|
+
To close out a completed backlog item, run the atomic close-out command rather
|
|
109
|
+
than moving files by hand:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
oat backlog archive <id> --summary "one-line outcome"
|
|
113
|
+
# abandoned work: oat backlog archive <id> --wont-do --summary "why"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This flips the item's `status` to a terminal value (`closed`/`wont_do` — never
|
|
117
|
+
invent variants like `done`), stamps `updated`, appends the canonical
|
|
118
|
+
newest-first `completed.md` entry, moves the item file from `items/` into
|
|
119
|
+
`archived/`, and regenerates the managed index in one step. Only when you edit
|
|
120
|
+
backlog files by hand outside this command (curated overview text, an enriched
|
|
121
|
+
`archived/` record) re-run the index regeneration:
|
|
102
122
|
|
|
103
123
|
```bash
|
|
104
124
|
oat backlog regenerate-index
|
|
@@ -10,4 +10,83 @@ This directory owns active project-management state.
|
|
|
10
10
|
- `current-state.md` records the present operating picture.
|
|
11
11
|
- `roadmap.md` records prioritized direction and sequencing.
|
|
12
12
|
- `backlog/` stores file-per-record backlog items and generated indexes.
|
|
13
|
-
-
|
|
13
|
+
- `handoffs/` stores one-shot project-kickoff prompts for backlog items
|
|
14
|
+
(see `handoffs/README.md`); each is deleted in the PR that ships its item.
|
|
15
|
+
- Do not store durable research, brainstorms, imported plans, or decision
|
|
16
|
+
history here.
|
|
17
|
+
|
|
18
|
+
## Backlog Lifecycle
|
|
19
|
+
|
|
20
|
+
Invariant: `backlog/items/` holds only active work (`status: open` or
|
|
21
|
+
`in_progress`). Completed and abandoned records live in `backlog/archived/`.
|
|
22
|
+
`backlog/completed.md` is the newest-first summary of what shipped.
|
|
23
|
+
|
|
24
|
+
**Trigger.** Close an item out when either is true:
|
|
25
|
+
|
|
26
|
+
- an item's `status` changes to `closed` or `wont_do` (these are the only
|
|
27
|
+
terminal values — never invent variants like `done`), or
|
|
28
|
+
- a commit or PR satisfies an item's acceptance criteria — **even when the work
|
|
29
|
+
happened outside an OAT project lifecycle** (small doc commits included).
|
|
30
|
+
|
|
31
|
+
The agent or person shipping the work owns the close-out, in the same
|
|
32
|
+
commit/PR as the work whenever practical.
|
|
33
|
+
|
|
34
|
+
**Close-out (primary path).** Run `oat backlog archive <id>` — add `--wont-do`
|
|
35
|
+
for abandoned work and `--summary "<text>"` to record the outcome. The command
|
|
36
|
+
performs the whole close-out atomically: it flips `status` to the terminal
|
|
37
|
+
value and bumps `updated`, appends the canonical `backlog/completed.md` entry
|
|
38
|
+
(always for `closed`; for `wont_do` only when `--summary` is given), moves the
|
|
39
|
+
item file from `backlog/items/` to `backlog/archived/`, and regenerates
|
|
40
|
+
`backlog/index.md`. Stage the resulting changes with the shipping commit/PR.
|
|
41
|
+
|
|
42
|
+
**Manual fallback.** These are the steps the command automates — follow them, in
|
|
43
|
+
order, only when closing out by hand:
|
|
44
|
+
|
|
45
|
+
1. In the item file, set `status: closed` (or `wont_do`) and bump `updated`.
|
|
46
|
+
2. Append a summary entry to `backlog/completed.md` (newest first; entry format
|
|
47
|
+
is documented at the top of that file). Add the entry for a `wont_do` item
|
|
48
|
+
only when the abandonment itself is worth recording (an explicit summary).
|
|
49
|
+
3. `git mv` the item file from `backlog/items/` to `backlog/archived/`.
|
|
50
|
+
4. Run `oat backlog regenerate-index` and stage the regenerated
|
|
51
|
+
`backlog/index.md`.
|
|
52
|
+
5. If the completion changes the operating picture, refresh
|
|
53
|
+
`current-state.md` and the curated overview section of `backlog/index.md`.
|
|
54
|
+
|
|
55
|
+
A partial close-out (status flipped or `completed.md` updated, but the file
|
|
56
|
+
left in `items/`) is how drift starts — finish all steps or none. `oat pjm
|
|
57
|
+
doctor` surfaces this drift.
|
|
58
|
+
|
|
59
|
+
**When reviewing backlog state** (e.g. `oat-pjm-review-backlog`), cross-check
|
|
60
|
+
recent commits against open items: work that shipped without a close-out
|
|
61
|
+
should be closed retroactively with a note.
|
|
62
|
+
|
|
63
|
+
## Project Kickoff Handoffs
|
|
64
|
+
|
|
65
|
+
`handoffs/` holds one-shot kickoff prompts — consumable context for turning a
|
|
66
|
+
backlog item into a project, not documentation. The item file and `reference/`
|
|
67
|
+
remain the source of truth. See `handoffs/README.md` for the directory
|
|
68
|
+
convention.
|
|
69
|
+
|
|
70
|
+
- **When to generate:** when a priority-alignment pass concludes (e.g. the
|
|
71
|
+
walkthrough at the end of `oat-pjm-review-backlog`), write or refresh one
|
|
72
|
+
handoff per item in the agreed kickoff stack. Kickoff-stack membership, lane
|
|
73
|
+
count, and ordering are the human's call — present them, do not choose them.
|
|
74
|
+
Do not generate handoffs for parked or queued items until they are actually
|
|
75
|
+
next.
|
|
76
|
+
- **Naming:** one file per backlog item, `handoffs/<BL-id>.md`.
|
|
77
|
+
- **Required content:**
|
|
78
|
+
- the backlog item reference — its ID **and** human-readable title **and**
|
|
79
|
+
path (never a bare ID);
|
|
80
|
+
- the recommended project mode (`oat-project-quick-start` vs
|
|
81
|
+
`oat-project-new`), including which artifacts (spec/design/plan) to
|
|
82
|
+
pre-populate from existing research when it exists;
|
|
83
|
+
- authoritative input pointers (research directories, decision records, code
|
|
84
|
+
paths);
|
|
85
|
+
- repo conventions and verification gates the item file does not restate;
|
|
86
|
+
- a close-out section requiring (a) the **Backlog Lifecycle** above executed
|
|
87
|
+
in the same PR that ships the item and (b) deletion of the handoff file
|
|
88
|
+
(`git rm`) in that same PR.
|
|
89
|
+
- **Staleness:** if a later alignment pass drops an item from the kickoff
|
|
90
|
+
stack, delete its handoff in that pass rather than letting it drift.
|
|
91
|
+
- Every backlog item reference — in review output, alignment docs, and
|
|
92
|
+
handoffs — pairs the ID with its human-readable title. No bare IDs.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
oat_template: true
|
|
3
|
+
oat_template_name: pjm-handoffs-readme
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PJM Handoffs
|
|
7
|
+
|
|
8
|
+
One-shot kickoff context for backlog items about to become OAT projects. Each
|
|
9
|
+
file stitches the backlog item, existing research, code pointers, and mode
|
|
10
|
+
guidance into a single prompt an operator passes as context when starting a
|
|
11
|
+
project (`oat-project-quick-start` or `oat-project-new`).
|
|
12
|
+
|
|
13
|
+
Conventions:
|
|
14
|
+
|
|
15
|
+
- One file per backlog item, named `<BL-id>.md`.
|
|
16
|
+
- Handoffs are **consumable, not durable**: once the project is created and the
|
|
17
|
+
content is absorbed into project artifacts, delete the handoff (`git rm`) in
|
|
18
|
+
the same PR that ships the work. Durable knowledge belongs in the item file,
|
|
19
|
+
`reference/`, or project artifacts — never here.
|
|
20
|
+
- Each handoff carries its own deletion instruction so the consuming agent
|
|
21
|
+
needs no outside context.
|
|
@@ -12,3 +12,16 @@ This directory owns append-mostly reference material.
|
|
|
12
12
|
- `external-plans/` stores imported provider or external plans.
|
|
13
13
|
- Create research, brainstorm, and deck folders on demand when a workflow needs them.
|
|
14
14
|
- Do not put active roadmap, current-state, or backlog records directly under `reference/`.
|
|
15
|
+
|
|
16
|
+
## Source of Truth
|
|
17
|
+
|
|
18
|
+
| Topic | Canonical location |
|
|
19
|
+
| -------------------------------------- | --------------------------------------------------------------------------- |
|
|
20
|
+
| Backlog close-out / lifecycle workflow | `../pjm/AGENTS.md` (Backlog Lifecycle) |
|
|
21
|
+
| Project kickoff handoffs | `../pjm/AGENTS.md` (Project Kickoff Handoffs) + `../pjm/handoffs/README.md` |
|
|
22
|
+
| Durable decisions | `decisions/` |
|
|
23
|
+
| Active operating picture / roadmap | `../pjm/current-state.md`, `../pjm/roadmap.md` |
|
|
24
|
+
|
|
25
|
+
**Update rule.** Do not restate the backlog close-out workflow here. When it
|
|
26
|
+
changes, edit `../pjm/AGENTS.md` and link to it — this directory defers to that
|
|
27
|
+
section rather than duplicating it.
|
|
@@ -11,3 +11,7 @@ Use this directory as the canonical OAT repo-reference root.
|
|
|
11
11
|
- Durable append-mostly references live in `reference/`.
|
|
12
12
|
- Keep generated indexes inside their managed marker pairs.
|
|
13
13
|
- When an index conflicts, regenerate it with the owning OAT command and stage the result.
|
|
14
|
+
- Backlog close-out follows the **Backlog Lifecycle** in `pjm/AGENTS.md` — run
|
|
15
|
+
`oat backlog archive <id>` in the same PR that ships the work.
|
|
16
|
+
- `README.md` is the human-facing orientation for this directory; agent-facing
|
|
17
|
+
rules live in the `AGENTS.md` files alongside each subdirectory.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
oat_template: true
|
|
3
|
+
oat_template_name: repo-readme
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# OAT Repo Reference
|
|
7
|
+
|
|
8
|
+
Human-facing orientation for this OAT repo-reference root — the canonical
|
|
9
|
+
project-management and durable-reference surface for the repository.
|
|
10
|
+
Agent-facing rules live in the `AGENTS.md` files alongside each directory.
|
|
11
|
+
|
|
12
|
+
## Layout
|
|
13
|
+
|
|
14
|
+
| Path | What it is | Maintained by |
|
|
15
|
+
| -------------------------- | -------------------------------------------------------- | ---------------------------------------------------------- |
|
|
16
|
+
| `pjm/current-state.md` | Present operating picture | Curated |
|
|
17
|
+
| `pjm/roadmap.md` | Now / Next / Later direction | Curated |
|
|
18
|
+
| `pjm/backlog/items/` | Active backlog items, one file each | Curated + `oat backlog` |
|
|
19
|
+
| `pjm/backlog/archived/` | Completed/abandoned item files | Moved here at close-out |
|
|
20
|
+
| `pjm/backlog/completed.md` | Newest-first completion summaries | Appended at close-out |
|
|
21
|
+
| `pjm/backlog/index.md` | Curated overview + generated item table | Overview curated; table via `oat backlog regenerate-index` |
|
|
22
|
+
| `pjm/handoffs/` | One-shot project-kickoff prompts (deleted when consumed) | Backlog walkthroughs; removed in the shipping PR |
|
|
23
|
+
| `reference/decisions/` | Durable decision records + generated index | `oat decision` |
|
|
24
|
+
|
|
25
|
+
## Conventions
|
|
26
|
+
|
|
27
|
+
- **Generated vs. curated.** Generated tables live inside `<!-- OAT ... -->`
|
|
28
|
+
marker pairs; regenerate them with the owning `oat` command instead of
|
|
29
|
+
hand-editing. Everything outside those markers is curated by hand.
|
|
30
|
+
- **IDs.** Backlog items follow `BL-YYMMDD-slug` (`oat backlog generate-id`);
|
|
31
|
+
decisions follow `DR-YYMMDD-slug`. Pair every backlog item ID with its
|
|
32
|
+
human-readable title — no bare IDs in prose or handoffs.
|
|
33
|
+
- **Close-out.** When work ships that satisfies a backlog item's acceptance
|
|
34
|
+
criteria, the item is closed and archived in the same commit/PR — run
|
|
35
|
+
`oat backlog archive <id>`. See the Backlog Lifecycle in `pjm/AGENTS.md`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type BacklogItemStatus } from './shared/item-status.js';
|
|
2
|
+
/** Result of an {@link archiveBacklogItem} run. */
|
|
3
|
+
export interface ArchiveBacklogItemResult {
|
|
4
|
+
id: string;
|
|
5
|
+
result: 'archived' | 'noop';
|
|
6
|
+
status: BacklogItemStatus | null;
|
|
7
|
+
completedEntry: 'written' | 'scaffolded' | 'skipped';
|
|
8
|
+
movedTo: string | null;
|
|
9
|
+
indexRegenerated: boolean;
|
|
10
|
+
warnings: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface ArchiveBacklogItemOptions {
|
|
13
|
+
wontDo?: boolean;
|
|
14
|
+
summary?: string;
|
|
15
|
+
/** Injectable clock for deterministic tests; defaults to the current time. */
|
|
16
|
+
now?: Date;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Actionable close-out failure (unknown id, invalid current status). The
|
|
20
|
+
* command wrapper maps `exitCode` onto `process.exitCode` and logs `message`.
|
|
21
|
+
*/
|
|
22
|
+
export declare class BacklogArchiveError extends Error {
|
|
23
|
+
readonly exitCode: 1 | 2;
|
|
24
|
+
constructor(message: string, exitCode?: 1 | 2);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Atomic backlog close-out. Validates the current status, sets the terminal
|
|
28
|
+
* status and `updated`, records a canonical `completed.md` entry, moves the
|
|
29
|
+
* item file into `archived/`, and regenerates the index. Idempotent when the
|
|
30
|
+
* item is already archived.
|
|
31
|
+
*/
|
|
32
|
+
export declare function archiveBacklogItem(backlogRoot: string, id: string, options?: ArchiveBacklogItemOptions): Promise<ArchiveBacklogItemResult>;
|
|
33
|
+
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/archive.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,iBAAiB,EAGvB,MAAM,sBAAsB,CAAC;AAoB9B,mDAAmD;AACnD,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC;IAC5B,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACjC,cAAc,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;IACrD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAE,CAAC,GAAG,CAAK;CAKjD;AAkID;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,wBAAwB,CAAC,CAkHnC"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { access, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { getFrontmatterBlock } from '../shared/frontmatter.js';
|
|
6
|
+
import YAML from 'yaml';
|
|
7
|
+
import { regenerateBacklogIndex } from './regenerate-index.js';
|
|
8
|
+
import { BACKLOG_ITEM_STATUSES, extractBacklogStatus, isValidBacklogStatus, } from './shared/item-status.js';
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const COMPLETED_HEADING = '## Completed Items';
|
|
11
|
+
const TODO_SUMMARY = 'TODO: summarize outcome';
|
|
12
|
+
const STARTER_COMPLETED = [
|
|
13
|
+
'# OAT Backlog Completed',
|
|
14
|
+
'',
|
|
15
|
+
'> Summary archive for completed backlog work. Keep newest entries first. Use `backlog/archived/` for full file-per-item historical records when a completed item still needs rich context.',
|
|
16
|
+
'',
|
|
17
|
+
'## Entry Format',
|
|
18
|
+
'',
|
|
19
|
+
'- `YYYY-MM-DD — BL-YYMMDD-slug — Title — one-line outcome summary`',
|
|
20
|
+
'',
|
|
21
|
+
COMPLETED_HEADING,
|
|
22
|
+
'',
|
|
23
|
+
].join('\n');
|
|
24
|
+
/**
|
|
25
|
+
* Actionable close-out failure (unknown id, invalid current status). The
|
|
26
|
+
* command wrapper maps `exitCode` onto `process.exitCode` and logs `message`.
|
|
27
|
+
*/
|
|
28
|
+
export class BacklogArchiveError extends Error {
|
|
29
|
+
exitCode;
|
|
30
|
+
constructor(message, exitCode = 1) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'BacklogArchiveError';
|
|
33
|
+
this.exitCode = exitCode;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function pathExists(path) {
|
|
37
|
+
try {
|
|
38
|
+
await access(path);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const code = error && typeof error === 'object' && 'code' in error
|
|
43
|
+
? String(error.code)
|
|
44
|
+
: null;
|
|
45
|
+
if (code !== 'ENOENT') {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function isInsideGitWorkTree(cwd) {
|
|
52
|
+
try {
|
|
53
|
+
await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read the item `title` from its frontmatter. Parses the YAML block (rather
|
|
62
|
+
* than a line regex) so a legitimate `#` inside a quoted title — e.g.
|
|
63
|
+
* `title: 'Fix #123 crash'` — is preserved instead of being stripped as an
|
|
64
|
+
* inline comment.
|
|
65
|
+
*/
|
|
66
|
+
function readItemTitle(content) {
|
|
67
|
+
const block = getFrontmatterBlock(content);
|
|
68
|
+
if (!block) {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
const parsed = YAML.parse(block);
|
|
72
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
return String(parsed.title ?? '').trim();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Minimal-diff frontmatter rewrite: replace only the `status:` value (keeping
|
|
79
|
+
* any inline enum comment) and the `updated:` value, leaving all surrounding
|
|
80
|
+
* formatting untouched.
|
|
81
|
+
*/
|
|
82
|
+
function rewriteFrontmatter(content, status, updatedIso) {
|
|
83
|
+
let next = content.replace(/^(status:[ \t]*)(\S+)([ \t]*#.*)?$/m, (_match, prefix, _value, comment) => `${prefix}${status}${comment ?? ''}`);
|
|
84
|
+
next = next.replace(/^(updated:)[ \t]*.*$/m, (_match, prefix) => `${prefix} '${updatedIso}'`);
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Insert `entryLine` as the first bullet beneath the `## Completed Items`
|
|
89
|
+
* heading. When the heading is absent it is scaffolded (with a warning). The
|
|
90
|
+
* returned flag distinguishes a plain append from one that had to scaffold the
|
|
91
|
+
* section.
|
|
92
|
+
*/
|
|
93
|
+
function insertCompletedEntry(completed, entryLine) {
|
|
94
|
+
const lines = completed.split('\n');
|
|
95
|
+
const headingIndex = lines.findIndex((line) => line.trim() === COMPLETED_HEADING);
|
|
96
|
+
if (headingIndex === -1) {
|
|
97
|
+
const trimmed = completed.replace(/\s*$/, '');
|
|
98
|
+
const content = `${trimmed}\n\n${COMPLETED_HEADING}\n\n${entryLine}\n`;
|
|
99
|
+
return {
|
|
100
|
+
content,
|
|
101
|
+
scaffolded: true,
|
|
102
|
+
warning: `Completed log was missing a \`${COMPLETED_HEADING}\` heading; a new section was scaffolded and the entry appended.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
let insertAt = headingIndex + 1;
|
|
106
|
+
while (insertAt < lines.length && lines[insertAt].trim() === '') {
|
|
107
|
+
insertAt += 1;
|
|
108
|
+
}
|
|
109
|
+
lines.splice(insertAt, 0, entryLine);
|
|
110
|
+
// Guarantee exactly one blank line between the heading and the first entry.
|
|
111
|
+
if (lines[headingIndex + 1] !== '') {
|
|
112
|
+
lines.splice(headingIndex + 1, 0, '');
|
|
113
|
+
}
|
|
114
|
+
let content = lines.join('\n');
|
|
115
|
+
if (!content.endsWith('\n')) {
|
|
116
|
+
content += '\n';
|
|
117
|
+
}
|
|
118
|
+
return { content, scaffolded: false, warning: null };
|
|
119
|
+
}
|
|
120
|
+
async function moveItemFile(backlogRoot, fromPath, toPath, warnings) {
|
|
121
|
+
if (await isInsideGitWorkTree(backlogRoot)) {
|
|
122
|
+
try {
|
|
123
|
+
await execFileAsync('git', ['mv', fromPath, toPath], {
|
|
124
|
+
cwd: backlogRoot,
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
warnings.push(`\`git mv\` failed for ${fromPath}; falling back to a plain filesystem rename (the move is no longer staged).`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
await rename(fromPath, toPath);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Atomic backlog close-out. Validates the current status, sets the terminal
|
|
136
|
+
* status and `updated`, records a canonical `completed.md` entry, moves the
|
|
137
|
+
* item file into `archived/`, and regenerates the index. Idempotent when the
|
|
138
|
+
* item is already archived.
|
|
139
|
+
*/
|
|
140
|
+
export async function archiveBacklogItem(backlogRoot, id, options = {}) {
|
|
141
|
+
const itemsPath = join(backlogRoot, 'items', `${id}.md`);
|
|
142
|
+
const archivedPath = join(backlogRoot, 'archived', `${id}.md`);
|
|
143
|
+
const warnings = [];
|
|
144
|
+
// Idempotent no-op: already archived.
|
|
145
|
+
if (await pathExists(archivedPath)) {
|
|
146
|
+
// Conflicting duplicate: the same id lives in BOTH `items/` and
|
|
147
|
+
// `archived/`. Treating this as a clean no-op would silently leave the live
|
|
148
|
+
// `items/` copy unarchived, while auto-archiving would clobber the existing
|
|
149
|
+
// archived record. Refuse and make the user reconcile the duplicate.
|
|
150
|
+
if (await pathExists(itemsPath)) {
|
|
151
|
+
throw new BacklogArchiveError(`Backlog item ${id} exists in both \`items/\` (${itemsPath}) and \`archived/\` (${archivedPath}). Auto-archiving would clobber the existing archived record, so this is left untouched. Fix: reconcile the duplicate manually — decide which of the two files is authoritative and remove the other — then re-run \`oat backlog archive ${id}\`.`);
|
|
152
|
+
}
|
|
153
|
+
let status = null;
|
|
154
|
+
try {
|
|
155
|
+
const extracted = extractBacklogStatus(await readFile(archivedPath, 'utf8'));
|
|
156
|
+
status = extracted && isValidBacklogStatus(extracted) ? extracted : null;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
status = null;
|
|
160
|
+
}
|
|
161
|
+
warnings.push(`Backlog item ${id} is already archived at ${archivedPath}; nothing to do.`);
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
result: 'noop',
|
|
165
|
+
status,
|
|
166
|
+
completedEntry: 'skipped',
|
|
167
|
+
movedTo: archivedPath,
|
|
168
|
+
indexRegenerated: false,
|
|
169
|
+
warnings,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (!(await pathExists(itemsPath))) {
|
|
173
|
+
throw new BacklogArchiveError(`Backlog item ${id} not found at ${itemsPath}. Confirm the id and that the file lives under \`items/\`, then re-run \`oat backlog archive ${id}\`.`);
|
|
174
|
+
}
|
|
175
|
+
const content = await readFile(itemsPath, 'utf8');
|
|
176
|
+
const currentStatus = extractBacklogStatus(content);
|
|
177
|
+
if (currentStatus === null || !isValidBacklogStatus(currentStatus)) {
|
|
178
|
+
throw new BacklogArchiveError(`Backlog item ${itemsPath} has invalid status "${currentStatus ?? ''}". Valid statuses: ${BACKLOG_ITEM_STATUSES.join(', ')}. Fix: correct the \`status\` field manually, then re-run \`oat backlog archive ${id}\`.`);
|
|
179
|
+
}
|
|
180
|
+
const targetStatus = options.wontDo ? 'wont_do' : 'closed';
|
|
181
|
+
const now = options.now ?? new Date();
|
|
182
|
+
const updatedIso = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
183
|
+
const entryDate = updatedIso.slice(0, 10);
|
|
184
|
+
const title = readItemTitle(content);
|
|
185
|
+
// 4. Rewrite frontmatter in place (before the move) so a crash leaves the
|
|
186
|
+
// detectable "terminal status still in items/" drift, never corruption.
|
|
187
|
+
await writeFile(itemsPath, rewriteFrontmatter(content, targetStatus, updatedIso), 'utf8');
|
|
188
|
+
// 5. completed.md entry — always for `closed`; only with a summary for
|
|
189
|
+
// `wont_do`.
|
|
190
|
+
let completedEntry = 'skipped';
|
|
191
|
+
const shouldWriteEntry = targetStatus === 'closed' || Boolean(options.summary);
|
|
192
|
+
if (shouldWriteEntry) {
|
|
193
|
+
const summary = options.summary?.trim()
|
|
194
|
+
? options.summary.trim()
|
|
195
|
+
: TODO_SUMMARY;
|
|
196
|
+
const entryLine = `- ${entryDate} — ${id} — ${title} — ${summary}`;
|
|
197
|
+
const completedPath = join(backlogRoot, 'completed.md');
|
|
198
|
+
let scaffoldedFile = false;
|
|
199
|
+
let existing;
|
|
200
|
+
if (await pathExists(completedPath)) {
|
|
201
|
+
existing = await readFile(completedPath, 'utf8');
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
existing = STARTER_COMPLETED;
|
|
205
|
+
scaffoldedFile = true;
|
|
206
|
+
}
|
|
207
|
+
const inserted = insertCompletedEntry(existing, entryLine);
|
|
208
|
+
if (inserted.warning) {
|
|
209
|
+
warnings.push(inserted.warning);
|
|
210
|
+
}
|
|
211
|
+
await writeFile(completedPath, inserted.content, 'utf8');
|
|
212
|
+
completedEntry =
|
|
213
|
+
scaffoldedFile || inserted.scaffolded ? 'scaffolded' : 'written';
|
|
214
|
+
}
|
|
215
|
+
// 6. Move items/<id>.md -> archived/<id>.md (git mv with rename fallback).
|
|
216
|
+
await moveItemFile(backlogRoot, itemsPath, archivedPath, warnings);
|
|
217
|
+
// 7. Regenerate the index via the exported core.
|
|
218
|
+
const regeneration = await regenerateBacklogIndex(backlogRoot);
|
|
219
|
+
warnings.push(...regeneration.warnings);
|
|
220
|
+
return {
|
|
221
|
+
id,
|
|
222
|
+
result: 'archived',
|
|
223
|
+
status: targetStatus,
|
|
224
|
+
completedEntry,
|
|
225
|
+
movedTo: archivedPath,
|
|
226
|
+
indexRegenerated: true,
|
|
227
|
+
warnings,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildCommandContext } from '../../app/command-context.js';
|
|
2
2
|
import { resolveProjectRoot } from '../../fs/paths.js';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
+
import { archiveBacklogItem } from './archive.js';
|
|
4
5
|
import { initializeBacklog } from './init.js';
|
|
5
6
|
import { regenerateBacklogIndex } from './regenerate-index.js';
|
|
6
7
|
interface BacklogCommandDependencies {
|
|
@@ -8,6 +9,7 @@ interface BacklogCommandDependencies {
|
|
|
8
9
|
resolveProjectRoot: typeof resolveProjectRoot;
|
|
9
10
|
initializeBacklog: typeof initializeBacklog;
|
|
10
11
|
regenerateBacklogIndex: typeof regenerateBacklogIndex;
|
|
12
|
+
archiveBacklogItem: typeof archiveBacklogItem;
|
|
11
13
|
pathExists: (path: string) => Promise<boolean>;
|
|
12
14
|
}
|
|
13
15
|
export declare function createBacklogCommand(overrides?: Partial<BacklogCommandDependencies>): Command;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,kBAAkB,EAAuB,MAAM,WAAW,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAqB5D,UAAU,0BAA0B;IAClC,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAChD;AA0CD,wBAAgB,oBAAoB,CAClC,SAAS,GAAE,OAAO,CAAC,0BAA0B,CAAM,GAClD,OAAO,CAiMT"}
|
|
@@ -4,6 +4,7 @@ import { buildCommandContext } from '../../app/command-context.js';
|
|
|
4
4
|
import { readGlobalOptions } from '../shared/shared.utils.js';
|
|
5
5
|
import { resolveProjectRoot } from '../../fs/paths.js';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
|
+
import { archiveBacklogItem, BacklogArchiveError } from './archive.js';
|
|
7
8
|
import { initializeBacklog } from './init.js';
|
|
8
9
|
import { regenerateBacklogIndex } from './regenerate-index.js';
|
|
9
10
|
import { generateBacklogId } from './shared/generate-id.js';
|
|
@@ -27,6 +28,7 @@ const DEFAULT_DEPENDENCIES = {
|
|
|
27
28
|
resolveProjectRoot,
|
|
28
29
|
initializeBacklog,
|
|
29
30
|
regenerateBacklogIndex,
|
|
31
|
+
archiveBacklogItem,
|
|
30
32
|
pathExists: pathExistsDefault,
|
|
31
33
|
};
|
|
32
34
|
async function resolveBacklogRoot(context, configuredRoot, dependencies = DEFAULT_DEPENDENCIES) {
|
|
@@ -65,15 +67,67 @@ export function createBacklogCommand(overrides = {}) {
|
|
|
65
67
|
.action(async (options, command) => {
|
|
66
68
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
67
69
|
const backlogRoot = await resolveBacklogRoot(context, options.backlogRoot, dependencies);
|
|
68
|
-
await dependencies.regenerateBacklogIndex(backlogRoot);
|
|
70
|
+
const { itemCount, warnings } = await dependencies.regenerateBacklogIndex(backlogRoot);
|
|
69
71
|
if (context.json) {
|
|
70
|
-
context.logger.json({ status: 'ok', backlogRoot });
|
|
72
|
+
context.logger.json({ status: 'ok', backlogRoot, itemCount, warnings });
|
|
71
73
|
}
|
|
72
74
|
else {
|
|
75
|
+
for (const warning of warnings) {
|
|
76
|
+
context.logger.warn(warning);
|
|
77
|
+
}
|
|
73
78
|
context.logger.info(`Regenerated backlog index at ${backlogRoot}`);
|
|
74
79
|
}
|
|
75
80
|
process.exitCode = 0;
|
|
76
81
|
});
|
|
82
|
+
cmd
|
|
83
|
+
.command('archive')
|
|
84
|
+
.description('Close out a backlog item: set a terminal status, record it in completed.md, move it to archived/, and regenerate the index')
|
|
85
|
+
.argument('<id>', 'Backlog item id (`BL-YYMMDD-slug`)')
|
|
86
|
+
.option('--wont-do', 'Close the item as `wont_do` instead of `closed`')
|
|
87
|
+
.option('--summary <text>', 'One-line outcome summary for completed.md')
|
|
88
|
+
.option('--backlog-root <path>', 'Backlog root directory (defaults to .oat/repo/pjm/backlog)')
|
|
89
|
+
.action(async (id, options, command) => {
|
|
90
|
+
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
91
|
+
const backlogRoot = await resolveBacklogRoot(context, options.backlogRoot, dependencies);
|
|
92
|
+
try {
|
|
93
|
+
const result = await dependencies.archiveBacklogItem(backlogRoot, id, {
|
|
94
|
+
wontDo: options.wontDo,
|
|
95
|
+
summary: options.summary,
|
|
96
|
+
});
|
|
97
|
+
if (context.json) {
|
|
98
|
+
context.logger.json(result);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
for (const warning of result.warnings) {
|
|
102
|
+
context.logger.warn(warning);
|
|
103
|
+
}
|
|
104
|
+
if (result.result === 'noop') {
|
|
105
|
+
context.logger.info(`Backlog item ${id} is already archived.`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
context.logger.success(`Archived ${id} as ${result.status} (moved to ${result.movedTo}).`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
process.exitCode = 0;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error instanceof BacklogArchiveError) {
|
|
115
|
+
if (context.json) {
|
|
116
|
+
context.logger.json({
|
|
117
|
+
result: 'error',
|
|
118
|
+
id,
|
|
119
|
+
message: error.message,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
context.logger.error(error.message);
|
|
124
|
+
}
|
|
125
|
+
process.exitCode = error.exitCode;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
77
131
|
cmd
|
|
78
132
|
.command('generate-id')
|
|
79
133
|
.description('Generate a backlog item identifier (`BL-YYMMDD-slug`) from a title or slug')
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface RegenerateBacklogIndexResult {
|
|
2
|
+
itemCount: number;
|
|
3
|
+
warnings: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function regenerateBacklogIndex(backlogRoot: string): Promise<RegenerateBacklogIndexResult>;
|
|
2
6
|
//# sourceMappingURL=regenerate-index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regenerate-index.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/regenerate-index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"regenerate-index.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/regenerate-index.ts"],"names":[],"mappings":"AAwGA,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,4BAA4B,CAAC,CAsDvC"}
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { getFrontmatterBlock } from '../shared/frontmatter.js';
|
|
4
4
|
import { computeManagedIndexUpdate } from '../shared/managed-index.js';
|
|
5
5
|
import YAML from 'yaml';
|
|
6
|
+
import { BACKLOG_ITEM_STATUSES, isValidBacklogStatus, } from './shared/item-status.js';
|
|
6
7
|
const INDEX_START = '<!-- OAT BACKLOG-INDEX -->';
|
|
7
8
|
const INDEX_END = '<!-- END OAT BACKLOG-INDEX -->';
|
|
8
9
|
const PRIORITY_ORDER = {
|
|
@@ -71,10 +72,14 @@ export async function regenerateBacklogIndex(backlogRoot) {
|
|
|
71
72
|
.map((entry) => entry.name)
|
|
72
73
|
.sort();
|
|
73
74
|
const items = [];
|
|
75
|
+
const warnings = [];
|
|
74
76
|
for (const entry of entries) {
|
|
75
77
|
const filePath = join(itemsDir, entry);
|
|
76
78
|
const item = parseItemFrontmatter(await readFile(filePath, 'utf8'), filePath);
|
|
77
79
|
if (item) {
|
|
80
|
+
if (item.status !== '' && !isValidBacklogStatus(item.status)) {
|
|
81
|
+
warnings.push(`Backlog item ${filePath} has invalid status "${item.status}". Valid statuses: ${BACKLOG_ITEM_STATUSES.join(', ')}.`);
|
|
82
|
+
}
|
|
78
83
|
items.push(item);
|
|
79
84
|
}
|
|
80
85
|
}
|
|
@@ -91,7 +96,8 @@ export async function regenerateBacklogIndex(backlogRoot) {
|
|
|
91
96
|
// write. Only rewrite on a genuine content change.
|
|
92
97
|
const { content: updated } = computeManagedIndexUpdate(content, startIndex, endIndex + INDEX_END.length, renderManagedSection(items));
|
|
93
98
|
if (updated === null) {
|
|
94
|
-
return;
|
|
99
|
+
return { itemCount: items.length, warnings };
|
|
95
100
|
}
|
|
96
101
|
await writeFile(indexPath, updated, 'utf8');
|
|
102
|
+
return { itemCount: items.length, warnings };
|
|
97
103
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const BACKLOG_ITEM_STATUSES: readonly ["open", "in_progress", "closed", "wont_do"];
|
|
2
|
+
export type BacklogItemStatus = (typeof BACKLOG_ITEM_STATUSES)[number];
|
|
3
|
+
export declare const TERMINAL_BACKLOG_STATUSES: readonly BacklogItemStatus[];
|
|
4
|
+
export declare function isValidBacklogStatus(value: string): value is BacklogItemStatus;
|
|
5
|
+
export declare function isTerminalBacklogStatus(value: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Extract the `status` value from backlog item content. Accepts either full
|
|
8
|
+
* item content (with `---` fences) or a bare frontmatter block. Inline enum
|
|
9
|
+
* comments (`status: closed # ...`) are stripped. Returns `null` when no
|
|
10
|
+
* `status` field is present.
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractBacklogStatus(frontmatterContent: string): string | null;
|
|
13
|
+
//# sourceMappingURL=item-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"item-status.d.ts","sourceRoot":"","sources":["../../../../src/commands/backlog/shared/item-status.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,qBAAqB,uDAKxB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE,eAAO,MAAM,yBAAyB,EAAE,SAAS,iBAAiB,EAGjE,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,iBAAiB,CAE5B;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,kBAAkB,EAAE,MAAM,GACzB,MAAM,GAAG,IAAI,CAUf"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Single source of truth for backlog item statuses. Kept dependency-free so
|
|
2
|
+
// cross-directory consumers (archive, regenerate-index, pjm doctor) can import
|
|
3
|
+
// it via the `@commands/...` alias without pulling in a dependency chain.
|
|
4
|
+
export const BACKLOG_ITEM_STATUSES = [
|
|
5
|
+
'open',
|
|
6
|
+
'in_progress',
|
|
7
|
+
'closed',
|
|
8
|
+
'wont_do',
|
|
9
|
+
];
|
|
10
|
+
export const TERMINAL_BACKLOG_STATUSES = [
|
|
11
|
+
'closed',
|
|
12
|
+
'wont_do',
|
|
13
|
+
];
|
|
14
|
+
export function isValidBacklogStatus(value) {
|
|
15
|
+
return BACKLOG_ITEM_STATUSES.includes(value);
|
|
16
|
+
}
|
|
17
|
+
export function isTerminalBacklogStatus(value) {
|
|
18
|
+
return TERMINAL_BACKLOG_STATUSES.includes(value);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extract the `status` value from backlog item content. Accepts either full
|
|
22
|
+
* item content (with `---` fences) or a bare frontmatter block. Inline enum
|
|
23
|
+
* comments (`status: closed # ...`) are stripped. Returns `null` when no
|
|
24
|
+
* `status` field is present.
|
|
25
|
+
*/
|
|
26
|
+
export function extractBacklogStatus(frontmatterContent) {
|
|
27
|
+
const fenced = frontmatterContent.match(/^---\n([\s\S]*?)\n---/);
|
|
28
|
+
const block = fenced ? fenced[1] : frontmatterContent;
|
|
29
|
+
const match = block.match(/^status:\s*(.+)$/m);
|
|
30
|
+
if (!match) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return match[1].replace(/\s*#.*$/, '').trim();
|
|
34
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instructions.utils.d.ts","sourceRoot":"","sources":["../../../src/commands/instructions/instructions.utils.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,4BAA4B,EAE5B,mBAAmB,EACpB,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AACtD,eAAO,MAAM,iCAAiC,EAAE,uBACrC,CAAC;
|
|
1
|
+
{"version":3,"file":"instructions.utils.d.ts","sourceRoot":"","sources":["../../../src/commands/instructions/instructions.utils.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,4BAA4B,EAE5B,mBAAmB,EACpB,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AACtD,eAAO,MAAM,iCAAiC,EAAE,uBACrC,CAAC;AAcZ,UAAU,4BAA4B;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,OAAO,EAAE,uBAAuB,EAAE,CAAC;CACpC;AAiCD,wBAAgB,8BAA8B,CAC5C,QAAQ,CAAC,EAAE,uBAAuB,GACjC,uBAAuB,CAEzB;AA2LD,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,uBAA4B,EACrC,SAAS,GAAE,OAAO,CAAC,4BAA4B,CAAM,GACpD,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAiO7B;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,OAAO,EAAE,uBAAuB,EAAE,GACjC,mBAAmB,CAoBrB;AAgBD,wBAAgB,wBAAwB,CAAC,EACvC,IAAI,EACJ,OAAO,EACP,OAAO,GACR,EAAE,4BAA4B,GAAG,uBAAuB,CAWxD;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,uBAAuB,EAChC,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,CAmCR"}
|
|
@@ -4,6 +4,23 @@ export const EXPECTED_CLAUDE_CONTENT = '@AGENTS.md\n';
|
|
|
4
4
|
export const DEFAULT_INSTRUCTION_SYNC_STRATEGY = 'pointer';
|
|
5
5
|
const ROOT_EXCLUDED_DIRECTORIES = new Set(['.git', '.oat', '.worktrees']);
|
|
6
6
|
const GLOBAL_EXCLUDED_DIRECTORIES = new Set(['node_modules']);
|
|
7
|
+
// The only excluded-subtree re-entry points: a root-level directory is excluded
|
|
8
|
+
// wholesale, but a named subdirectory holds tracked instruction files that
|
|
9
|
+
// sync/validate must manage, so it is carved back into the scan. `.oat` is
|
|
10
|
+
// excluded at the repo root, yet `.oat/repo/**` carries AGENTS.md/CLAUDE.md
|
|
11
|
+
// pairs; `.oat/templates`, `.oat/projects`, and `.oat/sync` stay unscanned.
|
|
12
|
+
const ROOT_EXCLUDED_DIRECTORY_CARVE_INS = new Map([
|
|
13
|
+
['.oat', 'repo'],
|
|
14
|
+
]);
|
|
15
|
+
async function directoryExists(dependencies, directoryPath) {
|
|
16
|
+
try {
|
|
17
|
+
const stats = await dependencies.stat(directoryPath);
|
|
18
|
+
return stats.isDirectory();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
7
24
|
function getErrorCode(error) {
|
|
8
25
|
return error && typeof error === 'object' && 'code' in error
|
|
9
26
|
? String(error.code)
|
|
@@ -97,6 +114,13 @@ async function scanInstructionDirectories(repoRoot, dependencies, debug) {
|
|
|
97
114
|
continue;
|
|
98
115
|
}
|
|
99
116
|
if (isRootLevel && ROOT_EXCLUDED_DIRECTORIES.has(entry.name)) {
|
|
117
|
+
const carveIn = ROOT_EXCLUDED_DIRECTORY_CARVE_INS.get(entry.name);
|
|
118
|
+
if (carveIn) {
|
|
119
|
+
const carveInPath = join(entryPath, carveIn);
|
|
120
|
+
if (await directoryExists(dependencies, carveInPath)) {
|
|
121
|
+
queue.push(carveInPath);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
100
124
|
continue;
|
|
101
125
|
}
|
|
102
126
|
queue.push(entryPath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/doctor.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA6L9C,MAAM,WAAW,gBAAgB;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,wBAAgB,sBAAsB,IAAI,WAAW,CAQpD;AAED,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,WAAW,EAAE,CAAC,CAoTxB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { BACKLOG_ITEM_STATUSES, extractBacklogStatus, isTerminalBacklogStatus, isValidBacklogStatus, } from '../backlog/shared/item-status.js';
|
|
3
4
|
import { getFrontmatterBlock } from '../shared/frontmatter.js';
|
|
4
5
|
import { CANONICAL_REPO_REFERENCE_PATHS } from './init.js';
|
|
5
6
|
const ALLOWED_TOP_LEVEL_DIRECTORIES = new Set([
|
|
@@ -108,6 +109,45 @@ function containsTemplateFrontmatter(content) {
|
|
|
108
109
|
(/\boat_template\s*:/i.test(frontmatter) ||
|
|
109
110
|
/\boat_template_name\s*:/i.test(frontmatter)));
|
|
110
111
|
}
|
|
112
|
+
const BACKLOG_ITEMS_DIRECTORY = 'pjm/backlog/items';
|
|
113
|
+
const BACKLOG_ARCHIVED_DIRECTORY = 'pjm/backlog/archived';
|
|
114
|
+
const BACKLOG_COMPLETED_FILE = 'pjm/backlog/completed.md';
|
|
115
|
+
// Best-effort backlog id scan: `BL-YYMMDD-slug` (legacy lowercase `bl-`
|
|
116
|
+
// matched case-insensitively). The six-digit requirement keeps the literal
|
|
117
|
+
// `BL-YYMMDD-slug` format placeholder in `completed.md` from matching.
|
|
118
|
+
const BACKLOG_ID_PATTERN = /\bbl-\d{6}-[a-z0-9][a-z0-9-]*/gi;
|
|
119
|
+
/**
|
|
120
|
+
* Single frontmatter pass over both backlog directories. Every drift check
|
|
121
|
+
* derives from this scan so the filesystem is walked once. `index.md` is the
|
|
122
|
+
* managed index, not an item, so it is excluded.
|
|
123
|
+
*/
|
|
124
|
+
async function collectBacklogItems(repoRoot) {
|
|
125
|
+
const records = [];
|
|
126
|
+
const directories = [
|
|
127
|
+
{ directory: 'items', relativeDir: BACKLOG_ITEMS_DIRECTORY },
|
|
128
|
+
{ directory: 'archived', relativeDir: BACKLOG_ARCHIVED_DIRECTORY },
|
|
129
|
+
];
|
|
130
|
+
for (const { directory, relativeDir } of directories) {
|
|
131
|
+
const fileNames = await listMarkdownFiles(join(repoRoot, relativeDir));
|
|
132
|
+
for (const fileName of fileNames) {
|
|
133
|
+
if (fileName === 'index.md') {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const content = await readIfExists(join(repoRoot, relativeDir, fileName));
|
|
137
|
+
records.push({
|
|
138
|
+
directory,
|
|
139
|
+
relativePath: `${relativeDir}/${fileName}`,
|
|
140
|
+
id: fileName.replace(/\.md$/, ''),
|
|
141
|
+
status: content ? extractBacklogStatus(content) : null,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return records;
|
|
146
|
+
}
|
|
147
|
+
/** Best-effort scan of completed-log content for backlog item ids. */
|
|
148
|
+
function extractCompletedIds(content) {
|
|
149
|
+
return [...content.matchAll(BACKLOG_ID_PATTERN)].map((match) => match[0]);
|
|
150
|
+
}
|
|
111
151
|
function checkStatus(missing) {
|
|
112
152
|
return missing.length === 0 ? 'pass' : 'fail';
|
|
113
153
|
}
|
|
@@ -246,5 +286,126 @@ export async function runPjmDoctorChecks(repoRoot, options = {}) {
|
|
|
246
286
|
? undefined
|
|
247
287
|
: 'Move active operational docs to pjm/ and leave reference/ for durable append-mostly artifacts.',
|
|
248
288
|
});
|
|
289
|
+
const backlogItems = await collectBacklogItems(repoRoot);
|
|
290
|
+
const terminalInItems = backlogItems
|
|
291
|
+
.filter((item) => item.directory === 'items' &&
|
|
292
|
+
item.status !== null &&
|
|
293
|
+
isTerminalBacklogStatus(item.status))
|
|
294
|
+
.map((item) => item.relativePath);
|
|
295
|
+
checks.push({
|
|
296
|
+
name: 'pjm:backlog_terminal_in_items',
|
|
297
|
+
description: 'Terminal-status backlog items awaiting archive',
|
|
298
|
+
status: checkStatus(terminalInItems),
|
|
299
|
+
message: terminalInItems.length === 0
|
|
300
|
+
? 'No terminal-status backlog items remain under items/.'
|
|
301
|
+
: `Terminal-status backlog items still under items/: ${terminalInItems.join(', ')}`,
|
|
302
|
+
fix: terminalInItems.length === 0
|
|
303
|
+
? undefined
|
|
304
|
+
: 'Run `oat backlog archive <id>` to move each closed/wont_do item into archived/.',
|
|
305
|
+
});
|
|
306
|
+
// A missing/empty `status:` is as much a drift as an out-of-enum value: the
|
|
307
|
+
// `oat backlog archive` command rejects both, so `pjm doctor` must surface
|
|
308
|
+
// both rather than leaving status-less items silently invisible.
|
|
309
|
+
const missingStatusItems = backlogItems
|
|
310
|
+
.filter((item) => item.status === null || item.status.trim() === '')
|
|
311
|
+
.map((item) => item.relativePath);
|
|
312
|
+
const outOfEnumStatusItems = backlogItems
|
|
313
|
+
.filter((item) => item.status !== null &&
|
|
314
|
+
item.status.trim() !== '' &&
|
|
315
|
+
!isValidBacklogStatus(item.status))
|
|
316
|
+
.map((item) => item.relativePath);
|
|
317
|
+
const invalidStatus = [...outOfEnumStatusItems, ...missingStatusItems];
|
|
318
|
+
const invalidStatusDetails = [];
|
|
319
|
+
if (outOfEnumStatusItems.length > 0) {
|
|
320
|
+
invalidStatusDetails.push(`out-of-enum status: ${outOfEnumStatusItems.join(', ')}`);
|
|
321
|
+
}
|
|
322
|
+
if (missingStatusItems.length > 0) {
|
|
323
|
+
invalidStatusDetails.push(`missing status: ${missingStatusItems.join(', ')}`);
|
|
324
|
+
}
|
|
325
|
+
checks.push({
|
|
326
|
+
name: 'pjm:backlog_invalid_status',
|
|
327
|
+
description: 'Backlog items with a missing or out-of-enum status',
|
|
328
|
+
status: checkStatus(invalidStatus),
|
|
329
|
+
message: invalidStatus.length === 0
|
|
330
|
+
? 'All backlog item statuses are within the valid enum.'
|
|
331
|
+
: `Backlog items with an invalid status (${invalidStatusDetails.join('; ')}). Valid statuses: ${BACKLOG_ITEM_STATUSES.join(', ')}.`,
|
|
332
|
+
fix: invalidStatus.length === 0
|
|
333
|
+
? undefined
|
|
334
|
+
: `Set a valid status (${BACKLOG_ITEM_STATUSES.join(', ')}) on each listed item.`,
|
|
335
|
+
});
|
|
336
|
+
const archivedOpen = backlogItems
|
|
337
|
+
.filter((item) => item.directory === 'archived' &&
|
|
338
|
+
item.status !== null &&
|
|
339
|
+
isValidBacklogStatus(item.status) &&
|
|
340
|
+
!isTerminalBacklogStatus(item.status))
|
|
341
|
+
.map((item) => item.relativePath);
|
|
342
|
+
checks.push({
|
|
343
|
+
name: 'pjm:backlog_archived_open',
|
|
344
|
+
description: 'Archived backlog items with a non-terminal status',
|
|
345
|
+
status: warnStatus(archivedOpen),
|
|
346
|
+
message: archivedOpen.length === 0
|
|
347
|
+
? 'No archived backlog items carry an open status.'
|
|
348
|
+
: `Archived backlog items still marked open/in_progress: ${archivedOpen.join(', ')}`,
|
|
349
|
+
fix: archivedOpen.length === 0
|
|
350
|
+
? undefined
|
|
351
|
+
: 'Set a terminal status (closed/wont_do) on each archived item, or move it back under items/.',
|
|
352
|
+
});
|
|
353
|
+
// A single id must never live in both `items/` and `archived/`: the live copy
|
|
354
|
+
// shadows the archived record and `oat backlog archive` refuses to reconcile
|
|
355
|
+
// it automatically (auto-archiving would clobber the archived file). Derive
|
|
356
|
+
// from the same single scan so the filesystem is not re-walked.
|
|
357
|
+
const backlogPathsByIdAndDir = new Map();
|
|
358
|
+
for (const item of backlogItems) {
|
|
359
|
+
const key = item.id.toLowerCase();
|
|
360
|
+
const entry = backlogPathsByIdAndDir.get(key) ?? {};
|
|
361
|
+
entry[item.directory] = item.relativePath;
|
|
362
|
+
backlogPathsByIdAndDir.set(key, entry);
|
|
363
|
+
}
|
|
364
|
+
const duplicateIdPairs = [];
|
|
365
|
+
for (const entry of backlogPathsByIdAndDir.values()) {
|
|
366
|
+
if (entry.items && entry.archived) {
|
|
367
|
+
duplicateIdPairs.push(`${entry.items} + ${entry.archived}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
checks.push({
|
|
371
|
+
name: 'pjm:backlog_duplicate_id',
|
|
372
|
+
description: 'Backlog ids present in both items/ and archived/',
|
|
373
|
+
status: checkStatus(duplicateIdPairs),
|
|
374
|
+
message: duplicateIdPairs.length === 0
|
|
375
|
+
? 'No backlog id is present in both items/ and archived/.'
|
|
376
|
+
: `Backlog ids present in both items/ and archived/: ${duplicateIdPairs.join('; ')}`,
|
|
377
|
+
fix: duplicateIdPairs.length === 0
|
|
378
|
+
? undefined
|
|
379
|
+
: 'Reconcile each duplicate manually: decide whether the active (items/) or archived copy is authoritative, remove the other, then re-run `oat backlog archive <id>` if the item still needs archiving.',
|
|
380
|
+
});
|
|
381
|
+
const itemPathsById = new Map();
|
|
382
|
+
for (const item of backlogItems) {
|
|
383
|
+
if (item.directory === 'items') {
|
|
384
|
+
itemPathsById.set(item.id.toLowerCase(), item.relativePath);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const completedContent = await readIfExists(join(repoRoot, BACKLOG_COMPLETED_FILE));
|
|
388
|
+
const completedUnarchived = [];
|
|
389
|
+
const seenUnarchived = new Set();
|
|
390
|
+
if (completedContent) {
|
|
391
|
+
for (const completedId of extractCompletedIds(completedContent)) {
|
|
392
|
+
const relativePath = itemPathsById.get(completedId.toLowerCase());
|
|
393
|
+
if (relativePath && !seenUnarchived.has(relativePath)) {
|
|
394
|
+
seenUnarchived.add(relativePath);
|
|
395
|
+
completedUnarchived.push(relativePath);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
checks.push({
|
|
400
|
+
name: 'pjm:backlog_completed_unarchived',
|
|
401
|
+
description: 'Completed-log entries whose item file is still under items/',
|
|
402
|
+
status: warnStatus(completedUnarchived),
|
|
403
|
+
message: completedUnarchived.length === 0
|
|
404
|
+
? 'No completed backlog entries reference an item still under items/.'
|
|
405
|
+
: `Completed log references items still under items/: ${completedUnarchived.join(', ')}`,
|
|
406
|
+
fix: completedUnarchived.length === 0
|
|
407
|
+
? undefined
|
|
408
|
+
: 'Run `oat backlog archive <id>` to archive each completed item still under items/.',
|
|
409
|
+
});
|
|
249
410
|
return checks;
|
|
250
411
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,uBAAuB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAA0B,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAiBnE,UAAU,sBAAsB;IAC9B,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,uBAAuB,EAAE,OAAO,uBAAuB,CAAC;IACxD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,cAAc,EAAE,OAAO,cAAc,CAAC;IACtC,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;CACvD;AA6FD,wBAAgB,gBAAgB,CAC9B,SAAS,GAAE,OAAO,CAAC,sBAAsB,CAAM,GAC9C,OAAO,CAuJT"}
|
|
@@ -7,7 +7,7 @@ import { resolveProjectRoot } from '../../fs/paths.js';
|
|
|
7
7
|
import { formatDoctorResults } from '../../ui/output.js';
|
|
8
8
|
import { Command } from 'commander';
|
|
9
9
|
import { runPjmDoctorChecks } from './doctor.js';
|
|
10
|
-
import { initializeRepoReference } from './init.js';
|
|
10
|
+
import { initializeRepoReference, INSTRUCTIONS_SYNC_HINT } from './init.js';
|
|
11
11
|
import { migratePjmRepo, readPjmMigrationPrompt } from './migrate.js';
|
|
12
12
|
const DEFAULT_DEPENDENCIES = {
|
|
13
13
|
buildCommandContext,
|
|
@@ -111,6 +111,7 @@ export function createPjmCommand(overrides = {}) {
|
|
|
111
111
|
if (result.skipped.length > 0) {
|
|
112
112
|
context.logger.info(`Skipped existing: ${result.skipped.join(', ')}`);
|
|
113
113
|
}
|
|
114
|
+
context.logger.info(INSTRUCTIONS_SYNC_HINT);
|
|
114
115
|
}
|
|
115
116
|
process.exitCode = 0;
|
|
116
117
|
}
|
|
@@ -8,6 +8,7 @@ export interface RepoReferenceInitResult {
|
|
|
8
8
|
created: string[];
|
|
9
9
|
skipped: string[];
|
|
10
10
|
}
|
|
11
|
-
export declare const
|
|
11
|
+
export declare const INSTRUCTIONS_SYNC_HINT: string;
|
|
12
|
+
export declare const CANONICAL_REPO_REFERENCE_PATHS: readonly [...("AGENTS.md" | "pjm/AGENTS.md" | "pjm/current-state.md" | "pjm/roadmap.md" | "reference/AGENTS.md" | "README.md" | "pjm/handoffs/README.md")[], "pjm/backlog/index.md", "pjm/backlog/completed.md", "pjm/backlog/items/.gitkeep", "pjm/backlog/archived/.gitkeep", "reference/decisions/index.md"];
|
|
12
13
|
export declare function initializeRepoReference(options: InitializeRepoReferenceOptions): Promise<RepoReferenceInitResult>;
|
|
13
14
|
//# sourceMappingURL=init.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/init.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/init.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAoBD,eAAO,MAAM,sBAAsB,QAEiD,CAAC;AAWrF,eAAO,MAAM,8BAA8B,iTAIjC,CAAC;AAyEX,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,uBAAuB,CAAC,CAgElC"}
|
|
@@ -9,7 +9,14 @@ const TEMPLATE_TARGETS = [
|
|
|
9
9
|
{ template: 'current-state.md', target: 'pjm/current-state.md' },
|
|
10
10
|
{ template: 'roadmap.md', target: 'pjm/roadmap.md' },
|
|
11
11
|
{ template: 'reference-agents.md', target: 'reference/AGENTS.md' },
|
|
12
|
+
{ template: 'repo-readme.md', target: 'README.md' },
|
|
13
|
+
{ template: 'pjm-handoffs-readme.md', target: 'pjm/handoffs/README.md' },
|
|
12
14
|
];
|
|
15
|
+
// Next-step hint printed after init/backfill. `oat pjm init` never writes
|
|
16
|
+
// CLAUDE.md shims itself — strategy ownership stays with `oat instructions
|
|
17
|
+
// sync`, so we point the operator at it (with the `--dry-run` preview).
|
|
18
|
+
export const INSTRUCTIONS_SYNC_HINT = 'Next step: run `oat instructions sync` to create CLAUDE.md shims for the ' +
|
|
19
|
+
'repo-reference AGENTS.md files (preview with `oat instructions sync --dry-run`).';
|
|
13
20
|
const BACKLOG_PATHS = [
|
|
14
21
|
'pjm/backlog/index.md',
|
|
15
22
|
'pjm/backlog/completed.md',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-agent-toolkit/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Open Agent Toolkit CLI",
|
|
6
6
|
"homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"ora": "^9.0.0",
|
|
35
35
|
"yaml": "2.8.2",
|
|
36
36
|
"zod": "^3.25.76",
|
|
37
|
-
"@open-agent-toolkit/control-plane": "0.1.
|
|
37
|
+
"@open-agent-toolkit/control-plane": "0.1.41"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.0",
|