@entelligentsia/forgecli 0.10.1 → 0.11.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.
Files changed (161) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +21 -3
  3. package/dist/CHANGELOG-forge-plugin.md +22 -0
  4. package/dist/extensions/forgecli/add-pipeline.d.ts +19 -0
  5. package/dist/extensions/forgecli/add-pipeline.js +143 -0
  6. package/dist/extensions/forgecli/add-pipeline.js.map +1 -0
  7. package/dist/extensions/forgecli/add-task.d.ts +20 -0
  8. package/dist/extensions/forgecli/add-task.js +154 -0
  9. package/dist/extensions/forgecli/add-task.js.map +1 -0
  10. package/dist/extensions/forgecli/calibrate.d.ts +61 -0
  11. package/dist/extensions/forgecli/calibrate.js +488 -0
  12. package/dist/extensions/forgecli/calibrate.js.map +1 -0
  13. package/dist/extensions/forgecli/fix-bug.d.ts +9 -1
  14. package/dist/extensions/forgecli/fix-bug.js +70 -8
  15. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  16. package/dist/extensions/forgecli/forge-commands.js +15 -22
  17. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  18. package/dist/extensions/forgecli/forge-subagent.js +34 -7
  19. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  20. package/dist/extensions/forgecli/forge-update-command.d.ts +9 -0
  21. package/dist/extensions/forgecli/forge-update-command.js +106 -7
  22. package/dist/extensions/forgecli/forge-update-command.js.map +1 -1
  23. package/dist/extensions/forgecli/health-check.d.ts +22 -1
  24. package/dist/extensions/forgecli/health-check.js +177 -4
  25. package/dist/extensions/forgecli/health-check.js.map +1 -1
  26. package/dist/extensions/forgecli/hook-dispatcher.d.ts +25 -1
  27. package/dist/extensions/forgecli/hook-dispatcher.js +104 -9
  28. package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
  29. package/dist/extensions/forgecli/hooks/check-update.d.ts +81 -0
  30. package/dist/extensions/forgecli/hooks/check-update.js +308 -0
  31. package/dist/extensions/forgecli/hooks/check-update.js.map +1 -0
  32. package/dist/extensions/forgecli/hooks/forge-permissions.d.ts +32 -0
  33. package/dist/extensions/forgecli/hooks/forge-permissions.js +119 -0
  34. package/dist/extensions/forgecli/hooks/forge-permissions.js.map +1 -0
  35. package/dist/extensions/forgecli/hooks/triage-error.d.ts +23 -0
  36. package/dist/extensions/forgecli/hooks/triage-error.js +62 -0
  37. package/dist/extensions/forgecli/hooks/triage-error.js.map +1 -0
  38. package/dist/extensions/forgecli/hooks/write-guard.d.ts +28 -0
  39. package/dist/extensions/forgecli/hooks/write-guard.js +225 -0
  40. package/dist/extensions/forgecli/hooks/write-guard.js.map +1 -0
  41. package/dist/extensions/forgecli/index.js +60 -0
  42. package/dist/extensions/forgecli/index.js.map +1 -1
  43. package/dist/extensions/forgecli/init-context.d.ts +1 -1
  44. package/dist/extensions/forgecli/init-context.js +21 -6
  45. package/dist/extensions/forgecli/init-context.js.map +1 -1
  46. package/dist/extensions/forgecli/materialize.d.ts +16 -0
  47. package/dist/extensions/forgecli/materialize.js +195 -0
  48. package/dist/extensions/forgecli/materialize.js.map +1 -0
  49. package/dist/extensions/forgecli/migrate.d.ts +19 -0
  50. package/dist/extensions/forgecli/migrate.js +258 -0
  51. package/dist/extensions/forgecli/migrate.js.map +1 -0
  52. package/dist/extensions/forgecli/migration-engine.d.ts +111 -0
  53. package/dist/extensions/forgecli/migration-engine.js +533 -0
  54. package/dist/extensions/forgecli/migration-engine.js.map +1 -0
  55. package/dist/extensions/forgecli/quiz-agent.d.ts +17 -0
  56. package/dist/extensions/forgecli/quiz-agent.js +98 -0
  57. package/dist/extensions/forgecli/quiz-agent.js.map +1 -0
  58. package/dist/extensions/forgecli/remove-command.d.ts +17 -0
  59. package/dist/extensions/forgecli/remove-command.js +124 -0
  60. package/dist/extensions/forgecli/remove-command.js.map +1 -0
  61. package/dist/extensions/forgecli/report-bug.d.ts +25 -0
  62. package/dist/extensions/forgecli/report-bug.js +159 -0
  63. package/dist/extensions/forgecli/report-bug.js.map +1 -0
  64. package/dist/extensions/forgecli/retrospective.d.ts +19 -0
  65. package/dist/extensions/forgecli/retrospective.js +156 -0
  66. package/dist/extensions/forgecli/retrospective.js.map +1 -0
  67. package/dist/extensions/forgecli/run-sprint.js +34 -0
  68. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  69. package/dist/extensions/forgecli/run-task.d.ts +9 -1
  70. package/dist/extensions/forgecli/run-task.js +64 -10
  71. package/dist/extensions/forgecli/run-task.js.map +1 -1
  72. package/dist/extensions/forgecli/session-registry.d.ts +27 -2
  73. package/dist/extensions/forgecli/session-registry.js +52 -1
  74. package/dist/extensions/forgecli/session-registry.js.map +1 -1
  75. package/dist/extensions/forgecli/status-command.d.ts +19 -0
  76. package/dist/extensions/forgecli/status-command.js +140 -0
  77. package/dist/extensions/forgecli/status-command.js.map +1 -0
  78. package/dist/extensions/forgecli/store-query.d.ts +22 -0
  79. package/dist/extensions/forgecli/store-query.js +107 -0
  80. package/dist/extensions/forgecli/store-query.js.map +1 -0
  81. package/dist/extensions/forgecli/store-repair.d.ts +17 -0
  82. package/dist/extensions/forgecli/store-repair.js +123 -0
  83. package/dist/extensions/forgecli/store-repair.js.map +1 -0
  84. package/dist/extensions/forgecli/thread-switcher.js +213 -28
  85. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  86. package/dist/extensions/forgecli/update-tools.d.ts +23 -0
  87. package/dist/extensions/forgecli/update-tools.js +136 -0
  88. package/dist/extensions/forgecli/update-tools.js.map +1 -0
  89. package/dist/extensions/forgecli/viewport-theme.js +4 -0
  90. package/dist/extensions/forgecli/viewport-theme.js.map +1 -1
  91. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  92. package/dist/forge-payload/.schemas/config.schema.json +83 -0
  93. package/dist/forge-payload/.schemas/migrations.json +2049 -0
  94. package/dist/forge-payload/commands/regenerate.md +17 -1
  95. package/dist/forge-payload/meta/personas/README.md +16 -0
  96. package/dist/forge-payload/meta/personas/meta-architect.md +70 -0
  97. package/dist/forge-payload/meta/personas/meta-bug-fixer.md +73 -0
  98. package/dist/forge-payload/meta/personas/meta-collator.md +72 -0
  99. package/dist/forge-payload/meta/personas/meta-engineer.md +70 -0
  100. package/dist/forge-payload/meta/personas/meta-orchestrator.md +71 -0
  101. package/dist/forge-payload/meta/personas/meta-product-manager.md +82 -0
  102. package/dist/forge-payload/meta/personas/meta-qa-engineer.md +91 -0
  103. package/dist/forge-payload/meta/personas/meta-supervisor.md +92 -0
  104. package/dist/forge-payload/meta/skill-recommendations.md +154 -0
  105. package/dist/forge-payload/meta/skills/meta-architect-skills.md +43 -0
  106. package/dist/forge-payload/meta/skills/meta-bug-fixer-skills.md +43 -0
  107. package/dist/forge-payload/meta/skills/meta-collator-skills.md +41 -0
  108. package/dist/forge-payload/meta/skills/meta-engineer-skills.md +43 -0
  109. package/dist/forge-payload/meta/skills/meta-generic-skills.md +58 -0
  110. package/dist/forge-payload/meta/skills/meta-qa-engineer-skills.md +46 -0
  111. package/dist/forge-payload/meta/skills/meta-supervisor-skills.md +43 -0
  112. package/dist/forge-payload/meta/store-schema/bug.schema.md +71 -0
  113. package/dist/forge-payload/meta/store-schema/event.schema.md +76 -0
  114. package/dist/forge-payload/meta/store-schema/feature.schema.md +65 -0
  115. package/dist/forge-payload/meta/store-schema/sprint.schema.md +64 -0
  116. package/dist/forge-payload/meta/store-schema/task.schema.md +78 -0
  117. package/dist/forge-payload/meta/templates/meta-code-review.md +26 -0
  118. package/dist/forge-payload/meta/templates/meta-plan-review.md +28 -0
  119. package/dist/forge-payload/meta/templates/meta-plan.md +28 -0
  120. package/dist/forge-payload/meta/templates/meta-progress.md +25 -0
  121. package/dist/forge-payload/meta/templates/meta-retrospective.md +28 -0
  122. package/dist/forge-payload/meta/templates/meta-sprint-manifest.md +26 -0
  123. package/dist/forge-payload/meta/templates/meta-sprint-requirements.md +91 -0
  124. package/dist/forge-payload/meta/templates/meta-task-prompt.md +26 -0
  125. package/dist/forge-payload/meta/tool-specs/collate.spec.md +88 -0
  126. package/dist/forge-payload/meta/tool-specs/generation-manifest.spec.md +139 -0
  127. package/dist/forge-payload/meta/tool-specs/manage-config.spec.md +143 -0
  128. package/dist/forge-payload/meta/tool-specs/seed-store.spec.md +91 -0
  129. package/dist/forge-payload/meta/tool-specs/store-cli.spec.md +328 -0
  130. package/dist/forge-payload/meta/tool-specs/validate-store.spec.md +191 -0
  131. package/dist/forge-payload/meta/workflows/_fragments/context-injection.md +75 -0
  132. package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +73 -0
  133. package/dist/forge-payload/meta/workflows/_fragments/finalize.md +13 -0
  134. package/dist/forge-payload/meta/workflows/_fragments/friction-emit.md +73 -0
  135. package/dist/forge-payload/meta/workflows/_fragments/progress-reporting.md +38 -0
  136. package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +39 -0
  137. package/dist/forge-payload/meta/workflows/meta-approve.md +119 -0
  138. package/dist/forge-payload/meta/workflows/meta-collate.md +89 -0
  139. package/dist/forge-payload/meta/workflows/meta-commit.md +93 -0
  140. package/dist/forge-payload/meta/workflows/meta-enhance.md +286 -0
  141. package/dist/forge-payload/meta/workflows/meta-fix-bug.md +501 -0
  142. package/dist/forge-payload/meta/workflows/meta-implement.md +132 -0
  143. package/dist/forge-payload/meta/workflows/meta-migrate.md +455 -0
  144. package/dist/forge-payload/meta/workflows/meta-orchestrate.md +993 -0
  145. package/dist/forge-payload/meta/workflows/meta-plan-task.md +133 -0
  146. package/dist/forge-payload/meta/workflows/meta-quiz-agent.md +135 -0
  147. package/dist/forge-payload/meta/workflows/meta-retrospective.md +65 -0
  148. package/dist/forge-payload/meta/workflows/meta-review-implementation.md +119 -0
  149. package/dist/forge-payload/meta/workflows/meta-review-plan.md +108 -0
  150. package/dist/forge-payload/meta/workflows/meta-review-sprint-completion.md +65 -0
  151. package/dist/forge-payload/meta/workflows/meta-sprint-intake.md +76 -0
  152. package/dist/forge-payload/meta/workflows/meta-sprint-plan.md +147 -0
  153. package/dist/forge-payload/meta/workflows/meta-update-implementation.md +76 -0
  154. package/dist/forge-payload/meta/workflows/meta-update-plan.md +76 -0
  155. package/dist/forge-payload/meta/workflows/meta-validate.md +111 -0
  156. package/dist/forge-payload/tools/check-structure.cjs +344 -0
  157. package/dist/forge-payload/tools/list-skills.js +76 -0
  158. package/dist/forge-payload/tools/store-cli.cjs +27 -1
  159. package/dist/forge-payload/tools/substitute-placeholders.cjs +60 -8
  160. package/dist/forge-payload/tools/verify-integrity.cjs +86 -0
  161. package/package.json +2 -2
