@brainervirus/workit-core 0.8.1 → 0.8.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.
@@ -124,5 +124,13 @@ export const buildHandoffContract = ({
124
124
  .replace(/<SDD_DIR>/g, sddDir)
125
125
  .replace(/<TASK_LIST>/g, taskList);
126
126
 
127
+ // Every generated destination contract carries the marker on its OWN LINE
128
+ // (CA-07): a canonical template that loses the sentinel — or embeds it
129
+ // mid-line — is a broken contract, not a handoff prompt. Anchored to the
130
+ // line start/end so a substring mid-line cannot satisfy the guard.
131
+ if (!/^<workflow-handoff-destination>true<\/workflow-handoff-destination>$/m.test(contract)) {
132
+ return { error: "handoff destination contract missing its destination marker" };
133
+ }
134
+
127
135
  return { prompt: contract };
128
136
  };
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { fail, ok, type Result } from "../core";
3
3
  import { resolveWorkflowPaths, buildHandoffContract } from "./handoff-context";
4
4
  import { assetRoot } from "./package-root";
5
+ import type { FlowGateResult } from "./flow-state";
5
6
 
6
7
  type ApiResponse<T> = { data?: T; error?: unknown };
7
8
  type ApiResult<T> = Promise<ApiResponse<T>>;
@@ -33,13 +34,21 @@ export type HandoffRequest = {
33
34
  title: string;
34
35
  prompt: string;
35
36
  stay: boolean;
37
+ /**
38
+ * Destination marking hook (CA-07): invoked only after `promptAsync` succeeds
39
+ * and before any optional selection. The adapter supplies the core
40
+ * `markHandoffDestination` binding so destination state is marked atomically
41
+ * after the child session is seeded — never before creation/seed succeed and
42
+ * never undone by a later selection failure.
43
+ */
44
+ afterSeed?: () => FlowGateResult | Promise<FlowGateResult>;
36
45
  };
37
46
 
38
47
  type HandoffData = {
39
48
  sessionID?: string;
40
49
  seeded?: boolean;
41
50
  selected?: boolean;
42
- stage?: "create" | "seed" | "select";
51
+ stage?: "create" | "seed" | "mark" | "select";
43
52
  };
44
53
 
45
54
  export const message = (error: unknown) =>
@@ -78,6 +87,17 @@ export async function handoffSession(
78
87
  return fail(message(error), { sessionID, seeded: false, selected: false, stage: "seed" });
79
88
  }
80
89
 
90
+ if (request.afterSeed) {
91
+ try {
92
+ const marked = await request.afterSeed();
93
+ if (!marked.ok) {
94
+ return fail(marked.error, { sessionID, seeded: true, selected: false, stage: "mark" });
95
+ }
96
+ } catch (error) {
97
+ return fail(message(error), { sessionID, seeded: true, selected: false, stage: "mark" });
98
+ }
99
+ }
100
+
81
101
  if (request.stay) return ok({ sessionID, seeded: true, selected: false });
82
102
 
83
103
  try {
@@ -0,0 +1,68 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Host-neutral post-plan menu wording and the handoff destination marker
6
+ * (CA-07, CA-08). Import-light by design: session-start hooks and reminder
7
+ * selection consume these constants WITHOUT dragging in the full flow-state
8
+ * graph (approval digests, verification, repo-context), so a read-only hook
9
+ * stays network-free by source inspection (RL-09/CA-25).
10
+ */
11
+
12
+ /** Display labels the source menu presents (CA-08). */
13
+ export const SOURCE_MENU_LABELS = [
14
+ "Subagent-driven",
15
+ "Inline",
16
+ "Handoff",
17
+ "Review spec first",
18
+ "Review plan first",
19
+ ] as const;
20
+
21
+ /** Display labels a marked destination presents — exactly four, no Handoff (CA-08). */
22
+ export const DESTINATION_MENU_LABELS = [
23
+ "Subagent-driven",
24
+ "Inline",
25
+ "Review spec first",
26
+ "Review plan first",
27
+ ] as const;
28
+
29
+ /**
30
+ * The exact sentinel every generated destination contract carries on its own
31
+ * line (CA-07). Host-neutral by design (CA-10): OpenCode seeded sessions,
32
+ * Cursor copy/paste prompts, and CLI output all detect a destination by this
33
+ * marker alone — no session IDs, parent IDs, or host metadata.
34
+ */
35
+ export const HANDOFF_DESTINATION_MARKER =
36
+ "<workflow-handoff-destination>true</workflow-handoff-destination>";
37
+
38
+ /**
39
+ * Marked handoff destinations in a workspace (CA-07, CA-08): the persisted
40
+ * `handoff_destination: true` flag that `markHandoffDestination` sets atomically
41
+ * AFTER a genuine generated destination prompt succeeds. Hosts use this at
42
+ * session start to select the destination reminder (four choices, no Handoff)
43
+ * without session or parent IDs. The flag is cleared by approval-drift resets
44
+ * AND by completion (a completed flow is never a destination); only a new-flow
45
+ * `prepareFlowState` initializes it (false), and `markHandoffDestination` is
46
+ * the sole true-setter. Raw read only: a session-start hook must never
47
+ * reconcile or rewrite flow state, so a stale flag may read true until the
48
+ * next effective read resets it — safe bias toward destination wording.
49
+ * ponytail: raw flag scan, not readEffectiveFlowState — a hook is read-only;
50
+ * reconcile-on-drift is the mutation path's job.
51
+ */
52
+ export const findMarkedDestinations = (root: string): string[] => {
53
+ const docsDir = path.join(root, "docs");
54
+ if (!existsSync(docsDir)) return [];
55
+ const slugs: string[] = [];
56
+ for (const slug of readdirSync(docsDir)) {
57
+ const file = path.join(docsDir, slug, "sdd", "flow.json");
58
+ if (!existsSync(file)) continue;
59
+ try {
60
+ const parsed = JSON.parse(readFileSync(file, "utf8")) as { handoff_destination?: unknown };
61
+ if (parsed.handoff_destination === true) slugs.push(slug);
62
+ } catch {
63
+ // a malformed flow.json excludes the entry without touching it
64
+ continue;
65
+ }
66
+ }
67
+ return slugs;
68
+ };
@@ -1,11 +1,41 @@
1
+ import { DESTINATION_MENU_LABELS, HANDOFF_DESTINATION_MARKER, SOURCE_MENU_LABELS } from "./menu";
2
+
3
+ // Display-only source label list: the "(new session only)" qualifier appears in
4
+ // the reminder PROSE like the other source surfaces (bootstrap.ts, session-start,
5
+ // superpowers-doc-contract.md, ask-question-only.mdc), never in the machine
6
+ // label tuple `SOURCE_MENU_LABELS` — the receipt label must stay exactly
7
+ // `Handoff` for the native-question match (AR-12).
8
+ const SOURCE_MENU_LABELS_DISPLAY = SOURCE_MENU_LABELS.map((label) =>
9
+ label === "Handoff" ? "Handoff (new session only)" : label,
10
+ );
11
+
1
12
  export const REMINDER_TEXT = `<workflow-contract-reminder>
2
13
  - Bounded user choices → call the native \`question\` tool (never A/B/C or 1/2/3 lists in prose).
3
- - After a plan is approved → native \`question\` menu with exactly: Subagent-driven, Inline, Handoff (new session only), Review spec first, Review plan first.
14
+ - After a plan is approved → native \`question\` menu with exactly: ${SOURCE_MENU_LABELS_DISPLAY.join(", ")}.
4
15
  - Tools with \`confirmed\` → call them; never fabricate their result.
5
16
  - Before the first \`workflow_spec_approve\`/\`workflow_plan_approve\` (self-review) run the superpowers writing-plans Self-Review checklist: spec coverage (every spec requirement maps to a task), placeholder scan, type consistency; fix findings inline.
6
17
  - Delivering docs → clickable markdown link \`[spec.md](docs/<slug>/spec.md)\` + 3-5 bullet summary.
7
18
  </workflow-contract-reminder>`;
8
19
 
20
+ /**
21
+ * Destination reminder (CA-07, CA-08): a marked handoff destination carries the
22
+ * exact marker and the four-label allow-list — never the originating Handoff
23
+ * choice. Hosts select source vs destination wording via `reminderTextFor`
24
+ * from `FlowState.handoff_destination`; the strings live here, not in adapters.
25
+ */
26
+ export const DESTINATION_REMINDER_TEXT = `<workflow-contract-reminder>
27
+ - Bounded user choices → call the native \`question\` tool (never A/B/C or 1/2/3 lists in prose).
28
+ - This session is a handoff destination: present the post-plan menu with exactly: ${DESTINATION_MENU_LABELS.join(", ")}.
29
+ - Tools with \`confirmed\` → call them; never fabricate their result.
30
+ - Before the first \`workflow_spec_approve\`/\`workflow_plan_approve\` (self-review) run the superpowers writing-plans Self-Review checklist: spec coverage (every spec requirement maps to a task), placeholder scan, type consistency; fix findings inline.
31
+ - Delivering docs → clickable markdown link \`[spec.md](docs/<slug>/spec.md)\` + 3-5 bullet summary.
32
+ ${HANDOFF_DESTINATION_MARKER}
33
+ </workflow-contract-reminder>`;
34
+
35
+ /** Select source vs destination reminder wording from the flow's destination flag (CA-08). */
36
+ export const reminderTextFor = (destination: boolean): string =>
37
+ destination ? DESTINATION_REMINDER_TEXT : REMINDER_TEXT;
38
+
9
39
  export const DETECTION_TEXT = `<workflow-detection>
10
40
  Your previous message presented choices as a numbered/bulleted list in prose.
11
41
  That is a bounded user choice — use the native \`question\` tool instead (re-ask with \`question\` if still relevant).
package/src/core/sdd.ts CHANGED
@@ -34,6 +34,63 @@ export function todosFromTasks(
34
34
  return todos;
35
35
  }
36
36
 
37
+ /** SDD ledger completeness facts shared by lifecycle migration, completion,
38
+ * and active-plan detection (Task 2). */
39
+ export type LedgerCompletion = {
40
+ /** True only when progress.md contains at least one `Task N:` record. */
41
+ started: boolean;
42
+ /** True only when every canonical plan task id appears completed. */
43
+ complete: boolean;
44
+ required: number[];
45
+ completed: number[];
46
+ missing: number[];
47
+ };
48
+
49
+ /**
50
+ * Read the canonical `docs/<slug>/sdd/progress.md` ledger and the plan's
51
+ * `### Task N:` headings. Fail-closed: a missing or unreadable ledger yields
52
+ * `started: false`, and a missing/unreadable plan yields an empty `required`
53
+ * set (never a partial trust of a stale ledger). `complete` requires a
54
+ * non-empty required set fully covered by completed task ids, so an empty plan
55
+ * can never read as "done".
56
+ */
57
+ export function ledgerCompletion(root: string, slug: string): LedgerCompletion {
58
+ let started = false;
59
+ const completed: number[] = [];
60
+ const absProgress = path.join(root, "docs", slug, "sdd", "progress.md");
61
+ if (existsSync(absProgress)) {
62
+ try {
63
+ for (const line of readFileSync(absProgress, "utf8").split("\n")) {
64
+ const match = /^Task\s+(\d+):/i.exec(line);
65
+ if (match) {
66
+ started = true;
67
+ if (/^Task\s+\d+:\s*complete\b/i.test(line)) completed.push(Number(match[1]));
68
+ }
69
+ }
70
+ } catch {
71
+ // unreadable ledger: started stays false (fail-closed)
72
+ }
73
+ }
74
+ const required: number[] = [];
75
+ try {
76
+ const absPlan = path.join(root, "docs", slug, "plan.md");
77
+ if (existsSync(absPlan)) {
78
+ for (const task of parseTasksFromPlan(readFileSync(absPlan, "utf8"))) required.push(task.id);
79
+ }
80
+ } catch {
81
+ // unreadable/missing plan: required stays empty (fail-closed)
82
+ }
83
+ const completedSet = new Set(completed);
84
+ const missing = required.filter((id) => !completedSet.has(id));
85
+ return {
86
+ started,
87
+ complete: required.length > 0 && missing.length === 0,
88
+ required,
89
+ completed,
90
+ missing,
91
+ };
92
+ }
93
+
37
94
  export function sddContext({
38
95
  slug,
39
96
  plan_path,
@@ -5,6 +5,17 @@ Load `using-superpowers`, `subagent-driven-development`, `test-driven-developmen
5
5
  **Branch:** <BRANCH>
6
6
  **SDD:** `<SDD_DIR>`
7
7
 
8
+ ## Handoff destination
9
+
10
+ This session is a handoff destination for a continued plan. The originating session already recorded the post-plan menu choice; present exactly these four choices and never re-offer the originating handoff option:
11
+
12
+ - Subagent-driven
13
+ - Inline
14
+ - Review spec first
15
+ - Review plan first
16
+
17
+ <workflow-handoff-destination>true</workflow-handoff-destination>
18
+
8
19
  ## Hard gates
9
20
 
10
21
  - The parent is coordinator-only: it does not edit product code or perform delegated exploration.
@@ -63,6 +63,8 @@ On success, use native `question` / Cursor `AskQuestion` with exactly these opti
63
63
 
64
64
  Never emit Superpowers text beginning “Two execution options”.
65
65
 
66
+ A handoff destination session (the seeded contract carries `<workflow-handoff-destination>true</workflow-handoff-destination>`) presents exactly four choices — Subagent-driven, Inline, Review spec first, Review plan first — and never re-offers the originating handoff option.
67
+
66
68
  - Specs/plans must follow `templates/spec-template.md` / `templates/plan-template.md` (mandated diagrams, tables, CA-XX).
67
69
 
68
70
  ## Doc delivery