@dieulc/workflow 0.9.0 → 0.9.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/README.md +14 -9
- package/edit-policy.ts +11 -64
- package/index.ts +309 -45
- package/package.json +7 -3
- package/permissions.ts +393 -93
- package/review.ts +80 -3
- package/shell-text.ts +542 -0
- package/thinking-levels.ts +67 -0
- package/utils.ts +42 -0
- package/workflow-status.ts +81 -0
package/utils.ts
CHANGED
|
@@ -931,6 +931,48 @@ export function isPlanWritePath(p: string, cwd: string): boolean {
|
|
|
931
931
|
return norm.includes(".pi/plans/") || norm.startsWith(cwdNorm + "/.pi/plans");
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
+
/**
|
|
935
|
+
* Whether two plan paths identify the same file.
|
|
936
|
+
*
|
|
937
|
+
* Plan targets arrive in several shapes: absolute Windows paths from a
|
|
938
|
+
* `tool_result`, relative paths from `/review <path>`, forward slashes from a
|
|
939
|
+
* handoff, and case differences from the host. A review-cycle boundary must
|
|
940
|
+
* not fire for a spelling difference, and must fire for a real move. Kept in
|
|
941
|
+
* this dependency-free module so the comparison is unit-testable; callers pass
|
|
942
|
+
* the session cwd for relative paths.
|
|
943
|
+
*/
|
|
944
|
+
export function samePlanPath(
|
|
945
|
+
a: string | null | undefined,
|
|
946
|
+
b: string | null | undefined,
|
|
947
|
+
cwd: string = process.cwd(),
|
|
948
|
+
): boolean {
|
|
949
|
+
const normalize = (raw: string | null | undefined): string => {
|
|
950
|
+
let value = String(raw ?? "").trim();
|
|
951
|
+
if (!value) return "";
|
|
952
|
+
value = value.replace(/\\/g, "/");
|
|
953
|
+
const absolute =
|
|
954
|
+
value.startsWith("/") || /^[A-Za-z]:\//.test(value);
|
|
955
|
+
if (!absolute) {
|
|
956
|
+
value = `${String(cwd ?? "").replace(/\\/g, "/").replace(/\/+$/, "")}/${value}`;
|
|
957
|
+
}
|
|
958
|
+
const parts: string[] = [];
|
|
959
|
+
for (const part of value.split("/")) {
|
|
960
|
+
if (!part || part === ".") continue;
|
|
961
|
+
if (part === "..") {
|
|
962
|
+
if (parts.length > 0) parts.pop();
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
parts.push(part);
|
|
966
|
+
}
|
|
967
|
+
let out = parts.join("/");
|
|
968
|
+
if (value.startsWith("/")) out = `/${out}`;
|
|
969
|
+
// Windows drive letters are case-insensitive; keep POSIX case-sensitive.
|
|
970
|
+
out = out.replace(/^([A-Za-z]):/, (_m, drive: string) => `${drive.toUpperCase()}:`);
|
|
971
|
+
return process.platform === "win32" ? out.toLowerCase() : out;
|
|
972
|
+
};
|
|
973
|
+
return normalize(a) === normalize(b);
|
|
974
|
+
}
|
|
975
|
+
|
|
934
976
|
/** Inputs for deciding whether this run may hand a plan off. */
|
|
935
977
|
export interface PlanHandoffEligibility {
|
|
936
978
|
mode: "plan" | "build" | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow footer segments (2026-10-31 UI plan, step 10).
|
|
3
|
+
*
|
|
4
|
+
* The extension used to publish one long `workflow` status containing the mode,
|
|
5
|
+
* progress, profile/role, commit/write policy and review text. Desktop hosts can
|
|
6
|
+
* only lay an arbitrary status string out as one column, and terminal Pi wraps
|
|
7
|
+
* the same line less efficiently. This module builds short, stable segments so
|
|
8
|
+
* each can be its own footer column.
|
|
9
|
+
*
|
|
10
|
+
* `workflow` stays the mode/progress string: it is what the Desktop parser
|
|
11
|
+
* (`src/shared/workflow-mode.ts`) and existing terminal Pi users already know.
|
|
12
|
+
* The other keys are additive.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const WORKFLOW_STATUS_KEYS = {
|
|
16
|
+
mode: "workflow",
|
|
17
|
+
profile: "workflow-profile",
|
|
18
|
+
policy: "workflow-policy",
|
|
19
|
+
review: "workflow-review",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export interface WorkflowStatusInput {
|
|
23
|
+
mode: "build" | "plan" | null;
|
|
24
|
+
/** Plannotator's Plan sub-state, shown inside the mode segment. */
|
|
25
|
+
plannotatorActive?: boolean;
|
|
26
|
+
done: number;
|
|
27
|
+
total: number;
|
|
28
|
+
stale?: boolean;
|
|
29
|
+
profileLabel?: string;
|
|
30
|
+
roleLabel?: string;
|
|
31
|
+
commitMode: string;
|
|
32
|
+
/** Only Default mode has a write policy; null/undefined omits it. */
|
|
33
|
+
writeMode?: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface WorkflowStatusSegments {
|
|
37
|
+
mode: string;
|
|
38
|
+
profile: string;
|
|
39
|
+
policy: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildWorkflowStatusSegments(
|
|
43
|
+
input: WorkflowStatusInput,
|
|
44
|
+
): WorkflowStatusSegments {
|
|
45
|
+
const done = Number.isFinite(input.done) ? Math.max(0, input.done) : 0;
|
|
46
|
+
const total = Number.isFinite(input.total) ? Math.max(0, input.total) : 0;
|
|
47
|
+
const stale = input.stale ? " ⚠" : "";
|
|
48
|
+
const profile = [input.profileLabel ?? "", input.roleLabel ?? ""]
|
|
49
|
+
.map((part) => part.trim())
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join(" | ");
|
|
52
|
+
const policy =
|
|
53
|
+
`commits:${input.commitMode}` +
|
|
54
|
+
(input.writeMode ? ` · writes:${input.writeMode}` : "");
|
|
55
|
+
|
|
56
|
+
let mode: string;
|
|
57
|
+
if (input.mode === "build") {
|
|
58
|
+
mode = total > 0 ? `▶ build ${done}/${total}${stale}` : `▶ build${stale}`;
|
|
59
|
+
} else if (input.mode === "plan") {
|
|
60
|
+
const base = input.plannotatorActive
|
|
61
|
+
? "⏸ plan — reviewing in browser"
|
|
62
|
+
: "⏸ plan";
|
|
63
|
+
mode = total > 0 ? `${base} ${done}/${total}${stale}` : `${base}${stale}`;
|
|
64
|
+
} else if (total > 0) {
|
|
65
|
+
// The default mode must stay parseable by the Desktop badge decoder.
|
|
66
|
+
mode = `◦ ${done}/${total}${stale}`;
|
|
67
|
+
} else {
|
|
68
|
+
mode = `◦ commits:${input.commitMode}${stale}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { mode, profile, policy };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Sets a status only while it has text; `undefined` removes a stale key. */
|
|
75
|
+
export function setWorkflowStatus(
|
|
76
|
+
ui: { setStatus?: (key: string, text: string | undefined) => void },
|
|
77
|
+
key: string,
|
|
78
|
+
text: string,
|
|
79
|
+
): void {
|
|
80
|
+
ui.setStatus?.(key, text.length > 0 ? text : undefined);
|
|
81
|
+
}
|