@@ -0,0 +1,107 @@
1
+ // store-query.ts — native atomic handler for /forge:store-query (FORGE-S23-T10)
2
+ //
3
+ // Dispatches to store-cli query (flags path) or store-cli nlp (intent path)
4
+ // and emits raw stdout via ctx.ui.notify.
5
+ //
6
+ // Archetype: Atomic (no LLM in loop — pack-04 §Atomic).
7
+ // Iron Laws:
8
+ // IL1 — new code lives in forge-cli/ only
9
+ // IL6 — no shell-string interpolation (argv-array throughout)
10
+ // IL7 — silent continuation past failures is never acceptable
11
+ import { execFile } from "node:child_process";
12
+ import * as path from "node:path";
13
+ import { promisify } from "node:util";
14
+ import { resolveToolDir } from "./store-resolver.js";
15
+ const execFileAsync = promisify(execFile);
16
+ // ── Usage message ─────────────────────────────────────────────────────────────
17
+ const USAGE = `Usage: /forge:store-query <intent or flags>
18
+
19
+ Examples:
20
+ /forge:store-query open bugs in S12
21
+ /forge:store-query FORGE-BUG-047
22
+ /forge:store-query --sprint FORGE-S12 --status in-progress
23
+ /forge:store-query --keyword auth
24
+ /forge:store-query schema`;
25
+ /**
26
+ * Run store-cli in query or nlp mode.
27
+ * - Args starting with "--" → query branch: `store-cli query ...flags`
28
+ * - Otherwise → nlp branch: `store-cli nlp "<args>"`
29
+ *
30
+ * Returns raw stdout for display. Does NOT JSON-parse — caller renders as-is.
31
+ * Exported so orchestrators can call directly without going through sendUserMessage.
32
+ */
33
+ export async function runStoreQuery(args, forgeRoot, cwd) {
34
+ const trimmed = args.trim();
35
+ const toolDir = resolveToolDir(forgeRoot);
36
+ const storeCliPath = path.join(toolDir, "store-cli.cjs");
37
+ let argv;
38
+ let timeoutMs;
39
+ if (trimmed.startsWith("--")) {
40
+ // Query branch: pass flags verbatim as argv tokens
41
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
42
+ argv = ["query", ...tokens];
43
+ timeoutMs = 10_000;
44
+ }
45
+ else {
46
+ // NLP branch: pass entire trimmed arg string as single argument
47
+ argv = ["nlp", trimmed];
48
+ timeoutMs = 30_000;
49
+ }
50
+ try {
51
+ const result = await execFileAsync("node", [storeCliPath, ...argv], {
52
+ cwd,
53
+ encoding: "utf8",
54
+ timeout: timeoutMs,
55
+ });
56
+ return { stdout: result.stdout, ok: true };
57
+ }
58
+ catch (err) {
59
+ const e = err;
60
+ // execFile rejects on non-zero exit; stdout may still contain partial results
61
+ const stderr = e.stderr || e.message || String(err);
62
+ const stdout = e.stdout || "";
63
+ return {
64
+ stdout,
65
+ ok: false,
66
+ errorMessage: stderr.slice(0, 500),
67
+ };
68
+ }
69
+ }
70
+ // ── Register ─────────────────────────────────────────────────────────────────
71
+ /**
72
+ * Register /forge:store-query.
73
+ * forgeRoot is nullable — null means we're outside a Forge project.
74
+ */
75
+ export function registerStoreQuery(pi, opts) {
76
+ pi.registerCommand("forge:store-query", {
77
+ description: "Query the Forge store by natural language or exact flags. " +
78
+ "Use flags (--sprint, --task, --keyword) for exact queries; " +
79
+ "use plain text for NLP intent queries.",
80
+ async handler(args, ctx) {
81
+ const cwd = process.cwd();
82
+ // Outside-project guard
83
+ if (!opts.forgeRoot) {
84
+ ctx.ui.notify("× forge:store-query — not inside a Forge project. Run /forge:init first.", "error");
85
+ return;
86
+ }
87
+ const trimmed = args.trim();
88
+ if (!trimmed) {
89
+ ctx.ui.notify(USAGE, "info");
90
+ return;
91
+ }
92
+ ctx.ui.setStatus?.("forge:store-query", "Querying store…");
93
+ const result = await runStoreQuery(trimmed, opts.forgeRoot, cwd);
94
+ ctx.ui.setStatus?.("forge:store-query", undefined);
95
+ if (!result.ok && result.errorMessage) {
96
+ ctx.ui.notify(`× forge:store-query — ${result.errorMessage}`, "error");
97
+ // Still show partial stdout if available
98
+ if (result.stdout.trim()) {
99
+ ctx.ui.notify(result.stdout.trim(), "info");
100
+ }
101
+ return;
102
+ }
103
+ ctx.ui.notify(result.stdout.trim() || "(no results)", "info");
104
+ },
105
+ });
106
+ }
107
+ //# sourceMappingURL=store-query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-query.js","sourceRoot":"","sources":["../../../src/extensions/forgecli/store-query.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export interface ParsedStoreRepairArgs {
3
+ dryRun: boolean;
4
+ seed: string;
5
+ sourceLabel: string;
6
+ }
7
+ export declare function parseStoreRepairArgs(rawArgs: string, cwd: string): ParsedStoreRepairArgs;
8
+ export interface ComposeStoreRepairKickoffOpts {
9
+ commandMd: string;
10
+ parsed: ParsedStoreRepairArgs;
11
+ }
12
+ export declare function composeStoreRepairKickoff(opts: ComposeStoreRepairKickoffOpts): string;
13
+ export interface RegisterStoreRepairOptions {
14
+ forgeRoot: string | null;
15
+ cwd?: string;
16
+ }
17
+ export declare function registerStoreRepair(pi: ExtensionAPI, options: RegisterStoreRepairOptions): void;
@@ -0,0 +1,123 @@
1
+ // forge:store-repair — native kickoff handler (FORGE-S23-T11).
2
+ //
3
+ // Replaces the auto-generated stub previously installed by
4
+ // registerAllForgeCommands (forge-commands.ts). Kickoff Shim archetype
5
+ // (Pack-04 + Pack-06): single LLM handoff in current context, no fork.
6
+ //
7
+ // --dry-run flag is forwarded in the kickoff body so the LLM calls
8
+ // validate-store --dry-run and skips write phases accordingly.
9
+ //
10
+ // Workflow source: <forgeRoot>/commands/store-repair.md (command file).
11
+ // File-existence check only — command files lack Pack-06 workflow markers.
12
+ //
13
+ // Iron Laws:
14
+ // IL1 — code only under forge-cli/src/extensions/forgecli/.
15
+ // IL4 — no JSON.stringify-into-subagent dispatch.
16
+ // IL6 — no shell-string interpolation; no spawn calls here.
17
+ // IL7 — every failure path emits ctx.ui.notify and returns; no silent
18
+ // continuation.
19
+ import * as fs from "node:fs";
20
+ import * as path from "node:path";
21
+ import { sendKickoff } from "./kickoff.js";
22
+ export function parseStoreRepairArgs(rawArgs, cwd) {
23
+ const trimmed = (rawArgs ?? "").trim();
24
+ const result = {
25
+ dryRun: false,
26
+ seed: "",
27
+ sourceLabel: "",
28
+ };
29
+ if (!trimmed) {
30
+ result.sourceLabel = "(no args — full repair run)";
31
+ return result;
32
+ }
33
+ const tokens = trimmed.split(/\s+/);
34
+ const remaining = [];
35
+ for (const tok of tokens) {
36
+ if (tok === "--dry-run") {
37
+ result.dryRun = true;
38
+ }
39
+ else {
40
+ remaining.push(tok);
41
+ }
42
+ }
43
+ const tail = remaining.join(" ").trim();
44
+ if (tail.startsWith("@")) {
45
+ const ref = tail.slice(1).trim();
46
+ const filePath = path.isAbsolute(ref) ? ref : path.resolve(cwd, ref);
47
+ result.seed = fs.readFileSync(filePath, "utf8");
48
+ result.sourceLabel = `(seed from file: ${ref})`;
49
+ }
50
+ else if (tail) {
51
+ result.seed = tail;
52
+ result.sourceLabel = "(seed from inline text)";
53
+ }
54
+ else {
55
+ result.sourceLabel = result.dryRun ? "(--dry-run mode)" : "(no args — full repair run)";
56
+ }
57
+ return result;
58
+ }
59
+ export function composeStoreRepairKickoff(opts) {
60
+ const { commandMd, parsed } = opts;
61
+ const sections = ["# /forge:store-repair", ""];
62
+ sections.push("## Dispatch", "");
63
+ sections.push("Diagnose and repair the Forge JSON store. Run the repair workflow below. Specifically:");
64
+ sections.push("");
65
+ if (parsed.dryRun) {
66
+ sections.push("**`--dry-run` mode.** Preview all repairs without writing anything. " +
67
+ "In Phase 2 (LLM repair) and Phase 3 (final validation), show what would change but skip all writes. " +
68
+ "Skip Phase 4 verification (nothing changed).");
69
+ }
70
+ else {
71
+ sections.push("Full repair mode: diagnose, propose fixes, apply approved repairs, verify.");
72
+ }
73
+ sections.push("", "Use `forge_store` for all store reads and writes (never raw file writes to `.forge/store/`).");
74
+ sections.push("", "---", "", "## Command", "", commandMd.trim(), "", "---");
75
+ if (parsed.seed) {
76
+ sections.push("", `## Additional Context — ${parsed.sourceLabel}`, "", parsed.seed.trim());
77
+ }
78
+ return sections.join("\n");
79
+ }
80
+ // ── Registration ──────────────────────────────────────────────────────────
81
+ const COMMAND_NAME = "store-repair";
82
+ export function registerStoreRepair(pi, options) {
83
+ pi.registerCommand("forge:store-repair", {
84
+ description: "Diagnose and repair corrupted Forge store records. " +
85
+ "Usage: /forge:store-repair [--dry-run]. " +
86
+ "--dry-run shows what would be repaired without making changes.",
87
+ async handler(args, ctx) {
88
+ const { forgeRoot } = options;
89
+ const cwd = options.cwd ?? process.cwd();
90
+ if (!forgeRoot) {
91
+ ctx.ui.notify("× forge:store-repair — no Forge project at cwd; run /forge:init to bootstrap", "warning");
92
+ return;
93
+ }
94
+ const commandPath = path.join(forgeRoot, "commands", `${COMMAND_NAME}.md`);
95
+ let commandMd;
96
+ try {
97
+ commandMd = fs.readFileSync(commandPath, "utf8");
98
+ }
99
+ catch (err) {
100
+ const e = err;
101
+ if (e.code === "ENOENT") {
102
+ ctx.ui.notify(`× forge:store-repair — command file not found at commands/${COMMAND_NAME}.md; run /forge:init or /forge:regenerate first.`, "error");
103
+ }
104
+ else {
105
+ ctx.ui.notify(`× forge:store-repair — failed to read command file: ${e.message ?? "unknown error"}`, "error");
106
+ }
107
+ return;
108
+ }
109
+ let parsed;
110
+ try {
111
+ parsed = parseStoreRepairArgs(args, cwd);
112
+ }
113
+ catch (err) {
114
+ const e = err;
115
+ ctx.ui.notify(`× forge:store-repair — failed to parse args: ${e.message ?? "unknown"}`, "error");
116
+ return;
117
+ }
118
+ const kickoff = composeStoreRepairKickoff({ commandMd, parsed });
119
+ sendKickoff(pi, kickoff);
120
+ },
121
+ });
122
+ }
123
+ //# sourceMappingURL=store-repair.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-repair.js","sourceRoot":"","sources":["../../../src/extensions/forgecli/store-repair.ts"],"names":[],"mappings":"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"}
@@ -130,11 +130,16 @@ class ViewportFooterComponent {
130
130
  };
