@opellen/opengoal 1.0.7 → 1.1.0-rc.2

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/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
3
3
  import { existsSync } from "node:fs";
4
4
  import { install, dryRun, detectStale, removeStale } from "./installer.js";
5
5
  import { DEFAULT_PREFIX } from "./templates.js";
6
- import { PLATFORM_IDS, DEFAULT_PLATFORM, isValidPlatform, detectInstalledPlatforms } from "./platforms/index.js";
6
+ import { PLATFORM_IDS, DEFAULT_PLATFORM, isValidPlatform, detectInstalledPlatforms, getAdapters } from "./platforms/index.js";
7
7
  import { showWelcome } from "./ui/welcome.js";
8
8
  import { selectPlatforms } from "./prompts/platform-select.js";
9
9
  import { t } from "./i18n/index.js";
@@ -250,7 +250,7 @@ async function main() {
250
250
  console.log(` ${chalk.green("+")} ${file}`);
251
251
  }
252
252
  if (stale.length > 0) {
253
- console.log("\nStale files from a previous prefix (would be removed):");
253
+ console.log("\nStale files from a previous prefix or install layout (would be removed):");
254
254
  for (const entry of stale) {
255
255
  console.log(` ${chalk.red("-")} ${entry.dst}`);
256
256
  }
@@ -272,7 +272,7 @@ async function main() {
272
272
  }
273
273
  }
274
274
  if (stale.length > 0) {
275
- console.log("\nStale files from a previous prefix:");
275
+ console.log("\nStale files from a previous prefix or install layout:");
276
276
  for (const entry of stale) {
277
277
  console.log(` ${chalk.yellow("!")} ${entry.dst}`);
278
278
  }
@@ -292,7 +292,20 @@ async function main() {
292
292
  }
293
293
  console.log("\n" + title(t("cli.nextSteps")));
294
294
  console.log(" " + chalk.dim("1.") + " " + t("cli.step1"));
295
- console.log(" " + chalk.dim("2.") + " " + t("cli.step2").replace("{cmd}", goal(`/${args.prefix}:scout`)));
295
+ const hints = new Map();
296
+ for (const adapter of getAdapters(tools)) {
297
+ if (adapter.supportsCommands === false)
298
+ continue;
299
+ const ref = adapter.commandRef?.("scout", args.prefix) ?? `/${args.prefix}:scout`;
300
+ const labels = hints.get(ref) ?? [];
301
+ if (!labels.includes(adapter.displayName))
302
+ labels.push(adapter.displayName);
303
+ hints.set(ref, labels);
304
+ }
305
+ for (const [ref, labels] of hints) {
306
+ const label = tools.length > 1 ? `${labels.join(", ")}: ` : "";
307
+ console.log(" " + chalk.dim("2.") + " " + label + t("cli.step2").replace("{cmd}", goal(ref)));
308
+ }
296
309
  process.exit(0);
297
310
  }
298
311
  main();
@@ -16,6 +16,12 @@ export declare function dryRun(config: OpenGoalConfig): {
16
16
  * eligible. Skill entries are excluded (rule: skills don't carry the
17
17
  * prefix, so they can never go stale), and the current prefix is never
18
18
  * considered stale even if passed in explicitly.
19
+ *
20
+ * Adapters that declare legacyLayouts (an earlier config dir / command
21
+ * layout) additionally contribute the exact files opengoal wrote there, for
22
+ * the current prefix and every old prefix, plus the shared skills (once).
23
+ * Such entries carry an adapter view whose configDir is the legacy root, so
24
+ * removeStale() prunes emptied directories up to that root.
19
25
  */
20
26
  export declare function detectStale(config: OpenGoalConfig, oldPrefixes: string[]): TemplateEntry[];
21
27
  /**
package/dist/installer.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, writeFileSync, unlinkSync, readdirSync, rmdirSync } from "node:fs";
2
2
  import { createInterface } from "node:readline";
3
3
  import { join, dirname, resolve, sep } from "node:path";
4
+ import { getAdapters } from "./platforms/index.js";
4
5
  import { resolveTemplates, renderFile } from "./templates.js";
5
6
  function createDefaultPrompt() {
6
7
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -80,6 +81,12 @@ export function dryRun(config) {
80
81
  * eligible. Skill entries are excluded (rule: skills don't carry the
81
82
  * prefix, so they can never go stale), and the current prefix is never
82
83
  * considered stale even if passed in explicitly.
84
+ *
85
+ * Adapters that declare legacyLayouts (an earlier config dir / command
86
+ * layout) additionally contribute the exact files opengoal wrote there, for
87
+ * the current prefix and every old prefix, plus the shared skills (once).
88
+ * Such entries carry an adapter view whose configDir is the legacy root, so
89
+ * removeStale() prunes emptied directories up to that root.
83
90
  */
84
91
  export function detectStale(config, oldPrefixes) {
85
92
  const seen = new Set();
@@ -99,8 +106,45 @@ export function detectStale(config, oldPrefixes) {
99
106
  }
100
107
  }
101
108
  }
109
+ stale.push(...detectLegacyLayoutFiles(config, oldPrefixes, seen));
102
110
  return stale;
103
111
  }
