@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.
- package/CHANGELOG.md +78 -0
- package/README.md +21 -3
- package/dist/CHANGELOG-forge-plugin.md +22 -0
- package/dist/extensions/forgecli/add-pipeline.d.ts +19 -0
- package/dist/extensions/forgecli/add-pipeline.js +143 -0
- package/dist/extensions/forgecli/add-pipeline.js.map +1 -0
- package/dist/extensions/forgecli/add-task.d.ts +20 -0
- package/dist/extensions/forgecli/add-task.js +154 -0
- package/dist/extensions/forgecli/add-task.js.map +1 -0
- package/dist/extensions/forgecli/calibrate.d.ts +61 -0
- package/dist/extensions/forgecli/calibrate.js +488 -0
- package/dist/extensions/forgecli/calibrate.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.d.ts +9 -1
- package/dist/extensions/forgecli/fix-bug.js +70 -8
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-commands.js +15 -22
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.js +34 -7
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/forge-update-command.d.ts +9 -0
- package/dist/extensions/forgecli/forge-update-command.js +106 -7
- package/dist/extensions/forgecli/forge-update-command.js.map +1 -1
- package/dist/extensions/forgecli/health-check.d.ts +22 -1
- package/dist/extensions/forgecli/health-check.js +177 -4
- package/dist/extensions/forgecli/health-check.js.map +1 -1
- package/dist/extensions/forgecli/hook-dispatcher.d.ts +25 -1
- package/dist/extensions/forgecli/hook-dispatcher.js +104 -9
- package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
- package/dist/extensions/forgecli/hooks/check-update.d.ts +81 -0
- package/dist/extensions/forgecli/hooks/check-update.js +308 -0
- package/dist/extensions/forgecli/hooks/check-update.js.map +1 -0
- package/dist/extensions/forgecli/hooks/forge-permissions.d.ts +32 -0
- package/dist/extensions/forgecli/hooks/forge-permissions.js +119 -0
- package/dist/extensions/forgecli/hooks/forge-permissions.js.map +1 -0
- package/dist/extensions/forgecli/hooks/triage-error.d.ts +23 -0
- package/dist/extensions/forgecli/hooks/triage-error.js +62 -0
- package/dist/extensions/forgecli/hooks/triage-error.js.map +1 -0
- package/dist/extensions/forgecli/hooks/write-guard.d.ts +28 -0
- package/dist/extensions/forgecli/hooks/write-guard.js +225 -0
- package/dist/extensions/forgecli/hooks/write-guard.js.map +1 -0
- package/dist/extensions/forgecli/index.js +60 -0
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/init-context.d.ts +1 -1
- package/dist/extensions/forgecli/init-context.js +21 -6
- package/dist/extensions/forgecli/init-context.js.map +1 -1
- package/dist/extensions/forgecli/materialize.d.ts +16 -0
- package/dist/extensions/forgecli/materialize.js +195 -0
- package/dist/extensions/forgecli/materialize.js.map +1 -0
- package/dist/extensions/forgecli/migrate.d.ts +19 -0
- package/dist/extensions/forgecli/migrate.js +258 -0
- package/dist/extensions/forgecli/migrate.js.map +1 -0
- package/dist/extensions/forgecli/migration-engine.d.ts +111 -0
- package/dist/extensions/forgecli/migration-engine.js +533 -0
- package/dist/extensions/forgecli/migration-engine.js.map +1 -0
- package/dist/extensions/forgecli/quiz-agent.d.ts +17 -0
- package/dist/extensions/forgecli/quiz-agent.js +98 -0
- package/dist/extensions/forgecli/quiz-agent.js.map +1 -0
- package/dist/extensions/forgecli/remove-command.d.ts +17 -0
- package/dist/extensions/forgecli/remove-command.js +124 -0
- package/dist/extensions/forgecli/remove-command.js.map +1 -0
- package/dist/extensions/forgecli/report-bug.d.ts +25 -0
- package/dist/extensions/forgecli/report-bug.js +159 -0
- package/dist/extensions/forgecli/report-bug.js.map +1 -0
- package/dist/extensions/forgecli/retrospective.d.ts +19 -0
- package/dist/extensions/forgecli/retrospective.js +156 -0
- package/dist/extensions/forgecli/retrospective.js.map +1 -0
- package/dist/extensions/forgecli/run-sprint.js +34 -0
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.d.ts +9 -1
- package/dist/extensions/forgecli/run-task.js +64 -10
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/session-registry.d.ts +27 -2
- package/dist/extensions/forgecli/session-registry.js +52 -1
- package/dist/extensions/forgecli/session-registry.js.map +1 -1
- package/dist/extensions/forgecli/status-command.d.ts +19 -0
- package/dist/extensions/forgecli/status-command.js +140 -0
- package/dist/extensions/forgecli/status-command.js.map +1 -0
- package/dist/extensions/forgecli/store-query.d.ts +22 -0
- package/dist/extensions/forgecli/store-query.js +107 -0
- package/dist/extensions/forgecli/store-query.js.map +1 -0
- package/dist/extensions/forgecli/store-repair.d.ts +17 -0
- package/dist/extensions/forgecli/store-repair.js +123 -0
- package/dist/extensions/forgecli/store-repair.js.map +1 -0
- package/dist/extensions/forgecli/thread-switcher.js +213 -28
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/update-tools.d.ts +23 -0
- package/dist/extensions/forgecli/update-tools.js +136 -0
- package/dist/extensions/forgecli/update-tools.js.map +1 -0
- package/dist/extensions/forgecli/viewport-theme.js +4 -0
- package/dist/extensions/forgecli/viewport-theme.js.map +1 -1
- package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
- package/dist/forge-payload/.schemas/config.schema.json +83 -0
- package/dist/forge-payload/.schemas/migrations.json +2049 -0
- package/dist/forge-payload/commands/regenerate.md +17 -1
- package/dist/forge-payload/meta/personas/README.md +16 -0
- package/dist/forge-payload/meta/personas/meta-architect.md +70 -0
- package/dist/forge-payload/meta/personas/meta-bug-fixer.md +73 -0
- package/dist/forge-payload/meta/personas/meta-collator.md +72 -0
- package/dist/forge-payload/meta/personas/meta-engineer.md +70 -0
- package/dist/forge-payload/meta/personas/meta-orchestrator.md +71 -0
- package/dist/forge-payload/meta/personas/meta-product-manager.md +82 -0
- package/dist/forge-payload/meta/personas/meta-qa-engineer.md +91 -0
- package/dist/forge-payload/meta/personas/meta-supervisor.md +92 -0
- package/dist/forge-payload/meta/skill-recommendations.md +154 -0
- package/dist/forge-payload/meta/skills/meta-architect-skills.md +43 -0
- package/dist/forge-payload/meta/skills/meta-bug-fixer-skills.md +43 -0
- package/dist/forge-payload/meta/skills/meta-collator-skills.md +41 -0
- package/dist/forge-payload/meta/skills/meta-engineer-skills.md +43 -0
- package/dist/forge-payload/meta/skills/meta-generic-skills.md +58 -0
- package/dist/forge-payload/meta/skills/meta-qa-engineer-skills.md +46 -0
- package/dist/forge-payload/meta/skills/meta-supervisor-skills.md +43 -0
- package/dist/forge-payload/meta/store-schema/bug.schema.md +71 -0
- package/dist/forge-payload/meta/store-schema/event.schema.md +76 -0
- package/dist/forge-payload/meta/store-schema/feature.schema.md +65 -0
- package/dist/forge-payload/meta/store-schema/sprint.schema.md +64 -0
- package/dist/forge-payload/meta/store-schema/task.schema.md +78 -0
- package/dist/forge-payload/meta/templates/meta-code-review.md +26 -0
- package/dist/forge-payload/meta/templates/meta-plan-review.md +28 -0
- package/dist/forge-payload/meta/templates/meta-plan.md +28 -0
- package/dist/forge-payload/meta/templates/meta-progress.md +25 -0
- package/dist/forge-payload/meta/templates/meta-retrospective.md +28 -0
- package/dist/forge-payload/meta/templates/meta-sprint-manifest.md +26 -0
- package/dist/forge-payload/meta/templates/meta-sprint-requirements.md +91 -0
- package/dist/forge-payload/meta/templates/meta-task-prompt.md +26 -0
- package/dist/forge-payload/meta/tool-specs/collate.spec.md +88 -0
- package/dist/forge-payload/meta/tool-specs/generation-manifest.spec.md +139 -0
- package/dist/forge-payload/meta/tool-specs/manage-config.spec.md +143 -0
- package/dist/forge-payload/meta/tool-specs/seed-store.spec.md +91 -0
- package/dist/forge-payload/meta/tool-specs/store-cli.spec.md +328 -0
- package/dist/forge-payload/meta/tool-specs/validate-store.spec.md +191 -0
- package/dist/forge-payload/meta/workflows/_fragments/context-injection.md +75 -0
- package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +73 -0
- package/dist/forge-payload/meta/workflows/_fragments/finalize.md +13 -0
- package/dist/forge-payload/meta/workflows/_fragments/friction-emit.md +73 -0
- package/dist/forge-payload/meta/workflows/_fragments/progress-reporting.md +38 -0
- package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +39 -0
- package/dist/forge-payload/meta/workflows/meta-approve.md +119 -0
- package/dist/forge-payload/meta/workflows/meta-collate.md +89 -0
- package/dist/forge-payload/meta/workflows/meta-commit.md +93 -0
- package/dist/forge-payload/meta/workflows/meta-enhance.md +286 -0
- package/dist/forge-payload/meta/workflows/meta-fix-bug.md +501 -0
- package/dist/forge-payload/meta/workflows/meta-implement.md +132 -0
- package/dist/forge-payload/meta/workflows/meta-migrate.md +455 -0
- package/dist/forge-payload/meta/workflows/meta-orchestrate.md +993 -0
- package/dist/forge-payload/meta/workflows/meta-plan-task.md +133 -0
- package/dist/forge-payload/meta/workflows/meta-quiz-agent.md +135 -0
- package/dist/forge-payload/meta/workflows/meta-retrospective.md +65 -0
- package/dist/forge-payload/meta/workflows/meta-review-implementation.md +119 -0
- package/dist/forge-payload/meta/workflows/meta-review-plan.md +108 -0
- package/dist/forge-payload/meta/workflows/meta-review-sprint-completion.md +65 -0
- package/dist/forge-payload/meta/workflows/meta-sprint-intake.md +76 -0
- package/dist/forge-payload/meta/workflows/meta-sprint-plan.md +147 -0
- package/dist/forge-payload/meta/workflows/meta-update-implementation.md +76 -0
- package/dist/forge-payload/meta/workflows/meta-update-plan.md +76 -0
- package/dist/forge-payload/meta/workflows/meta-validate.md +111 -0
- package/dist/forge-payload/tools/check-structure.cjs +344 -0
- package/dist/forge-payload/tools/list-skills.js +76 -0
- package/dist/forge-payload/tools/store-cli.cjs +27 -1
- package/dist/forge-payload/tools/substitute-placeholders.cjs +60 -8
- package/dist/forge-payload/tools/verify-integrity.cjs +86 -0
- 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
|
-
|
|
279
|
-
//
|
|
280
|
-
//
|
|
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
|
-
|
|
289
|
-
|
|
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
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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 =
|
|
319
|
-
|
|
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(
|
|
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}${
|
|
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
|
|
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
|
|
469
|
-
if (!
|
|
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 };
|