131
131
  }
132
132
  render(width) {
133
+ // Only show the footer when an orchestrator session is active.
134
+ // When all sessions are terminal (completed/failed/cancelled) or
135
+ // when no session exists, hide the footer — main viewport has no
136
+ // subagent aggregate to display.
137
+ const sessions = this.registry.listSessions();
138
+ const hasActive = sessions.some((s) => s.status === "running" || s.status === "cancelling");
139
+ if (!hasActive)
140
+ return [];
133
141
  const tokens = fmtTokenFooter(this.registry.getAggregateUsage());
134
142
  const orchModel = fmtModelLabel(this.getOrchestratorModel?.());
135
- // Cold-start: hide the row entirely (no model + no usage) so the
136
- // editor area doesn't carry a permanent blank line. As soon as we
137
- // know the orchestrator model OR have observed any usage, render.
138
143
  if (!tokens && !orchModel)
139
144
  return [];
140
145
  const left = orchModel ? `⌂ ${orchModel}` : "";
@@ -160,6 +165,9 @@ class ChipStripComponent {
160
165
  focusedChipId = "main";
161
166
  stripActive = false;
162
167
  invalidationCb;
168
+ /** When non-null, the strip shows a cancellation confirmation prompt
169
+ * for this chip instead of the normal chip view. */
170
+ cancelTarget = null;
163
171
  constructor(registry, theme) {
164
172
  this.registry = registry;
165
173
  this.theme = theme;
@@ -230,11 +238,20 @@ class ChipStripComponent {
230
238
  chipGlyph(chip) {
231
239
  if (chip.id === "main")
232
240
  return this.focusedChipId === "main" ? "●" : "○";
241
+ const session = this.activeSession();
233
242
  const p = this.chipPhase(chip);
234
243
  if (!p)
235
244
  return "·";
236
245
  if (this.focusedChipId === chip.id)
237
246
  return "●";
247
+ // Cancelling/cancelled glyphs are session-level, not phase-level.
248
+ // Show ⏳ for any phase when the session is cancelling, ⊘ when cancelled.
249
+ if (session?.status === "cancelled" && p.status !== "completed" && p.status !== "failed")
250
+ return "⊘";
251
+ if (session?.status === "cancelling" && p.status === "running")
252
+ return "⏳";
253
+ if (p.status === "cancelled")
254
+ return "⊘";
238
255
  if (p.status === "completed")
239
256
  return "✓";
240
257
  if (p.status === "failed")
@@ -253,7 +270,7 @@ class ChipStripComponent {
253
270
  return session.phases[session.phases.length - 1]?.role;
254
271
  }
255
272
  spinnerFrame(session) {
256
- if (session.status !== "running")
273
+ if (session.status !== "running" && session.status !== "cancelling")
257
274
  return "";
258
275
  const idx = Math.floor(Date.now() / SPINNER_INTERVAL_MS) % SPINNER_FRAMES.length;
259
276
  return SPINNER_FRAMES[idx];
@@ -262,6 +279,10 @@ class ChipStripComponent {
262
279
  const session = this.activeSession();
263
280
  if (!session)
264
281
  return []; // UX-B: hide entirely when no session.
282
+ // If user is confirming cancellation, show the confirmation prompt.
283
+ if (this.cancelTarget) {
284
+ return this.renderCancelPrompt(width, this.cancelTarget);
285
+ }
265
286
  const chips = this.chips();
266
287
  // Clamp cursor.
267
288
  if (this.cursorIdx >= chips.length)
@@ -273,29 +294,47 @@ class ChipStripComponent {
273
294
  renderInactive(width, session) {
274
295
  const dim = (s) => this.theme.fg("dim", s);
275
296
  const accent = (s) => this.theme.fg("accent", s);
276
- const spin = this.spinnerFrame(session);
277
297
  const chips = this.chips();
278
- // Orchestrator chip stays bracketed + accent-colored (anchor identity).
279
- // Subagent chips render after it as `<glyph> <role>`, dimmed —
280
- // completed phases show ✓, failed ✗, live ◇ (or ◆ with unread).
281
- // This makes phase progression visible at a glance without expanding
282
- // the strip.
298
+ const spin = this.spinnerFrame(session);
299
+ // Orchestrator chip: bracketed + accent-colored (anchor identity).
300
+ // Subagent chips: <glyph> <role>, dimmed.
283
301
  const orchChip = accent(`[${session.taskId}]`);
284
302
  const phaseChips = chips
285
303
  .filter((c) => c.id !== "main")
286
304
  .map((c) => dim(`${this.chipGlyph(c)} ${c.label}`));
287
305
  const chipsLine = [orchChip, ...phaseChips].join(" ");
288
- const prefix = dim("threads ");
289
- const hint = dim(" ↓ to navigate");
306
+ // Right-side: status · spinner · command hints.
307
+ // Cancelled sessions show "r resume" affordance; all others show "↓ to navigate".
308
+ const statusLabel = session.status === "cancelled" ? "cancelled"
309
+ : session.status === "cancelling" ? "cancelling…"
310
+ : "";
311
+ const statusPart = statusLabel ? dim(` ${statusLabel}`) : "";
290
312
  const spinPart = spin ? ` ${spin}` : "";
291
- const previewText = session.currentTurnPreview ? ` "${session.currentTurnPreview}"` : "";
292
- const fixedWidth = visibleWidth(prefix) +
293
- visibleWidth(chipsLine) +
294
- visibleWidth(spinPart) +
295
- visibleWidth(hint);
296
- const previewBudget = Math.max(0, width - fixedWidth);
297
- const preview = previewText ? dim(truncateToWidth(previewText, previewBudget)) : "";
298
- let line = `${prefix}${chipsLine}${spinPart}${preview}${hint}`;
313
+ const hint = session.status === "cancelled"
314
+ ? dim(" ↓ nav · r resume")
315
+ : dim(" ↓ to navigate");
316
+ // Truncate preview text from the MIDDLE of the line to keep chips and hints visible.
317
+ const previewText = session.currentTurnPreview ? `"${session.currentTurnPreview}"` : "";
318
+ // Priority: chips + status + spinner + hint are fixed.
319
+ // Truncate preview first, then truncate from the right as fallback.
320
+ const fixedRight = visibleWidth(statusPart) + visibleWidth(spinPart) + visibleWidth(hint);
321
+ const previewBudget = Math.max(0, width - fixedRight - 4); // 4 = safety margin
322
+ let preview = "";
323
+ if (previewText) {
324
+ // Truncate the preview text itself to fit the budget
325
+ const truncated = truncateToWidth(previewText, previewBudget);
326
+ if (visibleWidth(truncated) > 0)
327
+ preview = dim(` ${truncated}`);
328
+ }
329
+ // Build line; truncate from the right (preview tail) if still over-width.
330
+ let line = `${chipsLine}${statusPart}${spinPart}${preview}${hint}`;
331
+ if (visibleWidth(line) > width) {
332
+ // Truncate preview tail first (not chips)
333
+ const budget = Math.max(0, width - visibleWidth(chipsLine) - fixedRight);
334
+ const previewOnly = truncateToWidth(previewText, budget);
335
+ preview = previewOnly ? dim(` ${previewOnly}`) : "";
336
+ line = `${chipsLine}${statusPart}${spinPart}${preview}${hint}`;
337
+ }
299
338
  if (visibleWidth(line) > width)
300
339
  line = truncateToWidth(line, width);
301
340
  return [line];
@@ -315,8 +354,18 @@ class ChipStripComponent {
315
354
  return bold(inner);
316
355
  return dim(inner);
317
356
  });
318
- const prefix = accent("threads ─ ");
319
- const hint = dim(" ←→ · enter · back · esc back+main");
357
+ const prefix = "";
358
+ // "r resume" shown only for cancelled sessions; "x cancel" for running ones.
359
+ const cancelWord = session.status === "cancelled" ? dim("r resume") : dim("x cancel");
360
+ const navHints = dim(" ←→ · enter · ↑ back · esc back+main");
361
+ // Show status-based text for non-running sessions
362
+ let statusPart = "";
363
+ if (session.status === "cancelling") {
364
+ statusPart = " cancelling…";
365
+ }
366
+ else if (session.status === "cancelled") {
367
+ statusPart = " cancelled";
368
+ }
320
369
  const spin = this.spinnerFrame(session);
321
370
  const spinPart = spin ? ` ${spin}` : "";
322
371
  const previewText = session.currentTurnPreview ? ` "${session.currentTurnPreview}"` : "";
@@ -325,15 +374,44 @@ class ChipStripComponent {
325
374
  const fixed = visibleWidth(prefix) +
326
375
  visibleWidth(chipsJoined) +
327
376
  visibleWidth(spinPart) +
328
- visibleWidth(hint);
377
+ visibleWidth(statusPart) +
378
+ visibleWidth(cancelWord) +
379
+ visibleWidth(navHints);
329
380
  const previewBudget = Math.max(0, width - fixed);
330
381
  const preview = previewText ? dim(truncateToWidth(previewText, previewBudget)) : "";
331
- let line = `${prefix}${chipsJoined}${spinPart}${preview}${hint}`;
382
+ let line = `${prefix}${chipsJoined}${spinPart}${statusPart}${preview} ${cancelWord} ${navHints}`;
332
383
  // Hard cap as last-resort defence (visibleWidth is best-effort).
333
384
  if (visibleWidth(line) > width)
334
385
  line = truncateToWidth(line, width);
335
386
  return [line];
336
387
  }
388
+ /**
389
+ * Render the cancellation confirmation prompt. Replaces the normal
390
+ * chip strip when cancelTarget is non-null.
391
+ * ⚠ Cancel [taskId] → [phaseRole]? y/n · esc to abort
392
+ */
393
+ renderCancelPrompt(width, target) {
394
+ const dim = (s) => this.theme.fg("dim", s);
395
+ const warning = (s) => this.theme.fg("warning", s);
396
+ const bold = (s) => this.theme.bold(s);
397
+ const taskLabel = target.taskId ?? target.label;
398
+ const phaseLabel = target.id === "main" ? "session" : target.label;
399
+ // "cancel" sits right after the prompt — most visible position.
400
+ // Truncation sacrifices the dim dismiss-hints from the END, keeping
401
+ // the action word and the warning always readable.
402
+ const actionWord = dim("cancel");
403
+ const hints = dim(" · n/esc dismiss · y confirm");
404
+ const prompt = warning(`⚠ Cancel ${bold(taskLabel)} → ${bold(phaseLabel)}? `);
405
+ const budget = Math.max(0, width - visibleWidth(prompt) - visibleWidth(actionWord));
406
+ const tail = budget > 0 ? dim(` · n/esc dismiss · y confirm`) : "";
407
+ const budgetedTail = visibleWidth(tail) > budget
408
+ ? dim(truncateToWidth(` · n/esc dismiss · y confirm`, budget))
409
+ : tail;
410
+ let line = `${prompt}${actionWord}${budgetedTail}`;
411
+ if (visibleWidth(line) > width)
412
+ line = truncateToWidth(line, width);
413
+ return [line];
414
+ }
337
415
  invalidate() {
338
416
  // Re-render driven by external invalidationCb → tui.requestRender().
339
417
  }
@@ -345,6 +423,47 @@ class ChipStripComponent {
345
423
  this.stripActive = active;
346
424
  this.invalidationCb?.();
347
425
  }
426
+ /** Initiate cancel confirmation for a chip. Sets cancelTarget so the
427
+ * next render shows the confirmation prompt. */
428
+ requestCancelChip(chip) {
429
+ this.cancelTarget = chip;
430
+ this.invalidationCb?.();
431
+ }
432
+ /** Confirm the pending cancellation (user pressed y). */
433
+ confirmCancel() {
434
+ const target = this.cancelTarget;
435
+ this.cancelTarget = null;
436
+ this.invalidationCb?.();
437
+ return target;
438
+ }
439
+ /** Dismiss the cancel prompt (user pressed n/Esc). */
440
+ dismissCancel() {
441
+ this.cancelTarget = null;
442
+ this.invalidationCb?.();
443
+ }
444
+ /** Whether a cancel confirmation prompt is active. */
445
+ isCancelPromptActive() {
446
+ return this.cancelTarget !== null;
447
+ }
448
+ /** Check if the chip at the current cursor is a running phase that can be cancelled. */
449
+ isCursorCancellable() {
450
+ const chip = this.cursorChip();
451
+ if (!chip)
452
+ return false;
453
+ if (chip.id === "main") {
454
+ const session = this.activeSession();
455
+ return (session?.status ?? "") === "running";
456
+ }
457
+ const p = this.chipPhase(chip);
458
+ if (!p)
459
+ return false;
460
+ return p.status === "running";
461
+ }
462
+ /** True when the current session is cancelled — r key triggers resume. */
463
+ isCursorResumable() {
464
+ const session = this.activeSession();
465
+ return session?.status === "cancelled";
466
+ }
348
467
  getStripActive() {
349
468
  return this.stripActive;
350
469
  }
@@ -411,6 +530,18 @@ function isEsc(d) {
411
530
  // by the arrow checks above first.
412
531
  return d === "\x1b";
413
532
  }
533
+ function isXKey(d) {
534
+ return d === "x";
535
+ }
536
+ function isRKey(d) {
537
+ return d === "r" || d === "R";
538
+ }
539
+ function isYKey(d) {
540
+ return d === "y" || d === "Y";
541
+ }
542
+ function isNKey(d) {
543
+ return d === "n" || d === "N";
544
+ }
414
545
  // ── Registrar ───────────────────────────────────────────────────────────────
415
546
  /**
416
547
  * Custom renderer for the `forge:turn` chat-history rows we append on every
@@ -459,14 +590,14 @@ export function registerThreadSwitcher(pi) {
459
590
  // session_start and on every forge:threads command invocation.
460
591
  let currentCtx;
461
592
  function ensureSpinnerTimer() {
462
- // Tick re-renders while any session is "running" so the spinner
463
- // glyph animates and the preview text refreshes between user input.
593
+ // Tick re-renders while any session is "running" or "cancelling" so the
594
+ // spinner glyph animates and the preview text refreshes between user input.
464
595
  // When all sessions are terminal, the timer stops itself.
465
596
  if (spinnerTimer)
466
597
  return;
467
598
  spinnerTimer = setInterval(() => {
468
- const anyRunning = registry.listSessions().some((s) => s.status === "running");
469
- if (!anyRunning) {
599
+ const anyActive = registry.listSessions().some((s) => s.status === "running" || s.status === "cancelling");
600
+ if (!anyActive) {
470
601
  if (spinnerTimer)
471
602
  clearInterval(spinnerTimer);
472
603
  spinnerTimer = undefined;
@@ -569,6 +700,60 @@ export function registerThreadSwitcher(pi) {
569
700
  stripRef.parkCursorOnCurrentPhase();
570
701
  return { consume: true };
571
702
  }
703
+ // ── Cancel-confirmation handling (cancelTarget active) ────────
704
+ // When the strip shows a cancel prompt, y/Enter confirms,
705
+ // n/Esc dismisses. All other keys are consumed (no passthrough).
706
+ if (stripRef.isCancelPromptActive()) {
707
+ if (isYKey(data) || isEnter(data)) {
708
+ const target = stripRef.confirmCancel();
709
+ if (target?.taskId) {
710
+ registry.requestCancel(target.taskId);
711
+ }
712
+ stripRef.setStripActive(false);
713
+ setFocusToMain(live);
714
+ return { consume: true };
715
+ }
716
+ // Dismiss: n, Esc
717
+ if (isNKey(data) || isEsc(data)) {
718
+ stripRef.dismissCancel();
719
+ stripRef.setStripActive(false);
720
+ return { consume: true };
721
+ }
722
+ // Any other key in cancel-confirmation mode is consumed silently.
723
+ return { consume: true };
724
+ }
725
+ if (isXKey(data)) {
726
+ const chip = stripRef.cursorChip();
727
+ if (chip && stripRef.isCursorCancellable()) {
728
+ stripRef.requestCancelChip(chip);
729
+ return { consume: true };
730
+ }
731
+ return undefined;
732
+ }
733
+ if (isRKey(data)) {
734
+ // Resume a cancelled session. The state file is preserved on cancel
735
+ // (ADR-S21-01). Write the slash command to the editor and simulate
736
+ // Enter — exactly mirrors how a user types and submits the command.
737
+ const session = registry.listSessions()[0];
738
+ if (session && stripRef.isCursorResumable()) {
739
+ const entityId = session.taskId;
740
+ const cmd = entityId.startsWith("FORGE-BUG-")
741
+ ? `forge:fix-bug ${entityId}`
742
+ : `forge:run-task ${entityId}`;
743
+ stripRef.setStripActive(false);
744
+ try {
745
+ live.ui.setEditorText(`/${cmd}`);
746
+ }
747
+ catch {
748
+ // Non-fatal — editor may not be accessible in all contexts.
749
+ live.ui.notify(`↻ Resume: /${cmd}`, "info");
750
+ }
751
+ // Return Enter to submit the command. The router dispatches
752
+ // normally; pi processes it as a slash-command submit.
753
+ return { data: "\r" };
754
+ }
755
+ return undefined;
756
+ }
572
757
  if (isLeftArrow(data)) {
573
758
  stripRef.moveCursor(-1);
574
759
  return { consume: true };