diff --git a/.changeset/restore-handoff-grounding.md b/.changeset/restore-handoff-grounding.md new file mode 100644 index 00000000..50f9c104 --- /dev/null +++ b/.changeset/restore-handoff-grounding.md @@ -0,0 +1,5 @@ +--- +"@design-intelligence/ghost": minor +--- + +Add `ghost pull --with-cover` to restore the cover with the same selected guidance after a session handoff. diff --git a/packages/ghost/src/commands/pull-command.ts b/packages/ghost/src/commands/pull-command.ts index 2bd25c5d..743247cb 100644 --- a/packages/ghost/src/commands/pull-command.ts +++ b/packages/ghost/src/commands/pull-command.ts @@ -28,6 +28,10 @@ export function registerPullCommand(cli: CAC): void { "--no-materials", "Emit material locators only; do not inline files", ) + .option( + "--with-cover", + "Also pull the package's resolved cover without reopening selection", + ) .option("--order ", "Output order: steering or given", { default: "steering", }) @@ -49,9 +53,18 @@ export function registerPullCommand(cli: CAC): void { const paths = resolveGhostPackage(opts.package, process.cwd()); const snapshot = await loadGhostSnapshot(paths); + const pullIds = + opts.withCover && snapshot.cover.state === "resolved" + ? [snapshot.cover.id, ...ids] + : ids; + if (opts.withCover && snapshot.cover.state !== "resolved") { + console.error( + `Warning: --with-cover added nothing because the package cover is ${snapshot.cover.state}.`, + ); + } const repoRoot = await resolveGitRoot(process.cwd()); const result = await pullGhostNodes(snapshot, { - ids, + ids: pullIds, repoRoot, inlineMaterials: opts.materials !== false, order, diff --git a/packages/ghost/src/skill-bundle/references/ground.md b/packages/ghost/src/skill-bundle/references/ground.md index 44bd016e..6cc71c94 100644 --- a/packages/ghost/src/skill-bundle/references/ground.md +++ b/packages/ghost/src/skill-bundle/references/ground.md @@ -26,8 +26,12 @@ rewriting it into a brief or checklist. Inspect any material the output tells you to inspect before generating; [making.md](making.md) covers unavailable or external material. -`ghost pull` records the pulled ids. After compaction or a session handoff, -re-run it with the same ids to restore the guidance. +`ghost pull` records the pulled ids. Before compaction or a session handoff, +preserve those ids. Restore the same grounding with +`ghost pull --with-cover `; this reloads the resolved cover and +selected guidance without exposing the menu or reopening selection. If +`gather` supplied a no-guidance instruction that is not in the cover body, +preserve that instruction too. Then make the requested artifact. When the output includes a matching starting structure, begin from it verbatim and fill it with task content. diff --git a/packages/ghost/test/cli.test.ts b/packages/ghost/test/cli.test.ts index 86ab30ee..7d10459c 100644 --- a/packages/ghost/test/cli.test.ts +++ b/packages/ghost/test/cli.test.ts @@ -744,6 +744,51 @@ describe("ghost CLI", () => { ); }); + it("pull --with-cover restores the cover without exposing the menu", async () => { + await runCli(["init"], dir); + + const pull = await runCli( + ["pull", "foundation.layout", "--with-cover"], + dir, + ); + + expect(pull.code).toBe(0); + expect(pull.stdout).toContain("# `brand`"); + expect(pull.stdout).toContain("# `foundation.layout`"); + expect(pull.stdout.indexOf("# `brand`")).toBeLessThan( + pull.stdout.indexOf("# `foundation.layout`"), + ); + expect(pull.stdout).not.toContain("## Available guidance"); + + const events = (await readFile(join(dir, ".ghost", ".events"), "utf-8")) + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + expect(events.at(-1)).toMatchObject({ + event: "pull", + ids: ["brand", "foundation.layout"], + }); + }); + + it("pull --with-cover reports when no cover resolves", async () => { + await writeBareTestPackage(dir); + await writeFile( + join(dir, ".ghost", "manifest.yml"), + "schema: ghost.package/v1\nid: local\n", + ); + + const pull = await runCli( + ["pull", "standard.model-defaults", "--with-cover"], + dir, + ); + + expect(pull.code).toBe(0); + expect(pull.stdout).toContain("# `standard.model-defaults`"); + expect(pull.stderr).toContain( + "--with-cover added nothing because the package cover is absent", + ); + }); + it("keeps glossary kind purposes in JSON and only headings in Markdown", async () => { await runCli(["init"], dir); @@ -1745,6 +1790,16 @@ describe("ghost CLI", () => { await expect( readFile(join(dir, "skills", "ghost", "SKILL.md"), "utf-8"), ).resolves.toContain("When the package is silent"); + const installedGround = await readFile( + join(dir, "skills", "ghost", "references", "ground.md"), + "utf-8", + ); + expect(installedGround).toContain("ghost pull --with-cover "); + expect(installedGround).toContain( + "without exposing the menu or reopening selection", + ); + expect(installedGround).toContain("preserve that instruction too"); + expect(installedGround).not.toContain("ghost gather "); const collision = await runCli( ["skill", "install", "--dest", "skills/ghost"],