From 7360aed48b52aeacba3c27dd1aff8115a5a2aee1 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 14:15:29 +0100 Subject: [PATCH 01/19] inception(workflow-inception-entry): rewrite validate-phase as refinement-source signal Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/workflow-inception-entry/SKILL.md | 2 ++ .../references/invoke-skill.md | 9 +++++++- .../references/validate-phase.md | 23 +++---------------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/skills/workflow-inception-entry/SKILL.md b/skills/workflow-inception-entry/SKILL.md index 124a137a..9f23c962 100644 --- a/skills/workflow-inception-entry/SKILL.md +++ b/skills/workflow-inception-entry/SKILL.md @@ -117,6 +117,8 @@ Set `source` = `first-session`. Load **[validate-phase.md](references/validate-phase.md)** and follow its instructions as written. +→ Proceed to **Step 4**. + --- ## Step 4: Gather Context diff --git a/skills/workflow-inception-entry/references/invoke-skill.md b/skills/workflow-inception-entry/references/invoke-skill.md index d7f6f142..f41fdb6d 100644 --- a/skills/workflow-inception-entry/references/invoke-skill.md +++ b/skills/workflow-inception-entry/references/invoke-skill.md @@ -6,6 +6,13 @@ This skill's purpose is now fulfilled. Construct the handoff and invoke the processing skill. +The `Source:` line in the handoff carries the value of `source` set earlier in the entry flow: + +- `first-session` — set in **Step 2** when no inception items exist for this work unit. +- `refinement` — set in **Step 3** via `validate-phase.md` when inception items already exist. + +The processing skill reads this field at Step 0 to decide whether to run the initial-session flow or open a refinement session. + --- ## Handoff @@ -13,7 +20,7 @@ This skill's purpose is now fulfilled. Construct the handoff and invoke the proc ``` Inception session for: {work_unit} -Source: first-session +Source: {source:[first-session|refinement]} Output: .workflows/{work_unit}/inception/ Description (from manifest): diff --git a/skills/workflow-inception-entry/references/validate-phase.md b/skills/workflow-inception-entry/references/validate-phase.md index 2c2ce764..630635f2 100644 --- a/skills/workflow-inception-entry/references/validate-phase.md +++ b/skills/workflow-inception-entry/references/validate-phase.md @@ -4,25 +4,8 @@ --- -This is a refinement session — inception items already exist for this work unit. Refinement of the discovery map (adding, renaming, removing topics, editing summaries, changing routing) is a future-phase deliverable in this initiative. For now, surface the situation and stop. +Inception items already exist for "{work_unit}" — this is a refinement session. -> *Output the next fenced block as a code block:* +Set `source` = `refinement` for the handoff. -``` -●───────────────────────────────────────────────● - Inception Refinement -●───────────────────────────────────────────────● - -Refinement of the discovery map is not yet implemented. -``` - -> *Output the next fenced block as markdown (not a code block):* - -``` -> Inception items already exist for "{work_unit}". The -> refinement flow — adding, renaming, removing topics, editing -> summaries, changing routing — lands in a later phase of the -> inception/discovery-map initiative. -``` - -**STOP.** Do not proceed — terminal condition. +→ Return to caller. From ef5320077311a14ccb8d696ae0f09e7657fabde9 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 14:16:09 +0100 Subject: [PATCH 02/19] inception(workflow-inception-process): add source-aware routing to Step 0 Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/workflow-inception-process/SKILL.md | 33 ++++++++++++++-------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/skills/workflow-inception-process/SKILL.md b/skills/workflow-inception-process/SKILL.md index e7d51485..8f6756da 100644 --- a/skills/workflow-inception-process/SKILL.md +++ b/skills/workflow-inception-process/SKILL.md @@ -60,12 +60,23 @@ Do not guess at progress or continue from memory. The files on disk and git hist > *Output the next fenced block as markdown (not a code block):* ``` -> Checking for an existing inception session log on disk. The -> entry skill has already verified there are no inception items -> in the manifest — this check is purely about within-session -> recovery after a context refresh. +> Reading the handoff source. Refinement re-entry routes +> straight to the refinement flow; first-session entry checks +> for an interrupted draft session log on disk. ``` +Read the `Source:` field from the handoff in the prior message. + +#### If `source` is `refinement` + +Inception items already exist for this work unit. Open the refinement session — the initial-session detection logic below does not apply. + +Load **[refinement-session.md](references/refinement-session.md)** and follow its instructions as written. + +#### Otherwise (`source` is `first-session`) + +The entry skill has already verified there are no inception items in the manifest. The remaining checks below recover from a context refresh that interrupted a prior first-session draft. + Check whether any file matching `.workflows/{work_unit}/inception/session-*.md` exists. #### If no file exists @@ -104,25 +115,25 @@ Found an in-progress inception session log for **{work_unit:(titlecase)}**. #### If any `session-NNN.md` for N > 1 exists -The work unit has previously concluded an inception session and is being re-entered. Refinement is a future-phase deliverable in this initiative. +This is a defensive guard. The entry skill should have routed `source = refinement` when prior session logs and inception items exist, but the handoff said `first-session`. State is inconsistent — likely the inception items were removed from the manifest but the session logs were not. > *Output the next fenced block as a code block:* ``` ●───────────────────────────────────────────────● - Inception Refinement + Inception — Inconsistent State ●───────────────────────────────────────────────● -Refinement of the discovery map is not yet implemented. +Prior inception session logs exist but the manifest reports no +inception items for "{work_unit}". ``` > *Output the next fenced block as markdown (not a code block):* ``` -> A prior inception session has concluded for "{work_unit}". The -> refinement flow — adding, renaming, removing topics, editing -> summaries, changing routing — lands in a later phase of the -> inception/discovery-map initiative. +> Stopping here so you can reconcile. Either restore the +> manifest items (refinement re-entry) or archive the session +> logs out of the way (fresh first session). ``` **STOP.** Do not proceed — terminal condition. From 262bc5b09faab891c448393855e03086237b2694 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 14:16:32 +0100 Subject: [PATCH 03/19] inception(workflow-inception-process): add refinement-template.md Co-Authored-By: Claude Opus 4.7 (1M context) --- .../references/refinement-template.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 skills/workflow-inception-process/references/refinement-template.md diff --git a/skills/workflow-inception-process/references/refinement-template.md b/skills/workflow-inception-process/references/refinement-template.md new file mode 100644 index 00000000..72d29a61 --- /dev/null +++ b/skills/workflow-inception-process/references/refinement-template.md @@ -0,0 +1,50 @@ +# Refinement Session Log Template + +*Reference for **[workflow-inception-process](../SKILL.md)*** + +--- + +Structure for `.workflows/{work_unit}/inception/session-{NNN}.md` where `NNN` is the next zero-padded sequence number after the existing session logs (initial = `001`, first refinement = `002`, etc.). + +Keep all section headings — write `(none)` under any that have no content rather than removing the section. The empty section is a positive signal it was considered, not missed. The log is brief, rationale-focused, and keyed by event. + +## Template + +```markdown +# Inception Session {NNN} — Refinement + +Date: {YYYY-MM-DD} +Work unit: {work_unit} + +## Map State at Start + +{One-line summary: total topics and counts by lifecycle.} +Example: `8 topics — 2 decided · 3 in flight · 1 ready · 2 fresh` + +## Self-Healing Arrivals + +{Items added by analyses since the last session, if any. Phase 6 +leaves this as `(none)` — Phase 7 fills it when analyses run.} + +- {topic} (added by {analysis}, source: {provenance}) + +## Changes + +- Added: {topic} (routing: {research|discussion}, source: inception) — {reason} +- Edited summary: {topic} — {short note} +- Renamed: {old} → {new} — {reason} +- Removed: {topic} — {reason} +- Changed routing: {topic} → {new routing} — {reason} + +## Conclusion + +{N} changes applied. Map now has {M} topics. +``` + +## Anti-patterns + +- No transcript-style content. The log is rationale, not dialogue. +- No decisions, options, or trade-offs. That belongs in discussion. +- No investigation. The log records what changed, not what was uncovered. + +→ Return to caller. From 120edf5d1d04aa4ddf0da357f087a9160e01c739 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 14:18:26 +0100 Subject: [PATCH 04/19] inception(workflow-inception-process): add refinement-session.md (read-state, open, conclude) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../references/refinement-session.md | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 skills/workflow-inception-process/references/refinement-session.md diff --git a/skills/workflow-inception-process/references/refinement-session.md b/skills/workflow-inception-process/references/refinement-session.md new file mode 100644 index 00000000..bc3c07d6 --- /dev/null +++ b/skills/workflow-inception-process/references/refinement-session.md @@ -0,0 +1,187 @@ +# Refinement Session + +*Reference for **[workflow-inception-process](../SKILL.md)*** + +--- + +This reference drives the re-entry path into inception. Items already exist on the discovery map; the user's intent is to refine — add, edit, remove, rename, or re-route topics. + +The convention is conversational, not menu-driven. STOP gates wrap manifest writes, scaled to destructiveness — additive operations batch, destructive operations are per-item. The map-operations reference owns parsing, validation, and persistence; this file owns the conversation shape. + +Two anti-patterns to avoid: + +- **Do not call `knowledge index`.** Inception session logs (initial or refinement) are journey records, not retrievable artifacts. +- **Do not set a phase-level `status: completed`.** Inception remains alive as long as the work unit is in-progress. + +## A. Read State + +> *Output the next fenced block as a code block:* + +``` +── Read Map State ─────────────────────────────── +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +> Loading current inception items and per-topic lifecycle from +> the manifest. The map is the source of truth — no file reads +> needed for state. +``` + +Load the work unit's manifest and read `phases.inception.items.*`. For each topic, compute its lifecycle using `computeTopicLifecycle(manifest, topicName)` from `skills/workflow-shared/scripts/discovery-utils.cjs`. The returned `{ lifecycle, tier, current_phase }` informs which operations are allowed in the operations loop. + +Also read `phases.inception.dismissed` (an array of previously removed topic names; may be missing or empty). The dismissed list governs name collision and the show-dismissed flow. + +→ Proceed to **B. Self-Healing Check**. + +## B. Self-Healing Check + +> *Output the next fenced block as a code block:* + +``` +── Self-Healing Check ─────────────────────────── +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +> Phase 6 placeholder — self-healing analyses (research-analysis, +> gap-analysis) are wired in Phase 7. Nothing runs here yet. +``` + +No-op for Phase 6. Phase 7 will run the analyses inline at this point and apply results to the map (auto-add with `source: research-analysis` or `source: gap-analysis`, filtered against `phases.inception.dismissed`). + +→ Proceed to **C. Open Refinement**. + +## C. Open Refinement + +> *Output the next fenced block as a code block:* + +``` +── Open Refinement ────────────────────────────── +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +> Refining the discovery map. Tell me what to change — add, +> edit, remove, rename, or re-route topics. Multiple changes in +> one message are fine; I'll work through them. +``` + +### C.1. Initialise the refinement session log + +Determine the next session number: + +```bash +n=$(ls .workflows/{work_unit}/inception/session-*.md 2>/dev/null | wc -l) +next=$(printf "%03d" $((n + 1))) +``` + +Create `.workflows/{work_unit}/inception/session-{next}.md` from **[refinement-template.md](refinement-template.md)**. Populate the header (date, work unit), fill **Map State at Start** with the summary line computed in **A. Read State**, leave **Self-Healing Arrivals**, **Changes**, and **Conclusion** as `(none)` placeholders — they fill in during the session. + +Commit: + +```bash +git add -- .workflows/{work_unit}/inception/session-{next}.md +git commit -m "inception({work_unit}): seed refinement session log" +``` + +### C.2. Render the map and prompt + +Render the current map as a compact anchor for the conversation: + +> *Output the next fenced block as a code block:* + +``` +●───────────────────────────────────────────────● + Refinement — {work_unit:(titlecase)} +●───────────────────────────────────────────────● + + Discovery Map ({summary_line}) + +@foreach(topic in discovery_map) + @if(not last_topic) ├─ @else └─ @endif {topic.tier} {topic.name:(titlecase)} {lifecycle_label} +@endforeach +``` + +**Render rules** (subset of continue-epic's discovery map block): + +- `summary_line`: `{total} topics — {decided} decided · {in_flight} in flight · {ready} ready · {fresh} fresh · {cancelled} cancelled`. Omit zero-count categories. Always include `{total} topics`. +- Tier and ordering — sort by tier rank `→ ◐ ✓ ○ ⊘`, alphabetical within each tier (matches continue-epic). +- `lifecycle_label` by tier: + - `→` — `research complete · ready for discussion` + - `◐` — `researching` or `discussing` (use `current_phase`) + - `✓` — `decided` + - `○` — `fresh · routed to {topic.routing}` (omit ` · routed to ...` if `routing` is null) + - `⊘` — `cancelled` +- No source provenance sub-line, no key block, no menu — this is an anchor, not the continue-epic display. + +> *Output the next fenced block as a code block:* + +``` +What would you like to change? +``` + +**STOP.** Wait for user response. + +→ Proceed to **D. Operations Loop**. + +## D. Operations Loop + +Phase 6 wiring landed in a follow-up commit. Until then, this section is a stub — when the user's first message arrives, route directly to **E. Conclude** so the bridge still resolves cleanly. + +→ Proceed to **E. Conclude**. + +## E. Conclude + +> *Output the next fenced block as a code block:* + +``` +── Conclude Refinement ────────────────────────── +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +> Wrapping up. Finalising the session log, running compliance +> self-check, and bridging back to the epic menu. +``` + +### E.1. Finalise the session log + +Populate the **Conclusion** section of the in-progress `inception/session-{NNN}.md` with the change count and current map size. Skip the line if no changes were made (a "browse-only" refinement is a valid outcome). + +### E.2. Compliance self-check + +Load **[compliance-check.md](../../workflow-shared/references/compliance-check.md)** and follow its instructions as written. The check audits the refinement against this file and the parent SKILL.md (plus any other references loaded during the session). Apply silent corrections inline; surface significant issues per the shared protocol. + +### E.3. Final sweep + +Check `git status`. If the working tree is dirty, commit residual changes: + +```bash +git add -- .workflows/{work_unit}/ +git commit -m "inception({work_unit}): finalise refinement session log" +``` + +If clean, skip. + +### E.4. Bridge + +> *Output the next fenced block as markdown (not a code block):* + +``` +> Refinement complete. Returning to the epic menu so you can +> pick the next move from the updated map. +``` + +``` +Pipeline bridge for: {work_unit} +Completed phase: inception + +Invoke the workflow-bridge skill to enter plan mode with continuation instructions. +``` + +**STOP.** Do not proceed — terminal condition. From 23a4a79ce05b4e950a9dfeaa620dde897b32b680 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 14:19:50 +0100 Subject: [PATCH 05/19] inception(workflow-inception-process): add map-operations.md (validation, writes, commits) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../references/map-operations.md | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 skills/workflow-inception-process/references/map-operations.md diff --git a/skills/workflow-inception-process/references/map-operations.md b/skills/workflow-inception-process/references/map-operations.md new file mode 100644 index 00000000..cdf1e5c6 --- /dev/null +++ b/skills/workflow-inception-process/references/map-operations.md @@ -0,0 +1,389 @@ +# Map Operations + +*Reference for **[workflow-inception-process](../SKILL.md)*** + +--- + +Per-operation handling for refinement. Owns parsing, validation, manifest writes, session-log entries, and commits. Loaded by **[refinement-session.md](refinement-session.md)** when the user names one or more changes. + +The parent reference owns the conversation shape; this file owns the writes. After completing the user's batch, return to caller. + +## A. Parse Operations + +Read the user's most recent message. Extract one or more operations. Recognised intents: + +| User phrasing | Operation | Required values | +| ----------------------------------------------- | --------------- | ---------------------------------------- | +| *"add X as research"*, *"add Y as discussion"* | Add | name, routing | +| *"edit summary of X to Y"*, *"reword X's blurb"*| Edit summary | name, new summary | +| *"remove X"*, *"drop X"*, *"delete X"* | Remove | name | +| *"rename X to Y"* | Rename | old name, new name | +| *"change routing of X to discussion"* | Change routing | name, new routing | + +If routing is omitted on Add, infer from cues in the user's framing (factual unknowns → research; opinion or design → discussion). The proposal is tentative — the STOP gate is where the user flips it. + +If the message is ambiguous (e.g. *"fix X"*, *"that one looks wrong"*), ask one clarifying question before proceeding. No STOP gate is needed for clarification — it's part of conversational flow, not a manifest write. + +**Classify operations:** + +- **Additive** — Add, Edit summary. Batched. +- **Destructive** — Remove, Rename, Change routing. Per-item. + +**Process order:** in the order the user listed them. For pure additive batches, group into a single STOP gate, single commit, single session-log entry covering the batch. For pure destructive batches, per-item. For mixed batches, walk in order — destructive ops gate per-item, the contiguous additive ops in between can batch. + +→ Proceed to **B. Validate**. + +## B. Validate + +Apply per-operation validation gates **before** any STOP gate. If validation fails, surface the rejection with a clear next-step pointer (don't just say "blocked") and skip that operation. Continue with the rest of the batch. + +### B.1. Lifecycle gates + +For destructive operations (Remove, Rename, Change routing), compute the topic's lifecycle via `computeTopicLifecycle(manifest, topicName)` from `discovery-utils.cjs`. The operation is allowed only when: + +| Operation | Allowed lifecycles | Disallowed | +| --------------- | ------------------ | -------------------------------------------- | +| Remove | `fresh` | `researching`, `discussing`, `ready_for_discussion`, `decided`, `cancelled` | +| Rename | `fresh` | all others | +| Change routing | `fresh` | all others (routing is implicit once a phase item exists) | +| Edit summary | any | — | +| Add | n/a (new item) | — | + +Note: `cancelled` is also disallowed for Remove because the inception item is the historical record of the topic ever having existed. Removal is for never-started topics only; cancel-then-vanish would erase audit trail. The `a`/`cancel` flow in `/continue-epic` is the right tool for stopping in-flight work. + +**Rejection messages** — render in a code block, then continue with the rest of the batch: + +> *Output the next fenced block as a code block:* + +``` +"{topic}" can't be {removed|renamed|re-routed} from the map — +{lifecycle_phrase}. To stop work on it, use `a`/`cancel` in +/continue-epic instead. +``` + +`{lifecycle_phrase}` examples: + +- `researching` — `research is in flight on it` +- `discussing` — `discussion is in flight on it` +- `ready_for_discussion` — `research has completed and discussion is queued` +- `decided` — `discussion has concluded` +- `cancelled` — `it has phase work in cancelled state and stays on the map as historical record` + +### B.2. Name collision gates + +For Add and Rename, the new name is rejected if an **active** map item already uses it (case-sensitive match against `phases.inception.items.{name}`). Render: + +> *Output the next fenced block as a code block:* + +``` +"{name}" is already on the map. Pick a different name or use +edit-summary / change-routing on the existing item. +``` + +For Add, a name appearing in `phases.inception.dismissed` is **allowed** — it counts as a re-add. The Add flow pulls the name from the dismissed list before creating the new item. + +→ Proceed to **C. Apply Operations**. + +## C. Apply Operations + +Walk the validated operations in user order. For each, render the proposed change, gate per the safety rule, apply via the manifest CLI, append to the session log, and commit. + +The session log path is `.workflows/{work_unit}/inception/session-{NNN}.md` (already initialised by the parent reference's **C.1**). Append entries under the **Changes** section in user order. Replace `(none)` with the first entry. + +### C.1. Add (additive — batched) + +For a contiguous run of Add operations, render the proposal once: + +> *Output the next fenced block as a code block:* + +``` +Adding {N} topic(s): + + • {name_1} (routing: {research|discussion}, source: inception) + • {name_2} (routing: {research|discussion}, source: inception) + ... +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +· · · · · · · · · · · · +Add all? + +- **`y`/`yes`** +- **`n`/`no`** +· · · · · · · · · · · · +``` + +**STOP.** Wait for user response. + +#### If `no` + +Skip the batch. Continue with the next operation in the user's list (if any) or return to caller. + +#### If `yes` + +For each name in the batch: + +1. If the name is in `phases.inception.dismissed`, pull it: + + ```bash + node .claude/skills/workflow-manifest/scripts/manifest.cjs pull {work_unit}.inception dismissed "{name}" + ``` + +2. Initialise the inception item and set its fields: + + ```bash + node .claude/skills/workflow-manifest/scripts/manifest.cjs init-phase {work_unit}.inception.{name} + node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{name} summary "{one-line summary}" + node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{name} routing {research|discussion} + node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{name} source inception + ``` + + Source is `inception` for refinement-added topics — they are user-curated, indistinguishable from initial-session items for provenance purposes. + +3. Append a single batch entry to the session log under **Changes** (one bullet per name): + + ```markdown + - Added: {name_1} (routing: {research|discussion}, source: inception) — {short rationale} + - Added: {name_2} (routing: {research|discussion}, source: inception) — {short rationale} + ``` + +4. Single commit covering all adds in the batch: + + ```bash + git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-*.md + git commit -m "inception({work_unit}): add {N} topic(s) to map" + ``` + +→ Proceed to the next operation in the user's list. + +### C.2. Edit summary (additive — batched) + +For a contiguous run of Edit summary operations, render the proposal once: + +> *Output the next fenced block as a code block:* + +``` +Updating {N} summary(ies): + + • {name_1}: "{new summary}" + • {name_2}: "{new summary}" + ... +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +· · · · · · · · · · · · +Apply? + +- **`y`/`yes`** +- **`n`/`no`** +· · · · · · · · · · · · +``` + +**STOP.** Wait for user response. + +#### If `no` + +Skip the batch. Continue. + +#### If `yes` + +For each: + +```bash +node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{name} summary "{new summary}" +``` + +Append a single batch entry to the session log under **Changes**: + +```markdown +- Edited summary: {name_1} — {short note} +- Edited summary: {name_2} — {short note} +``` + +Single commit: + +```bash +git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-*.md +git commit -m "inception({work_unit}): edit {N} summary(ies)" +``` + +→ Proceed to the next operation in the user's list. + +### C.3. Remove (destructive — per-item) + +For each Remove operation: + +> *Output the next fenced block as a code block:* + +``` +Remove "{name}" from the map. + + Lifecycle: fresh — no work has started on this topic. + The name will be added to the dismissed list so analyses + won't auto-re-propose it. +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +· · · · · · · · · · · · +Confirm removal? + +- **`y`/`yes`** +- **`n`/`no`** +· · · · · · · · · · · · +``` + +**STOP.** Wait for user response. + +#### If `no` + +Skip this operation. Continue with the next. + +#### If `yes` + +Hard-delete the inception item and add the name to the dismissed list: + +```bash +node .claude/skills/workflow-manifest/scripts/manifest.cjs delete {work_unit}.inception items.{name} +node .claude/skills/workflow-manifest/scripts/manifest.cjs push {work_unit}.inception dismissed "{name}" +``` + +Append a Changes entry to the session log: + +```markdown +- Removed: {name} — {short reason} +``` + +Per-item commit: + +```bash +git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-*.md +git commit -m "inception({work_unit}): remove {name} from map" +``` + +→ Proceed to the next operation in the user's list. + +### C.4. Rename (destructive — per-item) + +For each Rename operation: + +> *Output the next fenced block as a code block:* + +``` +Rename "{old}" → "{new}". + + Lifecycle: fresh — no work has started, no files exist + under this name. Manifest mutation only. +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +· · · · · · · · · · · · +Confirm rename? + +- **`y`/`yes`** +- **`n`/`no`** +· · · · · · · · · · · · +``` + +**STOP.** Wait for user response. + +#### If `no` + +Skip this operation. Continue. + +#### If `yes` + +Read the existing fields, delete the old key, create the new key, re-write the fields: + +```bash +summary=$(node .claude/skills/workflow-manifest/scripts/manifest.cjs get {work_unit}.inception.{old} summary) +routing=$(node .claude/skills/workflow-manifest/scripts/manifest.cjs get {work_unit}.inception.{old} routing) +source=$(node .claude/skills/workflow-manifest/scripts/manifest.cjs get {work_unit}.inception.{old} source) + +node .claude/skills/workflow-manifest/scripts/manifest.cjs delete {work_unit}.inception items.{old} +node .claude/skills/workflow-manifest/scripts/manifest.cjs init-phase {work_unit}.inception.{new} +node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{new} summary "$summary" +node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{new} routing "$routing" +node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{new} source "$source" +``` + +If any command fails, surface the error and stop before the commit so the user can recover — the rename is partial otherwise. + +Append a Changes entry to the session log: + +```markdown +- Renamed: {old} → {new} — {short reason} +``` + +Per-item commit: + +```bash +git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-*.md +git commit -m "inception({work_unit}): rename {old} → {new}" +``` + +→ Proceed to the next operation in the user's list. + +### C.5. Change routing (destructive — per-item) + +For each Change-routing operation: + +> *Output the next fenced block as a code block:* + +``` +Change routing of "{name}": {old routing} → {new routing}. + + Lifecycle: fresh — no phase work yet, so the routing + hint is mutable. +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +· · · · · · · · · · · · +Confirm routing change? + +- **`y`/`yes`** +- **`n`/`no`** +· · · · · · · · · · · · +``` + +**STOP.** Wait for user response. + +#### If `no` + +Skip this operation. Continue. + +#### If `yes` + +```bash +node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{name} routing {research|discussion} +``` + +Append a Changes entry: + +```markdown +- Changed routing: {name} → {new routing} — {short reason} +``` + +Per-item commit: + +```bash +git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-*.md +git commit -m "inception({work_unit}): re-route {name} to {new routing}" +``` + +→ Proceed to the next operation in the user's list. + +## D. Done + +Once all operations in the user's batch have been processed (applied or skipped), return to caller. The parent reference re-prompts with `Anything else?`. + +→ Return to caller. From d764610c01e54b1af166ed1be42b8d62e1b43897 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 14:20:14 +0100 Subject: [PATCH 06/19] inception(workflow-inception-process): add show-dismissed.md recovery flow Co-Authored-By: Claude Opus 4.7 (1M context) --- .../references/show-dismissed.md | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 skills/workflow-inception-process/references/show-dismissed.md diff --git a/skills/workflow-inception-process/references/show-dismissed.md b/skills/workflow-inception-process/references/show-dismissed.md new file mode 100644 index 00000000..2395419d --- /dev/null +++ b/skills/workflow-inception-process/references/show-dismissed.md @@ -0,0 +1,74 @@ +# Show Dismissed + +*Reference for **[workflow-inception-process](../SKILL.md)*** + +--- + +Recovery flow for the refinement session. Surfaces topic names previously removed from the map and offers re-add. Loaded by **[refinement-session.md](refinement-session.md)** when the user asks to see dismissed items. + +## A. Read Dismissed List + +Read `phases.inception.dismissed` from the manifest: + +```bash +node .claude/skills/workflow-manifest/scripts/manifest.cjs get {work_unit}.inception dismissed +``` + +The result is either an array of topic names or empty (returns `2` exit code if the field is missing — treat that as empty). + +#### If empty + +> *Output the next fenced block as a code block:* + +``` +Dismissed Topics + + (none) +``` + +→ Return to caller. + +#### Otherwise + +→ Proceed to **B. Render and Prompt**. + +## B. Render and Prompt + +> *Output the next fenced block as a code block:* + +``` +Dismissed Topics + +@foreach(name in dismissed) + • {name} +@endforeach +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +· · · · · · · · · · · · +Re-add any of these to the map? + +- **Name them** — Tell me which to re-add (and routing if known) +- **`b`/`back`** — Return to the refinement session +· · · · · · · · · · · · +``` + +**STOP.** Wait for user response. + +#### If `back` + +→ Return to caller. + +#### If the user names one or more dismissed items to re-add + +Treat the response as an Add intent and dispatch to the Add flow: + +→ Load **[map-operations.md](map-operations.md)** and follow its instructions as written. + +`map-operations.md` validates each name (collision check is satisfied — dismissed-list match is allowed for Add and triggers a `pull` from the dismissed list before `init-phase`), STOP-gates on the batch, applies the writes, appends a Changes entry to the session log, and commits. + +When `map-operations.md` returns: + +→ Return to caller. From 24f9b5ce00aa950352a82d6564db6812f60a9937 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 14:20:35 +0100 Subject: [PATCH 07/19] inception(workflow-inception-process): wire refinement operations loop in refinement-session Co-Authored-By: Claude Opus 4.7 (1M context) --- .../references/refinement-session.md | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/skills/workflow-inception-process/references/refinement-session.md b/skills/workflow-inception-process/references/refinement-session.md index bc3c07d6..55ae431b 100644 --- a/skills/workflow-inception-process/references/refinement-session.md +++ b/skills/workflow-inception-process/references/refinement-session.md @@ -130,10 +130,41 @@ What would you like to change? ## D. Operations Loop -Phase 6 wiring landed in a follow-up commit. Until then, this section is a stub — when the user's first message arrives, route directly to **E. Conclude** so the bridge still resolves cleanly. +The user's message names one or more changes in natural language, asks to see dismissed items, or signals they're done. + +**Reserved phrase — show dismissed**: if the user's message is a request to see what's been removed (e.g. *"show dismissed"*, *"what was removed"*, *"let me see what I dropped"*), load **[show-dismissed.md](show-dismissed.md)** and follow its instructions as written. After it returns, → Proceed to **D.1. Anything Else?**. + +**Reserved phrase — conclude**: if the user signals they're done (e.g. *"no"*, *"done"*, *"that's it"*, *"all good"*, *"wrap up"*), → Proceed to **E. Conclude**. + +**Otherwise** — the message names operations. Dispatch: + +→ Load **[map-operations.md](map-operations.md)** and follow its instructions as written. + +`map-operations.md` parses operations, applies safety-by-destructiveness gating (additive batched, destructive per-item), writes the manifest, appends to the in-progress refinement session log, and commits per its own pattern. When it returns, → Proceed to **D.1. Anything Else?**. + +### D.1. Anything Else? + +> *Output the next fenced block as markdown (not a code block):* + +``` +· · · · · · · · · · · · +Anything else to change? + +- **Tell me what's next** — Name more changes (or "show dismissed") +- **`d`/`done`** — Conclude refinement and return to the epic menu +· · · · · · · · · · · · +``` + +**STOP.** Wait for user response. + +#### If `done` → Proceed to **E. Conclude**. +#### Otherwise + +→ Return to **D. Operations Loop**. + ## E. Conclude > *Output the next fenced block as a code block:* From 4e0ee446be064a1c5c36370255bdb70025f60faa Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 14:22:23 +0100 Subject: [PATCH 08/19] inception(workflow-manifest): tests for hard-delete + dismissed-list manifest surface Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/scripts/test-refinement-session.cjs | 287 ++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 tests/scripts/test-refinement-session.cjs diff --git a/tests/scripts/test-refinement-session.cjs b/tests/scripts/test-refinement-session.cjs new file mode 100644 index 00000000..e0cff836 --- /dev/null +++ b/tests/scripts/test-refinement-session.cjs @@ -0,0 +1,287 @@ +'use strict'; + +const { describe, it, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { spawnSync } = require('child_process'); + +const MANIFEST_CLI = path.resolve( + __dirname, '..', '..', 'skills', 'workflow-manifest', 'scripts', 'manifest.cjs' +); +const { computeTopicLifecycle } = require( + path.resolve(__dirname, '..', '..', 'skills', 'workflow-shared', 'scripts', 'discovery-utils.cjs') +); + +let dir; + +function setupFixture() { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'refinement-test-')); + fs.mkdirSync(path.join(dir, '.workflows'), { recursive: true }); +} + +function cleanupFixture() { + if (dir) fs.rmSync(dir, { recursive: true, force: true }); + dir = null; +} + +function runCli(...args) { + const result = spawnSync('node', [MANIFEST_CLI, ...args], { cwd: dir, encoding: 'utf8' }); + return { + stdout: result.stdout, + stderr: result.stderr, + status: result.status, + }; +} + +function readManifest(workUnit) { + return JSON.parse(fs.readFileSync( + path.join(dir, '.workflows', workUnit, 'manifest.json'), 'utf8' + )); +} + +function seedEpic(workUnit, items = {}) { + const manifestDir = path.join(dir, '.workflows', workUnit); + fs.mkdirSync(manifestDir, { recursive: true }); + const manifest = { + name: workUnit, + work_type: 'epic', + status: 'in-progress', + description: `Test: ${workUnit}`, + phases: { inception: { items } }, + }; + fs.writeFileSync( + path.join(manifestDir, 'manifest.json'), + JSON.stringify(manifest, null, 2), + ); + // Register in project manifest so workUnitNames() lists it. + const projDir = path.join(dir, '.workflows'); + const projPath = path.join(projDir, 'manifest.json'); + let proj = {}; + if (fs.existsSync(projPath)) { + proj = JSON.parse(fs.readFileSync(projPath, 'utf8')); + } + if (!proj.work_units) proj.work_units = {}; + proj.work_units[workUnit] = { work_type: 'epic' }; + fs.writeFileSync(projPath, JSON.stringify(proj, null, 2)); +} + +describe('refinement: hard-delete on remove', () => { + beforeEach(setupFixture); + afterEach(cleanupFixture); + + it('removes the named topic and leaves siblings intact', () => { + seedEpic('payments', { + 'auth-flow': { status: 'in-progress', summary: 'auth', routing: 'research', source: 'inception' }, + 'billing-history': { status: 'in-progress', summary: 'billing', routing: 'discussion', source: 'inception' }, + 'tax-handling': { status: 'in-progress', summary: 'tax', routing: 'research', source: 'inception' }, + }); + + const r = runCli('delete', 'payments.inception', 'items.billing-history'); + assert.strictEqual(r.status, 0, `delete failed: ${r.stderr}`); + + const m = readManifest('payments'); + assert.ok(!('billing-history' in m.phases.inception.items), 'billing-history still present'); + assert.ok('auth-flow' in m.phases.inception.items, 'auth-flow was removed'); + assert.ok('tax-handling' in m.phases.inception.items, 'tax-handling was removed'); + }); + + it('returns expected-miss exit code when the topic does not exist', () => { + seedEpic('payments', { + 'auth-flow': { status: 'in-progress', routing: 'research', source: 'inception' }, + }); + + const r = runCli('delete', 'payments.inception', 'items.nonexistent'); + assert.strictEqual(r.status, 2, `unexpected exit code: ${r.stderr}`); + }); +}); + +describe('refinement: dismissed list', () => { + beforeEach(setupFixture); + afterEach(cleanupFixture); + + it('push adds a name to phases.inception.dismissed', () => { + seedEpic('payments', { + 'auth-flow': { status: 'in-progress', routing: 'research', source: 'inception' }, + }); + + const r = runCli('push', 'payments.inception', 'dismissed', 'old-topic'); + assert.strictEqual(r.status, 0, `push failed: ${r.stderr}`); + + const m = readManifest('payments'); + assert.deepStrictEqual(m.phases.inception.dismissed, ['old-topic']); + }); + + it('push appends multiple names in order', () => { + seedEpic('payments', {}); + runCli('push', 'payments.inception', 'dismissed', 'first'); + runCli('push', 'payments.inception', 'dismissed', 'second'); + runCli('push', 'payments.inception', 'dismissed', 'third'); + + const m = readManifest('payments'); + assert.deepStrictEqual(m.phases.inception.dismissed, ['first', 'second', 'third']); + }); + + it('pull removes a named entry from the dismissed list', () => { + seedEpic('payments', {}); + runCli('push', 'payments.inception', 'dismissed', 'first'); + runCli('push', 'payments.inception', 'dismissed', 'second'); + runCli('push', 'payments.inception', 'dismissed', 'third'); + + const r = runCli('pull', 'payments.inception', 'dismissed', 'second'); + assert.strictEqual(r.status, 0, `pull failed: ${r.stderr}`); + + const m = readManifest('payments'); + assert.deepStrictEqual(m.phases.inception.dismissed, ['first', 'third']); + }); + + it('pull is a no-op when the entry is missing', () => { + seedEpic('payments', {}); + runCli('push', 'payments.inception', 'dismissed', 'first'); + + const r = runCli('pull', 'payments.inception', 'dismissed', 'nonexistent'); + assert.strictEqual(r.status, 0); + + const m = readManifest('payments'); + assert.deepStrictEqual(m.phases.inception.dismissed, ['first']); + }); + + it('get returns the dismissed list as JSON', () => { + seedEpic('payments', {}); + runCli('push', 'payments.inception', 'dismissed', 'a'); + runCli('push', 'payments.inception', 'dismissed', 'b'); + + const r = runCli('get', 'payments.inception', 'dismissed'); + assert.strictEqual(r.status, 0); + const parsed = JSON.parse(r.stdout); + assert.deepStrictEqual(parsed, ['a', 'b']); + }); +}); + +describe('refinement: rename mechanical sequence', () => { + beforeEach(setupFixture); + afterEach(cleanupFixture); + + it('preserves summary, routing, and source after a rename', () => { + seedEpic('payments', { + 'old-name': { + status: 'in-progress', + summary: 'an old summary', + routing: 'research', + source: 'inception', + }, + }); + + // 1. Read old fields (scalar values come back as raw text + trailing newline) + const summary = runCli('get', 'payments.inception.old-name', 'summary').stdout.trimEnd(); + const routing = runCli('get', 'payments.inception.old-name', 'routing').stdout.trimEnd(); + const src = runCli('get', 'payments.inception.old-name', 'source').stdout.trimEnd(); + + assert.strictEqual(summary, 'an old summary'); + assert.strictEqual(routing, 'research'); + assert.strictEqual(src, 'inception'); + + // 2. Delete old key, init new key, write fields + assert.strictEqual(runCli('delete', 'payments.inception', 'items.old-name').status, 0); + assert.strictEqual(runCli('init-phase', 'payments.inception.new-name').status, 0); + assert.strictEqual(runCli('set', 'payments.inception.new-name', 'summary', summary).status, 0); + assert.strictEqual(runCli('set', 'payments.inception.new-name', 'routing', routing).status, 0); + assert.strictEqual(runCli('set', 'payments.inception.new-name', 'source', src).status, 0); + + const m = readManifest('payments'); + assert.ok(!('old-name' in m.phases.inception.items), 'old-name still present'); + assert.ok('new-name' in m.phases.inception.items, 'new-name not created'); + assert.strictEqual(m.phases.inception.items['new-name'].summary, 'an old summary'); + assert.strictEqual(m.phases.inception.items['new-name'].routing, 'research'); + assert.strictEqual(m.phases.inception.items['new-name'].source, 'inception'); + assert.strictEqual(m.phases.inception.items['new-name'].status, 'in-progress'); + }); +}); + +describe('refinement: lifecycle gate via computeTopicLifecycle', () => { + beforeEach(setupFixture); + afterEach(cleanupFixture); + + it('reports fresh for a topic with no research/discussion items', () => { + const manifest = { + phases: { + inception: { items: { 'newtopic': { status: 'in-progress', routing: 'research' } } }, + }, + }; + const lc = computeTopicLifecycle(manifest, 'newtopic'); + assert.strictEqual(lc.lifecycle, 'fresh'); + assert.strictEqual(lc.tier, '○'); + }); + + it('reports researching when research is in-progress', () => { + const manifest = { + phases: { + inception: { items: { 'auth': { status: 'in-progress', routing: 'research' } } }, + research: { items: { 'auth': { status: 'in-progress' } } }, + }, + }; + const lc = computeTopicLifecycle(manifest, 'auth'); + assert.strictEqual(lc.lifecycle, 'researching'); + assert.strictEqual(lc.tier, '◐'); + }); + + it('reports discussing when discussion is in-progress', () => { + const manifest = { + phases: { + inception: { items: { 'auth': { status: 'in-progress', routing: 'discussion' } } }, + discussion: { items: { 'auth': { status: 'in-progress' } } }, + }, + }; + const lc = computeTopicLifecycle(manifest, 'auth'); + assert.strictEqual(lc.lifecycle, 'discussing'); + }); + + it('reports decided when discussion is completed', () => { + const manifest = { + phases: { + inception: { items: { 'auth': { status: 'in-progress', routing: 'discussion' } } }, + discussion: { items: { 'auth': { status: 'completed' } } }, + }, + }; + const lc = computeTopicLifecycle(manifest, 'auth'); + assert.strictEqual(lc.lifecycle, 'decided'); + }); + + it('reports cancelled only when both research and discussion are cancelled', () => { + const both = { + phases: { + inception: { items: { 'auth': { status: 'in-progress', routing: 'research' } } }, + research: { items: { 'auth': { status: 'cancelled' } } }, + discussion: { items: { 'auth': { status: 'cancelled' } } }, + }, + }; + assert.strictEqual(computeTopicLifecycle(both, 'auth').lifecycle, 'cancelled'); + + const onlyResearchCancelled = { + phases: { + inception: { items: { 'auth': { status: 'in-progress', routing: 'research' } } }, + research: { items: { 'auth': { status: 'cancelled' } } }, + }, + }; + assert.strictEqual(computeTopicLifecycle(onlyResearchCancelled, 'auth').lifecycle, 'fresh'); + }); + + it('only fresh is allowed for destructive map operations', () => { + const lifecycles = [ + { lifecycle: 'fresh', allowed: true }, + { lifecycle: 'researching', allowed: false }, + { lifecycle: 'ready_for_discussion', allowed: false }, + { lifecycle: 'discussing', allowed: false }, + { lifecycle: 'decided', allowed: false }, + { lifecycle: 'cancelled', allowed: false }, + ]; + const isAllowed = lc => lc === 'fresh'; + for (const { lifecycle, allowed } of lifecycles) { + assert.strictEqual( + isAllowed(lifecycle), allowed, + `lifecycle "${lifecycle}" expected allowed=${allowed}` + ); + } + }); +}); From d9a55681115a329f83cdf3039535f1d623c60fa7 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 14:23:31 +0100 Subject: [PATCH 09/19] docs(discovery-map): record PR #271 for Phase 6 Co-Authored-By: Claude Opus 4.7 (1M context) --- discovery-map/INDEX.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discovery-map/INDEX.md b/discovery-map/INDEX.md index 89b49847..d7e1f688 100644 --- a/discovery-map/INDEX.md +++ b/discovery-map/INDEX.md @@ -15,7 +15,7 @@ Each phase is its own PR off the previous phase's branch (stacked PRs — see *B 3. **[Inception Process Skill](phase-03-inception-process.md)** — the conversational inception session itself (initial-session flow). **Status:** Review (PR #268) 4. **[Wire start-epic to Inception](phase-04-wire-start-epic.md)** — collapses the research/discussion menu; user-visible flip. **Status:** Review (PR #269) 5. **[Discovery Map Render](phase-05-discovery-map-render.md)** — continue-epic display + menu collapse + auto-routing. **Status:** Review (PR #270) -6. **[Refinement Session](phase-06-refinement-session.md)** — re-entry path, map editing operations, safety-by-destructiveness. **Status:** Not started +6. **[Refinement Session](phase-06-refinement-session.md)** — re-entry path, map editing operations, safety-by-destructiveness. **Status:** Review (PR #271) 7. **[Self-Healing Re-point](phase-07-self-healing.md)** — research-analysis and gap-analysis re-point to map at continue-epic boot-up. **Status:** Not started 8. **[Imports](phase-08-imports.md)** — `imports/` directory, manifest tracking, KB indexing, behaviour change for features. **Status:** Not started 9. **[Topic Splitting and Elevation](phase-09-topic-splitting-elevation.md)** — write inception items alongside; name collision validation. **Status:** Not started @@ -87,7 +87,7 @@ The merge sequence at the end of the initiative is **strictly bottom-to-top of t | `idea/inception-pr-3-process` | Phase 2 branch | Phase 3 — Inception Process Skill | [#268](https://github.com/leeovery/agentic-workflows/pull/268) | Review | | `idea/inception-pr-4-wire-start-epic` | Phase 3 branch | Phase 4 — Wire start-epic to Inception | [#269](https://github.com/leeovery/agentic-workflows/pull/269) | Review | | `idea/inception-pr-5-map-render` | Phase 4 branch | Phase 5 — Discovery Map Render | [#270](https://github.com/leeovery/agentic-workflows/pull/270) | Review | -| `idea/inception-pr-6-refinement` | Phase 5 branch | Phase 6 — Refinement Session | — | Not started | +| `idea/inception-pr-6-refinement` | Phase 5 branch | Phase 6 — Refinement Session | [#271](https://github.com/leeovery/agentic-workflows/pull/271) | Review | | `idea/inception-pr-7-self-healing` | Phase 6 branch | Phase 7 — Self-Healing Re-point | — | Not started | | `idea/inception-pr-8-imports` | Phase 7 branch | Phase 8 — Imports | — | Not started | | `idea/inception-pr-9-split-elevation` | Phase 7 branch | Phase 9 — Topic Splitting and Elevation | — | Not started | From 0fba2ccf75c4df32e65381ea518541115bde768a Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 14:56:55 +0100 Subject: [PATCH 10/19] inception(workflow-inception-process): restructure map-operations.md to H2-only with per-branch routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote per-operation handlers (Add, Edit summary, Remove, Rename, Change routing) from H3 sub-sections to H2 letters D-H. Replace the single trailing "→ Proceed to the next operation" with explicit "→ Return to **C. Apply** for the next group." routing on every If yes/If no branch. Inline B.1/B.2 lifecycle and collision gates as bold paragraphs in B. Validate. C. Apply becomes the dispatcher, routing each parsed group by op type. Adds I. Done as the terminal return-to-caller. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../references/map-operations.md | 154 +++++++++++------- 1 file changed, 91 insertions(+), 63 deletions(-) diff --git a/skills/workflow-inception-process/references/map-operations.md b/skills/workflow-inception-process/references/map-operations.md index cdf1e5c6..fc2f73cb 100644 --- a/skills/workflow-inception-process/references/map-operations.md +++ b/skills/workflow-inception-process/references/map-operations.md @@ -6,7 +6,7 @@ Per-operation handling for refinement. Owns parsing, validation, manifest writes, session-log entries, and commits. Loaded by **[refinement-session.md](refinement-session.md)** when the user names one or more changes. -The parent reference owns the conversation shape; this file owns the writes. After completing the user's batch, return to caller. +The parent reference owns the conversation shape; this file owns the writes. After all of the user's operations have been processed, return to caller. ## A. Parse Operations @@ -24,34 +24,32 @@ If routing is omitted on Add, infer from cues in the user's framing (factual unk If the message is ambiguous (e.g. *"fix X"*, *"that one looks wrong"*), ask one clarifying question before proceeding. No STOP gate is needed for clarification — it's part of conversational flow, not a manifest write. -**Classify operations:** +**Group operations** for safety-by-destructiveness: -- **Additive** — Add, Edit summary. Batched. -- **Destructive** — Remove, Rename, Change routing. Per-item. +- **Additive group** — a contiguous run of Add operations *or* a contiguous run of Edit summary operations. Each group batches into one STOP gate, one commit, one session-log entry. +- **Destructive group** — a single Remove, Rename, or Change routing operation. Each is its own group of one with its own STOP gate and commit. -**Process order:** in the order the user listed them. For pure additive batches, group into a single STOP gate, single commit, single session-log entry covering the batch. For pure destructive batches, per-item. For mixed batches, walk in order — destructive ops gate per-item, the contiguous additive ops in between can batch. +Walk the groups in user order. For mixed batches (e.g. *"remove A, rename B to B2, add C"*), each destructive op is its own group; contiguous additive ops in between batch. → Proceed to **B. Validate**. ## B. Validate -Apply per-operation validation gates **before** any STOP gate. If validation fails, surface the rejection with a clear next-step pointer (don't just say "blocked") and skip that operation. Continue with the rest of the batch. +Apply per-operation validation gates **before** any STOP gate. If validation fails for an operation, surface the rejection with a clear next-step pointer (don't just say "blocked") and remove the operation from its group. Continue with the rest. -### B.1. Lifecycle gates +**Lifecycle gates** — for destructive operations (Remove, Rename, Change routing), compute the topic's lifecycle via `computeTopicLifecycle(manifest, topicName)` from `discovery-utils.cjs`. The operation is allowed only when: -For destructive operations (Remove, Rename, Change routing), compute the topic's lifecycle via `computeTopicLifecycle(manifest, topicName)` from `discovery-utils.cjs`. The operation is allowed only when: - -| Operation | Allowed lifecycles | Disallowed | -| --------------- | ------------------ | -------------------------------------------- | +| Operation | Allowed lifecycles | Disallowed | +| --------------- | ------------------ | --------------------------------------------------------------------------- | | Remove | `fresh` | `researching`, `discussing`, `ready_for_discussion`, `decided`, `cancelled` | -| Rename | `fresh` | all others | -| Change routing | `fresh` | all others (routing is implicit once a phase item exists) | -| Edit summary | any | — | -| Add | n/a (new item) | — | +| Rename | `fresh` | all others | +| Change routing | `fresh` | all others (routing is implicit once a phase item exists) | +| Edit summary | any | — | +| Add | n/a (new item) | — | -Note: `cancelled` is also disallowed for Remove because the inception item is the historical record of the topic ever having existed. Removal is for never-started topics only; cancel-then-vanish would erase audit trail. The `a`/`cancel` flow in `/continue-epic` is the right tool for stopping in-flight work. +`cancelled` is also disallowed for Remove because the inception item is the historical record of the topic ever having existed. Removal is for never-started topics only; cancel-then-vanish would erase the audit trail. The `a`/`cancel` flow in `/continue-epic` is the right tool for stopping in-flight work. -**Rejection messages** — render in a code block, then continue with the rest of the batch: +Render the rejection in a code block: > *Output the next fenced block as a code block:* @@ -69,9 +67,7 @@ Note: `cancelled` is also disallowed for Remove because the inception item is th - `decided` — `discussion has concluded` - `cancelled` — `it has phase work in cancelled state and stays on the map as historical record` -### B.2. Name collision gates - -For Add and Rename, the new name is rejected if an **active** map item already uses it (case-sensitive match against `phases.inception.items.{name}`). Render: +**Name collision gates** — for Add and Rename, the new name is rejected if an **active** map item already uses it (case-sensitive match against `phases.inception.items.{name}`): > *Output the next fenced block as a code block:* @@ -82,17 +78,39 @@ edit-summary / change-routing on the existing item. For Add, a name appearing in `phases.inception.dismissed` is **allowed** — it counts as a re-add. The Add flow pulls the name from the dismissed list before creating the new item. -→ Proceed to **C. Apply Operations**. +→ Proceed to **C. Apply**. + +## C. Apply + +Walk the validated operation groups in user order. For the next pending group: + +#### If the group is one or more Add operations + +→ Proceed to **D. Add**. + +#### If the group is one or more Edit summary operations + +→ Proceed to **E. Edit Summary**. + +#### If the group is a Remove operation -## C. Apply Operations +→ Proceed to **F. Remove**. -Walk the validated operations in user order. For each, render the proposed change, gate per the safety rule, apply via the manifest CLI, append to the session log, and commit. +#### If the group is a Rename operation -The session log path is `.workflows/{work_unit}/inception/session-{NNN}.md` (already initialised by the parent reference's **C.1**). Append entries under the **Changes** section in user order. Replace `(none)` with the first entry. +→ Proceed to **G. Rename**. -### C.1. Add (additive — batched) +#### If the group is a Change routing operation -For a contiguous run of Add operations, render the proposal once: +→ Proceed to **H. Change Routing**. + +#### Otherwise (no groups remain) + +→ Proceed to **I. Done**. + +## D. Add + +Render the proposal once for the whole batch: > *Output the next fenced block as a code block:* @@ -119,7 +137,9 @@ Add all? #### If `no` -Skip the batch. Continue with the next operation in the user's list (if any) or return to caller. +Skip the batch. No manifest writes, no session-log entry, no commit. + +→ Return to **C. Apply** for the next group. #### If `yes` @@ -142,7 +162,7 @@ For each name in the batch: Source is `inception` for refinement-added topics — they are user-curated, indistinguishable from initial-session items for provenance purposes. -3. Append a single batch entry to the session log under **Changes** (one bullet per name): +3. Append a single batch entry to the session log under **Changes** (one bullet per name). If the section currently reads `(none)`, replace it with the bullets: ```markdown - Added: {name_1} (routing: {research|discussion}, source: inception) — {short rationale} @@ -152,15 +172,15 @@ For each name in the batch: 4. Single commit covering all adds in the batch: ```bash - git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-*.md + git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-{NNN}.md git commit -m "inception({work_unit}): add {N} topic(s) to map" ``` -→ Proceed to the next operation in the user's list. +→ Return to **C. Apply** for the next group. -### C.2. Edit summary (additive — batched) +## E. Edit Summary -For a contiguous run of Edit summary operations, render the proposal once: +Render the proposal once for the whole batch: > *Output the next fenced block as a code block:* @@ -187,7 +207,9 @@ Apply? #### If `no` -Skip the batch. Continue. +Skip the batch. No manifest writes, no session-log entry, no commit. + +→ Return to **C. Apply** for the next group. #### If `yes` @@ -197,7 +219,7 @@ For each: node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{name} summary "{new summary}" ``` -Append a single batch entry to the session log under **Changes**: +Append a single batch entry to the session log under **Changes**. If the section currently reads `(none)`, replace it with the bullets: ```markdown - Edited summary: {name_1} — {short note} @@ -207,15 +229,15 @@ Append a single batch entry to the session log under **Changes**: Single commit: ```bash -git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-*.md +git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-{NNN}.md git commit -m "inception({work_unit}): edit {N} summary(ies)" ``` -→ Proceed to the next operation in the user's list. +→ Return to **C. Apply** for the next group. -### C.3. Remove (destructive — per-item) +## F. Remove -For each Remove operation: +Render the proposal: > *Output the next fenced block as a code block:* @@ -242,7 +264,9 @@ Confirm removal? #### If `no` -Skip this operation. Continue with the next. +Skip this operation. No manifest writes, no session-log entry, no commit. + +→ Return to **C. Apply** for the next group. #### If `yes` @@ -253,7 +277,7 @@ node .claude/skills/workflow-manifest/scripts/manifest.cjs delete {work_unit}.in node .claude/skills/workflow-manifest/scripts/manifest.cjs push {work_unit}.inception dismissed "{name}" ``` -Append a Changes entry to the session log: +Append a Changes entry to the session log. If the section currently reads `(none)`, replace it with the bullet: ```markdown - Removed: {name} — {short reason} @@ -262,15 +286,15 @@ Append a Changes entry to the session log: Per-item commit: ```bash -git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-*.md +git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-{NNN}.md git commit -m "inception({work_unit}): remove {name} from map" ``` -→ Proceed to the next operation in the user's list. +→ Return to **C. Apply** for the next group. -### C.4. Rename (destructive — per-item) +## G. Rename -For each Rename operation: +Render the proposal: > *Output the next fenced block as a code block:* @@ -296,27 +320,29 @@ Confirm rename? #### If `no` -Skip this operation. Continue. +Skip this operation. No manifest writes, no session-log entry, no commit. + +→ Return to **C. Apply** for the next group. #### If `yes` Read the existing fields, delete the old key, create the new key, re-write the fields: ```bash -summary=$(node .claude/skills/workflow-manifest/scripts/manifest.cjs get {work_unit}.inception.{old} summary) -routing=$(node .claude/skills/workflow-manifest/scripts/manifest.cjs get {work_unit}.inception.{old} routing) -source=$(node .claude/skills/workflow-manifest/scripts/manifest.cjs get {work_unit}.inception.{old} source) +saved_summary=$(node .claude/skills/workflow-manifest/scripts/manifest.cjs get {work_unit}.inception.{old} summary) +saved_routing=$(node .claude/skills/workflow-manifest/scripts/manifest.cjs get {work_unit}.inception.{old} routing) +saved_source=$(node .claude/skills/workflow-manifest/scripts/manifest.cjs get {work_unit}.inception.{old} source) node .claude/skills/workflow-manifest/scripts/manifest.cjs delete {work_unit}.inception items.{old} node .claude/skills/workflow-manifest/scripts/manifest.cjs init-phase {work_unit}.inception.{new} -node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{new} summary "$summary" -node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{new} routing "$routing" -node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{new} source "$source" +node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{new} summary "$saved_summary" +node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{new} routing "$saved_routing" +node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{new} source "$saved_source" ``` -If any command fails, surface the error and stop before the commit so the user can recover — the rename is partial otherwise. +If any command fails, surface the error and stop before the commit so the user can recover — a partial rename leaves the manifest in an inconsistent state otherwise. -Append a Changes entry to the session log: +Append a Changes entry to the session log. If the section currently reads `(none)`, replace it with the bullet: ```markdown - Renamed: {old} → {new} — {short reason} @@ -325,15 +351,15 @@ Append a Changes entry to the session log: Per-item commit: ```bash -git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-*.md +git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-{NNN}.md git commit -m "inception({work_unit}): rename {old} → {new}" ``` -→ Proceed to the next operation in the user's list. +→ Return to **C. Apply** for the next group. -### C.5. Change routing (destructive — per-item) +## H. Change Routing -For each Change-routing operation: +Render the proposal: > *Output the next fenced block as a code block:* @@ -359,7 +385,9 @@ Confirm routing change? #### If `no` -Skip this operation. Continue. +Skip this operation. No manifest writes, no session-log entry, no commit. + +→ Return to **C. Apply** for the next group. #### If `yes` @@ -367,7 +395,7 @@ Skip this operation. Continue. node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.inception.{name} routing {research|discussion} ``` -Append a Changes entry: +Append a Changes entry to the session log. If the section currently reads `(none)`, replace it with the bullet: ```markdown - Changed routing: {name} → {new routing} — {short reason} @@ -376,14 +404,14 @@ Append a Changes entry: Per-item commit: ```bash -git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-*.md +git add -- .workflows/{work_unit}/manifest.json .workflows/{work_unit}/inception/session-{NNN}.md git commit -m "inception({work_unit}): re-route {name} to {new routing}" ``` -→ Proceed to the next operation in the user's list. +→ Return to **C. Apply** for the next group. -## D. Done +## I. Done -Once all operations in the user's batch have been processed (applied or skipped), return to caller. The parent reference re-prompts with `Anything else?`. +All operation groups have been processed. → Return to caller. From 44ced9de515a3051510d5937e400c86be7e9dddd Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 15:00:59 +0100 Subject: [PATCH 11/19] inception(workflow-inception-process): make refinement-template (none) Conclusion the resume signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the (none) placeholder explicit at initialisation under Self-Healing Arrivals, Changes, and Conclusion. Document that the Conclusion is replaced at finalisation — including a "No changes applied — browse only" sentinel for browse-only refinements — so an interrupted session is unambiguously distinguishable from a concluded one on next refinement entry. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../references/refinement-template.md | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/skills/workflow-inception-process/references/refinement-template.md b/skills/workflow-inception-process/references/refinement-template.md index 72d29a61..0976a5c5 100644 --- a/skills/workflow-inception-process/references/refinement-template.md +++ b/skills/workflow-inception-process/references/refinement-template.md @@ -23,24 +23,29 @@ Example: `8 topics — 2 decided · 3 in flight · 1 ready · 2 fresh` ## Self-Healing Arrivals -{Items added by analyses since the last session, if any. Phase 6 -leaves this as `(none)` — Phase 7 fills it when analyses run.} - -- {topic} (added by {analysis}, source: {provenance}) +(none) ## Changes -- Added: {topic} (routing: {research|discussion}, source: inception) — {reason} -- Edited summary: {topic} — {short note} -- Renamed: {old} → {new} — {reason} -- Removed: {topic} — {reason} -- Changed routing: {topic} → {new routing} — {reason} +(none) ## Conclusion -{N} changes applied. Map now has {M} topics. +(none) ``` +## Initialisation vs finalisation + +The template is written to disk **at session start** with `(none)` placeholders under **Self-Healing Arrivals**, **Changes**, and **Conclusion**. The header and **Map State at Start** are populated immediately. + +- **Self-Healing Arrivals** — Phase 6 leaves `(none)`. Phase 7 will replace it with the items added by analyses (one bullet each: `- {topic} (added by {analysis}, source: {provenance})`) or leave `(none)` if no analyses ran. +- **Changes** — when the first operation is applied, the `(none)` placeholder is replaced with the operation bullet(s). Subsequent operations append. +- **Conclusion** — the `(none)` placeholder is replaced **only at finalisation** (after the operations loop ends). The replacement is one of: + - `{N} changes applied. Map now has {M} topics.` — when one or more changes were applied. + - `No changes applied — browse only. Map has {M} topics.` — when the user opened refinement, looked, and exited without changes. + +The `(none)` Conclusion is the **resume-detection signal**: if a later refinement entry finds a session log whose Conclusion is still `(none)`, that session was interrupted (context refresh, user exit) before finalisation. Always replace it at finalisation so the next entry sees a clean state. + ## Anti-patterns - No transcript-style content. The log is rationale, not dialogue. From ef0f88d39c8717e084ad9c54bd4a9684ddc645d1 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 15:01:08 +0100 Subject: [PATCH 12/19] inception(workflow-inception-process): update Resuming protocol to find latest session log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recovery instructions previously pointed at session-001.md only — wrong for a context refresh that lands during a refinement session. Updated to find the highest-numbered session log and distinguish initial (Topics Identified) from refinement (Changes section, with (none) Conclusion as the in-progress signal that routes through B. Resume Check in refinement-session.md). Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/workflow-inception-process/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/workflow-inception-process/SKILL.md b/skills/workflow-inception-process/SKILL.md index 8f6756da..d9e8453d 100644 --- a/skills/workflow-inception-process/SKILL.md +++ b/skills/workflow-inception-process/SKILL.md @@ -41,9 +41,9 @@ Follow these steps EXACTLY as written. Do not skip steps or combine them. Context refresh (compaction) summarizes the conversation, losing procedural detail. When you detect a context refresh has occurred — the conversation feels abruptly shorter, you lack memory of recent steps, or a summary precedes this message — follow this recovery protocol: 1. **Re-read this skill file completely.** Do not rely on your summary of it. The full process, steps, and rules must be reloaded. -2. **Read the session log** at `.workflows/{work_unit}/inception/session-001.md` if it exists. The Topics Identified section is your primary progress indicator — it shows which topics have already been surfaced and tentatively routed. +2. **Read the most recent session log** — find the highest-numbered file matching `.workflows/{work_unit}/inception/session-*.md`. For initial sessions this is `session-001.md` and the **Topics Identified** section is your primary progress indicator. For refinement sessions (`session-NNN.md`, NNN > 1) the **Changes** section shows what has already been applied; an unfinalised log has `(none)` under **Conclusion** and should be resumed via **B. Resume Check** in `references/refinement-session.md`. 3. **Check git state.** Run `git status` and `git log --oneline -10` to see recent commits. Commit messages reveal what has been completed. -4. **Announce your position** to the user before continuing: render the current working list of topics, state what step you believe you're at, and what comes next. Wait for confirmation. +4. **Announce your position** to the user before continuing: render the current working list (initial) or the changes applied so far (refinement), state what step you believe you're at, and what comes next. Wait for confirmation. Do not guess at progress or continue from memory. The files on disk and git history are authoritative — your recollection is not. From 7d20c7ce65939ac7931330d43317fb0aed5c93fc Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 15:01:23 +0100 Subject: [PATCH 13/19] inception(workflow-inception-process): restructure refinement-session.md to H2-only with Resume Check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote H3 sub-sections (C.1/C.2, D.1, E.1-E.4) to H2 letters so the file matches the H2-only convention used in other reference files (confirm-and-persist, conclude-inception, session-loop). New structure: A. Read State → B. Resume Check → C. Self-Healing → D. Initialise Session Log → E. Render and Prompt → F. Operations Loop → G. Anything Else? → H. Finalise Session Log → I. Compliance → J. Final Sweep → K. Bridge. Other corrections: - Drop step markers from inside lettered sections — step markers belong at backbone-step boundaries in SKILL.md, not at section transitions in reference files. - Convert F.'s bold "Reserved phrase" preludes to H4 conditionals, matching session-loop.md A. and conclude-inception.md A. - Make J. Final Sweep's dirty/clean branches each carry their own "→ Proceed to **K. Bridge**." routing per the conventional rule that every conditional branch is self-contained. - B. Resume Check adds the previously missing recovery path for a context refresh that lands mid-refinement: detect an in-progress refinement log via its (none) Conclusion, offer continue or restart, and either resume against the existing log or delete and fall through to a fresh seed. H. Finalise replaces (none) on every exit so the next entry sees a clean state. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../references/refinement-session.md | 166 ++++++++++++------ 1 file changed, 111 insertions(+), 55 deletions(-) diff --git a/skills/workflow-inception-process/references/refinement-session.md b/skills/workflow-inception-process/references/refinement-session.md index 55ae431b..708cd385 100644 --- a/skills/workflow-inception-process/references/refinement-session.md +++ b/skills/workflow-inception-process/references/refinement-session.md @@ -15,12 +15,6 @@ Two anti-patterns to avoid: ## A. Read State -> *Output the next fenced block as a code block:* - -``` -── Read Map State ─────────────────────────────── -``` - > *Output the next fenced block as markdown (not a code block):* ``` @@ -33,44 +27,77 @@ Load the work unit's manifest and read `phases.inception.items.*`. For each topi Also read `phases.inception.dismissed` (an array of previously removed topic names; may be missing or empty). The dismissed list governs name collision and the show-dismissed flow. -→ Proceed to **B. Self-Healing Check**. +→ Proceed to **B. Resume Check**. -## B. Self-Healing Check +## B. Resume Check -> *Output the next fenced block as a code block:* +Find the highest-numbered session log on disk: +```bash +ls .workflows/{work_unit}/inception/session-*.md 2>/dev/null | sort | tail -1 ``` -── Self-Healing Check ─────────────────────────── -``` + +If the result is empty or matches `session-001.md`, no refinement is in flight. Otherwise read the matched `session-NNN.md` and inspect its **Conclusion** section — `(none)` means the prior refinement was interrupted (context refresh or user exit) before finalisation. + +#### If only `session-001.md` exists or no log was found + +→ Proceed to **C. Self-Healing Check**. + +#### If a refinement log exists with non-`(none)` Conclusion + +The prior refinement concluded normally. Treat this as a fresh entry. + +→ Proceed to **C. Self-Healing Check**. + +#### If a refinement log exists with `(none)` Conclusion + +The prior refinement was interrupted. Offer continue or restart: > *Output the next fenced block as markdown (not a code block):* ``` -> Phase 6 placeholder — self-healing analyses (research-analysis, -> gap-analysis) are wired in Phase 7. Nothing runs here yet. +Found an in-progress refinement session log for **{work_unit:(titlecase)}**: `session-{NNN}.md`. + +· · · · · · · · · · · · +- **`c`/`continue`** — Pick up where you left off +- **`r`/`restart`** — Delete the draft refinement log and start fresh +· · · · · · · · · · · · ``` -No-op for Phase 6. Phase 7 will run the analyses inline at this point and apply results to the map (auto-add with `source: research-analysis` or `source: gap-analysis`, filtered against `phases.inception.dismissed`). +**STOP.** Wait for user response. -→ Proceed to **C. Open Refinement**. +**If `continue`:** -## C. Open Refinement +The active session log is `session-{NNN}.md`. No new log is initialised; subsequent operations append to the existing log. -> *Output the next fenced block as a code block:* +→ Proceed to **E. Render and Prompt**. +**If `restart`:** + +Delete the in-progress log and commit: + +```bash +rm .workflows/{work_unit}/inception/session-{NNN}.md +git add -- .workflows/{work_unit}/ +git commit -m "inception({work_unit}): restart refinement session" ``` -── Open Refinement ────────────────────────────── -``` + +→ Proceed to **C. Self-Healing Check**. + +## C. Self-Healing Check > *Output the next fenced block as markdown (not a code block):* ``` -> Refining the discovery map. Tell me what to change — add, -> edit, remove, rename, or re-route topics. Multiple changes in -> one message are fine; I'll work through them. +> Phase 6 placeholder — self-healing analyses (research-analysis, +> gap-analysis) are wired in Phase 7. Nothing runs here yet. ``` -### C.1. Initialise the refinement session log +No-op for Phase 6. Phase 7 will run the analyses inline at this point and apply results to the map (auto-add with `source: research-analysis` or `source: gap-analysis`, filtered against `phases.inception.dismissed`). Any items added by analyses will be recorded under **Self-Healing Arrivals** in the session log initialised in **D**. + +→ Proceed to **D. Initialise Session Log**. + +## D. Initialise Session Log Determine the next session number: @@ -79,7 +106,9 @@ n=$(ls .workflows/{work_unit}/inception/session-*.md 2>/dev/null | wc -l) next=$(printf "%03d" $((n + 1))) ``` -Create `.workflows/{work_unit}/inception/session-{next}.md` from **[refinement-template.md](refinement-template.md)**. Populate the header (date, work unit), fill **Map State at Start** with the summary line computed in **A. Read State**, leave **Self-Healing Arrivals**, **Changes**, and **Conclusion** as `(none)` placeholders — they fill in during the session. +Counts existing files (initial = `001`, prior refinements = `002+`) and increments to the next zero-padded value. + +Create `.workflows/{work_unit}/inception/session-{next}.md` from **[refinement-template.md](refinement-template.md)**. Populate the header (date, work unit) and **Map State at Start** with the summary line computed in **A**. Leave **Self-Healing Arrivals**, **Changes**, and **Conclusion** as `(none)` placeholders — they fill in as operations are applied and at finalisation. The `(none)` Conclusion is the resume-detection signal used by **B**. Commit: @@ -88,9 +117,19 @@ git add -- .workflows/{work_unit}/inception/session-{next}.md git commit -m "inception({work_unit}): seed refinement session log" ``` -### C.2. Render the map and prompt +→ Proceed to **E. Render and Prompt**. + +## E. Render and Prompt + +> *Output the next fenced block as markdown (not a code block):* + +``` +> Refining the discovery map. Tell me what to change — add, +> edit, remove, rename, or re-route topics. Multiple changes in +> one message are fine; I'll work through them. +``` -Render the current map as a compact anchor for the conversation: +Render the current map as a status-display anchor: > *Output the next fenced block as a code block:* @@ -118,6 +157,8 @@ Render the current map as a compact anchor for the conversation: - `⊘` — `cancelled` - No source provenance sub-line, no key block, no menu — this is an anchor, not the continue-epic display. +Then prompt the user: + > *Output the next fenced block as a code block:* ``` @@ -126,23 +167,33 @@ What would you like to change? **STOP.** Wait for user response. -→ Proceed to **D. Operations Loop**. +→ Proceed to **F. Operations Loop**. + +## F. Operations Loop -## D. Operations Loop +The user's most recent message names one or more changes in natural language, asks to see dismissed items, or signals they are done. -The user's message names one or more changes in natural language, asks to see dismissed items, or signals they're done. +#### If the user's message is a request to see dismissed items -**Reserved phrase — show dismissed**: if the user's message is a request to see what's been removed (e.g. *"show dismissed"*, *"what was removed"*, *"let me see what I dropped"*), load **[show-dismissed.md](show-dismissed.md)** and follow its instructions as written. After it returns, → Proceed to **D.1. Anything Else?**. +Triggers include *"show dismissed"*, *"what was removed"*, *"let me see what I dropped"*. -**Reserved phrase — conclude**: if the user signals they're done (e.g. *"no"*, *"done"*, *"that's it"*, *"all good"*, *"wrap up"*), → Proceed to **E. Conclude**. +Load **[show-dismissed.md](show-dismissed.md)** and follow its instructions as written. When it returns: -**Otherwise** — the message names operations. Dispatch: +→ Proceed to **G. Anything Else?**. -→ Load **[map-operations.md](map-operations.md)** and follow its instructions as written. +#### If the user's message signals they are done -`map-operations.md` parses operations, applies safety-by-destructiveness gating (additive batched, destructive per-item), writes the manifest, appends to the in-progress refinement session log, and commits per its own pattern. When it returns, → Proceed to **D.1. Anything Else?**. +Triggers include *"no"*, *"done"*, *"that's it"*, *"all good"*, *"wrap up"*. -### D.1. Anything Else? +→ Proceed to **H. Finalise Session Log**. + +#### Otherwise + +The message names operations. Load **[map-operations.md](map-operations.md)** and follow its instructions as written. It parses, validates, applies safety-by-destructiveness gating, writes the manifest, appends to the active session log, and commits per its own pattern. When it returns: + +→ Proceed to **G. Anything Else?**. + +## G. Anything Else? > *Output the next fenced block as markdown (not a code block):* @@ -159,47 +210,52 @@ Anything else to change? #### If `done` -→ Proceed to **E. Conclude**. +→ Proceed to **H. Finalise Session Log**. #### Otherwise -→ Return to **D. Operations Loop**. +→ Return to **F. Operations Loop**. -## E. Conclude +## H. Finalise Session Log -> *Output the next fenced block as a code block:* +Replace the `(none)` placeholder in the **Conclusion** section of `inception/session-{NNN}.md`. The replacement is non-optional — leaving `(none)` would make the log indistinguishable from an interrupted session on the next refinement entry. -``` -── Conclude Refinement ────────────────────────── -``` +#### If at least one operation was applied during the session -> *Output the next fenced block as markdown (not a code block):* +Replace `(none)` with: `{N} changes applied. Map now has {M} topics.` -``` -> Wrapping up. Finalising the session log, running compliance -> self-check, and bridging back to the epic menu. -``` +→ Proceed to **I. Compliance Self-Check**. + +#### Otherwise (browse-only refinement, no operations applied) -### E.1. Finalise the session log +Replace `(none)` with: `No changes applied — browse only. Map has {M} topics.` -Populate the **Conclusion** section of the in-progress `inception/session-{NNN}.md` with the change count and current map size. Skip the line if no changes were made (a "browse-only" refinement is a valid outcome). +→ Proceed to **I. Compliance Self-Check**. -### E.2. Compliance self-check +## I. Compliance Self-Check -Load **[compliance-check.md](../../workflow-shared/references/compliance-check.md)** and follow its instructions as written. The check audits the refinement against this file and the parent SKILL.md (plus any other references loaded during the session). Apply silent corrections inline; surface significant issues per the shared protocol. +Load **[compliance-check.md](../../workflow-shared/references/compliance-check.md)** and follow its instructions as written. The check audits the refinement against this file, the parent SKILL.md, and any other references loaded during the session (`map-operations.md`, `show-dismissed.md`, `refinement-template.md`). Apply silent corrections inline; surface significant issues per the shared protocol. -### E.3. Final sweep +→ Proceed to **J. Final Sweep**. -Check `git status`. If the working tree is dirty, commit residual changes: +## J. Final Sweep + +Check `git status`. + +#### If the working tree is dirty ```bash git add -- .workflows/{work_unit}/ git commit -m "inception({work_unit}): finalise refinement session log" ``` -If clean, skip. +→ Proceed to **K. Bridge**. + +#### If the working tree is clean + +→ Proceed to **K. Bridge**. -### E.4. Bridge +## K. Bridge > *Output the next fenced block as markdown (not a code block):* From cb79e66222a85f5e1330044652774768f9b2c1b4 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 15:03:28 +0100 Subject: [PATCH 14/19] =?UTF-8?q?inception(workflow-inception-process):=20?= =?UTF-8?q?use=20=E2=86=92=20Load=20for=20routing-decision=20loads=20in=20?= =?UTF-8?q?refinement-session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three load directives transfer control to a dependent reference and return — show-dismissed in F., map-operations in F., compliance-check in I. Per CONVENTIONS, reference-file loads that are routing decisions take a → prefix; the inline "use it to create" form (D. Initialise Session Log calling refinement-template.md) stays plain since it's a step-action, not control transfer. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../references/refinement-session.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/skills/workflow-inception-process/references/refinement-session.md b/skills/workflow-inception-process/references/refinement-session.md index 708cd385..66c55ff5 100644 --- a/skills/workflow-inception-process/references/refinement-session.md +++ b/skills/workflow-inception-process/references/refinement-session.md @@ -177,7 +177,9 @@ The user's most recent message names one or more changes in natural language, as Triggers include *"show dismissed"*, *"what was removed"*, *"let me see what I dropped"*. -Load **[show-dismissed.md](show-dismissed.md)** and follow its instructions as written. When it returns: +→ Load **[show-dismissed.md](show-dismissed.md)** and follow its instructions as written. + +When it returns: → Proceed to **G. Anything Else?**. @@ -189,7 +191,11 @@ Triggers include *"no"*, *"done"*, *"that's it"*, *"all good"*, *"wrap up"*. #### Otherwise -The message names operations. Load **[map-operations.md](map-operations.md)** and follow its instructions as written. It parses, validates, applies safety-by-destructiveness gating, writes the manifest, appends to the active session log, and commits per its own pattern. When it returns: +The message names operations. + +→ Load **[map-operations.md](map-operations.md)** and follow its instructions as written. + +`map-operations.md` parses, validates, applies safety-by-destructiveness gating, writes the manifest, appends to the active session log, and commits per its own pattern. When it returns: → Proceed to **G. Anything Else?**. @@ -234,7 +240,11 @@ Replace `(none)` with: `No changes applied — browse only. Map has {M} topics.` ## I. Compliance Self-Check -Load **[compliance-check.md](../../workflow-shared/references/compliance-check.md)** and follow its instructions as written. The check audits the refinement against this file, the parent SKILL.md, and any other references loaded during the session (`map-operations.md`, `show-dismissed.md`, `refinement-template.md`). Apply silent corrections inline; surface significant issues per the shared protocol. +→ Load **[compliance-check.md](../../workflow-shared/references/compliance-check.md)** and follow its instructions as written. + +The check audits the refinement against this file, the parent SKILL.md, and any other references loaded during the session (`map-operations.md`, `show-dismissed.md`, `refinement-template.md`). Apply silent corrections inline; surface significant issues per the shared protocol. + +When it returns: → Proceed to **J. Final Sweep**. From e0a4ddee22d7a4ca62c7806f211f2aeb75682c90 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 15:25:53 +0100 Subject: [PATCH 15/19] docs(discovery-map): add Phase 6 implementation notes to Phase 7 and Phase 11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two forward-looking heads-ups discovered while implementing Phase 6: - Phase 7 — refinement-session.md's B. Resume Check routes the `continue` choice directly to E. Render and Prompt, skipping C. Self-Healing. Phase 7 wires analyses into C and needs to decide whether continue re-runs them. Recommendation: don't. - Phase 11 — refinement assumes session-001.md exists. Legacy epics migrated into inception have no such log. Back-fill a placeholder session-001.md during migration so refinement's counter stays clean and "first refinement" doesn't get mis-titled "Initial Framing". Co-Authored-By: Claude Opus 4.7 (1M context) --- discovery-map/phase-07-self-healing.md | 1 + discovery-map/phase-11-migration.md | 1 + 2 files changed, 2 insertions(+) diff --git a/discovery-map/phase-07-self-healing.md b/discovery-map/phase-07-self-healing.md index 3b157f9f..d1863b34 100644 --- a/discovery-map/phase-07-self-healing.md +++ b/discovery-map/phase-07-self-healing.md @@ -61,3 +61,4 @@ Move `research-analysis` and `gap-analysis` from `workflow-discussion-entry` to - **Cache invalidation rules unchanged** — input checksum on the relevant files. Just the output target changes. - **Source deduplication** — if both analyses produce the same theme, dedupe at the analysis stage so it's added once with both source paths in the inception item's `source` field (e.g. `gap-analysis,research-analysis` or first-source-wins). - **Notification callout shows once.** When the user has seen the new items, the callout doesn't repeat on subsequent `continue-epic` boots — only when boot-up just added more. +- **`continue` resume bypasses C. Self-Healing Check** (Phase 6 left this as a no-op, so impact deferred to here). When `refinement-session.md` B. Resume Check routes the user's `continue` choice it goes straight to E. Render and Prompt — C and D are skipped because the existing session log is reused. When wiring analyses into C, decide whether continue should re-run them. Recommendation: don't — analyses ran on the prior entry, results are already on the map and recorded under the existing log's **Self-Healing Arrivals**, and re-running on resume would surface duplicate arrivals or churn the cache. The continue path effectively says "pick up the in-flight refinement as it stood"; fresh-entry / restart paths still flow through C and run normally. diff --git a/discovery-map/phase-11-migration.md b/discovery-map/phase-11-migration.md index 34ade7a0..5efef448 100644 --- a/discovery-map/phase-11-migration.md +++ b/discovery-map/phase-11-migration.md @@ -70,3 +70,4 @@ Seed the discovery map for existing in-progress epics so they continue to work u - **`migration-seeded` sub-classification** helps future analyses avoid re-proposing items the user accepted via migration. - **Hard-delete model applies** — no `cancelled` status on migrated inception items. They're all `active`. - The post-migration empty-summary walkthrough is part of refinement, not the migration script itself. Migration creates items with empty summaries; refinement detects and prompts. +- **Back-fill `session-001.md` for migrated epics.** Phase 6's refinement entry assumes `session-001.md` exists from initial inception. For legacy epics that never had inception, no such log is on disk — `refinement-session.md` D's `n=$(ls ... | wc -l)` would give `n=0`, seeding the first refinement as `session-001.md` and mis-titling it "Initial Framing". Either back-fill a placeholder `session-001.md` during migration (e.g. "Initial framing not recorded — pre-inception migration. Map seeded from existing research/discussion items.") or special-case the count in refinement when source classification is `migration-seeded`. Back-filling at migration time is the cleaner option — keeps refinement's logic simple and gives the user a discoverable record of where the map came from. From c249cde44b10ad33e43cac9d787f1d556752e98b Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 15:28:42 +0100 Subject: [PATCH 16/19] inception(workflow-inception-entry): inline source=refinement into Step 3 The validate-phase.md reference held three lines: signpost text (already in Step 3), set source=refinement, return. Per the skill file structure convention ("simple routing conditionals stay inline"), there's no progressive-disclosure value in keeping it separate. Inline the assignment in Step 3 and drop the reference file. invoke-skill.md doc updated to drop the via-validate-phase note. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/workflow-inception-entry/SKILL.md | 2 +- .../references/invoke-skill.md | 2 +- .../references/validate-phase.md | 11 ----------- 3 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 skills/workflow-inception-entry/references/validate-phase.md diff --git a/skills/workflow-inception-entry/SKILL.md b/skills/workflow-inception-entry/SKILL.md index 9f23c962..d35eea57 100644 --- a/skills/workflow-inception-entry/SKILL.md +++ b/skills/workflow-inception-entry/SKILL.md @@ -115,7 +115,7 @@ Set `source` = `first-session`. > session. ``` -Load **[validate-phase.md](references/validate-phase.md)** and follow its instructions as written. +Set `source` = `refinement` for the handoff. → Proceed to **Step 4**. diff --git a/skills/workflow-inception-entry/references/invoke-skill.md b/skills/workflow-inception-entry/references/invoke-skill.md index f41fdb6d..4434d733 100644 --- a/skills/workflow-inception-entry/references/invoke-skill.md +++ b/skills/workflow-inception-entry/references/invoke-skill.md @@ -9,7 +9,7 @@ This skill's purpose is now fulfilled. Construct the handoff and invoke the proc The `Source:` line in the handoff carries the value of `source` set earlier in the entry flow: - `first-session` — set in **Step 2** when no inception items exist for this work unit. -- `refinement` — set in **Step 3** via `validate-phase.md` when inception items already exist. +- `refinement` — set in **Step 3** when inception items already exist. The processing skill reads this field at Step 0 to decide whether to run the initial-session flow or open a refinement session. diff --git a/skills/workflow-inception-entry/references/validate-phase.md b/skills/workflow-inception-entry/references/validate-phase.md deleted file mode 100644 index 630635f2..00000000 --- a/skills/workflow-inception-entry/references/validate-phase.md +++ /dev/null @@ -1,11 +0,0 @@ -# Validate Phase - -*Reference for **[workflow-inception-entry](../SKILL.md)*** - ---- - -Inception items already exist for "{work_unit}" — this is a refinement session. - -Set `source` = `refinement` for the handoff. - -→ Return to caller. From d14e2b9ad9f0b95c52b6a57192c13c0ba44d0891 Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 15:41:59 +0100 Subject: [PATCH 17/19] inception(workflow-inception-process): extract first-session detection into its own reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 0 had grown into a flat stack of H4 conditionals at mixed nesting levels — source-route at top, then first-session resume detection (file checks, continue/restart STOP-gate, defensive N>1 guard) all hanging off the same H4 level despite being logically nested under the first-session branch. Visually it read like five peer conditions when it's actually two-deep. Mirror the refinement path: extract first-session resume into references/first-session-resume.md. Step 0 becomes a clean two-branch dispatcher — refinement loads refinement-session.md, first-session loads first-session-resume.md. Each loaded reference owns its own routing back to the appropriate Step (1 for fresh/restart, 2 for continue) or terminates on the defensive inconsistent-state guard. Renamed Step 0 to "Source-Aware Detection" since the resume detection logic now lives in the reference files. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/workflow-inception-process/SKILL.md | 71 ++--------------- .../references/first-session-resume.md | 77 +++++++++++++++++++ 2 files changed, 83 insertions(+), 65 deletions(-) create mode 100644 skills/workflow-inception-process/references/first-session-resume.md diff --git a/skills/workflow-inception-process/SKILL.md b/skills/workflow-inception-process/SKILL.md index d9e8453d..e70e870f 100644 --- a/skills/workflow-inception-process/SKILL.md +++ b/skills/workflow-inception-process/SKILL.md @@ -49,12 +49,12 @@ Do not guess at progress or continue from memory. The files on disk and git hist --- -## Step 0: Resume Detection +## Step 0: Source-Aware Detection > *Output the next fenced block as a code block:* ``` -── Resume Detection ───────────────────────────── +── Source-Aware Detection ─────────────────────── ``` > *Output the next fenced block as markdown (not a code block):* @@ -69,74 +69,15 @@ Read the `Source:` field from the handoff in the prior message. #### If `source` is `refinement` -Inception items already exist for this work unit. Open the refinement session — the initial-session detection logic below does not apply. +Inception items already exist for this work unit. Open the refinement session. Load **[refinement-session.md](references/refinement-session.md)** and follow its instructions as written. -#### Otherwise (`source` is `first-session`) +#### If `source` is `first-session` -The entry skill has already verified there are no inception items in the manifest. The remaining checks below recover from a context refresh that interrupted a prior first-session draft. +The entry skill has verified there are no inception items in the manifest. Check the inception directory for an interrupted draft before starting fresh. -Check whether any file matching `.workflows/{work_unit}/inception/session-*.md` exists. - -#### If no file exists - -→ Proceed to **Step 1**. - -#### If `session-001.md` is the only session file - -A prior in-session draft is on disk and the session was interrupted (likely a context refresh). Read the file, then offer continue or restart. - -> *Output the next fenced block as markdown (not a code block):* - -``` -Found an in-progress inception session log for **{work_unit:(titlecase)}**. - -· · · · · · · · · · · · -- **`c`/`continue`** — Pick up where you left off -- **`r`/`restart`** — Delete the draft session log and start fresh -· · · · · · · · · · · · -``` - -**STOP.** Wait for user response. - -#### If `continue` - -→ Proceed to **Step 2**. The draft session log is your working list — session-loop will brief the user on resume. - -#### If `restart` - -1. Delete the draft session log. -2. Commit: `inception({work_unit}): restart inception session`. - -→ Proceed to **Step 1**. - - - -#### If any `session-NNN.md` for N > 1 exists - -This is a defensive guard. The entry skill should have routed `source = refinement` when prior session logs and inception items exist, but the handoff said `first-session`. State is inconsistent — likely the inception items were removed from the manifest but the session logs were not. - -> *Output the next fenced block as a code block:* - -``` -●───────────────────────────────────────────────● - Inception — Inconsistent State -●───────────────────────────────────────────────● - -Prior inception session logs exist but the manifest reports no -inception items for "{work_unit}". -``` - -> *Output the next fenced block as markdown (not a code block):* - -``` -> Stopping here so you can reconcile. Either restore the -> manifest items (refinement re-entry) or archive the session -> logs out of the way (fresh first session). -``` - -**STOP.** Do not proceed — terminal condition. +Load **[first-session-resume.md](references/first-session-resume.md)** and follow its instructions as written. --- diff --git a/skills/workflow-inception-process/references/first-session-resume.md b/skills/workflow-inception-process/references/first-session-resume.md new file mode 100644 index 00000000..e92180ed --- /dev/null +++ b/skills/workflow-inception-process/references/first-session-resume.md @@ -0,0 +1,77 @@ +# First-Session Resume Detection + +*Reference for **[workflow-inception-process](../SKILL.md)*** + +--- + +The entry skill has already verified that no inception items exist in the manifest, so this is a first-session entry. This reference recovers from a context refresh that may have interrupted a prior first-session draft by checking the inception directory for session log files. + +## A. Detect Prior Session + +Check whether any file matching `.workflows/{work_unit}/inception/session-*.md` exists. + +#### If no file matches + +No prior draft on disk — start fresh. + +→ Return to **[the skill](../SKILL.md)** for **Step 1**. + +#### If `session-001.md` is the only matching file + +A prior in-session draft is on disk and the session was interrupted (likely a context refresh). Read the file, then offer continue or restart: + +> *Output the next fenced block as markdown (not a code block):* + +``` +Found an in-progress inception session log for **{work_unit:(titlecase)}**. + +· · · · · · · · · · · · +- **`c`/`continue`** — Pick up where you left off +- **`r`/`restart`** — Delete the draft session log and start fresh +· · · · · · · · · · · · +``` + +**STOP.** Wait for user response. + +**If `continue`:** + +The draft session log is your working list — `session-loop.md` will brief the user on resume. + +→ Return to **[the skill](../SKILL.md)** for **Step 2**. + +**If `restart`:** + +Delete the draft session log and commit: + +```bash +rm .workflows/{work_unit}/inception/session-001.md +git add -- .workflows/{work_unit}/ +git commit -m "inception({work_unit}): restart inception session" +``` + +→ Return to **[the skill](../SKILL.md)** for **Step 1**. + +#### If any `session-NNN.md` for N > 1 exists + +Defensive guard. The entry skill should have routed `source = refinement` when prior session logs and inception items both exist, but the handoff said `first-session`. State is inconsistent — likely the inception items were removed from the manifest while the session logs were retained. + +> *Output the next fenced block as a code block:* + +``` +●───────────────────────────────────────────────● + Inception — Inconsistent State +●───────────────────────────────────────────────● + +Prior inception session logs exist but the manifest reports no +inception items for "{work_unit}". +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +> Stopping here so you can reconcile. Either restore the +> manifest items (refinement re-entry) or archive the session +> logs out of the way (fresh first session). +``` + +**STOP.** Do not proceed — terminal condition. From 9164afe1ac3c89cc7f91d14e28204b075a3cfb9c Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 15:53:52 +0100 Subject: [PATCH 18/19] inception(workflow-inception-process): add discovery.cjs and route refinement state through it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refinement-session, map-operations, and show-dismissed had been asking Claude to import discovery-utils and call computeTopicLifecycle, read manifest fields, and parse session logs inline. That's not how discovery is handled anywhere else in the workflow system — each consumer skill ships its own scripts/discovery.cjs that imports from workflow-shared/scripts/discovery-utils.cjs and outputs structured text via Bash invocation. Mirror that pattern for refinement: - New skills/workflow-inception-process/scripts/discovery.cjs builds discovery_map (tier-sorted, with lifecycle/source_provenance/ current_phase per item), map_summary, dismissed list, and latest_session detection (filename, number, is_refinement, is_in_progress flag derived from the (none) Conclusion sentinel). Also derives next_session_number so reference files don't need to count files themselves. - SKILL.md allowed-tools whitelists the new script. - refinement-session.md A. Read State invokes discovery once; B. Resume Check, D. Initialise Session Log, E. Render and Prompt, H. Finalise Session Log all consume named output fields. D and H re-run discovery to pick up state changes (restart in B, applied operations in F). - map-operations.md A. Parse Operations re-runs discovery for fresh lifecycle data, B. Validate looks up lifecycle from discovery_map rather than calling computeTopicLifecycle inline, name-collision gate consults discovery_map and dismissed. - show-dismissed.md A re-runs discovery and reads the dismissed array from the output (replacing the manifest CLI get). - New tests/scripts/test-discovery-for-refinement.cjs covers discovery_map shape, sorting, lifecycle reflection, dismissed list extraction, latest_session in-progress detection via the (none) Conclusion sentinel, next_session_number derivation, and format() output. Tests: refinement-discovery 15/15, refinement-session manifest surface 14/14, manifest 214/214, continue-epic discovery 75/75, discovery-utils 94/94. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/workflow-inception-process/SKILL.md | 2 +- .../references/map-operations.md | 22 +- .../references/refinement-session.md | 80 ++--- .../references/show-dismissed.md | 12 +- .../scripts/discovery.cjs | 168 +++++++++++ .../scripts/test-discovery-for-refinement.cjs | 278 ++++++++++++++++++ 6 files changed, 516 insertions(+), 46 deletions(-) create mode 100644 skills/workflow-inception-process/scripts/discovery.cjs create mode 100644 tests/scripts/test-discovery-for-refinement.cjs diff --git a/skills/workflow-inception-process/SKILL.md b/skills/workflow-inception-process/SKILL.md index e70e870f..411230e8 100644 --- a/skills/workflow-inception-process/SKILL.md +++ b/skills/workflow-inception-process/SKILL.md @@ -1,7 +1,7 @@ --- name: workflow-inception-process user-invocable: false -allowed-tools: Bash(node .claude/skills/workflow-manifest/scripts/manifest.cjs) +allowed-tools: Bash(node .claude/skills/workflow-inception-process/scripts/discovery.cjs), Bash(node .claude/skills/workflow-manifest/scripts/manifest.cjs) --- # Inception Process diff --git a/skills/workflow-inception-process/references/map-operations.md b/skills/workflow-inception-process/references/map-operations.md index fc2f73cb..4b509940 100644 --- a/skills/workflow-inception-process/references/map-operations.md +++ b/skills/workflow-inception-process/references/map-operations.md @@ -6,11 +6,21 @@ Per-operation handling for refinement. Owns parsing, validation, manifest writes, session-log entries, and commits. Loaded by **[refinement-session.md](refinement-session.md)** when the user names one or more changes. -The parent reference owns the conversation shape; this file owns the writes. After all of the user's operations have been processed, return to caller. +The parent reference owns the conversation shape; this file owns the writes. State for validation comes from `skills/workflow-inception-process/scripts/discovery.cjs` — invoke it via Bash and read the structured output. Never invoke the underlying Node helpers inline. + +After all of the user's operations have been processed, return to caller. ## A. Parse Operations -Read the user's most recent message. Extract one or more operations. Recognised intents: +Re-run discovery to pick up state changes since the last invocation (operations applied earlier in the session, or the parent's initial discovery): + +```bash +node .claude/skills/workflow-inception-process/scripts/discovery.cjs {work_unit} +``` + +Read `discovery_map` (per-topic `tier`, `lifecycle`, `routing`, `summary`, `source`) and `dismissed`. These drive validation in **B**. + +Then read the user's most recent message. Extract one or more operations. Recognised intents: | User phrasing | Operation | Required values | | ----------------------------------------------- | --------------- | ---------------------------------------- | @@ -37,7 +47,7 @@ Walk the groups in user order. For mixed batches (e.g. *"remove A, rename B to B Apply per-operation validation gates **before** any STOP gate. If validation fails for an operation, surface the rejection with a clear next-step pointer (don't just say "blocked") and remove the operation from its group. Continue with the rest. -**Lifecycle gates** — for destructive operations (Remove, Rename, Change routing), compute the topic's lifecycle via `computeTopicLifecycle(manifest, topicName)` from `discovery-utils.cjs`. The operation is allowed only when: +**Lifecycle gates** — for destructive operations (Remove, Rename, Change routing), look up the operation's target topic in `discovery_map` and read its `lifecycle` field. The operation is allowed only when: | Operation | Allowed lifecycles | Disallowed | | --------------- | ------------------ | --------------------------------------------------------------------------- | @@ -67,7 +77,7 @@ Render the rejection in a code block: - `decided` — `discussion has concluded` - `cancelled` — `it has phase work in cancelled state and stays on the map as historical record` -**Name collision gates** — for Add and Rename, the new name is rejected if an **active** map item already uses it (case-sensitive match against `phases.inception.items.{name}`): +**Name collision gates** — for Add and Rename, check the new name against `discovery_map`'s topic names (case-sensitive). A match means an **active** map item already uses the name — reject: > *Output the next fenced block as a code block:* @@ -76,7 +86,7 @@ Render the rejection in a code block: edit-summary / change-routing on the existing item. ``` -For Add, a name appearing in `phases.inception.dismissed` is **allowed** — it counts as a re-add. The Add flow pulls the name from the dismissed list before creating the new item. +For Add, a name appearing in `dismissed` is **allowed** — it counts as a re-add. The Add flow pulls the name from the dismissed list before creating the new item. → Proceed to **C. Apply**. @@ -145,7 +155,7 @@ Skip the batch. No manifest writes, no session-log entry, no commit. For each name in the batch: -1. If the name is in `phases.inception.dismissed`, pull it: +1. If the name appears in `dismissed` (from the discovery output read in **A**), pull it: ```bash node .claude/skills/workflow-manifest/scripts/manifest.cjs pull {work_unit}.inception dismissed "{name}" diff --git a/skills/workflow-inception-process/references/refinement-session.md b/skills/workflow-inception-process/references/refinement-session.md index 66c55ff5..ae5f2a8a 100644 --- a/skills/workflow-inception-process/references/refinement-session.md +++ b/skills/workflow-inception-process/references/refinement-session.md @@ -8,6 +8,8 @@ This reference drives the re-entry path into inception. Items already exist on t The convention is conversational, not menu-driven. STOP gates wrap manifest writes, scaled to destructiveness — additive operations batch, destructive operations are per-item. The map-operations reference owns parsing, validation, and persistence; this file owns the conversation shape. +State for this reference comes from the discovery script at `skills/workflow-inception-process/scripts/discovery.cjs`. Sections invoke it via Bash and read the structured output — they never invoke the underlying Node helpers inline. + Two anti-patterns to avoid: - **Do not call `knowledge index`.** Inception session logs (initial or refinement) are journey records, not retrievable artifacts. @@ -18,45 +20,50 @@ Two anti-patterns to avoid: > *Output the next fenced block as markdown (not a code block):* ``` -> Loading current inception items and per-topic lifecycle from -> the manifest. The map is the source of truth — no file reads -> needed for state. +> Loading the current discovery map, dismissed list, and the latest +> session log status. +``` + +Run discovery for the work unit: + +```bash +node .claude/skills/workflow-inception-process/scripts/discovery.cjs {work_unit} ``` -Load the work unit's manifest and read `phases.inception.items.*`. For each topic, compute its lifecycle using `computeTopicLifecycle(manifest, topicName)` from `skills/workflow-shared/scripts/discovery-utils.cjs`. The returned `{ lifecycle, tier, current_phase }` informs which operations are allowed in the operations loop. +The output drives the rest of this file: -Also read `phases.inception.dismissed` (an array of previously removed topic names; may be missing or empty). The dismissed list governs name collision and the show-dismissed flow. +- **`map_summary`** — `{total} topics — ...` line. Used in **D** (session log header) and **E** (render). +- **`discovery_map`** — per-topic `tier`, `lifecycle`, `current_phase`, `routing`, `source`, `summary`. Used in **E** (render). +- **`dismissed`** — names of topics previously removed via refinement. Used by `show-dismissed.md`. +- **`latest_session`** — `{filename, number, is_refinement, is_in_progress, conclusion_text, relative_path}`. Used in **B** (resume detection). +- **`next_session_number`** — zero-padded next session number to seed in **D**. → Proceed to **B. Resume Check**. ## B. Resume Check -Find the highest-numbered session log on disk: - -```bash -ls .workflows/{work_unit}/inception/session-*.md 2>/dev/null | sort | tail -1 -``` +Read `latest_session` and `next_session_number` from the discovery output produced in **A**. -If the result is empty or matches `session-001.md`, no refinement is in flight. Otherwise read the matched `session-NNN.md` and inspect its **Conclusion** section — `(none)` means the prior refinement was interrupted (context refresh or user exit) before finalisation. +#### If `latest_session` is null or `latest_session.is_refinement` is `false` -#### If only `session-001.md` exists or no log was found +No refinement is in flight (only the initial session log exists, or no logs at all). → Proceed to **C. Self-Healing Check**. -#### If a refinement log exists with non-`(none)` Conclusion +#### If `latest_session.is_refinement` is `true` and `latest_session.is_in_progress` is `false` The prior refinement concluded normally. Treat this as a fresh entry. → Proceed to **C. Self-Healing Check**. -#### If a refinement log exists with `(none)` Conclusion +#### If `latest_session.is_refinement` is `true` and `latest_session.is_in_progress` is `true` -The prior refinement was interrupted. Offer continue or restart: +The prior refinement was interrupted (Conclusion is `(none)`). Offer continue or restart: > *Output the next fenced block as markdown (not a code block):* ``` -Found an in-progress refinement session log for **{work_unit:(titlecase)}**: `session-{NNN}.md`. +Found an in-progress refinement session log for **{work_unit:(titlecase)}**: `{latest_session.filename}`. · · · · · · · · · · · · - **`c`/`continue`** — Pick up where you left off @@ -68,7 +75,7 @@ Found an in-progress refinement session log for **{work_unit:(titlecase)}**: `se **If `continue`:** -The active session log is `session-{NNN}.md`. No new log is initialised; subsequent operations append to the existing log. +The active session log is `{latest_session.filename}`. No new log is initialised; subsequent operations append to the existing log. → Proceed to **E. Render and Prompt**. @@ -77,7 +84,7 @@ The active session log is `session-{NNN}.md`. No new log is initialised; subsequ Delete the in-progress log and commit: ```bash -rm .workflows/{work_unit}/inception/session-{NNN}.md +rm {latest_session.relative_path} git add -- .workflows/{work_unit}/ git commit -m "inception({work_unit}): restart refinement session" ``` @@ -93,27 +100,26 @@ git commit -m "inception({work_unit}): restart refinement session" > gap-analysis) are wired in Phase 7. Nothing runs here yet. ``` -No-op for Phase 6. Phase 7 will run the analyses inline at this point and apply results to the map (auto-add with `source: research-analysis` or `source: gap-analysis`, filtered against `phases.inception.dismissed`). Any items added by analyses will be recorded under **Self-Healing Arrivals** in the session log initialised in **D**. +No-op for Phase 6. Phase 7 will run the analyses inline at this point and apply results to the map (auto-add with `source: research-analysis` or `source: gap-analysis`, filtered against `dismissed`). Any items added by analyses will be recorded under **Self-Healing Arrivals** in the session log initialised in **D**. → Proceed to **D. Initialise Session Log**. ## D. Initialise Session Log -Determine the next session number: +Re-run discovery to pick up any state changes from a `restart` in **B**: ```bash -n=$(ls .workflows/{work_unit}/inception/session-*.md 2>/dev/null | wc -l) -next=$(printf "%03d" $((n + 1))) +node .claude/skills/workflow-inception-process/scripts/discovery.cjs {work_unit} ``` -Counts existing files (initial = `001`, prior refinements = `002+`) and increments to the next zero-padded value. +Read `next_session_number` and `map_summary` from the output. The new session log path is `.workflows/{work_unit}/inception/session-{next_session_number}.md`. -Create `.workflows/{work_unit}/inception/session-{next}.md` from **[refinement-template.md](refinement-template.md)**. Populate the header (date, work unit) and **Map State at Start** with the summary line computed in **A**. Leave **Self-Healing Arrivals**, **Changes**, and **Conclusion** as `(none)` placeholders — they fill in as operations are applied and at finalisation. The `(none)` Conclusion is the resume-detection signal used by **B**. +Create the file from **[refinement-template.md](refinement-template.md)**. Populate the header (date, work unit) and **Map State at Start** with the `map_summary` text. Leave **Self-Healing Arrivals**, **Changes**, and **Conclusion** as `(none)` placeholders — they fill in as operations are applied and at finalisation. The `(none)` Conclusion is the resume-detection signal used by **B**. Commit: ```bash -git add -- .workflows/{work_unit}/inception/session-{next}.md +git add -- .workflows/{work_unit}/inception/session-{next_session_number}.md git commit -m "inception({work_unit}): seed refinement session log" ``` @@ -129,7 +135,7 @@ git commit -m "inception({work_unit}): seed refinement session log" > one message are fine; I'll work through them. ``` -Render the current map as a status-display anchor: +Render the current map as a status-display anchor, using `discovery_map` and `map_summary` from **A** (or from the resumed log's matching state if **B** routed `continue`): > *Output the next fenced block as a code block:* @@ -147,13 +153,13 @@ Render the current map as a status-display anchor: **Render rules** (subset of continue-epic's discovery map block): -- `summary_line`: `{total} topics — {decided} decided · {in_flight} in flight · {ready} ready · {fresh} fresh · {cancelled} cancelled`. Omit zero-count categories. Always include `{total} topics`. -- Tier and ordering — sort by tier rank `→ ◐ ✓ ○ ⊘`, alphabetical within each tier (matches continue-epic). +- `summary_line`: `{total} topics — {decided} decided · {in_flight} in flight · {ready} ready · {fresh} fresh · {cancelled} cancelled`. Omit zero-count categories from the dot-separated tail. Always include `{total} topics`. +- Tier and ordering — discovery output is already tier-sorted (`→ ◐ ✓ ○ ⊘`, alphabetical within tier). Render in the order given. - `lifecycle_label` by tier: - `→` — `research complete · ready for discussion` - - `◐` — `researching` or `discussing` (use `current_phase`) + - `◐` — `researching` or `discussing` (use `topic.current_phase`) - `✓` — `decided` - - `○` — `fresh · routed to {topic.routing}` (omit ` · routed to ...` if `routing` is null) + - `○` — `fresh · routed to {topic.routing}` (omit ` · routed to ...` if `topic.routing` is null) - `⊘` — `cancelled` - No source provenance sub-line, no key block, no menu — this is an anchor, not the continue-epic display. @@ -195,7 +201,7 @@ The message names operations. → Load **[map-operations.md](map-operations.md)** and follow its instructions as written. -`map-operations.md` parses, validates, applies safety-by-destructiveness gating, writes the manifest, appends to the active session log, and commits per its own pattern. When it returns: +`map-operations.md` re-runs discovery for fresh state, parses, validates, applies safety-by-destructiveness gating, writes the manifest, appends to the active session log, and commits per its own pattern. When it returns: → Proceed to **G. Anything Else?**. @@ -224,17 +230,23 @@ Anything else to change? ## H. Finalise Session Log -Replace the `(none)` placeholder in the **Conclusion** section of `inception/session-{NNN}.md`. The replacement is non-optional — leaving `(none)` would make the log indistinguishable from an interrupted session on the next refinement entry. +Re-run discovery to pick up the post-operations state: + +```bash +node .claude/skills/workflow-inception-process/scripts/discovery.cjs {work_unit} +``` + +Read `map_summary.total` from the output. Replace the `(none)` placeholder in the **Conclusion** section of the active session log. The replacement is non-optional — leaving `(none)` would make the log indistinguishable from an interrupted session on the next refinement entry. #### If at least one operation was applied during the session -Replace `(none)` with: `{N} changes applied. Map now has {M} topics.` +Replace `(none)` with: `{N} changes applied. Map now has {map_summary.total} topics.` → Proceed to **I. Compliance Self-Check**. #### Otherwise (browse-only refinement, no operations applied) -Replace `(none)` with: `No changes applied — browse only. Map has {M} topics.` +Replace `(none)` with: `No changes applied — browse only. Map has {map_summary.total} topics.` → Proceed to **I. Compliance Self-Check**. diff --git a/skills/workflow-inception-process/references/show-dismissed.md b/skills/workflow-inception-process/references/show-dismissed.md index 2395419d..29d5ac32 100644 --- a/skills/workflow-inception-process/references/show-dismissed.md +++ b/skills/workflow-inception-process/references/show-dismissed.md @@ -6,17 +6,19 @@ Recovery flow for the refinement session. Surfaces topic names previously removed from the map and offers re-add. Loaded by **[refinement-session.md](refinement-session.md)** when the user asks to see dismissed items. +State comes from `skills/workflow-inception-process/scripts/discovery.cjs` — invoke it via Bash and read the structured output. Never invoke the underlying Node helpers inline. + ## A. Read Dismissed List -Read `phases.inception.dismissed` from the manifest: +Re-run discovery to pick up any state changes since the parent's initial discovery (a Remove earlier in the session may have added a new entry): ```bash -node .claude/skills/workflow-manifest/scripts/manifest.cjs get {work_unit}.inception dismissed +node .claude/skills/workflow-inception-process/scripts/discovery.cjs {work_unit} ``` -The result is either an array of topic names or empty (returns `2` exit code if the field is missing — treat that as empty). +Read the `dismissed` array from the output. -#### If empty +#### If `dismissed` is empty > *Output the next fenced block as a code block:* @@ -67,7 +69,7 @@ Treat the response as an Add intent and dispatch to the Add flow: → Load **[map-operations.md](map-operations.md)** and follow its instructions as written. -`map-operations.md` validates each name (collision check is satisfied — dismissed-list match is allowed for Add and triggers a `pull` from the dismissed list before `init-phase`), STOP-gates on the batch, applies the writes, appends a Changes entry to the session log, and commits. +`map-operations.md` re-runs discovery, validates each name (collision check is satisfied — dismissed-list match is allowed for Add and triggers a `pull` from the dismissed list before `init-phase`), STOP-gates on the batch, applies the writes, appends a Changes entry to the session log, and commits. When `map-operations.md` returns: diff --git a/skills/workflow-inception-process/scripts/discovery.cjs b/skills/workflow-inception-process/scripts/discovery.cjs new file mode 100644 index 00000000..e13b33dc --- /dev/null +++ b/skills/workflow-inception-process/scripts/discovery.cjs @@ -0,0 +1,168 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { + loadManifest, + phaseItems, + computeTopicLifecycle, + computeMapSummary, + computeSourceProvenance, + TIER_RANK, +} = require('../../workflow-shared/scripts/discovery-utils.cjs'); + +function buildDiscoveryMap(manifest) { + const inceptionItems = phaseItems(manifest, 'inception'); + if (inceptionItems.length === 0) return { map: [], summary: { total: 0, decided: 0, in_flight: 0, ready: 0, fresh: 0, cancelled: 0 } }; + const map = inceptionItems.map(item => { + const { lifecycle, tier, current_phase } = computeTopicLifecycle(manifest, item.name); + return { + name: item.name, + summary: item.summary || null, + routing: item.routing || null, + source: item.source || 'inception', + source_provenance: computeSourceProvenance(item.source), + lifecycle, + tier, + current_phase, + }; + }); + map.sort((a, b) => { + const ra = TIER_RANK[a.tier] != null ? TIER_RANK[a.tier] : 99; + const rb = TIER_RANK[b.tier] != null ? TIER_RANK[b.tier] : 99; + if (ra !== rb) return ra - rb; + return a.name.localeCompare(b.name); + }); + return { map, summary: computeMapSummary(map) }; +} + +function findLatestSessionLog(cwd, workUnit) { + const dir = path.join(cwd, '.workflows', workUnit, 'inception'); + let files; + try { + files = fs.readdirSync(dir).filter(f => /^session-\d+\.md$/.test(f)).sort(); + } catch { + return null; + } + if (files.length === 0) return null; + const filename = files[files.length - 1]; + const fullPath = path.join(dir, filename); + let content; + try { + content = fs.readFileSync(fullPath, 'utf8'); + } catch { + return null; + } + const m = filename.match(/^session-(\d+)\.md$/); + const number = parseInt(m[1], 10); + + // Detect Conclusion section status (placeholder = "(none)" on the first + // non-empty line after the heading). An in-progress refinement log is the + // resume signal used by refinement-session.md B. Resume Check. + let conclusionText = ''; + const conclusionMatch = content.match(/##\s+Conclusion\s*\n([\s\S]*?)(?:\n##\s|$)/); + if (conclusionMatch) { + const body = conclusionMatch[1].trim(); + conclusionText = body.split('\n')[0].trim(); + } + const isInProgress = conclusionText === '(none)'; + + return { + filename, + relative_path: path.posix.join('.workflows', workUnit, 'inception', filename), + number, + is_refinement: number > 1, + is_in_progress: isInProgress, + conclusion_text: conclusionText, + }; +} + +function discover(cwd, workUnit) { + const manifest = loadManifest(cwd, workUnit); + if (!manifest) { + return { error: `Work unit "${workUnit}" not found` }; + } + const inceptionPhase = (manifest.phases || {}).inception || {}; + const dismissed = Array.isArray(inceptionPhase.dismissed) ? inceptionPhase.dismissed.slice() : []; + const { map, summary } = buildDiscoveryMap(manifest); + const latestSession = findLatestSessionLog(cwd, workUnit); + const nextSessionNumber = latestSession ? latestSession.number + 1 : 1; + return { + work_unit: workUnit, + discovery_map: map, + map_summary: summary, + dismissed, + latest_session: latestSession, + next_session_number: nextSessionNumber, + }; +} + +function format(result) { + if (result.error) { + return `error: ${result.error}\n`; + } + const lines = []; + lines.push(`=== INCEPTION DISCOVERY: ${result.work_unit} ===`); + + const s = result.map_summary; + lines.push(`map_summary: ${s.total} topics — ${s.decided} decided, ${s.in_flight} in-flight, ${s.ready} ready, ${s.fresh} fresh, ${s.cancelled} cancelled`); + lines.push(''); + + lines.push(`discovery_map (${result.discovery_map.length}):`); + if (result.discovery_map.length === 0) { + lines.push(' (empty)'); + } else { + for (const t of result.discovery_map) { + let line = ` - ${t.tier} ${t.name} [${t.lifecycle}]`; + if (t.routing) line += ` routing=${t.routing}`; + if (t.source && t.source !== 'inception') line += ` source=${t.source}`; + if (t.current_phase) line += ` phase=${t.current_phase}`; + if (t.summary) line += ` — ${t.summary}`; + lines.push(line); + } + } + lines.push(''); + + lines.push(`dismissed (${result.dismissed.length}):`); + if (result.dismissed.length === 0) { + lines.push(' (none)'); + } else { + for (const name of result.dismissed) { + lines.push(` - ${name}`); + } + } + lines.push(''); + + lines.push('latest_session:'); + if (!result.latest_session) { + lines.push(' (no session logs on disk)'); + } else { + const ls = result.latest_session; + lines.push(` filename: ${ls.filename}`); + lines.push(` relative_path: ${ls.relative_path}`); + lines.push(` number: ${ls.number}`); + lines.push(` is_refinement: ${ls.is_refinement}`); + lines.push(` is_in_progress: ${ls.is_in_progress}`); + lines.push(` conclusion: ${ls.conclusion_text || '(empty)'}`); + } + lines.push(''); + + lines.push(`next_session_number: ${String(result.next_session_number).padStart(3, '0')}`); + + return lines.join('\n') + '\n'; +} + +if (require.main === module) { + const workUnit = process.argv[2]; + if (!workUnit) { + process.stderr.write('Error: work unit name required\nUsage: discovery.cjs \n'); + process.exit(1); + } + const result = discover(process.cwd(), workUnit); + process.stdout.write(format(result)); + if (result.error) { + process.exit(2); + } +} + +module.exports = { discover, format }; diff --git a/tests/scripts/test-discovery-for-refinement.cjs b/tests/scripts/test-discovery-for-refinement.cjs new file mode 100644 index 00000000..3ac7d8b3 --- /dev/null +++ b/tests/scripts/test-discovery-for-refinement.cjs @@ -0,0 +1,278 @@ +'use strict'; + +const { describe, it, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const { discover, format } = require( + path.resolve(__dirname, '..', '..', 'skills', 'workflow-inception-process', 'scripts', 'discovery.cjs') +); + +let dir; + +function setupFixture() { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'refinement-discovery-test-')); + fs.mkdirSync(path.join(dir, '.workflows'), { recursive: true }); +} + +function cleanupFixture() { + if (dir) fs.rmSync(dir, { recursive: true, force: true }); + dir = null; +} + +function seedEpic(workUnit, manifestExtras = {}) { + const manifestDir = path.join(dir, '.workflows', workUnit); + fs.mkdirSync(path.join(manifestDir, 'inception'), { recursive: true }); + const manifest = { + name: workUnit, + work_type: 'epic', + status: 'in-progress', + description: `Test: ${workUnit}`, + phases: {}, + ...manifestExtras, + }; + fs.writeFileSync( + path.join(manifestDir, 'manifest.json'), + JSON.stringify(manifest, null, 2), + ); + // Register in project manifest + const projPath = path.join(dir, '.workflows', 'manifest.json'); + let proj = {}; + if (fs.existsSync(projPath)) { + proj = JSON.parse(fs.readFileSync(projPath, 'utf8')); + } + if (!proj.work_units) proj.work_units = {}; + proj.work_units[workUnit] = { work_type: 'epic' }; + fs.writeFileSync(projPath, JSON.stringify(proj, null, 2)); +} + +function writeSessionLog(workUnit, number, conclusionBody) { + const padded = String(number).padStart(3, '0'); + const filename = `session-${padded}.md`; + const fullPath = path.join(dir, '.workflows', workUnit, 'inception', filename); + const title = number === 1 ? 'Initial Framing' : 'Refinement'; + const content = `# Inception Session ${padded} — ${title} + +Date: 2026-05-10 +Work unit: ${workUnit} + +## Map State at Start + +3 topics — 3 fresh + +## Self-Healing Arrivals + +(none) + +## Changes + +(none) + +## Conclusion + +${conclusionBody} +`; + fs.writeFileSync(fullPath, content); +} + +describe('refinement discovery: discovery_map', () => { + beforeEach(setupFixture); + afterEach(cleanupFixture); + + it('returns empty map for an epic with no inception items', () => { + seedEpic('payments', { phases: { inception: { items: {} } } }); + const r = discover(dir, 'payments'); + assert.deepStrictEqual(r.discovery_map, []); + assert.strictEqual(r.map_summary.total, 0); + }); + + it('builds map entries with lifecycle, tier, source, summary, routing', () => { + seedEpic('payments', { + phases: { + inception: { + items: { + 'auth-flow': { status: 'in-progress', summary: 'oauth + sessions', routing: 'research', source: 'inception' }, + 'billing': { status: 'in-progress', summary: 'invoices', routing: 'discussion', source: 'inception' }, + }, + }, + }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map.length, 2); + const auth = r.discovery_map.find(t => t.name === 'auth-flow'); + assert.strictEqual(auth.lifecycle, 'fresh'); + assert.strictEqual(auth.tier, '○'); + assert.strictEqual(auth.routing, 'research'); + assert.strictEqual(auth.source, 'inception'); + assert.strictEqual(auth.summary, 'oauth + sessions'); + }); + + it('reflects research in-progress lifecycle', () => { + seedEpic('payments', { + phases: { + inception: { items: { 'auth-flow': { status: 'in-progress', routing: 'research', source: 'inception' } } }, + research: { items: { 'auth-flow': { status: 'in-progress' } } }, + }, + }); + const r = discover(dir, 'payments'); + const auth = r.discovery_map.find(t => t.name === 'auth-flow'); + assert.strictEqual(auth.lifecycle, 'researching'); + assert.strictEqual(auth.tier, '◐'); + assert.strictEqual(auth.current_phase, 'research'); + }); + + it('sorts by tier rank then alphabetical within tier', () => { + seedEpic('payments', { + phases: { + inception: { + items: { + 'fresh-z': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'fresh-a': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'in-flight': { status: 'in-progress', routing: 'research', source: 'inception' }, + }, + }, + research: { items: { 'in-flight': { status: 'in-progress' } } }, + }, + }); + const r = discover(dir, 'payments'); + assert.deepStrictEqual(r.discovery_map.map(t => t.name), ['in-flight', 'fresh-a', 'fresh-z']); + }); + + it('computes map_summary counts from tier distribution', () => { + seedEpic('payments', { + phases: { + inception: { + items: { + 'a': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'b': { status: 'in-progress', routing: 'discussion', source: 'inception' }, + 'c': { status: 'in-progress', routing: 'discussion', source: 'inception' }, + }, + }, + discussion: { items: { 'c': { status: 'completed' } } }, + }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.map_summary.total, 3); + assert.strictEqual(r.map_summary.fresh, 2); + assert.strictEqual(r.map_summary.decided, 1); + }); +}); + +describe('refinement discovery: dismissed list', () => { + beforeEach(setupFixture); + afterEach(cleanupFixture); + + it('returns empty array when phases.inception.dismissed is missing', () => { + seedEpic('payments', { phases: { inception: { items: {} } } }); + const r = discover(dir, 'payments'); + assert.deepStrictEqual(r.dismissed, []); + }); + + it('returns the dismissed list verbatim when present', () => { + seedEpic('payments', { + phases: { + inception: { + items: {}, + dismissed: ['old-thing', 'another'], + }, + }, + }); + const r = discover(dir, 'payments'); + assert.deepStrictEqual(r.dismissed, ['old-thing', 'another']); + }); +}); + +describe('refinement discovery: latest_session detection', () => { + beforeEach(setupFixture); + afterEach(cleanupFixture); + + it('returns null when no session logs exist', () => { + seedEpic('payments', { phases: { inception: { items: {} } } }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.latest_session, null); + assert.strictEqual(r.next_session_number, 1); + }); + + it('detects the initial session log without flagging it as in-progress refinement', () => { + seedEpic('payments', { phases: { inception: { items: {} } } }); + writeSessionLog('payments', 1, '3 topics seeded.'); + const r = discover(dir, 'payments'); + assert.strictEqual(r.latest_session.number, 1); + assert.strictEqual(r.latest_session.is_refinement, false); + assert.strictEqual(r.latest_session.is_in_progress, false); + assert.strictEqual(r.next_session_number, 2); + }); + + it('flags an in-progress refinement when Conclusion is `(none)`', () => { + seedEpic('payments', { phases: { inception: { items: {} } } }); + writeSessionLog('payments', 1, '3 topics seeded.'); + writeSessionLog('payments', 2, '(none)'); + const r = discover(dir, 'payments'); + assert.strictEqual(r.latest_session.number, 2); + assert.strictEqual(r.latest_session.is_refinement, true); + assert.strictEqual(r.latest_session.is_in_progress, true); + }); + + it('does not flag a concluded refinement as in-progress', () => { + seedEpic('payments', { phases: { inception: { items: {} } } }); + writeSessionLog('payments', 1, '3 topics seeded.'); + writeSessionLog('payments', 2, '2 changes applied. Map now has 5 topics.'); + const r = discover(dir, 'payments'); + assert.strictEqual(r.latest_session.is_in_progress, false); + assert.strictEqual(r.latest_session.conclusion_text, '2 changes applied. Map now has 5 topics.'); + }); + + it('next_session_number always increments past the highest existing number', () => { + seedEpic('payments', { phases: { inception: { items: {} } } }); + writeSessionLog('payments', 1, '3 topics seeded.'); + writeSessionLog('payments', 2, 'concluded'); + writeSessionLog('payments', 3, 'concluded'); + const r = discover(dir, 'payments'); + assert.strictEqual(r.next_session_number, 4); + }); +}); + +describe('refinement discovery: error handling', () => { + beforeEach(setupFixture); + afterEach(cleanupFixture); + + it('returns an error when the work unit does not exist', () => { + const r = discover(dir, 'nonexistent'); + assert.ok(r.error); + assert.match(r.error, /not found/i); + }); +}); + +describe('refinement discovery: format()', () => { + beforeEach(setupFixture); + afterEach(cleanupFixture); + + it('produces parseable text output for a populated work unit', () => { + seedEpic('payments', { + phases: { + inception: { + items: { + 'auth-flow': { status: 'in-progress', summary: 'oauth', routing: 'research', source: 'inception' }, + }, + dismissed: ['old-topic'], + }, + }, + }); + writeSessionLog('payments', 1, '1 topic seeded.'); + const r = discover(dir, 'payments'); + const out = format(r); + assert.match(out, /=== INCEPTION DISCOVERY: payments ===/); + assert.match(out, /map_summary: 1 topics/); + assert.match(out, /auth-flow \[fresh\]/); + assert.match(out, /dismissed \(1\):/); + assert.match(out, /- old-topic/); + assert.match(out, /next_session_number: 002/); + }); + + it('renders errors prefixed with `error:`', () => { + const out = format({ error: 'Work unit "x" not found' }); + assert.match(out, /^error: Work unit "x" not found/); + }); +}); From 13132afe0dc87a53ba4a79d430cff789c9cf196d Mon Sep 17 00:00:00 2001 From: Lee Overy Date: Sun, 10 May 2026 16:08:36 +0100 Subject: [PATCH 19/19] inception(workflow-inception-process): expand discovery test coverage to match other discovery suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor to two top-level describe blocks (discover, format) matching the test-discovery-for-{bridge,discussion,...}.cjs convention. Use the shared discovery-test-utils.cjs helpers (setupFixture, cleanupFixture, createManifest, createFile) instead of bespoke seed helpers. Coverage added (15 → 59 tests): discover(): - Bare-manifest shape: missing inception phase, empty inception phase, empty items dict - Field defaults: null summary, null routing, default source=inception - All six lifecycles (fresh, researching, ready_for_discussion, discussing, decided, cancelled) and the alternate-path-open fall-through to fresh when only one phase is cancelled - source_provenance: null for inception, "from {source}" for plain non-inception sources, colon-prefixed unwrap to "from {parent}" - Sorting: tier rank order across all five tiers; alphabetical within the same tier - map_summary: full distribution across all categories; zero-shape for empty map - dismissed: missing/non-array defensive defaults; verbatim ordering; caller mutations don't leak into manifest - latest_session edge cases: missing Conclusion section, multi-line Conclusion (first line wins), trailing section after Conclusion, 10+ numbered files (numeric not alphabetic ordering), non-matching filenames in inception dir, relative_path shape - next_session_number: zero, increment past concluded, increment past in-progress format(): - Header includes work_unit - map_summary line shape with all six counts - "(empty)" placeholder for empty discovery_map - Map row format with tier/name/lifecycle, routing, source, current_phase, summary; correct omission when fields are null or source=inception - "(none)" placeholder for empty dismissed - "(no session logs on disk)" when latest_session is null - All latest_session subfields rendered - "(empty)" conclusion render when conclusion_text is empty - next_session_number zero-padding to 3 digits - Trailing newline; sections render in canonical order Tests: 59/59. Full node test suite: 242/242 across 30 suites. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scripts/test-discovery-for-refinement.cjs | 727 ++++++++++++++---- 1 file changed, 578 insertions(+), 149 deletions(-) diff --git a/tests/scripts/test-discovery-for-refinement.cjs b/tests/scripts/test-discovery-for-refinement.cjs index 3ac7d8b3..84bb92df 100644 --- a/tests/scripts/test-discovery-for-refinement.cjs +++ b/tests/scripts/test-discovery-for-refinement.cjs @@ -4,55 +4,14 @@ const { describe, it, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); -const os = require('os'); +const { setupFixture, cleanupFixture, createManifest, createFile } = require('./discovery-test-utils.cjs'); +const { discover, format } = require('../../skills/workflow-inception-process/scripts/discovery.cjs'); -const { discover, format } = require( - path.resolve(__dirname, '..', '..', 'skills', 'workflow-inception-process', 'scripts', 'discovery.cjs') -); - -let dir; - -function setupFixture() { - dir = fs.mkdtempSync(path.join(os.tmpdir(), 'refinement-discovery-test-')); - fs.mkdirSync(path.join(dir, '.workflows'), { recursive: true }); -} - -function cleanupFixture() { - if (dir) fs.rmSync(dir, { recursive: true, force: true }); - dir = null; -} - -function seedEpic(workUnit, manifestExtras = {}) { - const manifestDir = path.join(dir, '.workflows', workUnit); - fs.mkdirSync(path.join(manifestDir, 'inception'), { recursive: true }); - const manifest = { - name: workUnit, - work_type: 'epic', - status: 'in-progress', - description: `Test: ${workUnit}`, - phases: {}, - ...manifestExtras, - }; - fs.writeFileSync( - path.join(manifestDir, 'manifest.json'), - JSON.stringify(manifest, null, 2), - ); - // Register in project manifest - const projPath = path.join(dir, '.workflows', 'manifest.json'); - let proj = {}; - if (fs.existsSync(projPath)) { - proj = JSON.parse(fs.readFileSync(projPath, 'utf8')); - } - if (!proj.work_units) proj.work_units = {}; - proj.work_units[workUnit] = { work_type: 'epic' }; - fs.writeFileSync(projPath, JSON.stringify(proj, null, 2)); -} - -function writeSessionLog(workUnit, number, conclusionBody) { +function writeSessionLog(dir, workUnit, number, conclusionBody, opts = {}) { const padded = String(number).padStart(3, '0'); const filename = `session-${padded}.md`; - const fullPath = path.join(dir, '.workflows', workUnit, 'inception', filename); const title = number === 1 ? 'Initial Framing' : 'Refinement'; + const trailing = opts.trailingSection ? `\n\n## ${opts.trailingSection}\n\nfoo\n` : '\n'; const content = `# Inception Session ${padded} — ${title} Date: 2026-05-10 @@ -72,207 +31,677 @@ Work unit: ${workUnit} ## Conclusion -${conclusionBody} -`; - fs.writeFileSync(fullPath, content); +${conclusionBody}${trailing}`; + createFile(dir, `.workflows/${workUnit}/inception/${filename}`, content); } -describe('refinement discovery: discovery_map', () => { - beforeEach(setupFixture); - afterEach(cleanupFixture); +describe('workflow-inception-process discovery', () => { + let dir; + beforeEach(() => { dir = setupFixture(); }); + afterEach(() => { cleanupFixture(dir); }); + + // --- Error handling --- + + it('returns error for missing manifest', () => { + const r = discover(dir, 'nonexistent'); + assert.ok(r.error); + assert.match(r.error, /not found/i); + }); + + // --- Bare manifest shape --- - it('returns empty map for an epic with no inception items', () => { - seedEpic('payments', { phases: { inception: { items: {} } } }); + it('returns work_unit verbatim', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.work_unit, 'payments'); + }); + + it('returns empty discovery_map when no inception phase exists', () => { + createManifest(dir, 'payments', { work_type: 'epic', phases: {} }); const r = discover(dir, 'payments'); assert.deepStrictEqual(r.discovery_map, []); assert.strictEqual(r.map_summary.total, 0); }); - it('builds map entries with lifecycle, tier, source, summary, routing', () => { - seedEpic('payments', { + it('returns empty discovery_map when inception phase has no items', () => { + createManifest(dir, 'payments', { work_type: 'epic', phases: { inception: {} } }); + const r = discover(dir, 'payments'); + assert.deepStrictEqual(r.discovery_map, []); + }); + + it('returns empty discovery_map when inception items dict is empty', () => { + createManifest(dir, 'payments', { work_type: 'epic', phases: { inception: { items: {} } } }); + const r = discover(dir, 'payments'); + assert.deepStrictEqual(r.discovery_map, []); + }); + + // --- discovery_map content --- + + it('builds map entry with all named fields', () => { + createManifest(dir, 'payments', { + work_type: 'epic', phases: { inception: { items: { 'auth-flow': { status: 'in-progress', summary: 'oauth + sessions', routing: 'research', source: 'inception' }, - 'billing': { status: 'in-progress', summary: 'invoices', routing: 'discussion', source: 'inception' }, }, }, }, }); const r = discover(dir, 'payments'); - assert.strictEqual(r.discovery_map.length, 2); - const auth = r.discovery_map.find(t => t.name === 'auth-flow'); - assert.strictEqual(auth.lifecycle, 'fresh'); - assert.strictEqual(auth.tier, '○'); - assert.strictEqual(auth.routing, 'research'); - assert.strictEqual(auth.source, 'inception'); - assert.strictEqual(auth.summary, 'oauth + sessions'); + const t = r.discovery_map[0]; + assert.strictEqual(t.name, 'auth-flow'); + assert.strictEqual(t.summary, 'oauth + sessions'); + assert.strictEqual(t.routing, 'research'); + assert.strictEqual(t.source, 'inception'); + assert.strictEqual(t.lifecycle, 'fresh'); + assert.strictEqual(t.tier, '○'); + assert.strictEqual(t.current_phase, null); + assert.strictEqual(t.source_provenance, null); }); - it('reflects research in-progress lifecycle', () => { - seedEpic('payments', { + it('defaults summary to null when missing', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'inception' } } } }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].summary, null); + }); + + it('defaults routing to null when missing', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: { 'a': { status: 'in-progress', source: 'inception' } } } }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].routing, null); + }); + + it('defaults source to "inception" when missing', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: { 'a': { status: 'in-progress', routing: 'research' } } } }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].source, 'inception'); + }); + + // --- Lifecycle reflection (one assertion per branch in computeTopicLifecycle) --- + + it('reflects fresh lifecycle when no research/discussion items exist', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'inception' } } } }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].lifecycle, 'fresh'); + assert.strictEqual(r.discovery_map[0].tier, '○'); + }); + + it('reflects researching lifecycle when research is in-progress', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { + inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'inception' } } }, + research: { items: { 'a': { status: 'in-progress' } } }, + }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].lifecycle, 'researching'); + assert.strictEqual(r.discovery_map[0].tier, '◐'); + assert.strictEqual(r.discovery_map[0].current_phase, 'research'); + }); + + it('reflects ready_for_discussion lifecycle when research is completed', () => { + createManifest(dir, 'payments', { + work_type: 'epic', phases: { - inception: { items: { 'auth-flow': { status: 'in-progress', routing: 'research', source: 'inception' } } }, - research: { items: { 'auth-flow': { status: 'in-progress' } } }, + inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'inception' } } }, + research: { items: { 'a': { status: 'completed' } } }, }, }); const r = discover(dir, 'payments'); - const auth = r.discovery_map.find(t => t.name === 'auth-flow'); - assert.strictEqual(auth.lifecycle, 'researching'); - assert.strictEqual(auth.tier, '◐'); - assert.strictEqual(auth.current_phase, 'research'); + assert.strictEqual(r.discovery_map[0].lifecycle, 'ready_for_discussion'); + assert.strictEqual(r.discovery_map[0].tier, '→'); + assert.strictEqual(r.discovery_map[0].current_phase, 'research'); }); - it('sorts by tier rank then alphabetical within tier', () => { - seedEpic('payments', { + it('reflects discussing lifecycle when discussion is in-progress', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { + inception: { items: { 'a': { status: 'in-progress', routing: 'discussion', source: 'inception' } } }, + discussion: { items: { 'a': { status: 'in-progress' } } }, + }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].lifecycle, 'discussing'); + assert.strictEqual(r.discovery_map[0].tier, '◐'); + assert.strictEqual(r.discovery_map[0].current_phase, 'discussion'); + }); + + it('reflects decided lifecycle when discussion is completed', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { + inception: { items: { 'a': { status: 'in-progress', routing: 'discussion', source: 'inception' } } }, + discussion: { items: { 'a': { status: 'completed' } } }, + }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].lifecycle, 'decided'); + assert.strictEqual(r.discovery_map[0].tier, '✓'); + assert.strictEqual(r.discovery_map[0].current_phase, 'discussion'); + }); + + it('reflects cancelled lifecycle when both research and discussion are cancelled', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { + inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'inception' } } }, + research: { items: { 'a': { status: 'cancelled' } } }, + discussion: { items: { 'a': { status: 'cancelled' } } }, + }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].lifecycle, 'cancelled'); + assert.strictEqual(r.discovery_map[0].tier, '⊘'); + }); + + it('falls through to fresh when only research is cancelled (alternate path open)', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { + inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'inception' } } }, + research: { items: { 'a': { status: 'cancelled' } } }, + }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].lifecycle, 'fresh'); + }); + + // --- source_provenance --- + + it('source_provenance is null for source=inception', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'inception' } } } }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].source_provenance, null); + }); + + it('source_provenance reads "from {source}" for non-inception sources', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'research-analysis' } } } }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].source_provenance, 'from research-analysis'); + }); + + it('source_provenance unwraps colon-prefixed sources to "from {parent}"', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'research-split:kitchen-hardware' } } } }, + }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.discovery_map[0].source_provenance, 'from kitchen-hardware'); + }); + + // --- Sorting --- + + it('sorts by tier rank → first, then ◐, ✓, ○, ⊘', () => { + createManifest(dir, 'payments', { + work_type: 'epic', phases: { inception: { items: { - 'fresh-z': { status: 'in-progress', routing: 'research', source: 'inception' }, - 'fresh-a': { status: 'in-progress', routing: 'research', source: 'inception' }, - 'in-flight': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'fresh-item': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'inflight-item': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'ready-item': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'decided-item': { status: 'in-progress', routing: 'discussion', source: 'inception' }, }, }, - research: { items: { 'in-flight': { status: 'in-progress' } } }, + research: { items: { 'inflight-item': { status: 'in-progress' }, 'ready-item': { status: 'completed' } } }, + discussion: { items: { 'decided-item': { status: 'completed' } } }, }, }); const r = discover(dir, 'payments'); - assert.deepStrictEqual(r.discovery_map.map(t => t.name), ['in-flight', 'fresh-a', 'fresh-z']); + assert.deepStrictEqual( + r.discovery_map.map(t => t.name), + ['ready-item', 'inflight-item', 'decided-item', 'fresh-item'], + ); }); - it('computes map_summary counts from tier distribution', () => { - seedEpic('payments', { + it('sorts alphabetically within the same tier', () => { + createManifest(dir, 'payments', { + work_type: 'epic', phases: { inception: { items: { - 'a': { status: 'in-progress', routing: 'research', source: 'inception' }, - 'b': { status: 'in-progress', routing: 'discussion', source: 'inception' }, - 'c': { status: 'in-progress', routing: 'discussion', source: 'inception' }, + 'zeta': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'alpha': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'mu': { status: 'in-progress', routing: 'research', source: 'inception' }, + }, + }, + }, + }); + const r = discover(dir, 'payments'); + assert.deepStrictEqual(r.discovery_map.map(t => t.name), ['alpha', 'mu', 'zeta']); + }); + + // --- map_summary --- + + it('map_summary aggregates counts across all tiers', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { + inception: { + items: { + 'fresh-1': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'fresh-2': { status: 'in-progress', routing: 'discussion', source: 'inception' }, + 'inflight-1': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'ready-1': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'decided-1': { status: 'in-progress', routing: 'discussion', source: 'inception' }, + 'cancelled-1': { status: 'in-progress', routing: 'research', source: 'inception' }, + }, + }, + research: { + items: { + 'inflight-1': { status: 'in-progress' }, + 'ready-1': { status: 'completed' }, + 'cancelled-1': { status: 'cancelled' }, + }, + }, + discussion: { + items: { + 'decided-1': { status: 'completed' }, + 'cancelled-1': { status: 'cancelled' }, }, }, - discussion: { items: { 'c': { status: 'completed' } } }, }, }); const r = discover(dir, 'payments'); - assert.strictEqual(r.map_summary.total, 3); + assert.strictEqual(r.map_summary.total, 6); assert.strictEqual(r.map_summary.fresh, 2); + assert.strictEqual(r.map_summary.in_flight, 1); + assert.strictEqual(r.map_summary.ready, 1); assert.strictEqual(r.map_summary.decided, 1); + assert.strictEqual(r.map_summary.cancelled, 1); }); -}); -describe('refinement discovery: dismissed list', () => { - beforeEach(setupFixture); - afterEach(cleanupFixture); + it('map_summary returns all-zero shape when no inception items exist', () => { + createManifest(dir, 'payments', { work_type: 'epic', phases: {} }); + const r = discover(dir, 'payments'); + assert.deepStrictEqual(r.map_summary, { + total: 0, decided: 0, in_flight: 0, ready: 0, fresh: 0, cancelled: 0, + }); + }); + + // --- dismissed list --- it('returns empty array when phases.inception.dismissed is missing', () => { - seedEpic('payments', { phases: { inception: { items: {} } } }); + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: {} } }, + }); const r = discover(dir, 'payments'); assert.deepStrictEqual(r.dismissed, []); }); - it('returns the dismissed list verbatim when present', () => { - seedEpic('payments', { - phases: { - inception: { - items: {}, - dismissed: ['old-thing', 'another'], - }, - }, + it('returns the dismissed list verbatim and in order', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: {}, dismissed: ['first', 'second', 'third'] } }, }); const r = discover(dir, 'payments'); - assert.deepStrictEqual(r.dismissed, ['old-thing', 'another']); + assert.deepStrictEqual(r.dismissed, ['first', 'second', 'third']); }); -}); -describe('refinement discovery: latest_session detection', () => { - beforeEach(setupFixture); - afterEach(cleanupFixture); + it('returns an empty array when dismissed is non-array (defensive)', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: {}, dismissed: 'invalid-shape' } }, + }); + const r = discover(dir, 'payments'); + assert.deepStrictEqual(r.dismissed, []); + }); - it('returns null when no session logs exist', () => { - seedEpic('payments', { phases: { inception: { items: {} } } }); + it('does not mutate the manifest when reading dismissed', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: {}, dismissed: ['original'] } }, + }); + const r = discover(dir, 'payments'); + r.dismissed.push('mutation'); + const r2 = discover(dir, 'payments'); + assert.deepStrictEqual(r2.dismissed, ['original']); + }); + + // --- latest_session detection --- + + it('latest_session is null when no session logs exist', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); const r = discover(dir, 'payments'); assert.strictEqual(r.latest_session, null); - assert.strictEqual(r.next_session_number, 1); }); - it('detects the initial session log without flagging it as in-progress refinement', () => { - seedEpic('payments', { phases: { inception: { items: {} } } }); - writeSessionLog('payments', 1, '3 topics seeded.'); + it('detects session-001.md as initial (is_refinement=false)', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 1, '3 topics seeded.'); const r = discover(dir, 'payments'); + assert.strictEqual(r.latest_session.filename, 'session-001.md'); assert.strictEqual(r.latest_session.number, 1); assert.strictEqual(r.latest_session.is_refinement, false); assert.strictEqual(r.latest_session.is_in_progress, false); - assert.strictEqual(r.next_session_number, 2); }); - it('flags an in-progress refinement when Conclusion is `(none)`', () => { - seedEpic('payments', { phases: { inception: { items: {} } } }); - writeSessionLog('payments', 1, '3 topics seeded.'); - writeSessionLog('payments', 2, '(none)'); + it('flags is_in_progress=true when Conclusion is "(none)"', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 1, '3 topics seeded.'); + writeSessionLog(dir, 'payments', 2, '(none)'); const r = discover(dir, 'payments'); assert.strictEqual(r.latest_session.number, 2); assert.strictEqual(r.latest_session.is_refinement, true); assert.strictEqual(r.latest_session.is_in_progress, true); + assert.strictEqual(r.latest_session.conclusion_text, '(none)'); }); - it('does not flag a concluded refinement as in-progress', () => { - seedEpic('payments', { phases: { inception: { items: {} } } }); - writeSessionLog('payments', 1, '3 topics seeded.'); - writeSessionLog('payments', 2, '2 changes applied. Map now has 5 topics.'); + it('flags is_in_progress=false when Conclusion is concluded text', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 1, '3 topics seeded.'); + writeSessionLog(dir, 'payments', 2, '2 changes applied. Map now has 5 topics.'); const r = discover(dir, 'payments'); assert.strictEqual(r.latest_session.is_in_progress, false); assert.strictEqual(r.latest_session.conclusion_text, '2 changes applied. Map now has 5 topics.'); }); - it('next_session_number always increments past the highest existing number', () => { - seedEpic('payments', { phases: { inception: { items: {} } } }); - writeSessionLog('payments', 1, '3 topics seeded.'); - writeSessionLog('payments', 2, 'concluded'); - writeSessionLog('payments', 3, 'concluded'); + it('reads only the first line of Conclusion as conclusion_text', () => { + // Multi-line conclusion still picks first non-empty line. + const padded = '003'; + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 1, 'one'); + writeSessionLog(dir, 'payments', 2, 'two'); + const fullPath = path.join(dir, '.workflows', 'payments', 'inception', `session-${padded}.md`); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, `# Inception Session ${padded} — Refinement + +## Conclusion + +5 changes applied. Map now has 12 topics. + +Additional commentary on multiple lines. +`); + const r = discover(dir, 'payments'); + assert.strictEqual(r.latest_session.number, 3); + assert.strictEqual(r.latest_session.conclusion_text, '5 changes applied. Map now has 12 topics.'); + assert.strictEqual(r.latest_session.is_in_progress, false); + }); + + it('terminates Conclusion read at the next ## heading', () => { + // Defensive: hand-edited logs may add sections after Conclusion. + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 1, 'concluded', { trailingSection: 'Postscript' }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.latest_session.conclusion_text, 'concluded'); + }); + + it('treats missing Conclusion section as empty conclusion_text', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const fullPath = path.join(dir, '.workflows', 'payments', 'inception', 'session-001.md'); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, `# Inception Session 001 — Initial Framing\n\n## Map State at Start\n\n3 topics — 3 fresh\n`); + const r = discover(dir, 'payments'); + assert.strictEqual(r.latest_session.conclusion_text, ''); + assert.strictEqual(r.latest_session.is_in_progress, false); + }); + + it('latest_session picks the highest-numbered file regardless of alphabetic order', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 1, 'concluded'); + writeSessionLog(dir, 'payments', 10, 'concluded'); + writeSessionLog(dir, 'payments', 2, 'concluded'); + const r = discover(dir, 'payments'); + assert.strictEqual(r.latest_session.number, 10); + assert.strictEqual(r.latest_session.filename, 'session-010.md'); + }); + + it('ignores non-matching filenames in inception directory', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 1, 'concluded'); + createFile(dir, '.workflows/payments/inception/session-abc.md', 'should be ignored'); + createFile(dir, '.workflows/payments/inception/notes.md', 'should be ignored'); + const r = discover(dir, 'payments'); + assert.strictEqual(r.latest_session.filename, 'session-001.md'); + }); + + it('relative_path is project-relative and forward-slashed', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 2, '(none)'); + const r = discover(dir, 'payments'); + assert.strictEqual(r.latest_session.relative_path, '.workflows/payments/inception/session-002.md'); + }); + + // --- next_session_number --- + + it('next_session_number is 1 when no logs exist', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const r = discover(dir, 'payments'); + assert.strictEqual(r.next_session_number, 1); + }); + + it('next_session_number increments past the highest existing number', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 1, 'concluded'); + writeSessionLog(dir, 'payments', 2, 'concluded'); + writeSessionLog(dir, 'payments', 3, 'concluded'); const r = discover(dir, 'payments'); assert.strictEqual(r.next_session_number, 4); }); + + it('next_session_number still increments when latest is in-progress', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 1, 'concluded'); + writeSessionLog(dir, 'payments', 2, '(none)'); + const r = discover(dir, 'payments'); + assert.strictEqual(r.next_session_number, 3); + }); }); -describe('refinement discovery: error handling', () => { - beforeEach(setupFixture); - afterEach(cleanupFixture); +describe('workflow-inception-process format', () => { + let dir; + beforeEach(() => { dir = setupFixture(); }); + afterEach(() => { cleanupFixture(dir); }); - it('returns an error when the work unit does not exist', () => { - const r = discover(dir, 'nonexistent'); - assert.ok(r.error); - assert.match(r.error, /not found/i); + // --- Errors --- + + it('renders error result with "error:" prefix', () => { + const out = format({ error: 'Work unit "x" not found' }); + assert.match(out, /^error: Work unit "x" not found/); }); -}); -describe('refinement discovery: format()', () => { - beforeEach(setupFixture); - afterEach(cleanupFixture); + // --- Header --- + + it('header line includes the work_unit name', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const out = format(discover(dir, 'payments')); + assert.match(out, /=== INCEPTION DISCOVERY: payments ===/); + }); - it('produces parseable text output for a populated work unit', () => { - seedEpic('payments', { + // --- map_summary line --- + + it('map_summary line includes total and all six counts', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { + inception: { + items: { + 'a': { status: 'in-progress', routing: 'research', source: 'inception' }, + 'b': { status: 'in-progress', routing: 'discussion', source: 'inception' }, + }, + }, + }, + }); + const out = format(discover(dir, 'payments')); + assert.match(out, /map_summary: 2 topics — 0 decided, 0 in-flight, 0 ready, 2 fresh, 0 cancelled/); + }); + + it('map_summary line for empty map reads "0 topics — ..."', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const out = format(discover(dir, 'payments')); + assert.match(out, /map_summary: 0 topics — 0 decided/); + }); + + // --- discovery_map block --- + + it('renders "(empty)" placeholder when discovery_map has no entries', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const out = format(discover(dir, 'payments')); + assert.match(out, /discovery_map \(0\):\n {2}\(empty\)/); + }); + + it('renders map row with tier, name, lifecycle, routing, summary', () => { + createManifest(dir, 'payments', { + work_type: 'epic', phases: { inception: { items: { 'auth-flow': { status: 'in-progress', summary: 'oauth', routing: 'research', source: 'inception' }, }, - dismissed: ['old-topic'], }, }, }); - writeSessionLog('payments', 1, '1 topic seeded.'); - const r = discover(dir, 'payments'); - const out = format(r); - assert.match(out, /=== INCEPTION DISCOVERY: payments ===/); - assert.match(out, /map_summary: 1 topics/); - assert.match(out, /auth-flow \[fresh\]/); - assert.match(out, /dismissed \(1\):/); - assert.match(out, /- old-topic/); - assert.match(out, /next_session_number: 002/); + const out = format(discover(dir, 'payments')); + assert.match(out, /- ○ auth-flow \[fresh\] routing=research — oauth/); }); - it('renders errors prefixed with `error:`', () => { - const out = format({ error: 'Work unit "x" not found' }); - assert.match(out, /^error: Work unit "x" not found/); + it('omits source from map row when source=inception', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'inception' } } } }, + }); + const out = format(discover(dir, 'payments')); + assert.ok(!out.includes('source=inception')); + }); + + it('includes source=X in map row for non-inception sources', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'research-analysis' } } } }, + }); + const out = format(discover(dir, 'payments')); + assert.match(out, /source=research-analysis/); + }); + + it('includes phase=X in map row when current_phase is set', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { + inception: { items: { 'a': { status: 'in-progress', routing: 'research', source: 'inception' } } }, + research: { items: { 'a': { status: 'in-progress' } } }, + }, + }); + const out = format(discover(dir, 'payments')); + assert.match(out, /phase=research/); + }); + + it('omits routing= and summary suffix when those fields are null', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: { 'a': { status: 'in-progress' } } } }, + }); + const out = format(discover(dir, 'payments')); + assert.match(out, /- ○ a \[fresh\]/); + assert.ok(!out.includes('routing=')); + // Summary suffix is `— summary`; with no summary, no em dash should follow lifecycle. + assert.ok(!/\[fresh\] —/.test(out)); + }); + + // --- dismissed block --- + + it('renders "(none)" when dismissed list is empty', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const out = format(discover(dir, 'payments')); + assert.match(out, /dismissed \(0\):\n {2}\(none\)/); + }); + + it('renders dismissed names one per line', () => { + createManifest(dir, 'payments', { + work_type: 'epic', + phases: { inception: { items: {}, dismissed: ['old-thing', 'another'] } }, + }); + const out = format(discover(dir, 'payments')); + assert.match(out, /dismissed \(2\):\n {2}- old-thing\n {2}- another/); + }); + + // --- latest_session block --- + + it('renders "(no session logs on disk)" when latest_session is null', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const out = format(discover(dir, 'payments')); + assert.match(out, /latest_session:\n {2}\(no session logs on disk\)/); + }); + + it('renders all latest_session subfields when present', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 2, '(none)'); + const out = format(discover(dir, 'payments')); + assert.match(out, /filename: session-002\.md/); + assert.match(out, /relative_path: \.workflows\/payments\/inception\/session-002\.md/); + assert.match(out, /number: 2/); + assert.match(out, /is_refinement: true/); + assert.match(out, /is_in_progress: true/); + assert.match(out, /conclusion: \(none\)/); + }); + + it('renders conclusion as "(empty)" when conclusion_text is empty', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const fullPath = path.join(dir, '.workflows', 'payments', 'inception', 'session-001.md'); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, `# Inception Session 001 — Initial Framing\n\n## Map State at Start\n\n3 topics\n`); + const out = format(discover(dir, 'payments')); + assert.match(out, /conclusion: \(empty\)/); + }); + + // --- next_session_number line --- + + it('next_session_number line is zero-padded to 3 digits', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const out = format(discover(dir, 'payments')); + assert.match(out, /next_session_number: 001/); + }); + + it('next_session_number increments past the highest existing number', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + writeSessionLog(dir, 'payments', 1, 'concluded'); + writeSessionLog(dir, 'payments', 9, 'concluded'); + const out = format(discover(dir, 'payments')); + assert.match(out, /next_session_number: 010/); + }); + + // --- Output structure --- + + it('output ends with a trailing newline', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const out = format(discover(dir, 'payments')); + assert.ok(out.endsWith('\n')); + }); + + it('renders all five named sections in order', () => { + createManifest(dir, 'payments', { work_type: 'epic' }); + const out = format(discover(dir, 'payments')); + const order = [ + out.indexOf('=== INCEPTION DISCOVERY:'), + out.indexOf('map_summary:'), + out.indexOf('discovery_map ('), + out.indexOf('dismissed ('), + out.indexOf('latest_session:'), + out.indexOf('next_session_number:'), + ]; + for (let i = 1; i < order.length; i++) { + assert.ok(order[i] > order[i - 1], `section ${i} must follow section ${i - 1}; got ${order}`); + } }); });