From 25d82a2ee5a2e002b3faf9ce4fdee68c663636b5 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:22:49 -0400 Subject: [PATCH 1/6] fix(planning): expose per-question answered flag in /api/state state is dependency staleness, so a question with a terminal decision read "open" and looked unanswered. answered is computed with the same rule as the page (latest_decision) and documented in the README and schema. Co-Authored-By: Claude Opus 5.5 --- plugins/planning/surface/README.md | 2 +- .../surface/schema/questions.schema.json | 4 +++ plugins/planning/surface/server.py | 5 ++++ plugins/planning/surface/test_server.py | 28 +++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/plugins/planning/surface/README.md b/plugins/planning/surface/README.md index dc9515682d..fc57253514 100644 --- a/plugins/planning/surface/README.md +++ b/plugins/planning/surface/README.md @@ -68,7 +68,7 @@ Every command needs `--dir ''`; there is no default. Every write valid ## Data contract -`schema/` is the contract; other tools write these formats or read the exports, and the surface reads no other files except a file a visual names inside the data dir. Both documents carry `"schemaVersion": "1.0"`; a file without one reads as version 0 and loads unchanged. `responses.json` is an append-only event log with a global `seq`; undo marks an event `withdrawn` and nothing is deleted. Event kinds: `accept`, `alt`, `own`, `defer`, `reopen`, `ask`, `rephrase`, `note`, `undo`, `wrapup`, `confirm`, `confirm-understanding`. `confirm-understanding` has no id; its `alt` is `confirm` or `off` (`off` needs `text`), and its `contentRev` must equal `restatement.rev`: a missing restatement or `contentRev` is 400, an older rev is 409 `{"error": "stale", "contentRev": }`. A repeated Confirm (a `confirm` of the same commitment, or a `confirm-understanding` Confirm of the same rev) records nothing and returns the first event's seq. Question `state` (`open`, `stale`, `upstream-pending`, `archived`) is computed by the server from `dependsOn` and `archived`, never written. A visual is declared by `format` (`svg`, `mermaid`, `image`, `markdown`, `html`, `chart`; `kind` is read as an alias) and describes only its content. +`schema/` is the contract; other tools write these formats or read the exports, and the surface reads no other files except a file a visual names inside the data dir. Both documents carry `"schemaVersion": "1.0"`; a file without one reads as version 0 and loads unchanged. `responses.json` is an append-only event log with a global `seq`; undo marks an event `withdrawn` and nothing is deleted. Event kinds: `accept`, `alt`, `own`, `defer`, `reopen`, `ask`, `rephrase`, `note`, `undo`, `wrapup`, `confirm`, `confirm-understanding`. `confirm-understanding` has no id; its `alt` is `confirm` or `off` (`off` needs `text`), and its `contentRev` must equal `restatement.rev`: a missing restatement or `contentRev` is 400, an older rev is 409 `{"error": "stale", "contentRev": }`. A repeated Confirm (a `confirm` of the same commitment, or a `confirm-understanding` Confirm of the same rev) records nothing and returns the first event's seq. Question `state` (`open`, `stale`, `upstream-pending`, `archived`) is computed by the server from `dependsOn` and `archived`, never written; it is dependency staleness, not an answered flag. `answered` (server-computed, never written) is true when a page or terminal decision counts, so a question with a terminal decision reads `state` `open` and `answered` true. A visual is declared by `format` (`svg`, `mermaid`, `image`, `markdown`, `html`, `chart`; `kind` is read as an alias) and describes only its content. ## Security model diff --git a/plugins/planning/surface/schema/questions.schema.json b/plugins/planning/surface/schema/questions.schema.json index cdba367625..d6120f6338 100644 --- a/plugins/planning/surface/schema/questions.schema.json +++ b/plugins/planning/surface/schema/questions.schema.json @@ -246,6 +246,10 @@ "state": { "enum": ["open", "stale", "upstream-pending", "archived"], "description": "Derived by the server from dependsOn, decision events and archived; never written by Claude" + }, + "answered": { + "type": "boolean", + "description": "Derived by the server: true when a page or terminal decision counts; never written" } } } diff --git a/plugins/planning/surface/server.py b/plugins/planning/surface/server.py index 177530da7c..24641ed00e 100644 --- a/plugins/planning/surface/server.py +++ b/plugins/planning/surface/server.py @@ -732,9 +732,14 @@ def state(self): r = load_json(self.responses, EMPTY_RESPONSES) settings, theme = self.layers.resolve(self.dir, self.user_settings()) derived = question_states(q, r) + from exporters import latest_decision # exporters imports server at module top + for x in q.get("questions") or []: if isinstance(x, dict) and x.get("id") in derived: x["state"], x["revising"] = derived[x["id"]] + x["answered"] = ( + latest_decision(x, r.get("responses", {})) is not None + ) self._last_state = { "questions": q, "responses": r, diff --git a/plugins/planning/surface/test_server.py b/plugins/planning/surface/test_server.py index a35e29737d..e351ff0920 100644 --- a/plugins/planning/surface/test_server.py +++ b/plugins/planning/surface/test_server.py @@ -1187,6 +1187,34 @@ def test_terminal_dependent_goes_stale_when_its_prerequisite_changes(self): self.assertEqual(self.states(), {"A": "open", "B": "stale"}) +class TestAnswered(WaitCase): + """`answered` is true when a page or terminal decision counts; `state` stays dependency staleness.""" + + @classmethod + def prepare(cls): + seed_questions(cls.dir, question("A"), question("B"), question("C")) + + def answered(self): + return { + q["id"]: (q.get("answered"), q.get("state")) + for q in self.state()["questions"]["questions"] + } + + def test_1_unanswered_is_false(self): + self.assertEqual(self.answered()["A"], (False, "open")) + + def test_2_terminal_decision_is_answered_and_state_stays_open(self): + rc, out = self.rp("record-terminal", "A", "--decision", "accept") + self.assertEqual(rc, 0, out) + self.assertEqual(self.answered()["A"], (True, "open")) + + def test_3_page_accept_is_answered(self): + code, data = self.post({"id": "B", "kind": "accept"}) + self.assertEqual(code, 200, data) + self.assertEqual(self.answered()["B"], (True, "open")) + self.assertEqual(self.answered()["C"], (False, "open")) + + class TestConfirm(WaitCase): """The `confirm` event: ticks one commitment, records no decision, needs handling.""" From 3e6bcb4dcb706e2c01b7b072abe5297f840c5c18 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:27:55 -0400 Subject: [PATCH 2/6] chore(planning): bump to 0.46.2 with changelog entry for answered flag Co-Authored-By: Claude Opus 5.5 --- plugins/planning/.claude-plugin/plugin.json | 2 +- plugins/planning/CHANGELOG.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/planning/.claude-plugin/plugin.json b/plugins/planning/.claude-plugin/plugin.json index ac770bd68d..35b13fd097 100644 --- a/plugins/planning/.claude-plugin/plugin.json +++ b/plugins/planning/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "planning", - "version": "0.46.1", + "version": "0.46.2", "userConfig": { "surface": { "type": "string", diff --git a/plugins/planning/CHANGELOG.md b/plugins/planning/CHANGELOG.md index 8099512c48..6cf6d888ae 100644 --- a/plugins/planning/CHANGELOG.md +++ b/plugins/planning/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to the `planning` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.46.2] - 2026-09-29 + +### Added + +- **`/api/state` reports `answered` per question**, true when a page or terminal decision counts (the rule the page uses). `state` is dependency staleness, so a question with a terminal decision still reads `open`; the README and `questions.schema.json` say so ([#5459](https://github.com/melodic-software/claude-code-plugins/issues/5459)). + ## [0.46.1] - 2026-09-29 ### Changed From 87dd8d9591b50915387c1cff67346c4cd35d7938 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:45:07 -0400 Subject: [PATCH 3/6] fix(planning): wrap exporters import comment for ruff format, bump to 0.47.0 Move the inline import comment onto its own line so ruff format --check passes, and bump the minor version since the entry is filed under Added. Co-Authored-By: Claude Opus 5.5 --- plugins/planning/.claude-plugin/plugin.json | 2 +- plugins/planning/CHANGELOG.md | 2 +- plugins/planning/surface/server.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/planning/.claude-plugin/plugin.json b/plugins/planning/.claude-plugin/plugin.json index 35b13fd097..25ef6bc142 100644 --- a/plugins/planning/.claude-plugin/plugin.json +++ b/plugins/planning/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "planning", - "version": "0.46.2", + "version": "0.47.0", "userConfig": { "surface": { "type": "string", diff --git a/plugins/planning/CHANGELOG.md b/plugins/planning/CHANGELOG.md index 6cf6d888ae..24b6cbe089 100644 --- a/plugins/planning/CHANGELOG.md +++ b/plugins/planning/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to the `planning` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. -## [0.46.2] - 2026-09-29 +## [0.47.0] - 2026-09-29 ### Added diff --git a/plugins/planning/surface/server.py b/plugins/planning/surface/server.py index 24641ed00e..46ea61e592 100644 --- a/plugins/planning/surface/server.py +++ b/plugins/planning/surface/server.py @@ -732,7 +732,8 @@ def state(self): r = load_json(self.responses, EMPTY_RESPONSES) settings, theme = self.layers.resolve(self.dir, self.user_settings()) derived = question_states(q, r) - from exporters import latest_decision # exporters imports server at module top + # exporters imports server at module top + from exporters import latest_decision for x in q.get("questions") or []: if isinstance(x, dict) and x.get("id") in derived: From d526fc07f0be9c25a0b77c6edbf72f3af187fa21 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:22:55 -0400 Subject: [PATCH 4/6] fix(planning): reopened question reads answered false in /api/state Co-Authored-By: Claude Opus 5.5 --- plugins/planning/surface/server.py | 6 ++++-- plugins/planning/surface/test_server.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/planning/surface/server.py b/plugins/planning/surface/server.py index 8eb314e99d..b5d52c4d25 100644 --- a/plugins/planning/surface/server.py +++ b/plugins/planning/surface/server.py @@ -811,8 +811,10 @@ def state(self): for x in q.get("questions") or []: if isinstance(x, dict) and x.get("id") in derived: x["state"], x["revising"] = derived[x["id"]] - x["answered"] = ( - latest_decision(x, r.get("responses", {})) is not None + x["answered"] = bool( + (latest_decision(x, r.get("responses", {})) or {}).get( + "decision" + ) ) self._last_state = { "questions": q, diff --git a/plugins/planning/surface/test_server.py b/plugins/planning/surface/test_server.py index 34629c88a5..2f510f03bc 100644 --- a/plugins/planning/surface/test_server.py +++ b/plugins/planning/surface/test_server.py @@ -1380,6 +1380,12 @@ def test_3_page_accept_is_answered(self): self.assertEqual(self.answered()["B"], (True, "open")) self.assertEqual(self.answered()["C"], (False, "open")) + def test_4_reopen_is_not_answered(self): + for kind in ("accept", "reopen"): + code, data = self.post({"id": "C", "kind": kind}) + self.assertEqual(code, 200, data) + self.assertEqual(self.answered()["C"], (False, "open")) + class TestConfirm(WaitCase): """The `confirm` event: ticks one commitment, records no decision, needs handling.""" From 22074de1f9b0d3260b858c096a2b320e43d4f86c Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:03:31 -0400 Subject: [PATCH 5/6] fix(planning): resolve manifest merge markers Co-Authored-By: Claude Opus 5.5 --- plugins/planning/.claude-plugin/plugin.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/planning/.claude-plugin/plugin.json b/plugins/planning/.claude-plugin/plugin.json index 6c741506cb..aa6af30bfc 100644 --- a/plugins/planning/.claude-plugin/plugin.json +++ b/plugins/planning/.claude-plugin/plugin.json @@ -1,13 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "planning", -<<<<<<< HEAD "version": "0.48.0", -||||||| e17d8076b - "version": "0.47.6", -======= - "version": "0.47.7", ->>>>>>> origin/main "userConfig": { "surface": { "type": "string", From 2bd246aa304653b82d6678ea7a4324fdb35cc3ba Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:24:39 -0400 Subject: [PATCH 6/6] fix(planning): drop duplicate version key left by merge Co-Authored-By: Claude Opus 5.5 --- plugins/planning/.claude-plugin/plugin.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/planning/.claude-plugin/plugin.json b/plugins/planning/.claude-plugin/plugin.json index b6fd13d121..aa6af30bfc 100644 --- a/plugins/planning/.claude-plugin/plugin.json +++ b/plugins/planning/.claude-plugin/plugin.json @@ -2,7 +2,6 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "planning", "version": "0.48.0", - "version": "0.47.9", "userConfig": { "surface": { "type": "string",