pi-soly 1.15.0 → 1.16.0
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/ask/picker.ts +4 -0
- package/commands.ts +116 -9
- package/nudge.ts +15 -2
- package/package.json +1 -1
- package/workflows/done.ts +7 -4
- package/workflows/new.ts +14 -3
- package/workflows/parser.ts +62 -35
package/ask/picker.ts
CHANGED
|
@@ -498,7 +498,11 @@ export class AskProComponent extends Container {
|
|
|
498
498
|
* string in a multi answer). Returns "" if no custom string. */
|
|
499
499
|
private getCustomString(ans: AskAnswer | AskMultiAnswer | undefined): string {
|
|
500
500
|
if (ans === undefined) return "";
|
|
501
|
+
// Single-pick answers are numbers — not an array, so .find() would crash.
|
|
502
|
+
// Cover the string case (typed Other… in single-pick) and treat numeric
|
|
503
|
+
// answers as "no custom string yet".
|
|
501
504
|
if (typeof ans === "string") return ans;
|
|
505
|
+
if (typeof ans === "number") return "";
|
|
502
506
|
const found = (ans as AskMultiAnswer).find((a) => typeof a === "string");
|
|
503
507
|
return typeof found === "string" ? found : "";
|
|
504
508
|
}
|
package/commands.ts
CHANGED
|
@@ -46,6 +46,12 @@ import type { SolyConfig } from "./config.ts";
|
|
|
46
46
|
import { initSolyProject } from "./init.js";
|
|
47
47
|
import { ListPanel, type ListItem, type ListAction } from "./visual/list-panel.ts";
|
|
48
48
|
import { getArtifactServer, ensureArtifactServer, artifactDir } from "./artifact/session.ts";
|
|
49
|
+
import { parseSolyCommand, type SolyCommand, type WorkflowVerb } from "./workflows/parser.ts";
|
|
50
|
+
import { buildNewTransform } from "./workflows/new.ts";
|
|
51
|
+
import { buildDoneTransform } from "./workflows/done.ts";
|
|
52
|
+
import { buildMigrateTransform } from "./workflows/migrate.ts";
|
|
53
|
+
import { buildPlanTransform, buildDiscussTransform } from "./workflows/planning.ts";
|
|
54
|
+
import { buildExecuteTransform } from "./workflows/execute.ts";
|
|
49
55
|
|
|
50
56
|
/** Minimum ui surface the command handlers actually need. */
|
|
51
57
|
export interface CommandUI {
|
|
@@ -558,6 +564,70 @@ What must the LLM do?
|
|
|
558
564
|
description: string;
|
|
559
565
|
run: (parts: string[]) => void | Promise<void>;
|
|
560
566
|
};
|
|
567
|
+
/** Dispatch a workflow verb from the `/soly` slash-command picker.
|
|
568
|
+
* Same handlers used by the `soly <verb> ...` text-command path —
|
|
569
|
+
* registered here so humans can also drive them via
|
|
570
|
+
* `/soly <verb> ...` without having to type natural language and
|
|
571
|
+
* wait for the LLM to translate. */
|
|
572
|
+
const runWorkflow = async (
|
|
573
|
+
verb: WorkflowVerb,
|
|
574
|
+
parts: string[],
|
|
575
|
+
ctx: ExtensionCommandContext,
|
|
576
|
+
ui: CommandUI,
|
|
577
|
+
): Promise<void> => {
|
|
578
|
+
// parts[0] is the verb name; the args follow.
|
|
579
|
+
const args = parts.slice(1);
|
|
580
|
+
const cmd: SolyCommand = {
|
|
581
|
+
verb,
|
|
582
|
+
args,
|
|
583
|
+
raw: `soly ${verb}${args.length ? " " + args.join(" ") : ""}`,
|
|
584
|
+
};
|
|
585
|
+
const state = getState();
|
|
586
|
+
if (!state.exists) {
|
|
587
|
+
ui.notify("soly: no .agents/ project here — run `/soly init` to scaffold one", "info");
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
switch (verb) {
|
|
591
|
+
case "new": {
|
|
592
|
+
const r = buildNewTransform(cmd, state, ui, ctx.cwd);
|
|
593
|
+
if (r.handled && r.transformedText) ui.notify(r.transformedText, "info");
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
case "done": {
|
|
597
|
+
const r = buildDoneTransform(cmd, state, ui, ctx.cwd);
|
|
598
|
+
if (r.handled && r.transformedText) ui.notify(r.transformedText, "info");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
case "migrate": {
|
|
602
|
+
const r = buildMigrateTransform(state, ui, ctx.cwd);
|
|
603
|
+
if (r.handled && r.transformedText) ui.notify(r.transformedText, "info");
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
case "plan": {
|
|
607
|
+
const r = buildPlanTransform(cmd, state);
|
|
608
|
+
if (r.handled && r.transformedText) ui.notify(r.transformedText, "info");
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
case "discuss": {
|
|
612
|
+
// Slash-command path doesn't have live ask_pro detection.
|
|
613
|
+
// Pass `false` so the discuss transform falls back to its
|
|
614
|
+
// plain-text discussion format; the LLM-driven path still
|
|
615
|
+
// gets the richer ask_pro flow.
|
|
616
|
+
const r = buildDiscussTransform(cmd, state, { hasAskPro: false });
|
|
617
|
+
if (r.handled && r.transformedText) ui.notify(r.transformedText, "info");
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
case "execute": {
|
|
621
|
+
// Slash-command path doesn't have live interactive rules.
|
|
622
|
+
const r = buildExecuteTransform(cmd, state, []);
|
|
623
|
+
if (r.handled && r.transformedText) ui.notify(r.transformedText, "info");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
default:
|
|
627
|
+
ui.notify(`soly: workflow '${verb}' not yet wired into slash command`, "info");
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
};
|
|
561
631
|
const subcommands: Record<string, SolySub> = {
|
|
562
632
|
// `agent` subcommand REMOVED — moved to the separate `pi-switch`
|
|
563
633
|
// extension (rotor switcher removed in 1.4.0).
|
|
@@ -612,17 +682,25 @@ What must the LLM do?
|
|
|
612
682
|
run: () => showFile("STATE.md", getState().stateBody),
|
|
613
683
|
},
|
|
614
684
|
plan: {
|
|
615
|
-
description: "current PLAN.md body",
|
|
616
|
-
run: () => {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
685
|
+
description: "current PLAN.md body, OR `plan <slug>` to flesh out a plan",
|
|
686
|
+
run: (parts) => {
|
|
687
|
+
// Dual-purpose: no args = show current PLAN.md; with args
|
|
688
|
+
// = dispatch to the plan-mode workflow. Same key the
|
|
689
|
+
// state picker already had (state picker items and
|
|
690
|
+
// workflow verbs share the same `/soly` namespace).
|
|
691
|
+
if (parts.length <= 1) {
|
|
692
|
+
const s = getState();
|
|
693
|
+
if (!s.currentPlanPath) {
|
|
694
|
+
ui.notify("soly: no current plan", "error");
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
showFile(
|
|
698
|
+
`PLAN: ${path.basename(s.currentPlanPath)}`,
|
|
699
|
+
readIfExists(s.currentPlanPath),
|
|
700
|
+
);
|
|
620
701
|
return;
|
|
621
702
|
}
|
|
622
|
-
|
|
623
|
-
`PLAN: ${path.basename(s.currentPlanPath)}`,
|
|
624
|
-
readIfExists(s.currentPlanPath),
|
|
625
|
-
);
|
|
703
|
+
void runWorkflow("plan", parts, ctx, ui);
|
|
626
704
|
},
|
|
627
705
|
},
|
|
628
706
|
context: {
|
|
@@ -797,6 +875,35 @@ What must the LLM do?
|
|
|
797
875
|
);
|
|
798
876
|
},
|
|
799
877
|
},
|
|
878
|
+
// ------------------------------------------------------------------
|
|
879
|
+
// Workflow verbs — same handlers used by the `soly <verb> ...`
|
|
880
|
+
// text-command path. Registered here so humans can also drive
|
|
881
|
+
// them via `/soly <verb> ...` from the slash picker without
|
|
882
|
+
// typing natural language and waiting for the LLM to translate.
|
|
883
|
+
// Each verb's buildXxxTransform already calls ui.notify on
|
|
884
|
+
// success, so we just need to construct a fake `SolyCommand`
|
|
885
|
+
// from the slash args and call the handler.
|
|
886
|
+
// ------------------------------------------------------------------
|
|
887
|
+
new: {
|
|
888
|
+
description: "scaffold a new plan: create branch + .agents/plans/<slug>/PLAN.md (e.g. `/soly new statistic-preparation`)",
|
|
889
|
+
run: (parts) => runWorkflow("new", parts, ctx, ui),
|
|
890
|
+
},
|
|
891
|
+
execute: {
|
|
892
|
+
description: "execute a plan via subagent (e.g. `/soly execute statistic-preparation`)",
|
|
893
|
+
run: (parts) => runWorkflow("execute", parts, ctx, ui),
|
|
894
|
+
},
|
|
895
|
+
discuss: {
|
|
896
|
+
description: "interactive discussion of a plan (e.g. `/soly discuss statistic-preparation`)",
|
|
897
|
+
run: (parts) => runWorkflow("discuss", parts, ctx, ui),
|
|
898
|
+
},
|
|
899
|
+
done: {
|
|
900
|
+
description: "commit + push + open draft PR for a plan branch (e.g. `/soly done statistic-preparation`)",
|
|
901
|
+
run: (parts) => runWorkflow("done", parts, ctx, ui),
|
|
902
|
+
},
|
|
903
|
+
migrate: {
|
|
904
|
+
description: "one-shot: import .agents/phases/<NN>-<slug>/plans/PLAN.md as plan branches",
|
|
905
|
+
run: () => runWorkflow("migrate", [], ctx, ui),
|
|
906
|
+
},
|
|
800
907
|
};
|
|
801
908
|
|
|
802
909
|
// Single-width BMP glyphs only — astral/VS16 emoji are mis-measured by
|
package/nudge.ts
CHANGED
|
@@ -142,10 +142,23 @@ export function buildNudgeSection(
|
|
|
142
142
|
: "";
|
|
143
143
|
|
|
144
144
|
// When there's an active soly project and the task is non-trivial, steer the
|
|
145
|
-
// model toward the workflow lifecycle instead of ad-hoc edits.
|
|
145
|
+
// model toward the workflow lifecycle instead of ad-hoc edits. This block
|
|
146
|
+
// also tells the model it may *itself* scaffold a new plan after asking the
|
|
147
|
+
// user — the user explicitly opted into this in 1.16.0.
|
|
146
148
|
const workflowPoint =
|
|
147
149
|
opts.hasProject && heuristics.nonTrivial
|
|
148
|
-
? `\n\n4. **Route project work through the soly workflow.**
|
|
150
|
+
? `\n\n4. **Route project work through the soly plan workflow.** Each plan is a git branch named by a kebab-case slug (\`statistic-preparation\`, \`login-redirect-bug\`, \`stats-rollup\`, …) with \`.agents/plans/<slug>/PLAN.md\` on it. Two parallel plans don't collide.
|
|
151
|
+
|
|
152
|
+
Lifecycle (text commands you type — pi routes them through the workflow handlers):
|
|
153
|
+
- \`soly new <slug>\` — scaffold: branch + plan dir + stub PLAN.md + commit
|
|
154
|
+
- \`soly discuss <slug>\` — interactive discussion of the plan (uses ask_pro)
|
|
155
|
+
- \`soly plan <slug>\` — flesh out PLAN.md via ask_pro
|
|
156
|
+
- \`soly execute <slug>\` — execute the plan in a subagent
|
|
157
|
+
- \`soly done <slug>\` — commit + push + open draft PR via gh
|
|
158
|
+
- \`soly verify\` — self-review loop until clean
|
|
159
|
+
- \`soly status\` — current position + progress (no LLM round-trip)
|
|
160
|
+
|
|
161
|
+
**You may scaffold a new plan yourself** when the user asks for a new piece of work and the scope is clear (or the user just confirmed). Propose the slug first via ask_pro (\"I'll create plan 'statistic-preparation' — continue?\"), then type \`soly new statistic-preparation\` in your next output. Don't ask for confirmation on trivial one-liners or when the user already said "go". Skip only for a genuine one-off that doesn't deserve a plan branch.`
|
|
149
162
|
: "";
|
|
150
163
|
|
|
151
164
|
// Confirm-before-coding gate: for non-trivial implementation, pull the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-soly",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Project management + workflow framework for pi-coding-agent. Plans, state, MANDATORY rules, self-review, multi-question picker, native footer + welcome chrome — one npm install, zero config. The LLM drives execution and delegates to a worker subagent when available.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
package/workflows/done.ts
CHANGED
|
@@ -93,10 +93,10 @@ export function buildDoneTransform(
|
|
|
93
93
|
const raw = cmd.args.join(" ").trim();
|
|
94
94
|
const parsed = parsePlanName(raw);
|
|
95
95
|
if ("error" in parsed) {
|
|
96
|
-
return reply(`soly done: ${parsed.error}\n\nUsage: soly done <
|
|
96
|
+
return reply(`soly done: ${parsed.error}\n\nUsage: soly done <slug>`);
|
|
97
97
|
}
|
|
98
|
-
const {
|
|
99
|
-
const branchName =
|
|
98
|
+
const { name } = parsed;
|
|
99
|
+
const branchName = name;
|
|
100
100
|
|
|
101
101
|
// 1. Preconditions
|
|
102
102
|
const currentBranch = git(["branch", "--show-current"], { cwd: projectRoot });
|
|
@@ -126,7 +126,7 @@ export function buildDoneTransform(
|
|
|
126
126
|
// Re-check after add
|
|
127
127
|
const statusAfter = git(["status", "--short"], { cwd: projectRoot });
|
|
128
128
|
if (statusAfter) {
|
|
129
|
-
git(["commit", "-m", `${
|
|
129
|
+
git(["commit", "-m", `${name}: wip`], { cwd: projectRoot });
|
|
130
130
|
}
|
|
131
131
|
} catch (err) {
|
|
132
132
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -194,6 +194,9 @@ export function buildDoneTransform(
|
|
|
194
194
|
` Commit: ${commitHash.slice(0, 7)}\n` +
|
|
195
195
|
` Pushed: ${pushed ? "yes" : "no (no origin remote)"}\n` +
|
|
196
196
|
` ${prLine}`;
|
|
197
|
+
|
|
198
|
+
// (No registry file to update — the plan's git branch IS the source of
|
|
199
|
+
// truth for "active plans". See PLAN.md / "Out of scope" for the trade-off.)
|
|
197
200
|
ui.notify(notice, "info");
|
|
198
201
|
return {
|
|
199
202
|
handled: true,
|
package/workflows/new.ts
CHANGED
|
@@ -88,8 +88,11 @@ export function buildNewTransform(
|
|
|
88
88
|
const parsed = parsePlanName(cmd.args.join(" "));
|
|
89
89
|
if ("error" in parsed) return reply(`soly new: ${parsed.error}`);
|
|
90
90
|
|
|
91
|
-
const {
|
|
92
|
-
|
|
91
|
+
const { name } = parsed;
|
|
92
|
+
// Branch IS the slug (no <type>/ prefix). Convention chosen over
|
|
93
|
+
// Conventional-Branches because `soly new` users tend to give a full
|
|
94
|
+
// descriptive name already; the type prefix adds noise without info.
|
|
95
|
+
const branchName = name;
|
|
93
96
|
// Path used by `git add`/`commit` — relative to projectRoot (where `.git/` is).
|
|
94
97
|
// The plan lives at `<root>/.agents/plans/<name>/`, so the repo-relative path
|
|
95
98
|
// is `.agents/plans/<name>`.
|
|
@@ -116,7 +119,15 @@ export function buildNewTransform(
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
const currentBranch = git(["branch", "--show-current"], { cwd: projectRoot }) || "HEAD (detached)";
|
|
119
|
-
|
|
122
|
+
// A plan branch is a kebab-case slug (no `<type>/` prefix after 1.15.x).
|
|
123
|
+
// The branch can also be the long-lived integration branches `master`
|
|
124
|
+
// or `main`. Anything else (release tags, weird suffixes) → user must
|
|
125
|
+
// checkout first.
|
|
126
|
+
if (
|
|
127
|
+
currentBranch !== "master" &&
|
|
128
|
+
currentBranch !== "main" &&
|
|
129
|
+
!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(currentBranch)
|
|
130
|
+
) {
|
|
120
131
|
return reply(
|
|
121
132
|
`soly new: currently on "${currentBranch}" (not master/main, not a soly plan branch). ` +
|
|
122
133
|
`Switch back to master first with \`git checkout master\`.`,
|
package/workflows/parser.ts
CHANGED
|
@@ -21,36 +21,54 @@ export type WorkflowVerb =
|
|
|
21
21
|
| "plan" | "discuss" | "help" | "doctor" | "iterations" | "phase" | "todos" | "verify"
|
|
22
22
|
| "new" | "done" | "migrate";
|
|
23
23
|
|
|
24
|
-
/** Allowed Conventional Commits types for `soly new` / `soly plan <type>/<name>` / etc. */
|
|
25
|
-
export const PLAN_TYPES = ["feat", "fix", "chore", "refactor", "docs", "test", "perf", "build", "ci"] as const;
|
|
26
|
-
|
|
27
24
|
/**
|
|
28
|
-
* Validate `<
|
|
25
|
+
* Validate `<slug>` (e.g. `statistic-preparation`). Returns parsed parts or
|
|
29
26
|
* an error message. Pure — no I/O. Shared by `soly new` (workflows/new.ts)
|
|
30
27
|
* and the plan-mode dispatch in `describePlanTarget` / `describeExecuteTarget`.
|
|
28
|
+
*
|
|
29
|
+
* Convention: a plan is a kebab-case slug that doubles as the git branch
|
|
30
|
+
* name. No type prefix — we dropped the Conventional-Branches `<type>/<name>`
|
|
31
|
+
* shape after 1.15.x because the type adds noise without information for
|
|
32
|
+
* `soly new` users (the branch list itself is the registry, and the prefix
|
|
33
|
+
* doesn't help readers pick the right branch).
|
|
34
|
+
*
|
|
35
|
+
* Examples:
|
|
36
|
+
* soly new statistic-preparation
|
|
37
|
+
* soly new login-redirect-bug
|
|
38
|
+
* soly new stats-rollup
|
|
31
39
|
*/
|
|
32
|
-
export function parsePlanName(raw: string): {
|
|
40
|
+
export function parsePlanName(raw: string): { name: string } | { error: string } {
|
|
33
41
|
const trimmed = raw.trim();
|
|
34
42
|
if (!trimmed) return { error: "missing plan name" };
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
// Must contain at least one letter — pure-digit strings are reserved for
|
|
44
|
+
// phase numbers (`soly plan 11` → phase 11), not plan slugs.
|
|
45
|
+
if (!/[a-z]/.test(trimmed)) {
|
|
37
46
|
return {
|
|
38
47
|
error:
|
|
39
48
|
`bad plan name "${trimmed}".\n` +
|
|
40
|
-
`\nExpected: <type
|
|
41
|
-
`
|
|
42
|
-
`
|
|
43
|
-
|
|
49
|
+
`\nExpected: <slug> (kebab-case, no type prefix, must contain a letter)\n` +
|
|
50
|
+
` length: 2-64 chars\n` +
|
|
51
|
+
` chars: lowercase letters, digits, and hyphens\n` +
|
|
52
|
+
` must start and end with a letter or digit\n` +
|
|
53
|
+
`\nExample: soly new statistic-preparation`,
|
|
44
54
|
};
|
|
45
55
|
}
|
|
46
|
-
const
|
|
47
|
-
if (!
|
|
48
|
-
return {
|
|
56
|
+
const m = trimmed.match(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/);
|
|
57
|
+
if (!m) {
|
|
58
|
+
return {
|
|
59
|
+
error:
|
|
60
|
+
`bad plan name "${trimmed}".\n` +
|
|
61
|
+
`\nExpected: <slug> (kebab-case, no type prefix)\n` +
|
|
62
|
+
` length: 2-64 chars\n` +
|
|
63
|
+
` chars: lowercase letters, digits, and hyphens\n` +
|
|
64
|
+
` must start and end with a letter or digit\n` +
|
|
65
|
+
`\nExample: soly new statistic-preparation`,
|
|
66
|
+
};
|
|
49
67
|
}
|
|
50
|
-
if (
|
|
51
|
-
return { error: `name "${
|
|
68
|
+
if (trimmed.length > 64) {
|
|
69
|
+
return { error: `name "${trimmed}" is too long (max 64 chars)` };
|
|
52
70
|
}
|
|
53
|
-
return {
|
|
71
|
+
return { name: trimmed };
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
export interface SolyCommand {
|
|
@@ -160,7 +178,10 @@ function parsePhaseOnlyShape(s: string): { phase: number } | null {
|
|
|
160
178
|
|
|
161
179
|
/** Match task-id `<slug>-<4hex>`, case-insensitive. */
|
|
162
180
|
function parseTaskIdShape(s: string): string | null {
|
|
163
|
-
|
|
181
|
+
const m = s.match(/^([a-z0-9][a-z0-9-]*)-([a-f0-9]{4})$/i);
|
|
182
|
+
if (!m) return null;
|
|
183
|
+
// Normalize to lowercase hex so downstream comparisons match.
|
|
184
|
+
return `${m[1]}-${m[2].toLowerCase()}`;
|
|
164
185
|
}
|
|
165
186
|
|
|
166
187
|
/** Extract `--feature <name>` from args. Returns null if not present or invalid. */
|
|
@@ -193,7 +214,7 @@ function parseNewTaskFlag(
|
|
|
193
214
|
*/
|
|
194
215
|
export type ExecuteTarget =
|
|
195
216
|
| { kind: "phase"; phase: number; plan: number | null; raw: string }
|
|
196
|
-
| { kind: "plan";
|
|
217
|
+
| { kind: "plan"; name: string; raw: string }
|
|
197
218
|
| { kind: "task"; taskId: string; raw: string }
|
|
198
219
|
| { kind: "all"; raw: string }
|
|
199
220
|
| { kind: "feature"; feature: string; raw: string };
|
|
@@ -228,10 +249,18 @@ export function describeExecuteTarget(args: string[]): ExecuteTarget | null {
|
|
|
228
249
|
const target = positional.trim();
|
|
229
250
|
if (!target) return null;
|
|
230
251
|
|
|
231
|
-
// <
|
|
252
|
+
// <slug> plan name — same identifier model as `soly new`. Checked AFTER
|
|
253
|
+
// task ids because some task ids look exactly like plan slugs (e.g.
|
|
254
|
+
// `auth-be-login-a3f9` matches the kebab-case regex). Task-id check
|
|
255
|
+
// is more specific (trailing 4-hex) so it wins.
|
|
256
|
+
const taskId = parseTaskIdShape(target);
|
|
257
|
+
if (taskId) {
|
|
258
|
+
return { kind: "task", taskId, raw };
|
|
259
|
+
}
|
|
260
|
+
|
|
232
261
|
const plan = parsePlanName(target);
|
|
233
262
|
if (!("error" in plan)) {
|
|
234
|
-
return { kind: "plan",
|
|
263
|
+
return { kind: "plan", name: plan.name, raw };
|
|
235
264
|
}
|
|
236
265
|
|
|
237
266
|
const phase = parsePhaseShape(target);
|
|
@@ -239,11 +268,6 @@ export function describeExecuteTarget(args: string[]): ExecuteTarget | null {
|
|
|
239
268
|
return { kind: "phase", phase: phase.phase, plan: phase.plan, raw };
|
|
240
269
|
}
|
|
241
270
|
|
|
242
|
-
const taskId = parseTaskIdShape(target);
|
|
243
|
-
if (taskId) {
|
|
244
|
-
return { kind: "task", taskId, raw };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
271
|
return null;
|
|
248
272
|
}
|
|
249
273
|
|
|
@@ -262,7 +286,7 @@ export function describeExecuteTarget(args: string[]): ExecuteTarget | null {
|
|
|
262
286
|
*/
|
|
263
287
|
export type PlanTarget =
|
|
264
288
|
| { kind: "phase"; phase: number; raw: string }
|
|
265
|
-
| { kind: "plan";
|
|
289
|
+
| { kind: "plan"; name: string; raw: string }
|
|
266
290
|
| { kind: "task"; taskId: string; raw: string }
|
|
267
291
|
| { kind: "new-task"; slug: string; feature: string; raw: string }
|
|
268
292
|
| { kind: "feature"; feature: string; raw: string };
|
|
@@ -298,11 +322,19 @@ export function describePlanTarget(args: string[]): PlanTarget | null {
|
|
|
298
322
|
const target = positional.trim();
|
|
299
323
|
if (!target) return null;
|
|
300
324
|
|
|
301
|
-
// <
|
|
302
|
-
// .agents/plans/<
|
|
325
|
+
// <slug> plan name (e.g. statistic-preparation) — plans live at
|
|
326
|
+
// .agents/plans/<slug>/PLAN.md, identity is the branch name. Checked
|
|
327
|
+
// AFTER task ids because some task ids look exactly like plan slugs
|
|
328
|
+
// (e.g. `auth-be-login-a3f9` matches the kebab-case regex). Task-id
|
|
329
|
+
// check is more specific (trailing 4-hex) so it wins.
|
|
330
|
+
const taskId = parseTaskIdShape(target);
|
|
331
|
+
if (taskId) {
|
|
332
|
+
return { kind: "task", taskId, raw };
|
|
333
|
+
}
|
|
334
|
+
|
|
303
335
|
const plan = parsePlanName(target);
|
|
304
336
|
if (!("error" in plan)) {
|
|
305
|
-
return { kind: "plan",
|
|
337
|
+
return { kind: "plan", name: plan.name, raw };
|
|
306
338
|
}
|
|
307
339
|
|
|
308
340
|
// Plan target only matches plain N (no .MM — plan is per-phase, executed
|
|
@@ -312,10 +344,5 @@ export function describePlanTarget(args: string[]): PlanTarget | null {
|
|
|
312
344
|
return { kind: "phase", phase: phase.phase, raw };
|
|
313
345
|
}
|
|
314
346
|
|
|
315
|
-
const taskId = parseTaskIdShape(target);
|
|
316
|
-
if (taskId) {
|
|
317
|
-
return { kind: "task", taskId, raw };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
347
|
return null;
|
|
321
348
|
}
|