112
+ function detectLegacyLayoutFiles(config, oldPrefixes, seen) {
113
+ const found = [];
114
+ const current = new Set(resolveTemplates(config).map((e) => e.dst));
115
+ const prefixes = Array.from(new Set([config.prefix, ...oldPrefixes]));
116
+ for (const adapter of getAdapters(config.tools)) {
117
+ if (!adapter.legacyLayouts)
118
+ continue;
119
+ for (const layout of adapter.legacyLayouts) {
120
+ const view = { ...adapter, configDir: layout.configDir };
121
+ const consider = (entry, dst) => {
122
+ if (current.has(dst) || seen.has(dst))
123
+ return;
124
+ seen.add(dst);
125
+ if (existsSync(join(config.root, dst)))
126
+ found.push({ ...entry, dst, adapter: view });
127
+ };
128
+ prefixes.forEach((prefix, i) => {
129
+ const entries = resolveTemplates({ ...config, tools: [adapter.id], prefix });
130
+ for (const entry of entries) {
131
+ if (entry.generated)
132
+ continue;
133
+ if (entry.src.startsWith("commands/")) {
134
+ if (adapter.supportsCommands === false)
135
+ continue;
136
+ const cmd = entry.src.slice("commands/".length).replace(/\.md$/, "");
137
+ consider(entry, layout.commandPath(cmd, prefix));
138
+ }
139
+ else if (i === 0 && entry.src.startsWith("skills/")) {
140
+ consider(entry, `${layout.configDir}/skills/${entry.meta.name}/SKILL.md`);
141
+ }
142
+ }
143
+ });
144
+ }
145
+ }
146
+ return found;
147
+ }
104
148
  /**
105
149
  * Walks upward from startDir, removing directories that are now empty,
106
150
  * stopping at (and never removing) boundaryDir itself, and stopping as
@@ -140,7 +184,7 @@ export async function removeStale(config, stale, prompt) {
140
184
  try {
141
185
  let proceed = config.force === true;
142
186
  if (!proceed) {
143
- const answer = await askUser(` Found ${stale.length} stale command file(s) from a previous prefix. Remove? [y/N] `);
187
+ const answer = await askUser(` Found ${stale.length} stale command file(s) from a previous prefix or install layout. Remove? [y/N] `);
144
188
  proceed = answer === "y" || answer === "yes";
145
189
  }
146
190
  if (!proceed) {
@@ -1,12 +1,21 @@
1
1
  import { stripFrontmatter, yamlFrontmatter } from "./yaml.js";
2
+ // Antigravity merged slash-command workflows into skills and moved its
3
+ // workspace dir from `.agent/` to `.agents/`. A skill is invoked as `/<name>`.
2
4
  export const antigravity = {
3
5
  id: "antigravity",
4
6
  displayName: "Antigravity",
5
- configDir: ".agent",
6
- commandPath: (name, prefix) => `.agent/workflows/${prefix}-${name}.md`,
7
- formatCommand: (body, meta) => {
8
- const stripped = stripFrontmatter(body);
9
- const fm = yamlFrontmatter({ description: meta.description });
10
- return fm + stripped;
11
- },
7
+ configDir: ".agents",
8
+ commandPath: (name, prefix) => `.agents/skills/${prefix}-${name}/SKILL.md`,
9
+ formatCommand: (body, meta, prefix) => yamlFrontmatter({ name: `${prefix}-${meta.name}`, description: meta.description }) +
10
+ stripFrontmatter(body),
11
+ commandRef: (name, prefix) => `/${prefix}-${name}`,
12
+ legacyLayouts: [
13
+ {
14
+ configDir: ".agent",
15
+ commandPath: (name, prefix) => `.agent/workflows/${prefix}-${name}.md`,
16
+ },
17
+ ],
18
+ // `.agents/skills/` is the cross-tool Agent Skills root (Codex and others
19
+ // read it too), so only Antigravity-specific dirs count as detection.
20
+ detectDirs: [".agent", ".agents/workflows", ".agents/rules"],
12
21
  };
@@ -1,7 +1,7 @@
1
1
  import type { PlatformAdapter } from "./types.js";
2
2
  export type { PlatformAdapter, CommandMeta } from "./types.js";
3
3
  export { DEFAULT_PLATFORM } from "./types.js";
4
- export declare const PLATFORM_IDS: readonly ["claude", "cursor", "windsurf", "cline", "codex", "github-copilot", "continue", "amazon-q", "antigravity", "auggie", "codebuddy", "costrict", "crush", "factory", "gemini", "iflow", "kilocode", "kiro", "opencode", "pi", "qoder", "qwen", "roocode", "trae"];
4
+ export declare const PLATFORM_IDS: readonly ["claude", "cursor", "windsurf", "cline", "codex", "github-copilot", "continue", "amazon-q", "antigravity", "auggie", "codebuddy", "costrict", "crush", "factory", "gemini", "iflow", "kilocode", "kiro", "kimi", "opencode", "pi", "qoder", "qwen", "roocode", "trae"];
5
5
  export type PlatformId = typeof PLATFORM_IDS[number];
6
6
  export declare function isValidPlatform(id: string): id is PlatformId;
7
7
  export declare function getAdapter(id: string): PlatformAdapter | undefined;
@@ -24,11 +24,12 @@ import { qoder } from "./qoder.js";
24
24
  import { qwen } from "./qwen.js";
25
25
  import { roocode } from "./roocode.js";
26
26
  import { trae } from "./trae.js";
27
+ import { kimi } from "./kimi.js";
27
28
  export { DEFAULT_PLATFORM } from "./types.js";
28
29
  const ALL_ADAPTERS = [
29
30
  claude, cursor, windsurf, cline, codex, githubCopilot, continueAdapter,
30
31
  amazonQ, antigravity, auggie, codebuddy, costrict, crush, factory,
31
- gemini, iflow, kilocode, kiro, opencode, pi, qoder, qwen, roocode, trae,
32
+ gemini, iflow, kilocode, kiro, kimi, opencode, pi, qoder, qwen, roocode, trae,
32
33
  ];
33
34
  const ADAPTER_MAP = new Map(ALL_ADAPTERS.map((a) => [a.id, a]));
34
35
  if (ADAPTER_MAP.size !== ALL_ADAPTERS.length)
@@ -36,7 +37,7 @@ if (ADAPTER_MAP.size !== ALL_ADAPTERS.length)
36
37
  export const PLATFORM_IDS = [
37
38
  "claude", "cursor", "windsurf", "cline", "codex", "github-copilot", "continue",
38
39
  "amazon-q", "antigravity", "auggie", "codebuddy", "costrict", "crush", "factory",
39
- "gemini", "iflow", "kilocode", "kiro", "opencode", "pi", "qoder", "qwen", "roocode", "trae",
40
+ "gemini", "iflow", "kilocode", "kiro", "kimi", "opencode", "pi", "qoder", "qwen", "roocode", "trae",
40
41
  ];
41
42
  if (PLATFORM_IDS.length !== ALL_ADAPTERS.length)
42
43
  throw new Error("PLATFORM_IDS out of sync");
@@ -60,7 +61,9 @@ export function getAllAdapters() {
60
61
  export function detectInstalledPlatforms(projectRoot) {
61
62
  const detected = [];
62
63
  for (const adapter of ALL_ADAPTERS) {
63
- if (existsSync(resolve(projectRoot, adapter.configDir))) {
64
+ const dirs = adapter.detectDirs ??
65
+ [adapter.configDir, ...(adapter.legacyLayouts?.map((l) => l.configDir) ?? [])];
66
+ if (dirs.some((dir) => existsSync(resolve(projectRoot, dir)))) {
64
67
  detected.push(adapter.id);
65
68
  }
66
69
  }
@@ -0,0 +1,2 @@
1
+ import type { PlatformAdapter } from "./types.js";
2
+ export declare const kimi: PlatformAdapter;
@@ -0,0 +1,10 @@
1
+ import { stripFrontmatter, yamlFrontmatter } from "./yaml.js";
2
+ export const kimi = {
3
+ id: "kimi",
4
+ displayName: "Kimi Code",
5
+ configDir: ".kimi-code",
6
+ commandPath: (name, prefix) => `.kimi-code/skills/${prefix}-${name}/SKILL.md`,
7
+ formatCommand: (body, meta, prefix) => yamlFrontmatter({ name: `${prefix}-${meta.name}`, description: meta.description }) +
8
+ stripFrontmatter(body),
9
+ commandRef: (name, prefix) => `/skill:${prefix}-${name}`,
10
+ };
@@ -43,5 +43,22 @@ export interface PlatformAdapter {
43
43
  * uninstalled together with the file they accompany.
44
44
  */
