Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions template/PCDA/quality-cycle/02-cycle-artifacts.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ DO ── reads brief, writes ──▶ patch.diff + test + build-notes.md
│ accept ──▶ mark PR ready / merge → cycle complete │
│ iterate-to-Do ──▶ rebuild (back to DO with same brief) │
│ iterate-to-Plan ──▶ revise brief (back to PLAN, same cycle) │
park ──▶ discontinue — no transition, bundle out
discontinue ──▶ no transitionbundle dropped from set
└────────────────────────────────────────────────────────────────────┘
Expand Down Expand Up @@ -167,10 +167,10 @@ Assembled across Do (spec + claim) and Check (evidence + verdict). Its job: let

## 9. Check sign-off ← human completes Check here
- Disposition confirmed / overridden:
- Outcome: merged-wider | closed-<reason> | iterated-to-Do | iterated-to-Plan | parked
- Outcome: merged-wider | closed-<reason> | iterated-to-Do | iterated-to-Plan | discontinued
- Iteration delta (if iterating): <what the next Do or Plan must change
for THIS cycle — distinct from process-level Act deltas, which go to §10;
on a park, the rationale for discontinuing / where the work goes instead>
on a discontinue, the rationale for discontinuing / where the work goes instead>
- By / date:

## 10. Act candidates (hints for the next Act review)
Expand Down
4 changes: 2 additions & 2 deletions template/PCDA/quality-cycle/03-cycle-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ SUMMARY.md §9 set → sign-off applied:
accept → cycle COMPLETE (frozen bundle)
iterate-to-Do → driver archives every Do+Check artifact into iteration-v<N>/; state ← PLANNED (re-run Do against same brief)
iterate-to-Plan → driver archives the attempt (incl. brief.md) into iteration-v<N>/; state ← UNPLANNED (human authors a new brief.md, then Do re-runs)
park → state ← DISCONTINUED (no transition, no archive; bundle deliberately abandoned and dropped from the active set)
discontinue → state ← DISCONTINUED (no transition, no archive; bundle deliberately abandoned and dropped from the active set)
```

The driver stops the issue at AWAITING_SIGNOFF every time — including on iteration. After sign-off, an accepted bundle is **frozen**: it becomes input for the *next* Act review (a separate, cross-cycle pass — see below).
Expand Down Expand Up @@ -84,7 +84,7 @@ Check is one beat with three components, each automating at a different level. C
- **accept** → driver performs the sign-off-gated transitions: marks the draft PR ready, posts the §8 tracker comment, and (where the project's per-repo spec allows it) merges. The push and draft-PR-open may already have happened during Do or Check assembly — accept only performs the steps that *required* sign-off. Cycle closes; bundle frozen.
- **iterate-to-Do** → driver archives `patch.diff`, the test, and the rest of the Do+Check downstream into `iteration-v<N>/` (preserving `brief.md`), state returns to PLANNED, driver re-invokes the builder. Same cycle.
- **iterate-to-Plan** → driver archives the whole attempt — incl. `brief.md` — into `iteration-v<N>/`, state returns to UNPLANNED; the human authors a new `brief.md`, then Do re-runs. Same cycle.
- **park** → driver records §9 and performs **no** transition or archive; state becomes DISCONTINUED (terminal) and the bundle drops out of the active/pending set. For work that, on inspection, doesn't fit the cycle (e.g. handled out-of-band by hand) — a deliberate abandon, independent of §6 (no C6 accept-guard). The human records why parked / where the work goes instead, like the iterate rationale.
- **discontinue** → driver records §9 and performs **no** transition or archive; state becomes DISCONTINUED (terminal) and the bundle drops out of the active/pending set. For work that, on inspection, doesn't fit the cycle (e.g. handled out-of-band by hand) — a deliberate abandon, independent of §6 (no C6 accept-guard). The human records why discontinued / where the work goes instead, like the iterate rationale.

Optionally, the human jots §10 Act candidates while at the bundle — these are hints for the next Act review, not gates for this sign-off.

Expand Down
8 changes: 4 additions & 4 deletions template/src/pdca_harness/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def main(argv: list[str] | None = None) -> int:
p_init.add_argument("issue_id")
p_init.add_argument("--from-brief", type=Path, help="copy this file as brief.md")

p_run = sub.add_parser("run", help="advance an issue to a parked state")
p_run = sub.add_parser("run", help="advance an issue to a halted state")
p_run.add_argument("issue_id")

p_flow = sub.add_parser("flow", help="continuous Claude-driven cycle (Plan→Do→Check[→publish]→Act)")
Expand Down Expand Up @@ -84,7 +84,7 @@ def main(argv: list[str] | None = None) -> int:
g.add_argument("--accept", action="store_true", help="accept — merge wider")
g.add_argument("--iterate-do", action="store_true", help="rebuild against same brief")
g.add_argument("--iterate-plan", action="store_true", help="revise the brief")
g.add_argument("--park", action="store_true",
g.add_argument("--discontinue", action="store_true",
help="discontinue — record §9, no transition, drop from the pending set")
p_signoff.add_argument("--by", default="", help="who signed off")
p_signoff.add_argument("--delta", default="", help="iteration delta note")
Expand Down Expand Up @@ -370,8 +370,8 @@ def _signoff(cfg: Config, args: argparse.Namespace) -> int:
action = "iterate-do"
elif args.iterate_plan:
action = "iterate-plan"
else: # --park: deliberate abandon, no C6 guard
action = "park"
else: # --discontinue: deliberate abandon, no C6 guard
action = "discontinue"

date = datetime.date.today().isoformat()
signoff.record(summary, action=action, by=args.by or "unknown", date=date, delta=args.delta)
Expand Down
4 changes: 2 additions & 2 deletions template/src/pdca_harness/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ def advance(d: Path, cfg: Config) -> None:


def run_issue(d: Path, cfg: Config) -> str:
"""Advance until the bundle reaches a parked state; return that state."""
while state.state(d) not in state.PARKED:
"""Advance until the bundle reaches a halted state; return that state."""
while state.state(d) not in state.HALTED:
advance(d, cfg)
return state.state(d)

Expand Down
10 changes: 5 additions & 5 deletions template/src/pdca_harness/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def flow(
if not _plan_if_unplanned(cfg, d, csv):
break
if driver.run_issue(d, cfg) != state.AWAITING_SIGNOFF:
break # reached COMPLETE, or parked somewhere the human must look at
break # reached COMPLETE, or halted somewhere the human must look at
if _signoff_and_apply(cfg, d, by=by, today=today) in (None, "blocked"):
break
if state.state(d) == state.COMPLETE:
Expand Down Expand Up @@ -415,16 +415,16 @@ def flow_batch(
Runs the batch Plan session, then builds / checks / signs off EVERY bundle that
has work left — the ones this session briefed AND any already in flight — so
re-running ``flow --from-csv`` picks up where it left off instead of failing on
"no new briefs". COMPLETE bundles (done), DISCONTINUED ones (parked) and UNPLANNED
"no new briefs". COMPLETE bundles (done), DISCONTINUED ones (abandoned) and UNPLANNED
ones (no brief — e.g. an issue the planner chose to skip) are left alone. Returns
``{issue_id: state}``.
"""
today = today or datetime.date.today().isoformat()

leaves.do_plan_batch(cfg, csv)
# Resume set: every bundle with a brief that isn't finished. UNPLANNED (skipped /
# un-briefed), COMPLETE (done) and DISCONTINUED (parked — deliberately abandoned)
# are excluded, so a re-run is idempotent and a parked bundle stays out of the sweep.
# un-briefed), COMPLETE (done) and DISCONTINUED (deliberately abandoned)
# are excluded, so a re-run is idempotent and a discontinued bundle stays out of the sweep.
bundles = sorted(
(cfg.bundle_root / name for name in _bundle_dirs(cfg)
if state.state(cfg.bundle_root / name)
Expand Down Expand Up @@ -457,7 +457,7 @@ def flow_ids(
Like :func:`flow_batch` but seeded by explicit ids with **no Plan beat** — the
bundles must already have a brief. Missing / un-briefed (UNPLANNED) ids are
skipped with a note (brief them at Plan first); terminal ids (COMPLETE or
DISCONTINUED/parked) are left alone. Returns ``{issue_id: state}``.
DISCONTINUED) are left alone. Returns ``{issue_id: state}``.
"""
today = today or datetime.date.today().isoformat()
bundles: list[Path] = []
Expand Down
6 changes: 3 additions & 3 deletions template/src/pdca_harness/leaves.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
# The interactive sign-off leaf writes its decision here; the flow reads it and
# routes it through the C6-guarded signoff.record (never a model-written §9).
SIGNOFF_DECISION = "signoff-decision"
VALID_DECISIONS = frozenset({"accept", "iterate-do", "iterate-plan", "park"})
VALID_DECISIONS = frozenset({"accept", "iterate-do", "iterate-plan", "discontinue"})


# ----------------------------------------------------------------------------
Expand Down Expand Up @@ -410,7 +410,7 @@ def _signoff_prompt(d: Path) -> str:
f"`- [ ]` to `- [x]` only with their explicit OK). Then write the agreed "
f"decision as a single token — one of: {', '.join(sorted(VALID_DECISIONS))} — "
f"into {d}/{SIGNOFF_DECISION}. For an iterate, add the rationale (why rejected / "
f"what to change) on the lines below the token; for park, the rationale (why "
f"what to change) on the lines below the token; for discontinue, the rationale (why "
f"discontinued / where the work goes instead). Do not edit §9 yourself; the "
"driver records it under a deterministic guard."
)
Expand All @@ -426,7 +426,7 @@ def _stub_signoff(d: Path, cfg: Config) -> None:


def run_signoff_batch(cfg: Config, bundles: list[Path]) -> None:
"""Batch sign-off: ONE interactive session walks several parked bundles.
"""Batch sign-off: ONE interactive session walks several halted bundles.

Mirrors :func:`do_plan_batch` — command mode runs a single seeded session over
the whole (cheap-first) chunk, so the human signs off N bundles without N session
Expand Down
2 changes: 1 addition & 1 deletion template/src/pdca_harness/queue.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""The cheap-first sign-off queue (docs 03 §sign-off queue, §Batch fan-out).

After the driver fans out over a batch, the human works a burn-down: bundles
parked at AWAITING_SIGNOFF, ordered so the near-instant confirms come first
halted at AWAITING_SIGNOFF, ordered so the near-instant confirms come first
(empty §6 — typically already-fixed / wontfix / by-design) and the real
adjudications (non-empty §6 NEEDS-HUMAN) come last. This module is the pure
ordering logic; the CLI renders it.
Expand Down
8 changes: 4 additions & 4 deletions template/src/pdca_harness/signoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
# mapping lives in :mod:`pdca_harness.state` (which owns the state names); this
# module knows only the tokens, so there is no import cycle between the two.
VALID_OUTCOMES = frozenset(
{"merged-wider", "accepted", "iterated-to-Do", "iterated-to-Plan", "parked"})
{"merged-wider", "accepted", "iterated-to-Do", "iterated-to-Plan", "discontinued"})

# What `signoff --accept/--iterate-do/--iterate-plan/--park` writes into the Outcome line.
# What `signoff --accept/--iterate-do/--iterate-plan/--discontinue` writes into the Outcome line.
ACTION_TO_OUTCOME = {
"accept": "merged-wider",
"iterate-do": "iterated-to-Do",
"iterate-plan": "iterated-to-Plan",
"park": "parked",
"discontinue": "discontinued",
}

_OUTCOME_RE = re.compile(r"^- Outcome:\s*(.*?)\s*$", re.MULTILINE)
Expand Down Expand Up @@ -77,7 +77,7 @@ def open_needs_human(summary_path: Path) -> list[str]:
def record(summary_path: Path, *, action: str, by: str, date: str, delta: str = "") -> None:
"""Write the human's §9 decision into ``SUMMARY.md`` in place.

``action`` is one of ``accept`` / ``iterate-do`` / ``iterate-plan`` / ``park``.
``action`` is one of ``accept`` / ``iterate-do`` / ``iterate-plan`` / ``discontinue``.
"""
outcome = ACTION_TO_OUTCOME[action]
text = summary_path.read_text(encoding="utf-8")
Expand Down
8 changes: 4 additions & 4 deletions template/src/pdca_harness/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from . import signoff

# The ordered states a bundle moves through. The terminal/parked states
# The ordered states a bundle moves through. The terminal/halted states
# (UNPLANNED, AWAITING_SIGNOFF, COMPLETE) are where the driver stops and a human
# acts; the rest the driver advances through unattended.
UNPLANNED = "UNPLANNED" # no brief — human authors it (Plan)
Expand All @@ -23,10 +23,10 @@
ITERATE_DO = "ITERATE_DO" # sign-off chose iterate-to-Do
ITERATE_PLAN = "ITERATE_PLAN" # sign-off chose iterate-to-Plan
COMPLETE = "COMPLETE" # sign-off accepted — bundle frozen
DISCONTINUED = "DISCONTINUED" # sign-off chose park — deliberately abandoned, no transition
DISCONTINUED = "DISCONTINUED" # sign-off chose discontinue — deliberately abandoned, no transition

# States where the driver does nothing (human work, or done).
PARKED = {UNPLANNED, AWAITING_SIGNOFF, COMPLETE, DISCONTINUED}
HALTED = {UNPLANNED, AWAITING_SIGNOFF, COMPLETE, DISCONTINUED}

# §9 outcome token → bundle state. state owns the state names, so the mapping
# lives here; signoff knows only the tokens (no import cycle).
Expand All @@ -35,7 +35,7 @@
"accepted": COMPLETE,
"iterated-to-Do": ITERATE_DO,
"iterated-to-Plan": ITERATE_PLAN,
"parked": DISCONTINUED,
"discontinued": DISCONTINUED,
}


Expand Down
26 changes: 13 additions & 13 deletions template/tests/test_driver_slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,32 +98,32 @@ def test_iterate_to_plan_archives_attempt(self) -> None:
self.assertTrue((self.d / "iteration-v1" / "brief.md").exists()) # preserved
self.assertTrue((self.d / "iteration-v1" / "patch.diff").exists()) # attempt preserved

def test_park_derives_discontinued_and_does_not_transition(self) -> None:
# park is terminal: §9 records `parked`, state derives DISCONTINUED, and
def test_discontinue_derives_discontinued_and_does_not_transition(self) -> None:
# discontinue is terminal: §9 records `discontinued`, state derives DISCONTINUED, and
# run_issue performs NO transition (no archive — the attempt stays in place,
# the bundle just drops out of the active set).
driver.run_issue(self.d, self.cfg)
signoff.record(self.d / "SUMMARY.md", action="park", by="tester", date="2026-01-01")
self.assertEqual(signoff.outcome_token(self.d / "SUMMARY.md"), "parked")
signoff.record(self.d / "SUMMARY.md", action="discontinue", by="tester", date="2026-01-01")
self.assertEqual(signoff.outcome_token(self.d / "SUMMARY.md"), "discontinued")
self.assertEqual(state.state(self.d), state.DISCONTINUED)
self.assertEqual(driver.run_issue(self.d, self.cfg), state.DISCONTINUED) # no-op
self.assertFalse((self.d / "iteration-v1").exists()) # nothing archived
self.assertTrue((self.d / "patch.diff").exists()) # attempt left untouched

def test_park_not_guarded_by_open_needs_human(self) -> None:
# Parking is a deliberate abandon, independent of §6 — unlike accept (C6), a
# bundle with open NEEDS-HUMAN items can still be parked at the record layer.
def test_discontinue_not_guarded_by_open_needs_human(self) -> None:
# Discontinue is a deliberate abandon, independent of §6 — unlike accept (C6), a
# bundle with open NEEDS-HUMAN items can still be discontinued at the record layer.
driver.run_issue(self.d, self.cfg)
self.assertTrue(signoff.open_needs_human(self.d / "SUMMARY.md")) # §6 still open
signoff.record(self.d / "SUMMARY.md", action="park", by="tester", date="2026-01-01")
signoff.record(self.d / "SUMMARY.md", action="discontinue", by="tester", date="2026-01-01")
self.assertEqual(state.state(self.d), state.DISCONTINUED)

def test_signoff_decision_accepts_park_token(self) -> None:
# The `park` token was silently dropped before (#42); leaves now recognises it
def test_signoff_decision_accepts_discontinue_token(self) -> None:
# The `discontinue` token was silently dropped before (#42); leaves now recognises it
# and reads the rationale written below it.
(self.d / leaves.SIGNOFF_DECISION).write_text(
"park\nrestructuring task — handled by hand upstream\n", encoding="utf-8")
self.assertEqual(leaves.signoff_decision(self.d), "park")
"discontinue\nrestructuring task — handled by hand upstream\n", encoding="utf-8")
self.assertEqual(leaves.signoff_decision(self.d), "discontinue")
self.assertEqual(leaves.signoff_rationale(self.d),
"restructuring task — handled by hand upstream")

Expand Down Expand Up @@ -305,7 +305,7 @@ def test_index_only_sees_frozen(self) -> None:
live = self.cfg.bundle("LIVE")
live.mkdir(parents=True)
shutil.copyfile(TOY_BRIEF, live / "brief.md")
driver.run_issue(live, self.cfg) # parks at AWAITING_SIGNOFF
driver.run_issue(live, self.cfg) # halts at AWAITING_SIGNOFF
names = [e.bundle.name for e in act.index(self.cfg)]
self.assertEqual(names, ["issue_DONE"])

Expand Down
Loading
Loading