@opsee/cli 0.11.9 → 0.11.11
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/README.md +6 -6
- package/package.json +2 -2
- package/skills/README.md +1 -1
- package/skills/{to-issues → initiative-decompose}/SKILL.md +14 -5
- package/skills/initiative-decompose/agents/openai.yaml +5 -0
- package/skills/{wayfinder → initiative-map}/SKILL.md +3 -3
- package/skills/initiative-map/agents/openai.yaml +5 -0
- package/skills/{to-spec → initiative-spec}/SKILL.md +4 -4
- package/skills/initiative-spec/agents/openai.yaml +5 -0
- package/src/args.ts +9 -5
- package/src/commands/foreman-plan.ts +1 -1
- package/src/commands/init.ts +39 -6
- package/src/commands/initiative.ts +19 -4
- package/src/foreman/core/plan-context.ts +4 -4
- package/src/init/pointer-block.ts +1 -1
- package/src/init/project.ts +5 -0
- package/src/init/retire.ts +41 -0
- package/src/init/skills.ts +1 -1
- package/src/init/tracker-doc.ts +13 -1
- package/templates/issue-tracker.md +10 -7
- package/skills/to-issues/agents/openai.yaml +0 -5
- package/skills/to-spec/agents/openai.yaml +0 -5
- package/skills/wayfinder/agents/openai.yaml +0 -5
package/README.md
CHANGED
|
@@ -271,7 +271,7 @@ node bin/opsee.js foreman pause 17 # no new dispatch in the Run; in
|
|
|
271
271
|
node bin/opsee.js foreman resume 17
|
|
272
272
|
node bin/opsee.js foreman cancel 1436 # stop the Worker; the attempt is recorded as cancelled
|
|
273
273
|
node bin/opsee.js foreman plan 17 # an attended planning session on the Initiative, its context as the first message
|
|
274
|
-
node bin/opsee.js foreman plan 17 --account work --skill
|
|
274
|
+
node bin/opsee.js foreman plan 17 --account work --skill initiative-map
|
|
275
275
|
```
|
|
276
276
|
|
|
277
277
|
### Slots: how many run at once, and in what order
|
|
@@ -1651,7 +1651,7 @@ second RPC that has to agree with the first about which ids exist.
|
|
|
1651
1651
|
|
|
1652
1652
|
### Planning sessions: `foreman plan`
|
|
1653
1653
|
|
|
1654
|
-
`opsee foreman plan <initiativeId> [--account <name>] [--skill <
|
|
1654
|
+
`opsee foreman plan <initiativeId> [--account <name>] [--skill <initiative-decompose|initiative-map|initiative-spec>] [--memory <n>]`
|
|
1655
1655
|
(story 10, `src/commands/foreman-plan.ts`) opens an attended session that starts already knowing
|
|
1656
1656
|
last night's results. It is attended only: nothing in it launches or resumes a Worker turn, and
|
|
1657
1657
|
the session is never resumed unattended. Git in that session is the human's own, outside the
|
|
@@ -1669,7 +1669,7 @@ for Claude Code, `.agents/skills/<skill>/SKILL.md` for Codex; `opsee init` write
|
|
|
1669
1669
|
command never writes it, since the worktree is the human's to commit from, and a checkout without
|
|
1670
1670
|
it is refused before the Tracker or git is touched, with the line that fixes it (run `opsee init`,
|
|
1671
1671
|
commit, merge); a skill in the checkout but not yet on origin's default branch is refused after
|
|
1672
|
-
the Workspace is made, with the command that removes it. The default skill is `
|
|
1672
|
+
the Workspace is made, with the command that removes it. The default skill is `initiative-decompose`.
|
|
1673
1673
|
|
|
1674
1674
|
The context (`assemblePlanContext`, `core/plan-context.ts`) is Context Assembly turned to the
|
|
1675
1675
|
Initiative, read through the Tracker Adapter's `getInitiativeContext` (the backend's
|
|
@@ -1734,8 +1734,8 @@ integration tier (`src/foreman/__tests__/opsee-tracker-adapter.integration.test.
|
|
|
1734
1734
|
real backend. The check with a real vendor stays manual: on the Foreman Initiative after a
|
|
1735
1735
|
scratch Run, `foreman plan 17 --account <name>`; confirm the terminal's `context:` lines count
|
|
1736
1736
|
the Completion Reports and Claude Code (or Codex) opens in the new worktree with the context and
|
|
1737
|
-
"Start by running the /
|
|
1738
|
-
Completion Reports listed under "## Completion Reports"; run `/
|
|
1737
|
+
"Start by running the /initiative-decompose skill against Initiative 17" as the first message and the prior
|
|
1738
|
+
Completion Reports listed under "## Completion Reports"; run `/initiative-decompose` on a small addition and
|
|
1739
1739
|
confirm the Tasks it files appear in Initiative 17 with `## Goal`, `## Acceptance Criteria`,
|
|
1740
1740
|
`## Verification` and `## Boundaries`; leave, and confirm the closing line names the worktree and
|
|
1741
1741
|
its removal.
|
|
@@ -1854,7 +1854,7 @@ the fake code host and the two Tasks' boards and labels through the raw RPCs.
|
|
|
1854
1854
|
After a change to `init`, in a scratch git repo:
|
|
1855
1855
|
|
|
1856
1856
|
1. Run `node <path>/cli/bin/opsee.js init --project <key>`; it must list what it created and end
|
|
1857
|
-
with the counts line. Start Claude Code there: `/
|
|
1857
|
+
with the counts line. Start Claude Code there: `/initiative-map`, `/initiative-spec` and `/initiative-decompose` must
|
|
1858
1858
|
be offered and the `opsee` MCP server must connect (OAuth on first use). Codex must show the same
|
|
1859
1859
|
three skills and the server from `.codex/config.toml`.
|
|
1860
1860
|
2. Run it again; every line must read `unchanged`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opsee/cli",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.11",
|
|
4
4
|
"description": "Opsee CLI — the opsee binary: login, whoami, and the home of the Foreman",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@bufbuild/protobuf": "^2.14.0",
|
|
18
18
|
"@connectrpc/connect": "^2.1.2",
|
|
19
19
|
"@connectrpc/connect-node": "^2.1.2",
|
|
20
|
-
"@opsee/mcp-server": "0.11.
|
|
20
|
+
"@opsee/mcp-server": "0.11.11",
|
|
21
21
|
"tsx": "^4.23.12"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
package/skills/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Planning skills
|
|
2
2
|
|
|
3
|
-
Opsee-owned forks of Matt Pocock's `wayfinder`, `to-spec`, and `to-issues` skills (https://github.com/mattpocock/skills, MIT; attribution kept in each `SKILL.md`), changed only where Opsee requires it: they read the per-repo tracker doc `docs/agents/issue-tracker.md`, publish through the `opsee_*` MCP tools into an Initiative, and write the Foreman work contract (Goal, Acceptance Criteria, Verification, Boundaries) into every Task description, with blocking as TaskDependency edges and `ready-for-agent` as the dispatch label. Opsee owns these copies because the work contract is the product's opinion (ADR-0011); `opsee init` copies each folder into the repo's `.claude/skills/` and `.agents/skills/` alongside the tracker doc, and the Codex `agents/openai.yaml` beside each `SKILL.md` is what Codex reads. Edit them here, never in a consuming repo. The tracker doc they read is rendered from `../templates/issue-tracker.md`; `src/__tests__/skills.test.ts` checks the skills and the template stay in step (section names, the Wayfinding operations the wayfinder fork quotes).
|
|
3
|
+
Opsee-owned forks of Matt Pocock's `wayfinder`, `to-spec`, and `to-issues` skills, shipped under Opsee's own names (`initiative-map`, `initiative-spec`, `initiative-decompose`) so a repo full of skills groups them by what they act on (https://github.com/mattpocock/skills, MIT; attribution kept in each `SKILL.md`), changed only where Opsee requires it: they read the per-repo tracker doc `docs/agents/issue-tracker.md`, publish through the `opsee_*` MCP tools into an Initiative, and write the Foreman work contract (Goal, Acceptance Criteria, Verification, Boundaries) into every Task description, with blocking as TaskDependency edges and `ready-for-agent` as the dispatch label. Opsee owns these copies because the work contract is the product's opinion (ADR-0011); `opsee init` copies each folder into the repo's `.claude/skills/` and `.agents/skills/` alongside the tracker doc, and the Codex `agents/openai.yaml` beside each `SKILL.md` is what Codex reads. Edit them here, never in a consuming repo. The tracker doc they read is rendered from `../templates/issue-tracker.md`; `src/__tests__/skills.test.ts` checks the skills and the template stay in step (section names, the Wayfinding operations the wayfinder fork quotes).
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: initiative-decompose
|
|
3
3
|
description: Break a plan, spec, or PRD into independently-grabbable Tasks inside an Opsee Initiative using tracer-bullet vertical slices, each carrying the Foreman work contract. Use when user wants to convert a plan into issues, create implementation tickets, or break down work into issues.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# To Issues
|
|
7
7
|
|
|
8
|
-
> Forked from the `to-issues` skill by Matt Pocock (https://github.com/mattpocock/skills, MIT). This copy is owned by Opsee: the process is upstream's, the tracker is Opsee and every issue carries the Foreman work contract.
|
|
8
|
+
> Forked from the `to-issues` skill by Matt Pocock (https://github.com/mattpocock/skills, MIT). Renamed `to-issues` -> `initiative-decompose` for Opsee's vocabulary; the attribution is to the original. This copy is owned by Opsee: the process is upstream's, the tracker is Opsee and every issue carries the Foreman work contract.
|
|
9
9
|
|
|
10
10
|
Break a plan into independently-grabbable issues using vertical slices (tracer bullets).
|
|
11
11
|
|
|
12
12
|
The issue tracker is Opsee, described in `docs/agents/issue-tracker.md` at the repo root; `opsee init` writes that file. Publishing goes through the `opsee_*` MCP tools. If the doc is missing, stop and ask the user to run `opsee init`; if the tools are missing, stop and ask the user to attach the Opsee MCP server (`/mcp`). Do neither yourself, and publish nothing until both are present. In the doc, "issue" means an Opsee **Task**, and every issue lives inside an **Initiative**.
|
|
13
13
|
|
|
14
|
-
Read the doc's "Work contract" and `/
|
|
14
|
+
Read the doc's "Work contract" and `/initiative-decompose` sections before step 5.
|
|
15
15
|
|
|
16
16
|
## Process
|
|
17
17
|
|
|
@@ -55,10 +55,10 @@ Iterate until the user approves the breakdown.
|
|
|
55
55
|
|
|
56
56
|
### 5. Publish the issues to Opsee
|
|
57
57
|
|
|
58
|
-
Slices become **Tasks inside the Initiative** that holds the plan; a spec published by `/
|
|
58
|
+
Slices become **Tasks inside the Initiative** that holds the plan; a spec published by `/initiative-spec` already is one, and a bare plan gets one first (`status: "draft"`, the plan as its core idea, as the tracker doc's `/initiative-spec` hook describes). Each Task's description is the issue body template below. Publish the whole approved breakdown in one call, following the tracker doc's `/initiative-decompose` hook for the exact arguments:
|
|
59
59
|
|
|
60
60
|
- An Initiative with **no Tasks yet**: `opsee_decompose_initiative`, every slice as an entry with a temporary `ref` and the blocking edges between `ref`s. This also moves a `draft` Initiative to `active`.
|
|
61
|
-
- An Initiative that **already has Tasks** (re-planning, or a
|
|
61
|
+
- An Initiative that **already has Tasks** (re-planning, or a map that has become executable): `opsee_reconcile_initiative`, kept Tasks tagged with `existingTaskId` and new ones without. Run it with `dryRun: true` first, show the human the changeset, and only after their approval run it for real. Keep a map's closed wayfinder tickets so the record of the route survives; Tasks that carry execution state are never deleted by reconcile, and the human decides their fate.
|
|
62
62
|
|
|
63
63
|
**Blocked by** is a `BLOCKS` TaskDependency edge from blocker to blocked, never a line in the body; an absent edge is the absence of a blocker.
|
|
64
64
|
|
|
@@ -90,3 +90,12 @@ Optional: files or areas a Worker must not touch. Omit the section if none.
|
|
|
90
90
|
The headings are the Foreman's work contract (`cli/CONTEXT.md`): a `ready-for-agent` Task that lacks them is not dispatched but sent to a Triage turn, so write them in full. Acceptance Criteria go in the description as `- [ ]` checkboxes, not through `opsee_add_acceptance_criterion`. A Task that descends from another Task gets `parentTaskId`; there is no Parent section in the body.
|
|
91
91
|
|
|
92
92
|
Do NOT close or modify the Initiative or any parent Task.
|
|
93
|
+
|
|
94
|
+
### 6. Say where the work went, and what can start now
|
|
95
|
+
|
|
96
|
+
Finish by reporting the Initiative's **link in Opsee** (the tracker doc's `/initiative-decompose` hook prints the URL shape), how many Tasks were filed, and how many carry `ready-for-agent`. Then name both ways forward — `ready-for-agent` means nothing to a human who does not know what reads it:
|
|
97
|
+
|
|
98
|
+
- **Unattended:** `opsee foreman run <initiativeId>` takes those Tasks one at a time, each in its own workspace and branch, opening a draft MR per Task and writing what happened back into the Initiative's memory. It needs the Foreman set up on their machine first (`opsee foreman account add`); if it is not, say so rather than assuming.
|
|
99
|
+
- **By hand:** they are ordinary issues. Work them in this session or any other. The Initiative is the record either way.
|
|
100
|
+
|
|
101
|
+
Do not run the Foreman yourself, and do not offer to: it dispatches unattended Workers against the user's own subscription on their machine, which is theirs to start.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: initiative-map
|
|
3
3
|
description: Plan a huge chunk of work — more than one agent session can hold — as a shared map of decision tickets in Opsee, and resolve them one at a time until the way to the destination is clear.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
> Forked from the `wayfinder` skill by Matt Pocock (https://github.com/mattpocock/skills, MIT). This copy is owned by Opsee: the process is upstream's, the tracker is Opsee.
|
|
7
|
+
> Forked from the `wayfinder` skill by Matt Pocock (https://github.com/mattpocock/skills, MIT). Renamed `wayfinder` -> `initiative-map` for Opsee's vocabulary; the attribution is to the original. This copy is owned by Opsee: the process is upstream's, the tracker is Opsee.
|
|
8
8
|
|
|
9
9
|
A loose idea has arrived — too big for one agent session, and wrapped in fog: the way from here to the **destination** isn't visible yet. Wayfinding is about finding that way, not charging at the destination. This skill charts the way as a **shared map** on the repo's issue tracker, then works its **decision tickets** — questions whose resolution is a decision, not slices of a build to execute — one at a time until the route is clear.
|
|
10
10
|
|
|
@@ -135,4 +135,4 @@ User invokes with a map (its title, `OPS-nnn`, or the Initiative). A ticket is *
|
|
|
135
135
|
|
|
136
136
|
The user may run unblocked tickets in parallel, so expect other sessions to be editing the tracker concurrently.
|
|
137
137
|
|
|
138
|
-
When the map has no open tickets, the way is clear: hand off to `/
|
|
138
|
+
When the map has no open tickets, the way is clear: hand off to `/initiative-decompose` against the same Initiative, as the tracker doc's "Graduate to execution" describes, so the route's decisions stay in the memory the execution Tasks are read against. Report the map's Initiative by **link in Opsee**, not by id alone — the route you charted is the thing the user will want to reread, and it lives there now.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: initiative-spec
|
|
3
3
|
description: Turn the current conversation into a spec and publish it to Opsee as a draft Initiative — no interview, just synthesis of what you've already discussed.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
> Forked from the `to-spec` skill by Matt Pocock (https://github.com/mattpocock/skills, MIT). This copy is owned by Opsee: the process is upstream's, the tracker is Opsee.
|
|
7
|
+
> Forked from the `to-spec` skill by Matt Pocock (https://github.com/mattpocock/skills, MIT). Renamed `to-spec` -> `initiative-spec` for Opsee's vocabulary; the attribution is to the original. This copy is owned by Opsee: the process is upstream's, the tracker is Opsee.
|
|
8
8
|
|
|
9
9
|
This skill takes the current conversation context and codebase understanding and produces a spec. Do NOT interview the user — just synthesize what you already know.
|
|
10
10
|
|
|
11
11
|
The issue tracker is Opsee, described in `docs/agents/issue-tracker.md` at the repo root; `opsee init` writes that file. Publishing goes through the `opsee_*` MCP tools. If the doc is missing, stop and ask the user to run `opsee init`; if the tools are missing, stop and ask the user to attach the Opsee MCP server (`/mcp`). Do neither yourself, and publish nothing until both are present. In the doc, "issue" means an Opsee **Task**, and every issue lives inside an **Initiative**.
|
|
12
12
|
|
|
13
|
-
Read the doc's `/
|
|
13
|
+
Read the doc's `/initiative-spec` hook before step 3.
|
|
14
14
|
|
|
15
15
|
## Process
|
|
16
16
|
|
|
@@ -20,7 +20,7 @@ Read the doc's `/to-spec` hook before step 3.
|
|
|
20
20
|
|
|
21
21
|
Check with the user that these seams match their expectations.
|
|
22
22
|
|
|
23
|
-
3. Write the spec using the template below, then publish it to Opsee as an **Initiative**, not a Task: `opsee_create_initiative` with the spec title, a one- or two-line `summary`, `status: "draft"`, and the full spec body as the `coreIdea` in markdown. If the spec was written from a repo file, name the file path in the summary so the two stay linked. Do **not** apply a triage label: labels are Task-level in Opsee, and the spec's readiness is recorded by `/
|
|
23
|
+
3. Write the spec using the template below, then publish it to Opsee as an **Initiative**, not a Task: `opsee_create_initiative` with the spec title, a one- or two-line `summary`, `status: "draft"`, and the full spec body as the `coreIdea` in markdown. If the spec was written from a repo file, name the file path in the summary so the two stay linked. Do **not** apply a triage label: labels are Task-level in Opsee, and the spec's readiness is recorded by `/initiative-decompose` when it files the Tasks and labels them `ready-for-agent`. Report the Initiative by title, id and **its link in Opsee** (the tracker doc's `/initiative-spec` hook prints the URL shape), and say what it is waiting for: a breakdown with `/initiative-decompose` against this same Initiative. The spec now lives in Opsee rather than in this conversation, and a person who cannot open it will not read it.
|
|
24
24
|
|
|
25
25
|
<spec-template>
|
|
26
26
|
|
package/src/args.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface InitiativeCreateArgs {
|
|
|
45
45
|
project?: string;
|
|
46
46
|
summary?: string;
|
|
47
47
|
status?: string;
|
|
48
|
+
/** The core idea as text, for an idea short enough to type. */
|
|
49
|
+
coreIdea?: string;
|
|
48
50
|
/** A file holding the core idea, or `-` for stdin: a markdown body does not fit in argv. */
|
|
49
51
|
coreIdeaFile?: string;
|
|
50
52
|
}
|
|
@@ -438,6 +440,7 @@ const INITIATIVE_CREATE_OPTIONS: Record<string, keyof InitiativeCreateArgs> = {
|
|
|
438
440
|
"--summary": "summary",
|
|
439
441
|
"--status": "status",
|
|
440
442
|
"--core-idea-file": "coreIdeaFile",
|
|
443
|
+
"--core-idea": "coreIdea",
|
|
441
444
|
};
|
|
442
445
|
const INITIATIVE_MEMORY_OPTIONS: Record<string, "kind" | "limit"> = { "--kind": "kind", "--limit": "limit" };
|
|
443
446
|
|
|
@@ -492,7 +495,7 @@ function parseInitiative(argv: string[]): Command {
|
|
|
492
495
|
case "create": {
|
|
493
496
|
const flags = parseFlags(rest, INITIATIVE_CREATE_OPTIONS, "initiative create");
|
|
494
497
|
if (typeof flags === "string") return { kind: "usage-error", message: flags };
|
|
495
|
-
if (!flags.title) return { kind: "usage-error", message: "initiative create needs a title: opsee initiative create --title <title> [--project <key>] [--summary <text>] [--core-idea-file <path|->]" };
|
|
498
|
+
if (!flags.title) return { kind: "usage-error", message: "initiative create needs a title: opsee initiative create --title <title> [--project <key>] [--summary <text>] [--core-idea <text> | --core-idea-file <path|->]" };
|
|
496
499
|
return { kind: "initiative-create", args: flags };
|
|
497
500
|
}
|
|
498
501
|
case "memory": {
|
|
@@ -606,7 +609,7 @@ export function usage(): string {
|
|
|
606
609
|
" Run the Initiative's Ready Tasks one by one from this checkout:",
|
|
607
610
|
" a Workspace and branch per Task, one unattended Worker turn each,",
|
|
608
611
|
" Completion Reports into Initiative memory, moves on the board",
|
|
609
|
-
" opsee foreman plan <initiativeId> [--account <name>] [--skill <
|
|
612
|
+
" opsee foreman plan <initiativeId> [--account <name>] [--skill <initiative-decompose|initiative-map|initiative-spec>]",
|
|
610
613
|
" [--memory <n>]",
|
|
611
614
|
" Open an attended planning session on the Initiative: a Workspace on",
|
|
612
615
|
" a planning branch, the vendor's interactive session there with the",
|
|
@@ -660,9 +663,10 @@ export function usage(): string {
|
|
|
660
663
|
" Title, status, summary and core idea. --context prints the whole",
|
|
661
664
|
" work session instead: task tree, memory log and pull requests",
|
|
662
665
|
" opsee initiative create --title <title> [--project <key>] [--summary <text>]",
|
|
663
|
-
" [--status <draft|active|paused|done|archived>]
|
|
664
|
-
"
|
|
665
|
-
"
|
|
666
|
+
" [--status <draft|active|paused|done|archived>]",
|
|
667
|
+
" [--core-idea <text> | --core-idea-file <path|->]",
|
|
668
|
+
" A new Initiative. The core idea is text given inline, or markdown",
|
|
669
|
+
" read from a file, or from stdin when the path is -",
|
|
666
670
|
" opsee initiative memory <initiativeId> [--kind <kind>] [--limit <n>]",
|
|
667
671
|
" The append-only memory log, newest first",
|
|
668
672
|
" opsee initiative note <initiativeId> \"what happened\" [--kind <kind>]",
|
|
@@ -76,7 +76,7 @@ export const SKILL_DIR_BY_VENDOR: Readonly<Record<Vendor, string>> = {
|
|
|
76
76
|
codex: ".agents/skills",
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
/** `<dir>/.claude/skills/
|
|
79
|
+
/** `<dir>/.claude/skills/initiative-decompose/SKILL.md`: the file that makes a skill available to a vendor. */
|
|
80
80
|
export function skillPath(dir: string, vendor: Vendor, skill: PlanningSkill): string {
|
|
81
81
|
return join(dir, SKILL_DIR_BY_VENDOR[vendor], skill, "SKILL.md");
|
|
82
82
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
3
|
import type { Client } from "@connectrpc/connect";
|
|
4
4
|
import type { ProjectService } from "@opsee/mcp-server/gen/api/v1/project_pb.js";
|
|
5
5
|
import type { LabelService } from "@opsee/mcp-server/gen/api/v1/label_pb.js";
|
|
@@ -12,6 +12,7 @@ import { mergeClaudeMcpJson, mergeCodexConfigToml } from "../init/mcp-config.js"
|
|
|
12
12
|
import { pickInstructionFile, pointerBlock, upsertPointerBlock } from "../init/pointer-block.js";
|
|
13
13
|
import { CLAUDE_MCP_PATH, CODEX_CONFIG_PATH, SKILL_TARGETS, TRACKER_DOC_PATH } from "../init/paths.js";
|
|
14
14
|
import { pickProject, type ProjectChoice } from "../init/project.js";
|
|
15
|
+
import { planRetire, RETIRED_SKILLS } from "../init/retire.js";
|
|
15
16
|
import { listSkillFiles, skillNames } from "../init/skills.js";
|
|
16
17
|
import { findReadyLabel, READY_LABEL, renderTrackerDoc, type Named, type TrackerDocData } from "../init/tracker-doc.js";
|
|
17
18
|
import { existingRecipe, inferRecipe, mergeRecipeJson, mergeRecipeYaml, NO_START_COMMAND, type RecipeDefaults, type RecipeFlags } from "../init/run-recipe-config.js";
|
|
@@ -62,7 +63,7 @@ export async function runInit(deps: InitDeps): Promise<number> {
|
|
|
62
63
|
}
|
|
63
64
|
const res = await deps.api.projects.getProjects({});
|
|
64
65
|
const project = await pickProject(
|
|
65
|
-
res.projects.filter((p) => p.isActive).map((p) => ({ id: p.id, key: p.shortName, name: p.name })),
|
|
66
|
+
res.projects.filter((p) => p.isActive).map((p) => ({ id: p.id, key: p.shortName, name: p.name, slug: p.slug, companySlug: p.company?.slug })),
|
|
66
67
|
deps.projectKey,
|
|
67
68
|
deps.choose,
|
|
68
69
|
);
|
|
@@ -81,6 +82,15 @@ export async function runInit(deps: InitDeps): Promise<number> {
|
|
|
81
82
|
report.apply(rel, planWrite(read(deps.root, rel), file.content, file.style));
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
// The renamed skills' old folders, removed only where init wrote them and nobody edited them.
|
|
86
|
+
// Without this a repo set up before the rename keeps both, and /wayfinder goes on working
|
|
87
|
+
// beside /initiative-map with nothing to say which one is current.
|
|
88
|
+
for (const target of SKILL_TARGETS) {
|
|
89
|
+
for (const [retired, replacement] of Object.entries(RETIRED_SKILLS)) {
|
|
90
|
+
report.retire(join(target, retired), replacement);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
84
94
|
report.apply(TRACKER_DOC_PATH, planWrite(read(deps.root, TRACKER_DOC_PATH), doc, "markdown"));
|
|
85
95
|
report.apply(CLAUDE_MCP_PATH, mergeClaudeMcpJson(read(deps.root, CLAUDE_MCP_PATH), deps.mcpUrl));
|
|
86
96
|
report.apply(CODEX_CONFIG_PATH, mergeCodexConfigToml(read(deps.root, CODEX_CONFIG_PATH), deps.mcpUrl));
|
|
@@ -129,7 +139,7 @@ async function collect(api: InitApi, project: ProjectChoice): Promise<TrackerDoc
|
|
|
129
139
|
.sort((a, b) => a.level - b.level);
|
|
130
140
|
const labels = (await api.labels.getLabels({ projectId })).labels.filter((l) => l.isActive);
|
|
131
141
|
return {
|
|
132
|
-
project: { id: projectId, name: project.name, key: project.key },
|
|
142
|
+
project: { id: projectId, name: project.name, key: project.key, slug: project.slug, companySlug: project.companySlug },
|
|
133
143
|
labels: named(labels),
|
|
134
144
|
board: { id: board.id, name: board.name },
|
|
135
145
|
columns: named(columns),
|
|
@@ -150,6 +160,8 @@ const EXPLAIN: Partial<Record<WriteAction, string>> = {
|
|
|
150
160
|
|
|
151
161
|
class Reporter {
|
|
152
162
|
private counts = new Map<WriteAction, number>();
|
|
163
|
+
private removed = 0;
|
|
164
|
+
private keptRetired = 0;
|
|
153
165
|
|
|
154
166
|
constructor(
|
|
155
167
|
private root: string,
|
|
@@ -168,9 +180,30 @@ class Reporter {
|
|
|
168
180
|
this.out(`${verb.padEnd(9)} ${rel}${why ? ` — ${why}` : ""}`);
|
|
169
181
|
}
|
|
170
182
|
|
|
183
|
+
/** A folder from a skill this version no longer ships: every file of it that is still exactly
|
|
184
|
+
* what init wrote goes, and the folder with them once it is empty. Anything else is reported and
|
|
185
|
+
* left where it is. */
|
|
186
|
+
retire(rel: string, replacement: string) {
|
|
187
|
+
const dir = join(this.root, rel);
|
|
188
|
+
if (!existsSync(dir)) return;
|
|
189
|
+
const files = readdirSync(dir, { recursive: true, withFileTypes: true }).filter((e) => e.isFile());
|
|
190
|
+
for (const file of files) {
|
|
191
|
+
const path = join(file.parentPath ?? dir, file.name);
|
|
192
|
+
const plan = planRetire(readFileSync(path, "utf8"), replacement);
|
|
193
|
+
if (plan.action === "removed") rmSync(path);
|
|
194
|
+
this.removed += plan.action === "removed" ? 1 : 0;
|
|
195
|
+
this.keptRetired += plan.action === "kept" ? 1 : 0;
|
|
196
|
+
const shown = relative(this.root, path);
|
|
197
|
+
this.out(`${plan.action === "removed" ? "removed " : "kept "} ${shown}${plan.reason ? ` — ${plan.reason}` : ""}`);
|
|
198
|
+
}
|
|
199
|
+
// Only when nothing of the user's is left in it.
|
|
200
|
+
if (readdirSync(dir, { recursive: true, withFileTypes: true }).every((e) => !e.isFile())) rmSync(dir, { recursive: true });
|
|
201
|
+
}
|
|
202
|
+
|
|
171
203
|
summary(): string {
|
|
172
204
|
const n = (a: WriteAction) => this.counts.get(a) ?? 0;
|
|
173
|
-
const kept = n("kept-edited") + n("kept-unmanaged") + n("kept-invalid");
|
|
174
|
-
|
|
205
|
+
const kept = n("kept-edited") + n("kept-unmanaged") + n("kept-invalid") + this.keptRetired;
|
|
206
|
+
const removed = this.removed === 0 ? "" : `, ${this.removed} removed`;
|
|
207
|
+
return `${n("create")} created, ${n("update")} updated, ${n("unchanged")} unchanged, ${kept} kept${removed}.`;
|
|
175
208
|
}
|
|
176
209
|
}
|
|
@@ -77,7 +77,10 @@ function oneOf<T extends readonly string[]>(flag: string, value: string | undefi
|
|
|
77
77
|
*/
|
|
78
78
|
async function resolveProject(deps: InitiativeDeps, key: string | undefined): Promise<ProjectChoice> {
|
|
79
79
|
const res = await deps.clients.projects.getProjects({ pagination: { page: 1, pageSize: 200 } });
|
|
80
|
-
|
|
80
|
+
// shortName, not `key`: api.v1.Project has no such field, so this read `undefined` for every
|
|
81
|
+
// project and printed "undefined (DevZero), undefined (Opsee), ..." before throwing on
|
|
82
|
+
// `undefined.toLowerCase()` the moment anyone passed --project. `opsee init` had it right.
|
|
83
|
+
const projects: ProjectChoice[] = res.projects.map((p) => ({ id: p.id, key: p.shortName, name: p.name }));
|
|
81
84
|
const picked = await pickProject(projects, key, async () => null);
|
|
82
85
|
if (typeof picked === "string") throw new InitiativeError(picked);
|
|
83
86
|
return picked;
|
|
@@ -113,8 +116,20 @@ export async function runInitiativeShow(deps: InitiativeDeps, args: InitiativeSh
|
|
|
113
116
|
return 0;
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
119
|
+
/**
|
|
120
|
+
* The core idea's body: the text given inline, a file, or stdin when the path is `-`. Absent when
|
|
121
|
+
* neither flag was given.
|
|
122
|
+
*
|
|
123
|
+
* Inline text is here because the file was the only way in and a shell here-doc is a poor answer
|
|
124
|
+
* to "I already have the sentence". A body long enough to need markdown still wants a file; argv
|
|
125
|
+
* has a length limit and no way to hold a newline comfortably.
|
|
126
|
+
*/
|
|
127
|
+
async function coreIdeaOf(deps: InitiativeDeps, path: string | undefined, text: string | undefined): Promise<string | undefined> {
|
|
128
|
+
if (text !== undefined) {
|
|
129
|
+
if (path !== undefined) throw new InitiativeError("Pass --core-idea or --core-idea-file, not both");
|
|
130
|
+
if (text.trim() === "") throw new InitiativeError("The core idea given with --core-idea is empty");
|
|
131
|
+
return text;
|
|
132
|
+
}
|
|
118
133
|
if (path === undefined) return undefined;
|
|
119
134
|
if (path === "-") {
|
|
120
135
|
const read = deps.readStdin ?? readStdin;
|
|
@@ -145,7 +160,7 @@ function readStdin(): Promise<string> {
|
|
|
145
160
|
export async function runInitiativeCreate(deps: InitiativeDeps, args: InitiativeCreateArgs): Promise<number> {
|
|
146
161
|
const status = oneOf("--status", args.status, INITIATIVE_STATUSES) ?? "draft";
|
|
147
162
|
// Before the project is resolved: a bad flag should be refused without a round trip.
|
|
148
|
-
const coreIdea = await coreIdeaOf(deps, args.coreIdeaFile);
|
|
163
|
+
const coreIdea = await coreIdeaOf(deps, args.coreIdeaFile, args.coreIdea);
|
|
149
164
|
const project = await resolveProject(deps, args.project);
|
|
150
165
|
const res = await deps.clients.initiatives.addInitiative({
|
|
151
166
|
projectId: project.id,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The planning session's context (story 10, `opsee foreman plan`): what an attended session is
|
|
3
|
-
* handed as its first message so `/
|
|
3
|
+
* handed as its first message so `/initiative-decompose` (or `/initiative-map`, `/initiative-spec`) starts already knowing
|
|
4
4
|
* last night's results. It is Context Assembly (core/context.ts) turned to the Initiative rather
|
|
5
5
|
* than to one Task: the core idea, the whole task tree with statuses and blockers, the memory log
|
|
6
6
|
* newest first with the Completion Reports (`outcome` entries) pulled out first, and the pull
|
|
@@ -14,10 +14,10 @@ import type { InitiativeContext, MemoryRecord, TrackerTask } from "../tracker-ad
|
|
|
14
14
|
import { count, fenced, oneLine } from "./text.js";
|
|
15
15
|
|
|
16
16
|
/** The Opsee-owned planning skills `opsee init` writes into the repo; the ones a session may be
|
|
17
|
-
* opened on. `
|
|
18
|
-
export const PLANNING_SKILLS = ["
|
|
17
|
+
* opened on. `initiative-decompose` is the default: it is what files Tasks into the Initiative. */
|
|
18
|
+
export const PLANNING_SKILLS = ["initiative-decompose", "initiative-map", "initiative-spec"] as const;
|
|
19
19
|
export type PlanningSkill = (typeof PLANNING_SKILLS)[number];
|
|
20
|
-
export const DEFAULT_PLANNING_SKILL: PlanningSkill = "
|
|
20
|
+
export const DEFAULT_PLANNING_SKILL: PlanningSkill = "initiative-decompose";
|
|
21
21
|
|
|
22
22
|
export function isPlanningSkill(name: string): name is PlanningSkill {
|
|
23
23
|
return (PLANNING_SKILLS as readonly string[]).includes(name);
|
|
@@ -14,7 +14,7 @@ export function pointerBlock(trackerDocPath: string): string {
|
|
|
14
14
|
POINTER_BEGIN,
|
|
15
15
|
"## Agent skills",
|
|
16
16
|
"",
|
|
17
|
-
"`opsee init` installs Opsee's planning skills (`/
|
|
17
|
+
"`opsee init` installs Opsee's planning skills (`/initiative-map`, `/initiative-spec`, `/initiative-decompose`) into",
|
|
18
18
|
"`.claude/skills/` and `.agents/skills/`, and writes the tracker doc they read to",
|
|
19
19
|
`\`${trackerDocPath}\`. Opsee owns those files; edit them upstream and re-run \`opsee init\`,`,
|
|
20
20
|
"which updates what it wrote and leaves any file you changed alone.",
|
package/src/init/project.ts
CHANGED
|
@@ -2,6 +2,11 @@ export interface ProjectChoice {
|
|
|
2
2
|
id: number;
|
|
3
3
|
key: string;
|
|
4
4
|
name: string;
|
|
5
|
+
/** The project's and its company's URL slugs, when the response carried them: the tracker doc
|
|
6
|
+
* prints an Initiative's page from the pair, and a doc that can link is what sends a person
|
|
7
|
+
* back to Opsee at the end of a planning session. Absent leaves the doc saying so. */
|
|
8
|
+
slug?: string;
|
|
9
|
+
companySlug?: string;
|
|
5
10
|
}
|
|
6
11
|
|
|
7
12
|
/** Which project the repo is set up for: `--project <key>` when given, the only one when there
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { hashOf, parseMarker } from "./managed.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Skill folders `opsee init` used to write and no longer does.
|
|
5
|
+
*
|
|
6
|
+
* The names changed to Opsee's vocabulary (`/initiative-map`, `/initiative-spec`,
|
|
7
|
+
* `/initiative-decompose`). A repo set up before that has both sets on disk, and the old ones
|
|
8
|
+
* still work — which is the confusing part, not a broken one: two commands that do the same thing
|
|
9
|
+
* and no signal which is current. So init removes what it wrote, and only that.
|
|
10
|
+
*/
|
|
11
|
+
export const RETIRED_SKILLS: Readonly<Record<string, string>> = {
|
|
12
|
+
wayfinder: "initiative-map",
|
|
13
|
+
"to-spec": "initiative-spec",
|
|
14
|
+
"to-issues": "initiative-decompose",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type RetireAction =
|
|
18
|
+
/** Written by opsee init and untouched since: safe to delete, because opsee init can write it again. */
|
|
19
|
+
| "removed"
|
|
20
|
+
/** Carries our marker but has been edited, or carries none at all: the user's file now. */
|
|
21
|
+
| "kept";
|
|
22
|
+
|
|
23
|
+
export interface RetirePlan {
|
|
24
|
+
action: RetireAction;
|
|
25
|
+
/** Why it was kept, for the line the reporter prints. */
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether a file left behind by a retired skill is ours to delete.
|
|
31
|
+
*
|
|
32
|
+
* Deleting only unmodified files is the whole point: someone who edited a fork of `/wayfinder`
|
|
33
|
+
* meant it, and losing that to a rename would be an unrecoverable surprise. The kept file is
|
|
34
|
+
* reported rather than removed, with the new name to move to.
|
|
35
|
+
*/
|
|
36
|
+
export function planRetire(existing: string, replacement: string): RetirePlan {
|
|
37
|
+
const marked = parseMarker(existing);
|
|
38
|
+
if (!marked) return { action: "kept", reason: `not written by opsee init; delete it yourself once /${replacement} suits you` };
|
|
39
|
+
if (hashOf(marked.body) !== marked.hash) return { action: "kept", reason: `edited since opsee init wrote it; /${replacement} replaces it` };
|
|
40
|
+
return { action: "removed" };
|
|
41
|
+
}
|
package/src/init/skills.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join, relative } from "node:path";
|
|
|
3
3
|
import type { MarkerStyle } from "./managed.js";
|
|
4
4
|
|
|
5
5
|
export interface SkillFile {
|
|
6
|
-
/** Path relative to the skills dir, e.g. `
|
|
6
|
+
/** Path relative to the skills dir, e.g. `initiative-decompose/SKILL.md`. */
|
|
7
7
|
rel: string;
|
|
8
8
|
content: string;
|
|
9
9
|
style: MarkerStyle;
|
package/src/init/tracker-doc.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface Named {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface TrackerDocData {
|
|
12
|
-
project: { id: number; name: string; key: string };
|
|
12
|
+
project: { id: number; name: string; key: string; slug?: string; companySlug?: string };
|
|
13
13
|
labels: Named[];
|
|
14
14
|
board: Named;
|
|
15
15
|
/** In board order. */
|
|
@@ -69,6 +69,17 @@ export function findReadyLabel(labels: Named[]): Named | undefined {
|
|
|
69
69
|
|
|
70
70
|
const idList = (items: Named[]) => items.map((i) => `${i.name} \`${i.id}\``).join(", ");
|
|
71
71
|
|
|
72
|
+
/** Where an Initiative lives in the app, for the doc to hand back to a human at the end of a
|
|
73
|
+
* planning session. Both slugs or nothing: half a URL is worse than a sentence saying where to
|
|
74
|
+
* look, and a link with an empty segment reads as a bug in Opsee rather than a missing slug.
|
|
75
|
+
* Returns the phrase already formatted, backticks included, so the fallback is not a sentence
|
|
76
|
+
* rendered as code. */
|
|
77
|
+
function initiativeUrl(project: TrackerDocData["project"]): string {
|
|
78
|
+
const app = (process.env.OPSEE_APP_URL || "https://opsee.ai").replace(/\/+$/, "");
|
|
79
|
+
if (!project.slug || !project.companySlug) return "the Initiatives page in Opsee";
|
|
80
|
+
return `\`${app}/${project.companySlug}/${project.slug}/initiatives/<uuid>\``;
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
export function renderTrackerDoc(template: string, data: TrackerDocData): string {
|
|
73
84
|
const ready = findReadyLabel(data.labels);
|
|
74
85
|
const bug = data.types.find((t) => /bug/.test(norm(t.name))) ?? data.types[0];
|
|
@@ -94,6 +105,7 @@ export function renderTrackerDoc(template: string, data: TrackerDocData): string
|
|
|
94
105
|
BUG_TYPE_ID: String(bug.id),
|
|
95
106
|
TYPES_LINE: idList(data.types),
|
|
96
107
|
PRIORITIES_LINE: idList(data.priorities),
|
|
108
|
+
INITIATIVE_URL: initiativeUrl(data.project),
|
|
97
109
|
};
|
|
98
110
|
const rendered = template.replace(/\{\{([A-Z_]+)\}\}/g, (whole, key: string) => {
|
|
99
111
|
const value = values[key];
|
|
@@ -95,15 +95,15 @@ Avoid file paths and code in the description except for a prototype-derived snip
|
|
|
95
95
|
|
|
96
96
|
## Skill hooks
|
|
97
97
|
|
|
98
|
-
### `/
|
|
98
|
+
### `/initiative-spec` (and an upstream `/to-prd`): "publish to the issue tracker"
|
|
99
99
|
|
|
100
100
|
A spec or PRD becomes an **Initiative**, not a Task.
|
|
101
101
|
|
|
102
102
|
1. `opsee_create_initiative` with `projectId: {{PROJECT_ID}}`, the spec title, a one- or two-line `summary`, `status: "draft"`, and the full spec body (the skill's template) as `coreIdea` in markdown.
|
|
103
|
-
2. Do **not** try to apply `ready-for-agent` to the Initiative; labels are Task-level. The spec's readiness is recorded by `/
|
|
104
|
-
3. Report the Initiative by title and
|
|
103
|
+
2. Do **not** try to apply `ready-for-agent` to the Initiative; labels are Task-level. The spec's readiness is recorded by `/initiative-decompose` when it files the Tasks and labels them. If the spec was written from a repo file, note the file path in the summary so the two stay linked.
|
|
104
|
+
3. Report the Initiative by title, id **and link** — {{INITIATIVE_URL}} — and say what it is now waiting for: a breakdown, which is `/initiative-decompose` against this same Initiative. A spec nobody can open is a spec nobody reads.
|
|
105
105
|
|
|
106
|
-
### `/
|
|
106
|
+
### `/initiative-decompose`: "publish the issues"
|
|
107
107
|
|
|
108
108
|
Slices become **Tasks inside the Initiative** that holds the spec. Find or create that Initiative first.
|
|
109
109
|
|
|
@@ -112,6 +112,9 @@ Slices become **Tasks inside the Initiative** that holds the spec. Find or creat
|
|
|
112
112
|
3. If the Initiative **already has Tasks** (re-planning, or a wayfinder map that has become executable), use `opsee_reconcile_initiative`: tag kept Tasks with `existingTaskId`, add new ones without, run with `dryRun: true`, show the human the changeset, and only then run with `dryRun: false`. Tasks that carry execution state are never deleted by reconcile; the human decides their fate.
|
|
113
113
|
4. Label the **AFK** slices `ready-for-agent` with `opsee_bulk_update_tasks` (`labelIds: [{{READY_LABEL_ID}}]`, plus an area label if useful). **HITL** slices get no `ready-for-agent`; put the reason a human is needed in the Goal and leave them for an attended turn.
|
|
114
114
|
5. Never edit or close the Initiative or any parent Task.
|
|
115
|
+
6. **Report where the work went and what can start.** Give the Initiative's link — {{INITIATIVE_URL}} — the count of Tasks filed, and how many carry `ready-for-agent`. Then name the two ways forward, because the label means nothing to a human who does not know what reads it:
|
|
116
|
+
- **Unattended:** `opsee foreman run <initiativeId>` takes the `ready-for-agent` Tasks one at a time, each in its own workspace and branch, and opens a draft MR per Task. It needs the Foreman set up on their machine (`opsee foreman account add`, see the CLI's README).
|
|
117
|
+
- **By hand, here:** the Tasks are ordinary issues. They can be worked in this session or any other, and the Initiative's memory is where the record goes either way.
|
|
115
118
|
|
|
116
119
|
The dependency-ordered publish the skill describes ("blockers first so you can reference real ids") is unnecessary here: decompose and reconcile take the whole graph with `ref`s and create it atomically.
|
|
117
120
|
|
|
@@ -148,10 +151,10 @@ Commit messages and MR titles carry `OPS-nnn`. Resolve it with `opsee_list_tasks
|
|
|
148
151
|
|
|
149
152
|
## Wayfinding operations
|
|
150
153
|
|
|
151
|
-
Used by `/
|
|
154
|
+
Used by `/initiative-map`. The whole effort, from charting to execution, lives in **one Initiative**, so the decisions the map records are already in the memory log the Foreman's Context Assembly reads when `/initiative-decompose` later files execution Tasks into it.
|
|
152
155
|
|
|
153
156
|
- **Map**: a Task labelled `wayfinder:map` inside the Initiative, type Task, in **To Do**, whose description is the map body (Destination, Notes, Decisions so far, Not yet specified, Out of scope). The Initiative's `coreIdea` carries the Destination and Notes as well, so `opsee_get_initiative_context` orients a session without opening the map. Create the Initiative first (`status: "draft"`), then the map Task, then `opsee_link_task_to_initiative`.
|
|
154
|
-
- **Labels**: `wayfinder:map` and `wayfinder:<type>` for `research`, `prototype`, `grilling`, `task` do not exist in the project until first use. Check `opsee_list_labels`; create missing ones with `opsee_create_label` (`projectId: {{PROJECT_ID}}`) and record the ids in the map's Notes so later sessions skip the lookup.
|
|
157
|
+
- **Labels**: `wayfinder:map` and `wayfinder:<type>` (the prefix is the label's, not the skill's: the skill is `/initiative-map`, and renaming the labels would orphan every map already charted) for `research`, `prototype`, `grilling`, `task` do not exist in the project until first use. Check `opsee_list_labels`; create missing ones with `opsee_create_label` (`projectId: {{PROJECT_ID}}`) and record the ids in the map's Notes so later sessions skip the lookup.
|
|
155
158
|
- **Child ticket**: a Task with `parentTaskId` = the map's id, linked to the same Initiative, labelled `wayfinder:<type>`, description `## Question` only. Never label a wayfinder ticket `ready-for-agent`: decision tickets are for planning sessions, not the Foreman. Create tickets first, then wire blocking in a second pass once ids exist.
|
|
156
159
|
- **Blocking**: native `BLOCKS` TaskDependency edges (`opsee_create_task_dependency`, blocker → blocked). Opsee renders them on the Task and in the Initiative graph, so the frontier is visible without opening the map. A ticket is unblocked when every Task blocking it is in **Done**.
|
|
157
160
|
- **Frontier query**: `opsee_list_tasks` with `projectId: {{PROJECT_ID}}`, `parentTaskId: <map id>`, `unassigned: true`, `sortBy: "display_order"`, then drop any Task in Done/Archived and any whose `opsee_list_task_dependencies` shows an incoming `BLOCKED_BY` from a Task not yet Done. The slices in `opsee_get_initiative_context` give the same answer for the whole Initiative at once. First in map order wins.
|
|
@@ -159,4 +162,4 @@ Used by `/wayfinder`. The whole effort, from charting to execution, lives in **o
|
|
|
159
162
|
- **Resolve**: post the answer with `opsee_add_comment` on the ticket; record it in the Initiative with `opsee_add_initiative_memory` (`kind: "decision"`, `sourceType: "task"`, `sourceId: <ticket id>`); move the ticket to **Done**; then `opsee_update_task` on the map to append one line to *Decisions so far*: `- <ticket title> (OPS-nnn) — <one-line gist>`. Re-read the map's description immediately before editing it, because other sessions may be working the map concurrently.
|
|
160
163
|
- **Out of scope**: move the ticket to **Done**, add a line to the map's *Out of scope* section linking it, and leave *Decisions so far* untouched.
|
|
161
164
|
- **Research tickets**: findings are captured on a throwaway `research/<name>` branch; link the branch or file path from the ticket's resolution comment rather than pasting the findings.
|
|
162
|
-
- **Graduate to execution**: when the map has no open tickets, run `/
|
|
165
|
+
- **Graduate to execution**: when the map has no open tickets, run `/initiative-decompose` against the same Initiative. Use `opsee_reconcile_initiative` (the Initiative already has Tasks) and keep the closed wayfinder tickets with `existingTaskId` so the record of the route survives.
|