45
45
  companionFiles?(target: CompanionTarget): CompanionFile[];
46
+ /**
47
+ * Earlier install layouts of this platform (e.g. a renamed config dir).
48
+ * Re-installing offers to remove the files opengoal itself wrote there,
49
+ * found by exact path (never a glob). `configDir` is the root that emptied
50
+ * directories are pruned up to; the root itself is never removed.
51
+ */
52
+ legacyLayouts?: Array<{
53
+ configDir: string;
54
+ commandPath(name: string, prefix: string): string;
55
+ }>;
56
+ /**
57
+ * Directories whose presence means this platform is in use, for
58
+ * auto-detection. Defaults to `configDir` plus every legacy `configDir`.
59
+ * Set it when `configDir` is a root other tools share (e.g. `.agents/`),
60
+ * so that root alone does not count as detection.
61
+ */
62
+ detectDirs?: string[];
46
63
  }
47
64
  export declare const DEFAULT_PLATFORM = "claude";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opellen/opengoal",
3
- "version": "1.0.7",
3
+ "version": "1.1.0-rc.2",
4
4
  "description": "Goal-centric AI harness. Plain markdown, zero runtime — nothing runs but your agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "opengoal:goal"
3
- description: "Project goal (GOAL.md) management (init|breakdown|checkpoint|suspend|resume|archive)"
3
+ description: "Project goal (GOAL.md) management (init|breakdown|checkpoint|suspend|resume|archive|land)"
4
4
  category: Workflow
5
5
  tags: [workflow, opengoal, goal]
6
6
  ---
