@dieulc/workflow 0.8.0 → 0.9.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/README.md +10 -9
- package/index.ts +580 -224
- package/package.json +2 -2
- package/review-runtime.ts +7 -2
- package/utils.ts +77 -5
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dieulc/workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"keywords": ["pi-package", "workflow", "plan-mode", "review-mode", "questionnaire", "rewind"],
|
|
7
7
|
"description": "Plan↔Build mode with an independent Review Mode reviewer, questionnaire, subagent explore, todos and shadow-checkpoint rewind for Pi.",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node --test utils.todo.test.ts permissions.test.ts roles.test.ts profiles.test.ts picker.test.ts questionnaire-layout.test.ts questionnaire-view.test.ts model-default.test.ts review.test.ts review-ui.test.ts commit-policy.test.ts edit-policy.test.ts tab-policy.test.ts tab-editor.test.ts",
|
|
10
|
+
"test": "node --test utils.todo.test.ts permissions.test.ts roles.test.ts profiles.test.ts thinking-levels.test.ts workflow-status.test.ts picker.test.ts questionnaire-layout.test.ts questionnaire-view.test.ts model-default.test.ts review.test.ts review-ui.test.ts extension.test.ts commit-policy.test.ts edit-policy.test.ts tab-policy.test.ts tab-editor.test.ts",
|
|
11
11
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
12
12
|
"check:policy": "node --experimental-transform-types scripts/verify-write-policy.mjs"
|
|
13
13
|
},
|
package/review-runtime.ts
CHANGED
|
@@ -532,6 +532,10 @@ export function createReviewRuntime(deps: ReviewRuntimeDeps): ReviewRuntime {
|
|
|
532
532
|
const remaining = (): number | undefined =>
|
|
533
533
|
deadline === undefined ? undefined : Math.max(1, deadline - Date.now());
|
|
534
534
|
let sendError: unknown;
|
|
535
|
+
// The losing waiters in the pass race must be cancelled when the winner
|
|
536
|
+
// settles. Otherwise their timers (up to `timeoutMs`, 5 minutes by
|
|
537
|
+
// default) stay referenced and keep the process alive after the round.
|
|
538
|
+
const waitAbort = new AbortController();
|
|
535
539
|
|
|
536
540
|
const stop = await new Promise<PassStop>((resolve) => {
|
|
537
541
|
let done = false;
|
|
@@ -542,6 +546,7 @@ export function createReviewRuntime(deps: ReviewRuntimeDeps): ReviewRuntime {
|
|
|
542
546
|
done = true;
|
|
543
547
|
if (timer !== undefined) clearTimeout(timer);
|
|
544
548
|
rc.signal.removeEventListener("abort", onAbort);
|
|
549
|
+
waitAbort.abort();
|
|
545
550
|
resolve(s);
|
|
546
551
|
};
|
|
547
552
|
if (rc.signal.aborted) {
|
|
@@ -551,10 +556,10 @@ export function createReviewRuntime(deps: ReviewRuntimeDeps): ReviewRuntime {
|
|
|
551
556
|
const cap = remaining();
|
|
552
557
|
if (cap !== undefined) timer = setTimeout(() => finish("timeout"), cap);
|
|
553
558
|
rc.signal.addEventListener("abort", onAbort, { once: true });
|
|
554
|
-
void gate.waitForPassDone(remaining(),
|
|
559
|
+
void gate.waitForPassDone(remaining(), waitAbort.signal).then((result) => {
|
|
555
560
|
if (result) finish("passDone");
|
|
556
561
|
});
|
|
557
|
-
void waitForSettle(baseline, remaining(),
|
|
562
|
+
void waitForSettle(baseline, remaining(), waitAbort.signal).then((did) => {
|
|
558
563
|
if (did) finish("settled");
|
|
559
564
|
});
|
|
560
565
|
void send().catch((e: unknown) => {
|
package/utils.ts
CHANGED
|
@@ -934,22 +934,27 @@ export function isPlanWritePath(p: string, cwd: string): boolean {
|
|
|
934
934
|
/** Inputs for deciding whether this run may hand a plan off. */
|
|
935
935
|
export interface PlanHandoffEligibility {
|
|
936
936
|
mode: "plan" | "build" | null;
|
|
937
|
-
/** The run started in Plan mode (not a Build turn that flipped to Plan). */
|
|
938
|
-
runStartedInPlan: boolean;
|
|
939
937
|
/** This run wrote/edited a file under `.pi/plans/`. */
|
|
940
938
|
planWrittenThisRun: boolean;
|
|
939
|
+
/**
|
|
940
|
+
* The write happened while Plan mode was active. A run that starts in
|
|
941
|
+
* Default/Build and switches to Plan mid-run counts; a plan written before
|
|
942
|
+
* the switch does not. `agent_end`'s current-run message scan sets this too
|
|
943
|
+
* when the run ends in Plan (the tool_result handler was missed).
|
|
944
|
+
*/
|
|
945
|
+
planWrittenInPlanMode: boolean;
|
|
941
946
|
}
|
|
942
947
|
|
|
943
948
|
/**
|
|
944
949
|
* A handoff requires all three: the workflow is in Plan mode NOW, the run
|
|
945
|
-
*
|
|
950
|
+
* wrote a plan, and that write happened under Plan mode. Session-lifetime
|
|
946
951
|
* plan files and old todos never authorize a handoff.
|
|
947
952
|
*/
|
|
948
953
|
export function shouldHandoffPlan(input: PlanHandoffEligibility): boolean {
|
|
949
954
|
return (
|
|
950
955
|
input.mode === "plan" &&
|
|
951
|
-
input.
|
|
952
|
-
input.
|
|
956
|
+
input.planWrittenThisRun === true &&
|
|
957
|
+
input.planWrittenInPlanMode === true
|
|
953
958
|
);
|
|
954
959
|
}
|
|
955
960
|
|
|
@@ -978,6 +983,73 @@ export function findPlanWritePathInMessages(
|
|
|
978
983
|
return undefined;
|
|
979
984
|
}
|
|
980
985
|
|
|
986
|
+
/** Where a manual `/review` found the plan it is about to audit. */
|
|
987
|
+
export type PlanPathSource =
|
|
988
|
+
| "explicit"
|
|
989
|
+
| "run"
|
|
990
|
+
| "session"
|
|
991
|
+
| "handoff"
|
|
992
|
+
| "branch";
|
|
993
|
+
|
|
994
|
+
export interface PlanPathCandidate {
|
|
995
|
+
path?: string | null;
|
|
996
|
+
source: PlanPathSource;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
export interface PlanPathSelection {
|
|
1000
|
+
path: string;
|
|
1001
|
+
source: PlanPathSource;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Pick the first readable plan for a manual `/review`.
|
|
1006
|
+
*
|
|
1007
|
+
* Candidates arrive in priority order (`explicit` → `run` → `session` →
|
|
1008
|
+
* `handoff` → `branch`). Missing, empty and unreadable candidates are skipped;
|
|
1009
|
+
* a plan that disappeared since it was written must never win over the next
|
|
1010
|
+
* candidate. Pure: the caller owns path resolution and the filesystem probe.
|
|
1011
|
+
*/
|
|
1012
|
+
export function choosePlanPath(
|
|
1013
|
+
candidates: readonly PlanPathCandidate[],
|
|
1014
|
+
isReadable: (absolutePath: string) => boolean,
|
|
1015
|
+
): PlanPathSelection | undefined {
|
|
1016
|
+
for (const candidate of candidates) {
|
|
1017
|
+
const p = candidate.path;
|
|
1018
|
+
if (typeof p !== "string" || p.length === 0) continue;
|
|
1019
|
+
if (isReadable(p)) return { path: p, source: candidate.source };
|
|
1020
|
+
}
|
|
1021
|
+
return undefined;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Newest plan reference in a session branch, used as the last manual
|
|
1026
|
+
* `/review` fallback after a reload or when the in-memory path was lost.
|
|
1027
|
+
*
|
|
1028
|
+
* Recognises the persisted `workflow` entry (`planFile`), the
|
|
1029
|
+
* `workflow-handoff-resolve` entry (`planPath`), and an assistant `write`/
|
|
1030
|
+
* `edit` tool call under `.pi/plans/`. Scans newest-first and never throws.
|
|
1031
|
+
*/
|
|
1032
|
+
export function findLatestPlanWriteInBranch(
|
|
1033
|
+
entries: readonly unknown[],
|
|
1034
|
+
cwd: string,
|
|
1035
|
+
): string | undefined {
|
|
1036
|
+
if (!Array.isArray(entries) || entries.length === 0) return undefined;
|
|
1037
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1038
|
+
const e = entries[i] as any;
|
|
1039
|
+
if (!e || typeof e !== "object") continue;
|
|
1040
|
+
if (e.type === "custom") {
|
|
1041
|
+
const p = e.data?.planPath ?? e.data?.planFile;
|
|
1042
|
+
if (typeof p === "string" && p && isPlanWritePath(p, cwd)) return p;
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
if (e.type === "message" && e.message) {
|
|
1046
|
+
const found = findPlanWritePathInMessages([e.message], cwd);
|
|
1047
|
+
if (found) return found;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return undefined;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
981
1053
|
// ── Plan date helpers (UTC) ──────────────────────────────────────
|
|
982
1054
|
|
|
983
1055
|
/** Return YYYY-MM-DD in UTC for the given date (default: now). Uses toISOString slice, deterministic across locales. */
|