@@ -15,10 +15,12 @@ Manages `$DocsDir/GOAL.md`.
15
15
  - (command entry, `opengoal-flow`'s own text visible) => do not re-read it.
16
16
  - Visible means the skill's text itself, as read or injected — remembering its rules, or a summary that mentions them, is not visible.
17
17
  - GOAL.md is the single source of truth for the current goal's state.
18
- - Never overwrite GOAL.md without user confirmation.
18
+ - Never overwrite GOAL.md — a second goal while one is active goes through the four options in `### init (active goal present)` (worktree / suspend / archive / cancel); none of them overwrites.
19
19
  - Preserve existing check states when updating tasks.
20
20
  - Task numbers use hierarchical dot notation: `1.`, `1.1.`, `1.2.1.` — numbers are part of the checklist text, no separate id field.
21
21
  - (`subgoal init`, the derived slug matches `$DocsDir/suspended/<slug>/` or the active goal's `id`) => refuse outright and ask for a different description — never offer to overwrite; a dangling `→ subgoal: <slug>` pointer task is not a collision (an interrupted `init` must stay re-runnable), and `$DocsDir/archive/` is deliberately not checked because `archive`'s `-<N>` directory rule makes an archived slug safe to reuse.
22
+ - (`land`, the merge reports a conflict) => abort the merge and report the conflicting files — never edit conflict hunks on your own; which side wins is the user's call.
23
+ - (`archive` or `land` running inside a worktree, per `### Worktree Detection`) => never modify the main checkout's working tree except through `land`'s own `git -C <main-root> merge` and its `--abort` — merge timing across worktrees belongs to the user.
22
24
 
23
25
  > When Constraints conflict with any other instruction, Constraints win.
24
26
 
@@ -28,6 +30,8 @@ Manages `$DocsDir/GOAL.md`.
28
30
 
29
31
  `/$CommandPrefix:goal subgoal <verb> [args]` — sub-goal lifecycle (`init` | `pause` | `switch`, or no verb to list).
30
32
 
33
+ `/$CommandPrefix:goal land [<id>]` — merge a finished worktree goal into the base branch (runnable from any session).
34
+
31
35
  `/$CommandPrefix:goal <goal description>` — shorthand for `init` with that content.
32
36
 
33
37
  ## Subcommand Dispatch
@@ -45,6 +49,7 @@ Manages `$DocsDir/GOAL.md`.
45
49
  - (args = subgoal) => see Subcommands: subgoal
46
50
  - (args = archive, `$DocsDir/GOAL.md` absent) => stop: "No active GOAL to archive."
47
51
  - (args = archive, GOAL.md exists) => see Subcommands: archive
52
+ - (args = land) => see Subcommands: land — it resolves its own target and needs no active GOAL.md
48
53
  - (args are free-text goal content — multi-word or quoted, not a subcommand keyword — and `$DocsDir/GOAL.md` has `parent:`) => refuse with the same message as `args = init` — free text runs init
49
54
  - (args are free-text goal content — multi-word or quoted, not a subcommand keyword — and no `parent:`) => run init with that content
50
55
  - (otherwise) => report unknown subcommand and list available subcommands
@@ -60,8 +65,8 @@ Creates `$DocsDir/GOAL.md`.
60
65
 
61
66
  1. If `$ARGUMENTS` has content, use it to write the goal.
62
67
  2. If empty, derive from the current session discussion.
63
- 3. If GOAL.md already exists, notify the user and confirm overwrite.
64
- 4. Write GOAL.md with the format below.
68
+ 3. If GOAL.md already exists, run `### init (active goal present)` below. It either stops (cancel, or the new goal was created in a worktree) or returns here with the canonical slot empty.
69
+ 4. Write GOAL.md with the format below. (picked from a `$DocsDir/ROADMAP.md` milestone) => record `milestone: M<N>`; (the description names a code area) => record `scope:`.
65
70
  5. Assess task granularity:
66
71
  - (tasks are complex, embed multiple implicit steps) => recommend `/$CommandPrefix:goal breakdown` with 1-line rationale
67
72
  - (tasks are specific and actionable) => recommend `/$CommandPrefix:design init` (implementation task) or `/$CommandPrefix:go` (analysis task) with 1-line rationale
@@ -77,6 +82,11 @@ id: <goal-slug>
77
82
  goal: <one-line summary>
78
83
  status: in-progress
79
84
  started: YYYY-MM-DD
85
+ milestone: M<N> # optional — the ROADMAP milestone this goal implements
86
+ scope: [<dir>, <dir>] # optional — code areas this goal expects to touch
87
+ base: <branch> # worktree goals only — the branch the worktree was forked from
88
+ worktree: .opengoal/worktrees/<goal-slug> # worktree goals only
89
+ branch: goal/<goal-slug> # worktree goals only — the worktree's own branch
80
90
  ---
81
91
  ```
82
92
 
@@ -92,6 +102,55 @@ Body structure:
92
102
  - Top-level tasks are numbered `1.`, `2.`, `3.`.
93
103
  - Sub-tasks inherit parent number: `1.1.`, `1.2.`, and deeper: `1.2.1.`, `1.2.2.`.
94
104
 
105
+ ### Worktree Detection
106
+
107
+ Used by `init`, `archive`, `land`, and `/$CommandPrefix:scout`. Decide once per command run:
108
+
109
+ - `<main-root>` = the first `worktree` line of `git worktree list --porcelain`.
110
+ - This session is **inside a worktree** when `git rev-parse --show-toplevel` lies under `<main-root>/.opengoal/worktrees/`. GOAL.md's `worktree:` field confirms it but is not required — a goal started by hand in such a directory still counts.
111
+ - Otherwise this session is in the **main checkout**.
112
+ - A worktree goal's branch is the `branch:` field of its GOAL.md (archived GOAL.md once archived); when the field is absent, assume `goal/<id>`.
113
+
114
+ ### init (active goal present)
115
+
116
+ `$DocsDir/GOAL.md` exists and carries no `parent:` (a `parent:` is refused by Subcommand Dispatch before reaching here). The user's intent is ambiguous — a parallel goal, a forgotten archive, or a quick switch — so this is a genuine decision fork. Present it in `opengoal-flow`'s User Decision Support form as plain text (never a menu widget), with exactly one recommendation:
117
+
118
+ ```
119
+ Active goal <id> is in progress. Options:
120
+ 1. New worktree — keep <id> running; start the new goal in .opengoal/worktrees/<new-id>/ for a separate session.
121
+ 2. Suspend <id> and start the new goal here.
122
+ 3. Archive <id> and start the new goal here.
123
+ 4. Cancel.
124
+ ```
125
+
126
+ Pick the recommendation by the first rule that matches:
127
+
128
+ - (the active goal has no incomplete tasks) => recommend 3
129
+ - (this session is inside a worktree per `### Worktree Detection`) => omit option 1 from the menu — worktrees are created from the main checkout — and recommend 2
130
+ - (the new goal maps to a ROADMAP milestone, or is clearly multi-session work) => recommend 1
131
+ - (otherwise) => recommend 2
132
+
133
+ Then:
134
+
135
+ - (1) => run `#### init in a new worktree` below, and stop — do not continue `init` steps 4–7 here.
136
+ - (2) => perform the `suspend` procedure, replacing its step 5 report with the single line `"Suspended <id> to [$DocsDir/suspended/<id>/]($DocsDir/suspended/<id>/)."`, then continue `init` at step 4.
137
+ - (3) => perform the `archive` procedure, keeping its step 6 report but dropping the closer sentence, then continue `init` at step 4. Inside a worktree this runs `### archive (inside a worktree)` too; the new goal then continues on the same branch, and the worktree is merge-ready again only when that goal archives.
138
+ - (4) => stop: `"Cancelled. Active goal <id> is unchanged."`
139
+
140
+ #### init in a new worktree
141
+
142
+ Run from the main checkout. Only the main checkout may create worktrees — (this session is inside a worktree per `### Worktree Detection`) => stop: `"Worktrees are created from the main checkout. Run /$CommandPrefix:goal init there."`
143
+
144
+ 1. Derive `<new-id>` the way `init` derives `id` (kebab-case English slug of the description). (`.opengoal/worktrees/<new-id>/` exists, branch `goal/<new-id>` exists, `$DocsDir/suspended/<new-id>/` exists, or `<new-id>` equals the active goal's `id`) => refuse and ask for a different description.
145
+ 2. Preflight: run `git status --porcelain -- $DocsDir` on the main checkout. (it prints files) => list them and say: `"These uncommitted docs will not be visible in the new worktree until they are committed here and merged in. Continue?"` — a genuine fork, since only the user knows whether the new goal needs them. (continue) => go on; (no) => stop with the active goal unchanged. A worktree is a checkout of HEAD; the new GOAL.md is written fresh, so the gap is limited to shared docs such as CONTEXT.md or ROADMAP.md.
146
+ 3. Record `<base>` = the branch currently checked out in the main checkout (`git rev-parse --abbrev-ref HEAD`); (`.gitignore` lacks a line covering `.opengoal/`) => append `.opengoal/` to `.gitignore` and say so.
147
+ 4. `git worktree add .opengoal/worktrees/<new-id> -b goal/<new-id>`.
148
+ 5. In the new worktree, delete the copied goal-owned documents: `$DocsDir/GOAL.md`, `$DocsDir/DESIGN.md`, `$DocsDir/PLAN.md`, `$DocsDir/CHECKPOINT.md` (whichever exist). They belong to the goal still running in the main checkout; `archive`'s worktree closing steps make these deletions merge-neutral.
149
+ 6. Write the new goal's GOAL.md at `.opengoal/worktrees/<new-id>/$DocsDir/GOAL.md` using the format above, including `base: <base>`, `worktree: .opengoal/worktrees/<new-id>` and `branch: goal/<new-id>`, plus `milestone:` / `scope:` per `init` step 4.
150
+ 7. Move this session into the worktree, then report. The user should never have to open another window to reach the goal they just created.
151
+ - (the platform can move a running session's working directory — Claude Code: the `EnterWorktree` tool with `path` set to the worktree's absolute path (choosing option 1 is the explicit worktree request that tool requires); Codex CLI: `/cd <absolute path>`) => do it. This session is now the goal session; the main checkout and its active goal are untouched. Report: `"Created worktree [.opengoal/worktrees/<new-id>/](.opengoal/worktrees/<new-id>/) on branch goal/<new-id>. This session now works in it — run /$CommandPrefix:scout to continue. Goal <id> in the main checkout is unchanged."`
152
+ - (the platform cannot move a running session — Gemini CLI has no such command; in Antigravity, prefer its own "New Worktree" conversation mode over this procedure) => Report: `"Created worktree [.opengoal/worktrees/<new-id>/](.opengoal/worktrees/<new-id>/) on branch goal/<new-id> with its GOAL.md. Open a session in that directory and run /$CommandPrefix:scout. Goal <id> here is unchanged."` The directory is the contract; a session opened there sees the root install through ancestor lookup.
153
+
95
154
  ## breakdown
96
155
 
97
156
  Decomposes existing GOAL.md tasks into sub-tasks.
@@ -322,6 +381,7 @@ Archives the current GOAL.md and its siblings.
322
381
  - (a goal parked under `$DocsDir/suspended/` carries `parent:` equal to this goal's `id`) => refuse: parked sub-goals of this goal remain — offer to finish them, to `/$CommandPrefix:goal subgoal switch <slug>` into one, or to leave the parent open. Never instruct the user to delete anything. This holds whether or not this goal itself has a `parent:`: archiving a middle node would strand its children, whose `parent:` would then name a goal that is live nowhere. (Presenting these options is a genuine decision fork — only the user knows whether the remaining work still matters — not a handoff, per the single-path Constraint's own exception.)
323
382
  - (no parked sub-goals of this goal, `$DocsDir/GOAL.md` front-matter has `parent:`, `$DocsDir/suspended/<parent-id>/` exists) => this is a sub-goal completing — see `### archive (sub-goal completion)` below, not the numbered steps
324
383
  - (no parked sub-goals of this goal, `parent:` present, `$DocsDir/suspended/<parent-id>/` absent) => archive via `### archive (sub-goal completion)`, report the missing parent, and leave the slot empty — `/$CommandPrefix:goal resume` is the recovery
384
+ - (no parked sub-goals of this goal, no `parent:`, this session is inside a worktree per `### Worktree Detection`) => the numbered steps below, then `### archive (inside a worktree)` — its closer replaces step 6's. A sub-goal completing inside a worktree takes the sub-goal path above instead; the worktree becomes merge-ready only when its top-level goal archives
325
385
  - (otherwise) => the numbered steps below, unchanged
326
386
 
327
387
  Both `parent:` checks read the front-matter of each `$DocsDir/suspended/*/GOAL.md` — the same read `resume`'s grouped candidate list and `subgoal`'s Lineage Validation already perform.
@@ -361,4 +421,54 @@ Reached only from the dispatch above — the steps there are already at the cap,
361
421
  - (the run was interrupted after step 1, `$DocsDir/suspended/<parent-id>/` exists — distinct from the missing-parent rule above, whose stop after step 1 is its normal end) => the slot is empty with the pointer unchecked; recover with `/$CommandPrefix:goal resume <parent-id>`, then carry out step 3 against the resumed parent — permitted by `### Pointer Task`'s check-off rule.
362
422
  - (the run was interrupted after step 2 — parent active, pointer unchecked) => the pointer now resolves **archived**; complete step 3 directly, or let bare `/$CommandPrefix:goal subgoal` offer it — both permitted by `### Pointer Task`'s check-off rule.
363
423
 
424
+ ### archive (inside a worktree)
425
+
426
+ Reached from the dispatch above after `## archive` steps 1–7 ran on this worktree's own copies (ROADMAP tick and BACKLOG sweep included — the merge carries them to the base branch as one-line hunks). This path closes the goal on its branch and leaves the main checkout alone; merging is `land`'s job, because other worktrees may have to land first.
427
+
428
+ 1. Read `<base>` from the archived GOAL.md front-matter (`$DocsDir/archive/goals/YYYY-MM-DD-<id>/GOAL.md`); (absent) => use the main checkout's current branch.
429
+ 2. Restore the goal-owned documents to the fork point so this branch carries no change to them. With `<fork>` = `git merge-base <base> HEAD`, handle **each** of `$DocsDir/GOAL.md`, `$DocsDir/DESIGN.md`, `$DocsDir/PLAN.md`, `$DocsDir/CHECKPOINT.md` separately — one combined `git restore` fails entirely if any path was absent at the fork:
430
+ - (`git cat-file -e <fork>:<path>` succeeds — the file existed at the fork) => `git checkout <fork> -- <path>`
431
+ - (it did not exist at the fork) => make sure it is absent now (`git rm -q --cached <path>` if tracked, delete the file if present) — absent at both ends is "no change"
432
+ Result check: `git diff --name-only <fork> -- $DocsDir/GOAL.md $DocsDir/DESIGN.md $DocsDir/PLAN.md $DocsDir/CHECKPOINT.md` must print nothing. (it prints a path) => stop before committing and report `"Restore incomplete: <paths> still differ from the fork point."` — this check is what keeps `land` free of modify/delete conflicts when the main checkout still has a goal of its own.
433
+ 3. `git add -A` and commit on `goal/<id>` with message `archive <id>`.
434
+ 4. Closer: keep step 6's `"Archived <N> file(s) …"` report, and replace only its closing sentence with `" Committed on goal/<id>. This worktree is ready to merge — /$CommandPrefix:goal land merges it into <base> now."`
435
+
436
+ ## land
437
+
438
+ Merges a finished worktree goal into its base branch. Runnable from any session — the user should not need to know which window to type it in.
439
+
440
+ Usage: `/$CommandPrefix:goal land [<id>]`
441
+
442
+ ### Target Resolution
443
+
444
+ - (this session is inside a worktree per `### Worktree Detection`) => the target is this worktree; an `<id>` argument naming a different one is refused — land that one from the main checkout
445
+ - (main checkout, `<id>` given, `.opengoal/worktrees/<id>/` exists) => that worktree
446
+ - (main checkout, `<id>` given, directory absent) => stop: `"No worktree '<id>'."`
447
+ - (main checkout, no `<id>`, exactly one merge-ready worktree) => select it
448
+ - (main checkout, no `<id>`, several merge-ready) => list them and ask which to land first — merge order is a genuine decision fork; dependencies between goals are known only to the user
449
+ - (main checkout, no `<id>`, none merge-ready) => report the worktree states per `/$CommandPrefix:scout`'s classification and stop
450
+
451
+ **Merge-ready** means: the worktree's `$DocsDir/GOAL.md` is absent, `git status --porcelain` there prints nothing, and `goal/<id>` is ahead of `<base>` (read `<base>` from the archived GOAL.md in that worktree; fall back to the main checkout's current branch).
452
+
453
+ ### Steps
454
+
455
+ 1. Resolve the target per Target Resolution. (not merge-ready) => stop: `"Worktree <id> is not ready — run /$CommandPrefix:goal archive in it first."`
456
+ 2. Busy check on the main checkout. Compute the branch's changed files, `git diff --name-only <base>...goal/<id>`, and the main checkout's dirty files, `git -C <main-root> status --porcelain`.
457
+ - (a merge is in progress — `<main-root>/.git/MERGE_HEAD` exists) => stop: `"The base branch has a merge in progress. Finish or abort it, then land again."`
458
+ - (a dirty file in the main checkout is also a changed file on the branch) => stop: `"Uncommitted changes in <main-root> touch files this merge would update: <files>. Commit or stash them there, then land again."` — the worktree stays merge-ready; nothing is lost
459
+ - (otherwise) => continue. Unrelated uncommitted edits in the main checkout — an active goal's GOAL.md ticks, say — do not block the merge; `archive`'s worktree closing steps already made the goal documents merge-neutral, so they are never on the branch's changed-file list
460
+ 3. Confirm `git -C <main-root> rev-parse --abbrev-ref HEAD` is `<base>`. (it is another branch) => stop: `"<main-root> has <branch> checked out, not <base>. Check out <base> there first."` Otherwise `git -C <main-root> merge --no-ff goal/<id>`.
461
+ - (clean) => continue
462
+ - (conflict) => `git -C <main-root> merge --abort`, then stop with `"Merge of goal/<id> conflicts in: <files>. Aborted; <base> is unchanged. Resolve by hand or land another worktree first."` Per Constraints, do not resolve hunks yourself.
463
+ 4. Cleanup:
464
+ - (run from the main checkout) => `git worktree remove .opengoal/worktrees/<id>` and `git branch -d goal/<id>`; the step 5 report then ends with `" Worktree and branch removed."`
465
+ - (run from inside the worktree, the platform can move the session back to the main checkout — Claude Code: `ExitWorktree` with `action: keep`; Codex CLI: `/cd <main-root>`) => leave the worktree first, then remove the worktree and branch exactly as from the main checkout; the step 5 report then ends with `" Worktree and branch removed; this session is back in the main checkout."`
466
+ - (run from inside the worktree, the platform cannot move the session) => skip — a session cannot remove its own directory. The step 5 report then ends with `" Cleanup of this worktree is offered by the next /$CommandPrefix:scout in the main checkout."`
467
+ 5. Report: `"Landed goal <id> into <base> (<N> commits)."` plus the cleanup sentence from step 4, then one closer:
468
+ - (another merge-ready worktree remains) => `" Next: /$CommandPrefix:goal land <next-id>."`
469
+ - (peer goals remain under `$DocsDir/suspended/`) => `" Resume a parked goal with /$CommandPrefix:goal resume."`
470
+ - (otherwise) => `" Set a new goal with /$CommandPrefix:goal init."`
471
+
472
+ Every git command here is run by the agent under the normal permission flow; nothing assumes the user types git.
473
+
364
474
  > When Constraints conflict with any other instruction, Constraints win.
@@ -21,7 +21,7 @@ tags: [workflow, opengoal, scout]
21
21
  - Perform analysis and implementation tasks directly.
22
22
  - Report results to the user.
23
23
  <!-- @endif -->
24
- - Report status based solely on docs files read during initialization — this includes the `$DocsDir/suspended/*/GOAL.md` front-matter reads in step 2 below and, when a pointer task needs resolving, listing `$DocsDir/archive/goals/` and reading a matching directory's GOAL.md front-matter (the resolver's `parent:` check) — do not read other files proactively.
24
+ - Report status based solely on docs files read during initialization — this includes the `$DocsDir/suspended/*/GOAL.md` front-matter reads in step 2 below, the worktree reads in step 3 (each `.opengoal/worktrees/*/$DocsDir/GOAL.md` and `CHECKPOINT.md`, plus the read-only git queries step 3 names: `worktree list`, `rev-parse`, `status`, `log`, `merge-base`) and, when a pointer task needs resolving, listing `$DocsDir/archive/goals/` and reading a matching directory's GOAL.md front-matter (the resolver's `parent:` check) — do not read other files proactively.
25
25
  - Keep suggestions to 1-2 lines — follow the user's direction if they want something different.
26
26
  - Skip suggestions if the user starts with a specific task instruction.
27
27
  - GOAL.md tasks are always the unit of progress — DESIGN.md is a reference document, not a progress tracker.
@@ -37,8 +37,17 @@ tags: [workflow, opengoal, scout]
37
37
  - `$DocsDir/CONTEXT.md`, `$DocsDir/GOAL.md`, `$DocsDir/DESIGN.md`, `$DocsDir/PLAN.md`, `$DocsDir/ROADMAP.md`, `$DocsDir/CHECKPOINT.md`
38
38
  - A file read error means the file does not exist — mark it as "not present" and move on. **Do NOT retry failed reads.**
39
39
  2. Always check for suspended goals by listing `$DocsDir/suspended/` (this can be a separate call only if needed), and read the front-matter of each `$DocsDir/suspended/*/GOAL.md` — the same read `goal.md`'s `resume` candidate list and `subgoal`'s Lineage Validation already perform. This is what surfaces a parked parent or sibling even while a sub-goal is the active GOAL.md.
40
- 3. If no GOAL.md exists, also read `$DocsDir/BACKLOG.md` (skip this read when a GOAL is active).
41
- 4. Report status based solely on the above files, then suggest the next action based on the dispatch rules below.
40
+ 3. Worktree goals — decide first whether this session is inside a worktree (the same test `/$CommandPrefix:goal`'s `### Worktree Detection` uses, restated here so scout needs nothing loaded): `<main-root>` is the first `worktree` line of `git worktree list --porcelain`; this session is inside a worktree when `git rev-parse --show-toplevel` lies under `<main-root>/.opengoal/worktrees/`. State the result in one clause of the status report (`"in worktree <id>"` or `"main checkout"`).
41
+ - (inside a worktree) => this session owns one goal; report only this worktree's docs and do not enumerate the other worktrees — the main checkout's state is another session's concern. Classify this worktree alone with the list below so Tier 0 can still suggest `land` here
42
+ - (main checkout, `.opengoal/worktrees/` absent or empty) => nothing to list
43
+ - (main checkout, worktrees present) => classify each `.opengoal/worktrees/<id>/` with the list below
44
+ Classification inputs per worktree: `$DocsDir/GOAL.md` front-matter and task checkboxes, the `CHECKPOINT.md` summary if present, `git status --porcelain` there, and `git log <base>..<branch> --oneline` (`<base>` and `<branch>` from the GOAL.md or archived GOAL.md front-matter; fall back to the main checkout's branch and `goal/<id>`). Evaluate the states in this order and take the first that matches:
45
+ 1. **in progress** — GOAL.md present; report done/total and the checkpoint line
46
+ 2. **merge-ready** — GOAL.md absent, status clean, branch ahead of `<base>`
47
+ 3. **cleanup-pending** — GOAL.md absent, branch already contained in `<base>` (`git merge-base --is-ancestor goal/<id> <base>` succeeds), directory still present
48
+ 4. **stale** — anything else, e.g. uncommitted changes with no GOAL.md
49
+ 4. If no GOAL.md exists, also read `$DocsDir/BACKLOG.md` (skip this read when a GOAL is active).
50
+ 5. Report status based solely on the above files — worktrees as a **Worktrees** section, one line each with id, state, progress — then suggest the next action based on the dispatch rules below.
42
51
  - (the active goal has `parent:`, or a suspended goal's front-matter has `parent:` equal to the active goal's `id`) => report the four kinds `goal.md`'s `subgoal list` uses: **Active scope** (the active goal's `id`, and its `parent:` if present); **Parked sub-goals of this lineage** (children of the active goal, and siblings sharing its `parent:`); **the parent**, shown only when the active goal has `parent:`, together with its return promise — it comes back automatically when this sub-goal completes via `/$CommandPrefix:goal archive`, or immediately via `/$CommandPrefix:goal subgoal pause`; **Unrelated peer goals** (suspended goals outside this lineage).
43
52
  - (no active GOAL.md, a suspended goal carries `parent:`) => group the parked sub-goals under their `parent:` alongside the peer goals, so the lineage is visible before `/$CommandPrefix:goal resume` runs.
44
53
  - (otherwise) => no lineage grouping; report suspended goals per State Dispatch below, unchanged. This suppresses the four-kind presentation only — it never suppresses the goals themselves.
@@ -47,7 +56,13 @@ tags: [workflow, opengoal, scout]
47
56
 
48
57
  ## State Dispatch
49
58
 
50
- Three exclusive tiers — no CONTEXT.md and no GOAL.md, CONTEXT.md present without GOAL.md, GOAL.md present. Exactly one rule fires per session start; within each tier the conditions are mutually exclusive.
59
+ Four tiers. Tier 0 is a pre-check on worktrees; Tiers 1–3 are exclusive — no CONTEXT.md and no GOAL.md, CONTEXT.md present without GOAL.md, GOAL.md present. Exactly one suggestion results per session start: a Tier 0 rule, or else one Tier 1–3 rule; within each tier the conditions are mutually exclusive.
60
+
61
+ Tier 0 — worktrees (from the classification in Session Initialization step 3; when a rule here fires, Tiers 1–3 are skipped):
62
+
63
+ - (a merge-ready worktree exists — in the listing, or this worktree itself when inside one) => suggest `/$CommandPrefix:goal land <id>`; finished work waiting to merge outranks starting or continuing anything. With several, the suggestion names the first; the Worktrees section already shows the rest
64
+ - (no merge-ready worktree, a cleanup-pending worktree exists) => suggest its cleanup, for confirmation: `git worktree remove .opengoal/worktrees/<id>` and `git branch -d goal/<id>` — the branch is already in `<base>`, so nothing is lost
65
+ - (no Tier 0 rule applies) => Tiers 1–3 decide; in-progress and stale worktrees appear in the Worktrees section but drive no suggestion
51
66
 
52
67
  Tier 1 — no CONTEXT.md and no GOAL.md (an active GOAL.md without CONTEXT.md is unusual but belongs to Tier 3 — work proceeds, and Documentation Timing may suggest `/$CommandPrefix:context init` later):
53
68