@mgiles/perk 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extension/adapters/planAdapterPlannotator.ts +12 -9
- package/extension/doors/commitCompact.ts +98 -10
- package/extension/doors/draftReviewWaveTools.ts +43 -15
- package/extension/doors/dreamWaveTools.ts +475 -0
- package/extension/doors/objectiveReviewBrowser.ts +36 -13
- package/extension/doors/objectiveStack.ts +1 -1
- package/extension/doors/planReviewBrowser.ts +30 -8
- package/extension/doors/prReview.ts +156 -49
- package/extension/doors/prReviewDynamic.ts +33 -13
- package/extension/doors/reviewWaveTools.ts +37 -14
- package/extension/factories/objectiveDraft.ts +95 -27
- package/extension/factories/objectiveDreamReport.ts +347 -0
- package/extension/factories/objectiveSave.ts +74 -1
- package/extension/factories/planReview.ts +173 -10
- package/extension/index.ts +62 -15
- package/extension/substrate/agentScratch.ts +171 -0
- package/extension/substrate/bindingDelivery.ts +9 -11
- package/extension/substrate/cache.ts +92 -2
- package/extension/substrate/command.ts +9 -6
- package/extension/substrate/config.ts +6 -1
- package/extension/substrate/git.ts +85 -2
- package/extension/substrate/result.ts +3 -2
- package/extension/substrate/sessionData.ts +6 -4
- package/extension/substrate/sessionPointers.ts +3 -4
- package/extension/substrate/toolGating.ts +9 -0
- package/extension/substrate/workflowState.ts +44 -2
- package/extension/surfaces/report.ts +38 -12
- package/extension/surfaces/surfaces.ts +129 -7
- package/extension/vendor/btw/btw.ts +38 -6
- package/extension/waves/adversarialReviewWave.ts +19 -2
- package/extension/waves/draftReviewWave.ts +17 -1
- package/extension/waves/dreamReducerWave.ts +700 -0
- package/extension/waves/dreamReport.ts +1494 -0
- package/extension/waves/dreamWave.ts +927 -0
- package/extension/waves/harvestWave.ts +1 -1
- package/extension/waves/ponytail.ts +104 -0
- package/extension/waves/prReviewDynamicWave.ts +115 -34
- package/extension/waves/prReviewWave.ts +122 -17
- package/extension/waves/reportWave.ts +103 -7
- package/extension/worker/readOnlySession.ts +2 -3
- package/package.json +6 -3
- package/prompts/_fixtures/live.yaml +49 -0
- package/prompts/commit-and-compact-continuation.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +7 -1
- package/prompts/contexts/adapters/plannotator-plan.md +7 -1
- package/prompts/stages/conflict-resolution.md +1 -1
- package/prompts/stages/learn-dream.md +10 -0
- package/prompts/stages/objective-review-browser.md +1 -1
- package/prompts/stages/plan-review-browser.md +1 -1
- package/prompts/stages/pr-review-browser/active.md +1 -1
- package/prompts/stages/pr-review-browser/foreign.md +1 -1
- package/prompts/stages/pr-review-dynamic.md +5 -5
- package/prompts/stages/pr-review-terminal/active.md +1 -1
- package/prompts/stages/pr-review-terminal/foreign.md +1 -1
- package/prompts/stages/pr-review-terminal/local.md +1 -1
- package/prompts/stages/pr-review.md +5 -5
- package/shared/bindings.yaml +3 -0
- package/shared/contracts.md +2176 -500
- package/shared/registry.yaml +12 -12
- package/shared/schemas/inputs/review-post-batch.schema.json +14 -1
- package/shared/schemas/outputs/objective-doctor.schema.json +39 -1
- package/shared/schemas/outputs/pr-land.schema.json +3 -3
package/shared/contracts.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# perk cross-plane contracts
|
|
2
2
|
|
|
3
3
|
The language-neutral contracts both planes obey, authored once here and bundled into each
|
|
4
|
-
build artifact. This document holds the numbered **prose contract sections** (`§8.1`–`§8.
|
|
4
|
+
build artifact. This document holds the numbered **prose contract sections** (`§8.1`–`§8.65`,
|
|
5
5
|
non-contiguous: `§8.8` is skipped and `§8.6a` exists; no parser): the Python CLI (`perk`)
|
|
6
6
|
and the TS extension (`@mgiles/perk`) each implement one side, against the exact names/paths/
|
|
7
7
|
fields pinned in each section. `perk doctor` verifies conformance. The numbering convention:
|
|
@@ -32,7 +32,8 @@ The local cache tier — written and read by **both** the CLI (exterior) and the
|
|
|
32
32
|
├── plan.md # cache.plan: the per-worktree plan snapshot for review fidelity (Python-only; transient)
|
|
33
33
|
├── plan-ref.json # cache.plan-ref: the active plan->branch ref pointer (local mirror)
|
|
34
34
|
├── scratch/runs/<run_id>/ # per-run inter-process workflow files (diffs, generated bodies)
|
|
35
|
-
│
|
|
35
|
+
│ ├── agent/ # disposable, non-authoritative model intermediates (mode 0700)
|
|
36
|
+
│ └── data/ # pointer-validated run-scoped session artifacts
|
|
36
37
|
├── handoff/<run_id>.json # pre-session CLI->extension cold-door state (claimed on session_start)
|
|
37
38
|
├── agent-session.json # cache.agent-session: the Linear AgentSession pointer (§8.22)
|
|
38
39
|
├── hunk-watch/ # the watch-feedback bridge (§8.58): worktree-local, disposable
|
|
@@ -132,12 +133,18 @@ The local cache tier — written and read by **both** the CLI (exterior) and the
|
|
|
132
133
|
fixed constant `objective-draft.json` — `OBJECTIVE_DRAFT_ARTIFACT`,
|
|
133
134
|
`extension/factories/objectiveDraft.ts` — and the path derives exclusively through the accessor seam;
|
|
134
135
|
the gate's `edit`/`write`/bash blocking is unchanged). The artifact is a **single JSON file**
|
|
135
|
-
carrying `{schema_version: 1, title?, prose, roadmap}` —
|
|
136
|
+
carrying `{schema_version: 1, title?, prose, roadmap}` — plus, in a `perk learn dream`
|
|
137
|
+
session, the **tool-written** `dream_report` block `{input, generated_at, parts}` (§8.63;
|
|
138
|
+
`readObjectiveDraft` refuses the WHOLE draft on a malformed block — deliberately stricter
|
|
139
|
+
than the lenient junk→absent `base`/`delivery` handling, because silently dropping a
|
|
140
|
+
malformed report is exactly what §8.63 forbids) — the structured roadmap rides
|
|
136
141
|
**verbatim** (node-shape validation stays with the Python plane at save time, the
|
|
137
142
|
`parse_structured_roadmap` path; an empty roadmap is allowed — only creation rejects
|
|
138
143
|
roadmap-free objectives). **The JSON is storage/transport only** — the human review surface
|
|
139
144
|
(node 2.2, Plannotator or the first-party editor) displays rendered markdown (the prose + a
|
|
140
|
-
markdown roadmap table
|
|
145
|
+
markdown roadmap table; when the draft carries `dream_report`, the stored CANONICAL parts
|
|
146
|
+
append as the final section — the render is the objective+report **approval bundle**,
|
|
147
|
+
§8.63) derived from the artifact, never raw JSON. Semantics: full rewrite per
|
|
141
148
|
call, non-terminating, NOT a save — `objective_save`/`/objective-save` remain the canonical
|
|
142
149
|
GitHub persist surface. Failure taxonomy (soft results, never throws): mistyped params →
|
|
143
150
|
`bad_input`; empty/whitespace prose → `invalid_input`; no session `run_id` → `no_run_id`;
|
|
@@ -172,6 +179,61 @@ The local cache tier — written and read by **both** the CLI (exterior) and the
|
|
|
172
179
|
dir persist through the LWW rebuild. Consumers fail open to their fallback when validation
|
|
173
180
|
refuses (the reader returns `null`; mismatched-run_id refusals are silent by design, broken
|
|
174
181
|
promises — missing file, digest mismatch — warn on stderr).
|
|
182
|
+
- **Agent scratch.** `.perk/workflow/scratch/runs/<run_id>/agent/` is the run-owned directory for
|
|
183
|
+
disposable command/model intermediates. Interior run-directory creation shares one hardened
|
|
184
|
+
boundary (`extension/substrate/cache.ts::ensureRunScratch`): before any write, `run_id` must be
|
|
185
|
+
one non-empty path segment (not `.`/`..`, with no `/`, `\\`, or NUL); every existing component
|
|
186
|
+
from the checkout's `.perk` child through the run root must be a real directory, never a symlink
|
|
187
|
+
or non-directory, and must have no group/world write bit; missing components are materialized in
|
|
188
|
+
order with an explicit maximum mode `0755` even under a permissive umask; and the run root's
|
|
189
|
+
realpath must equal the expected descendant of `realpath(cwd)` (a symlink above `cwd` remains
|
|
190
|
+
legal). All interior run-root writers use that boundary. `ensureAgentScratch` then applies the
|
|
191
|
+
same symlink/non-directory and resolved-containment checks to `agent/`, creates it as POSIX mode
|
|
192
|
+
`0700` from the outset, and re-applies `0700` on reuse. This is static redirected-path protection
|
|
193
|
+
and privacy from other OS users, not a defense against a concurrent process running as the same
|
|
194
|
+
user.
|
|
195
|
+
|
|
196
|
+
The extension provisions the directory before every eligible model turn and injects one hidden
|
|
197
|
+
`customType: "perk:agent-scratch"` block naming the repository-relative current-run path. A
|
|
198
|
+
context is eligible unless branch-LWW workflow mode is explicitly `read-only` or
|
|
199
|
+
`PI_SUBAGENT_CHILD_AGENT` names one of perk's report-only children (`perk.adversarial-reviewer`,
|
|
200
|
+
`perk.draft-reviewer`, `perk.dream-analyst`, `perk.dream-reducer`, `perk.harvest-analyst`, `perk.learn-analyst`,
|
|
201
|
+
`perk.objective-explorer`, `perk.pr-reviewer`, `perk.review-angle-selector`,
|
|
202
|
+
`perk.review-classifier`). Main sessions, `perk.conflict-resolver`, and unknown/custom children
|
|
203
|
+
remain eligible; absent generic foreign-agent metadata, inherited parent mode is the fallback.
|
|
204
|
+
Directory repair happens before prompt deduplication. The exact run-id/path-derived block is
|
|
205
|
+
deduplicated only inside the active post-compaction context window; context filtering keeps one
|
|
206
|
+
exact current-run **scratch custom block** and strips direct inherited/stale scratch custom blocks
|
|
207
|
+
(or all direct copies while ineligible), so a compacted-away block is re-injected. Compaction
|
|
208
|
+
summary prose is not a scratch custom block and may quote an older marker/path; it is deliberately
|
|
209
|
+
not redacted and is neither a live guidance delivery nor authoritative provenance. Only the
|
|
210
|
+
current-run direct block counts as live scratch guidance, and durable decisions still re-read the
|
|
211
|
+
canonical repository/backend source.
|
|
212
|
+
Because this is a universal pre-turn side effect that can become eligible after an in-session
|
|
213
|
+
read-only gate exit, every registry stage declares the existing `cache.scratch` key in `writes`.
|
|
214
|
+
|
|
215
|
+
A write-capable `/btw` side session receives the same rendered block once in its effective
|
|
216
|
+
appended system prompt after scratch custom messages are removed from its seed. Provisioning is
|
|
217
|
+
rechecked before every side-model prompt: deletion is repaired while reusing the session, and a
|
|
218
|
+
transition between unavailable and available scratch recreates the cached session so its
|
|
219
|
+
immutable prompt matches the current turn. The same gate that selects its tools excludes the
|
|
220
|
+
block from the read-only side-session shape; the tool-less summary shape also receives none.
|
|
221
|
+
Direct SDK read-only sessions remain unguided:
|
|
222
|
+
they load with `noExtensions: true` and the bounded `read`/`grep`/`find`/`ls` tool set. With no
|
|
223
|
+
settled run id the resolver is silent; an unsafe id or filesystem/permission failure injects no
|
|
224
|
+
path, warns through the report seam, and continues the turn. A failure is retried on later
|
|
225
|
+
eligible turns, with duplicate warnings suppressed per run within one extension activation and
|
|
226
|
+
suppression cleared after a successful retry. Fork/adopt run-root setup likewise reports and
|
|
227
|
+
continues after settling the derived workflow identity, without an unsafe fallback write.
|
|
228
|
+
|
|
229
|
+
The guidance asks agents to use descriptive non-colliding names instead of shared `/tmp` and to
|
|
230
|
+
re-read canonical repository/backend sources before durable decisions. Agent scratch is never
|
|
231
|
+
canonical evidence: it has no filename policy, atomic per-file writer, manifest, digest, or
|
|
232
|
+
`session_artifacts` pointer. Remote run diagnostics explicitly include hidden `.perk` files but
|
|
233
|
+
exclude the matching `agent/**` subtree from `actions/upload-artifact`; local cleanup remains the
|
|
234
|
+
enclosing run directory's existing `perk state prune` policy (no exit-time deletion). Non-goals:
|
|
235
|
+
no model-facing scratch writer, `PERK_SCRATCH_DIR`, `TMPDIR`, FFF/search change, shell/path
|
|
236
|
+
enforcement, OS sandbox, provenance protocol, or session-exit cleanup.
|
|
175
237
|
- **Atomic workflow writes + corruption posture.** Every `.perk/workflow/` file write on both
|
|
176
238
|
planes goes through the per-plane atomic-write seam — `perk/state/cache.py::atomic_write_text`
|
|
177
239
|
(exterior) / `extension/substrate/cache.ts::atomicWriteFileSync` (interior): a temp file in the
|
|
@@ -253,6 +315,25 @@ The local cache tier — written and read by **both** the CLI (exterior) and the
|
|
|
253
315
|
dispatch and never re-read from the mutable selector after selection. `--worktree NAME` is
|
|
254
316
|
directory positioning only (never plan identity or branch — the branch is always
|
|
255
317
|
`plan-<id>` from selection) and is refused with `--remote` (`invalid_input`).
|
|
318
|
+
- **Positive plan identification (the kind guard).** Explicit-id selection at the four
|
|
319
|
+
guarded doors — the three `select_plan` callers (`implement`, `pr address`/flat `address`,
|
|
320
|
+
`pr ready`) plus `plan resume`'s own read — refuses an existing issue that carries **no
|
|
321
|
+
plan-header** (`PlanState.has_plan_header`, presence-only kind evidence from the backend's
|
|
322
|
+
own storage: body metadata blocks on GitHub, perk attachments on Linear — never a payload
|
|
323
|
+
decode): typed `issue_kind_mismatch` via `plan_selection.require_plan_kind`. A GitHub
|
|
324
|
+
objective-header'd issue names the right door (`perk objective plan <N>`); the hint is
|
|
325
|
+
GitHub-only — a Linear metadata-sentinel issue refuses with the generic message, and a
|
|
326
|
+
Linear **Project** id never reaches the arm (`get_plan` returns `None` → `plan_not_found`,
|
|
327
|
+
the honest miss). A present-but-malformed header still identifies a plan (kind vs health
|
|
328
|
+
are separate concerns) — a **GitHub-only** reachable state: GitHub's tolerant body-block
|
|
329
|
+
read degrades a damaged block to `header={}` with `has_plan_header` true, while a corrupt
|
|
330
|
+
Linear plan-header attachment fails loud inside `get_plan`'s strict decode before
|
|
331
|
+
selection ever classifies kind. A **both-headers** carrier still selects as a plan (its plan
|
|
332
|
+
side can be legitimate mid-incident; `perk objective doctor`'s corruption check is the
|
|
333
|
+
both-headers surface, §8.54). `plan watch`/the positioner are explicitly **not**
|
|
334
|
+
entry-guarded — their wrong-kind protection is the §8.4 merge-only write seam. `pr ready
|
|
335
|
+
--dry-run` performs no backend read, so the offline preview classifies nothing (kind
|
|
336
|
+
included; the existing exception, kept).
|
|
256
337
|
|
|
257
338
|
State keys (registry vocabulary): `cache.plan`, `cache.plan-ref`, `cache.scratch`,
|
|
258
339
|
`cache.handoff`, `cache.markers`, `cache.session-data`.
|
|
@@ -384,13 +465,23 @@ end of the section).
|
|
|
384
465
|
| `active_plan_ref` | object \| null | the provider-agnostic plan ref (§8.4); null during early `plan` |
|
|
385
466
|
| `active_objective` | string \| null | the active objective id (`/objective <id>` sets it, `/objective clear` nulls it) |
|
|
386
467
|
| `last_review_batch` | object \| null | the last fully processed review batch, appended by `finalize_address` only after publication and thread resolution succeed: `{ pr, counts:{actionable,informational,praise,question}, resolved_thread_ids:[…], at:ISO }` |
|
|
387
|
-
| `last_pr_review` | object \| null | the last `/pr-review` (or the experimental `/pr-review-dynamic`) outcome posted via the shared warm `post_pr_review` tool: `{ pr, verdict, angles, comment_count, mode, at:ISO }`; best-effort tier (the PR review is the canonical record) |
|
|
468
|
+
| `last_pr_review` | object \| null | the last `/pr-review` (or the experimental `/pr-review-dynamic`) outcome posted via the shared warm `post_pr_review` tool: `{ pr, verdict, angles, covered_angles, comment_count, mode, at:ISO }`; a recorded wave is PR-bound and single-use, and supplies authoritative ordered `angles` / schema-valid `covered_angles`; standalone posting before any valid wave uses caller-supplied angles for both (or `[]`); best-effort tier (the PR review is the canonical record) |
|
|
388
469
|
| `last_review` | object \| null | the last review-door outcome posted via the warm `submit_pr_review` tool: `{ pr, event, comment_count, mode, at:ISO }`; best-effort tier (the submitted PR review is the canonical record) |
|
|
389
470
|
| `session_artifacts` | object \| null | per-name session-artifact provenance pointers `{run_id, name, path, digest, at}` (§8.1); appends carry the **whole merged map** (per-field LWW); strict-append tier |
|
|
390
471
|
| `objective_node_claim` | object \| null | the objective node this session has claimed `planning` (`{ objective, node }`); written by the warm `objective_node` tool on a successful `planning` transition, cleared on a successful non-planning transition for the same node and after a successful node-linked plan save; best-effort tier (cheaply reconstructable; loud-but-non-fatal) |
|
|
391
472
|
| `conflict_resolution_attempts` | number | the bounded conflict-resolution re-drive counter: incremented each time `/submit` drives the `perk.conflict-resolver` subagent on a definitively-unmergeable PR (cap `CONFLICT_RESOLUTION_ATTEMPT_CAP = 2`), reset to 0 on a clean submit; best-effort tier (cheaply reconstructable) |
|
|
473
|
+
| `dream_bundle_digest` | string | the dream-wave finalized-bundle digest marker (§8.61): `""` = invalidated (cleared unconditionally at wave entry, BEFORE the stale-bundle removal attempt — the invalidation record); `sha256:<hex>` = the digest of the current finalized run-scratch bundle bytes, set only after a successful finalize write; the §8.63 dream-report recovery refuses unless the marker is present, non-empty, and byte-matches the bundle just read; per-field LWW, no rebuild change |
|
|
392
474
|
| `perk_version` | string | the running perk (extension) version, stamped when run identity is established (the claim/fork/adopt/mint arms, §8.2) — the session-audit **exact-vintage** basis (the key literal is the cross-plane coordination point; the read side is `perk-dev`'s audit corpus/vintage layer); omitted when only the `perkVersion()` failure sentinel is available; best-effort tier |
|
|
393
475
|
|
|
476
|
+
Automated PR-review postability is session-local interior state, not an appended workflow-state
|
|
477
|
+
field: `null` permits the backwards-compatible standalone post; valid static/dynamic wave input
|
|
478
|
+
moves immediately to `pending` before target resolution (`review_wave_unavailable` on either
|
|
479
|
+
verdict); every normalized outcome records `{pr, complete, attempted, covered}`; one successful
|
|
480
|
+
post consumes it (`review_wave_consumed` thereafter). Bad wave input preserves the prior state.
|
|
481
|
+
A mutation-time PR mismatch returns `stale_review_wave` and moves back to `pending`; other post
|
|
482
|
+
failures keep the recorded outcome retryable. `last_pr_review` is appended only after the
|
|
483
|
+
mutation succeeds.
|
|
484
|
+
|
|
394
485
|
**Persistence channel:** `pi.appendEntry("perk:workflow-state", data)`. (The *other* Pi
|
|
395
486
|
channel — tool-result `details` — is for state that *is* a tool's output; this is not that.)
|
|
396
487
|
|
|
@@ -456,10 +547,13 @@ make the engine-required `structured_output` completion call — stripping it fa
|
|
|
456
547
|
`outputSchema` run with `structuredOutputFailed`) — a static union of foreign
|
|
457
548
|
tool names, inert when a package is absent — plus `run_audit_wave` (the gated audit-judge
|
|
458
549
|
session's wave call: its one write is structurally bound to the cold door's handoff
|
|
459
|
-
`audit_bundle_dir`, §8.50 — no caller-supplied path exists)
|
|
550
|
+
`audit_bundle_dir`, §8.50 — no caller-supplied path exists), `run_harvest_wave` (the gated
|
|
460
551
|
learn-harvest session's wave call: its manifest read is structurally bound to the session's
|
|
461
552
|
claimed run-scoped scratch path, §8.48 — the relayed param is verified against it and any
|
|
462
|
-
other path refused; no worktree writes)
|
|
553
|
+
other path refused; no worktree writes), and `run_dream_wave` (the gated learn-dream session's
|
|
554
|
+
wave call: NO parameters — its manifest read AND its one write, the fixed-name run-scratch
|
|
555
|
+
bundle beside that manifest, are both derived from the claimed run's manifest path, §8.61 —
|
|
556
|
+
the no-aimable-writer posture on both sides)) via `pi.setActiveTools`, **snapshot-then-restore** (the restore
|
|
463
557
|
falls back to the full configured `pi.getAllTools()` set — never a hardcoded list); (2) blocks
|
|
464
558
|
`edit`/`write` and non-allowlisted `bash` at `tool_call`. The bash sub-allowlist covers read-only
|
|
465
559
|
inspection commands (read-only `git` queries, `jq`, `curl`, …), read-only `gh` **query**
|
|
@@ -597,7 +691,11 @@ create_plan_issue{ title, body, labels[], run_id } -> PlanIssue{ number, url, e
|
|
|
597
691
|
add_issue_comment{ issue, body } -> CommentResult{ posted }
|
|
598
692
|
# POST repos/{o}/{r}/issues/{n}/comments (the plan-body first comment)
|
|
599
693
|
find_plan_issue{ run_id } -> PlanIssue | null
|
|
600
|
-
# GET repos/{o}/{r}/issues?labels=perk:plan&state=open + header run_id match
|
|
694
|
+
# GET repos/{o}/{r}/issues?labels=perk:plan&state=open + header run_id match. EXHAUSTIVE on
|
|
695
|
+
# GitHub: `gh api --paginate --slurp` with per_page=100 scans the FULL open set (never just
|
|
696
|
+
# the default ~30-row first page — a first-page miss would mint a duplicate issue); the
|
|
697
|
+
# parameterized learn/gist/objective finders inherit the same scan. An unexpected slurped
|
|
698
|
+
# page shape raises GitHubError (fail-closed — no partial census).
|
|
601
699
|
update_plan_issue{ number, title, body_comment } -> PlanUpdate{ number, body_updated, title_updated, dry_run }
|
|
602
700
|
# find the plan-body comment by marker -> PATCH .../issues/comments/{id} (-F body=@file)
|
|
603
701
|
# (fallback: POST a fresh comment, body_updated:false) ; PATCH .../issues/{n} (-f title=)
|
|
@@ -647,12 +745,27 @@ reopen_pr{ number } -> void
|
|
|
647
745
|
update_plan_header{ issue, fields } -> PlanHeaderUpdate{ fields_updated[], dry_run }
|
|
648
746
|
# GET issue body -> merge fields into the plan-header block -> PATCH .../issues/{n}
|
|
649
747
|
# rejects unknown header keys (LBYL on the schema); submit sets branch/pr/lifecycle_stage=impl
|
|
748
|
+
# MERGE-ONLY on both backends: refuses (before the dry-run return) when the backend's own
|
|
749
|
+
# storage carries no plan-header for the issue — GitHub: no plan-header body block;
|
|
750
|
+
# Linear: no plan-header attachment (the old run_id-keyed creation fallback is gone).
|
|
751
|
+
# Plan-header creation is confined to create_plan_issue, §8.29 adoption, and the Linear
|
|
752
|
+
# node-plan unification writer (save_node_plan). The refusal rides the backend error
|
|
753
|
+
# channel (GitHubError/IssueBackendError ⇒ github_error at CLI boundaries) — an invariant
|
|
754
|
+
# violation of a later-lifecycle write, not a selection error. Malformed-but-present
|
|
755
|
+
# GitHub shapes keep today's behavior (presence is kind evidence either way): open+close
|
|
756
|
+
# markers with unparseable YAML merge over {} (block replaced wholesale — an incidental
|
|
757
|
+
# self-heal); an open marker with no close marker makes replace_metadata_block a no-op
|
|
758
|
+
# (the write PATCHes an unchanged body while reporting fields updated).
|
|
650
759
|
prepend_plan_callout{ issue, callout, command } -> bool
|
|
651
760
|
# GET issue body -> plan.prepend_callout(body, callout, command=) -> PATCH .../issues/{n}
|
|
652
761
|
# idempotent on `command`; True iff a write occurred (False when already present / dry-run)
|
|
653
|
-
get_plan{ number } -> PlanState{ number, url, title, header, pr, state
|
|
762
|
+
get_plan{ number } -> PlanState{ number, url, title, header, pr, state,
|
|
763
|
+
has_plan_header, has_objective_header } | null
|
|
654
764
|
# gh issue view --json (+ pulls/{n} when the header carries pr); the `perk resume` read.
|
|
655
765
|
# `state` is the issue's OPEN/CLOSED state (the `replan` OPEN guard reads it).
|
|
766
|
+
# has_plan_header/has_objective_header: presence-only kind evidence from the body's own
|
|
767
|
+
# metadata blocks (the §8.1 kind guard reads them); a malformed block still reads
|
|
768
|
+
# header={} with its flag true.
|
|
656
769
|
```
|
|
657
770
|
|
|
658
771
|
- **PR body:** `Closes #<issue>` (so the squash-merge closes the plan) + a `Plan: #<issue>` link
|
|
@@ -670,24 +783,54 @@ merge_pr{ number, commit_message? } -> PullRequest (state MERGED
|
|
|
670
783
|
- **`Closes #<issue>`** rides in the PR body so the squash-merge closes the plan issue;
|
|
671
784
|
`commit_message` repeats it belt-and-suspenders. Post-merge state is **derived from PR**, never
|
|
672
785
|
stored (Q8).
|
|
673
|
-
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
786
|
+
- **`perk pr land` is a thin mapper over `Delivery.land`.** The command reconstructs the cached
|
|
787
|
+
plan-ref into one `LandRequest(kind="plan", plan_id, branch, objective_id, consumed_learn,
|
|
788
|
+
delivery_lineage, dry_run)` and makes exactly one
|
|
789
|
+
`resolve_delivery(repo_root).land(request)` call — never a consent callback: the plan
|
|
790
|
+
variant (and the objective dry-run) has no confirmation boundary and the façade rejects
|
|
791
|
+
one with `ValueError` (consent belongs to the objective mutation, §8.56). The flat
|
|
792
|
+
kind-guarded request family gives the plan-ref-derived intent fields
|
|
793
|
+
(`objective_id`/`consumed_learn`/`delivery_lineage`) dataclass defaults — an **accepted
|
|
794
|
+
residual**: their omission on a plan request is no longer construction-detectable,
|
|
795
|
+
mitigated because the header-half stacked refusal still protects every real run (only the
|
|
796
|
+
fully offline dry-run relies on the cached half) and the single production construction
|
|
797
|
+
site passes every field explicitly under exact-request pins; `plan_id`/`branch` omission
|
|
798
|
+
stays guard-detectable, and a plan request rejects `run_id` (objective-mutation-only). The
|
|
799
|
+
façade (engine:
|
|
800
|
+
`perk.delivery.land_plan`) owns refusal ordering, the mutation protocol, and the
|
|
801
|
+
finalization dispatch, and returns the strict kind↔detail `LandResult` (nested
|
|
802
|
+
`PrSummary`/`ObjectiveUpdate`/`LearnUpdate`/`Plan` records — the `--json` envelope maps them
|
|
803
|
+
field-for-field, byte-unchanged). Façade failures are the bounded `DeliveryError` vocabulary
|
|
804
|
+
with `phase="land"`: domain refusals (`stacked_plan`, `plan_not_found`, `no_pr`) carry
|
|
805
|
+
`origin="domain"` and render **bare**; infra failures translate to `github_error` with
|
|
806
|
+
`origin="github"` and keep the CLI's `PR land failed\n<detail>` prefix (no Git authority
|
|
807
|
+
call exists on this path, so no speculative `git_error` arm).
|
|
808
|
+
- **Stacked lineage refuses before any mutation.** `Delivery.land` applies the §8.47 routing
|
|
809
|
+
discriminator — stacked ⟺ the request carries the cached ref's `delivery_lineage` OR the
|
|
810
|
+
fetched plan header does (**header wins**: a stale cached ref must not silently land a stacked
|
|
811
|
+
layer) — and refuses fail-closed (`stacked_plan`) before mark-ready/merge: stacked layers land
|
|
812
|
+
only as one atomic train, never individually. The cached half refuses **before** the dry-run
|
|
813
|
+
early return (`--dry-run` stays fully offline by contract — zero authority access before the
|
|
814
|
+
early return, pinned for GitHub and Linear configuration; the real run enforces the header
|
|
815
|
+
half).
|
|
816
|
+
- **The pre-merge plan read is load-bearing.** Land reads the plan
|
|
817
|
+
(`DeliveryPersistence.get_plan`) right after the dry-run early-return, before any mutation: a
|
|
818
|
+
missing plan fails as `plan_not_found` (submit/ready's exact posture), the header feeds the
|
|
819
|
+
stacked discriminator, and the squash title/url ride the same read.
|
|
684
820
|
- **Deepened squash commit message.** Land passes `merge_pr(commit_message=)` = plain
|
|
685
|
-
`"<plan title>\n\
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
821
|
+
`"<plan title>\n\n<footer>"` composed façade-side from the **authoritative** `PlanState`
|
|
822
|
+
title/url + the persistence backend identity (`DeliveryPersistence.backend_id`): GitHub keeps
|
|
823
|
+
the `Closes #<issue>` footer, non-github backends the `Plan: <id> — <url>` line (fallback to
|
|
824
|
+
the bare footer on an empty title; one shared implementation with the stacked singleton —
|
|
825
|
+
`landing.squash_commit_message`). The PR's real `base_ref` is captured **before** the merge
|
|
826
|
+
(the synthetic merged `PullRequest` carries none; an idempotent re-land still sees it). Plain
|
|
827
|
+
text only — the second of the **two PR targets** (the GitHub HTML body is the other); HTML
|
|
828
|
+
never leaks into `git log`.
|
|
829
|
+
- **Post-merge finalization is a package-internal delivery seam.** The four durable bookkeeping
|
|
830
|
+
effects (learn-state stamp §8.36 → explicit plan-issue close → objective reconciliation →
|
|
831
|
+
learn-issue consume) live in `perk.delivery.finalize.finalize_landed_plan` — **no public
|
|
832
|
+
export**; `Delivery.land` binds it through its private land runtime, and stacked
|
|
833
|
+
landing/recovery keep their module-path bindings. **Reconstructed inputs only**
|
|
691
834
|
(a narrow `LandedPlan` + the captured `pr_base` merge evidence), never the worktree cache, and
|
|
692
835
|
**convergent-final-state idempotency** over the four effects (never-downgrade stamp,
|
|
693
836
|
terminal-node skip, backend-idempotent re-closes; sub-steps may re-issue idempotent backend
|
|
@@ -696,7 +839,9 @@ merge_pr{ number, commit_message? } -> PullRequest (state MERGED
|
|
|
696
839
|
never calls `close_objective` — the aggregate objective close is that caller's obligation after
|
|
697
840
|
every layer verifies; incremental land keeps the default. Activity reporting (the Linear agent
|
|
698
841
|
"landed" emission) is worktree-session-scoped and **caller-owned** — it stays in `land_cmd.py`,
|
|
699
|
-
outside the
|
|
842
|
+
outside the façade; the pending-learn marker (worktree-cache state) likewise stays in the
|
|
843
|
+
caller (written after the façade call — the durable §8.36 stamp is the authority; the marker
|
|
844
|
+
is the local retry signal).
|
|
700
845
|
|
|
701
846
|
### Learn ops
|
|
702
847
|
|
|
@@ -711,8 +856,11 @@ create_learn_issue{ title, body, run_id, plan_number } -> PlanIssue{ number, url
|
|
|
711
856
|
# renders a learn-header block { run_id, created, plan } into the body so the finder matches.
|
|
712
857
|
list_learn_issues{} -> LearnIssueSummary[]{ number, title, url, body }
|
|
713
858
|
# GET .../issues?labels=perk:learn&state=open (the find_plan_issue list call, label-scoped to
|
|
714
|
-
# perk:learn). Returns every open learn issue's full body for the factory inbox
|
|
715
|
-
#
|
|
859
|
+
# perk:learn). Returns every open learn issue's full body for the factory inbox — "every
|
|
860
|
+
# open" backed by full pagination on GitHub (`gh api --paginate --slurp`, per_page=100;
|
|
861
|
+
# Linear already cursor-paginates); an unexpected page shape raises (fail-closed, no partial
|
|
862
|
+
# census). Raises on infra failure (never masks as empty); skips non-dict / pull_request
|
|
863
|
+
# entries.
|
|
716
864
|
list_plans_pending_learn{ limit } -> PendingLearnPlan[]{ id, title, url, closed_at }
|
|
717
865
|
# GET .../issues?labels=perk:plan&state=closed&sort=updated&direction=desc&per_page=<limit>
|
|
718
866
|
# (Linear: terminal-state label query + plan-header attachment decode, paginated then truncated).
|
|
@@ -773,9 +921,10 @@ get_pr_review_context{ pr_number, branch, plan_body } -> PrReviewContext{ pr_num
|
|
|
773
921
|
# (`perk pr review-context`) — the materialized `cache.plan` mirror first, else
|
|
774
922
|
# `IssueBackend.get_plan_body` via the resolver — and passed straight in (best-effort; null
|
|
775
923
|
# lets the review run from the diff). What the spawned child runs.
|
|
776
|
-
# CLI
|
|
777
|
-
#
|
|
778
|
-
#
|
|
924
|
+
# CLI arms: `--pr <n>` resolves an arbitrary PR by number (existence + head ref via `get_pr`,
|
|
925
|
+
# `plan_body` null, clean `pr_not_found` arm). `--expected-pr <n>` stays on the active-plan,
|
|
926
|
+
# plan-body-preserving arm and compares the branch-selected target before context fetch;
|
|
927
|
+
# mismatch is `review_target_changed`. The two flags are mutually exclusive.
|
|
779
928
|
post_pr_review{ pr_number, summary, comments:[{path,line,body,side?}], event? } -> ReviewPostResult{ ok, mode, pr_number, comment_count }
|
|
780
929
|
# ONE atomic review via POST .../pulls/{n}/reviews — comments + body + event land together or
|
|
781
930
|
# not at all. `event` defaults to COMMENT (wire spelling: COMMENT|APPROVE|REQUEST_CHANGES) and
|
|
@@ -794,18 +943,41 @@ post_pr_review{ pr_number, summary, comments:[{path,line,body,side?}], event? }
|
|
|
794
943
|
# `/pr-review`'s parent-side `post_pr_review` tool, which delegates via
|
|
795
944
|
# `perk pr review-post --json --batch <path>` (the reviewer children report findings to the
|
|
796
945
|
# parent; they never post; review-post never passes `event` — hardcoded-COMMENT posture).
|
|
946
|
+
# A recorded wave adds strict positive `expected_pr` to that batch; the CLI compares it with
|
|
947
|
+
# the freshly resolved active PR before mutation (`review_target_changed` on drift). Dry-run
|
|
948
|
+
# validates the field while remaining offline. Standalone batches omit it.
|
|
797
949
|
add_pr_reaction{ pr_number } -> void
|
|
798
950
|
# the clean-verdict 👍 (issues-reactions endpoint — idempotent on rerun); a hard error on
|
|
799
951
|
# failure (mutations raise; nothing review-shaped is lost).
|
|
800
952
|
```
|
|
801
953
|
|
|
802
|
-
The
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
954
|
+
The static `/pr-review` input remains 2–4 selected angles with `plan-fidelity` mandatory. After
|
|
955
|
+
parameter decode the parent invalidates any older report state, resolves the active target once via
|
|
956
|
+
`perk pr url --json`, and binds every child to that number. Its effective manifest is those angles
|
|
957
|
+
followed by exactly one **required automatic** final `ponytail` lane, outside the input menu/cap.
|
|
958
|
+
Every reviewer uses only `perk pr review-context --expected-pr <bound-number> --json`; target drift
|
|
959
|
+
therefore yields no schema-valid report. Ponytail uses the same `perk.pr-reviewer` model, directive,
|
|
960
|
+
timeout, and report schema family, with invocation-private `skill: "ponytail-review"` source-bound
|
|
961
|
+
by the agent's exact package `skillPath`. Perk preflights package name, `pi.skills`, the exact
|
|
962
|
+
readable skill file, and its frontmatter name. A failed preflight never dispatches/spawns that
|
|
963
|
+
child; static report waves additionally omit it from rendered lane items. The keyed non-retryable
|
|
964
|
+
`skill-unavailable` failure retains Ponytail in the logical attempted manifest and leaves it
|
|
965
|
+
uncovered/incomplete; ordinary failed lanes retain the one bounded retry. No same-named
|
|
966
|
+
project/user skill fallback is possible. A normalized result records the bound PR plus explicit
|
|
967
|
+
effective attempted and covered arrays for §8.3's single-use post state.
|
|
968
|
+
|
|
969
|
+
The experimental `/pr-review-dynamic` door shares the same PR-bound, single-use
|
|
970
|
+
`post_pr_review`/`review-post` state — angle selection is delegated to a fresh
|
|
971
|
+
`perk.review-angle-selector` lane and normalized in module-rendered code
|
|
972
|
+
(`extension/waves/prReviewDynamicWave.ts`); the baseline
|
|
973
|
+
`/pr-review` stays canonical. One Ponytail promise starts independently alongside plan-fidelity
|
|
974
|
+
and the selector, never consumes selector output, and is appended last to `selection.effective`;
|
|
975
|
+
`ponytail` is a reserved custom key and the lane remains outside the selector cap. The selector
|
|
976
|
+
picks from the six-slug additional-angle allowlist
|
|
806
977
|
(`correctness`/`tests`/`quality`/`api-design`/`code-organization`/`idioms` — the shared
|
|
807
978
|
seven-angle vocabulary minus the structural `plan-fidelity`), capped at 3 additional angles
|
|
808
|
-
(2–4 lanes
|
|
979
|
+
(2–4 selectable lanes, the same window as `/pr-review`, plus automatic final Ponytail;
|
|
980
|
+
`force_angles` takes 1–3 slugs, merged
|
|
809
981
|
forced → picks → custom). The selector may additionally propose AT MOST ONE change-specific
|
|
810
982
|
custom angle: validated deterministically in the module-rendered normalization (kebab-case slug
|
|
811
983
|
3–32 chars, not a reserved lane key, whitespace-collapsed non-empty scope ≤ 300 chars; an
|
|
@@ -813,10 +985,21 @@ invalid proposal degrades to no-custom, never a failed selector lane), its lane
|
|
|
813
985
|
a fixed scope-definition-only template (the one sanctioned exception to "reviewer tasks come
|
|
814
986
|
only from the embedded vocabulary"), its per-item report schema locked to echo the custom slug,
|
|
815
987
|
and the lane retryable through the per-lane `outputSchema` seam on `WaveLane`; the
|
|
816
|
-
correctness+tests fallback fires only with zero valid picks AND no valid custom.
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
988
|
+
correctness+tests fallback fires only with zero valid picks AND no valid custom. Dynamic Ponytail
|
|
989
|
+
uses the same source-bound preflight/non-retryable failure semantics as static review; its
|
|
990
|
+
pre-launch receipt manifest is `plan-fidelity`, `angle-selector`, `ponytail`, and a pre-selection
|
|
991
|
+
failure still records the deterministic attempted reviewer manifest `plan-fidelity`, `ponytail`.
|
|
992
|
+
The selector, plan-fidelity, Ponytail, fixed/custom fan-out, and static retry tasks all carry the
|
|
993
|
+
same parent-resolved expected PR. Both wave tools persist ordered attempt receipts in their
|
|
994
|
+
tool-result details (observability only — §8.35's output-free receipt contract; completeness is
|
|
995
|
+
unchanged).
|
|
996
|
+
|
|
997
|
+
**Residual source-binding race.** Source-bound waves perform one exact-path preflight per pass and
|
|
998
|
+
each Ponytail child makes the exact file/frontmatter check its first action. Perk cannot atomically
|
|
999
|
+
pin upstream package name resolution between those reads; package files are assumed stable for the
|
|
1000
|
+
short pass. If a file changes or disappears after preflight, the child terminates without a
|
|
1001
|
+
schema-valid report, so Ponytail remains uncovered and the wave incomplete—never accepted as
|
|
1002
|
+
coverage from another source.
|
|
820
1003
|
|
|
821
1004
|
### PR-review toolbox ops (checkout / cleanup / review-submit)
|
|
822
1005
|
|
|
@@ -971,12 +1154,18 @@ prompt; the contracts pin the output shape, not the judgment rubric.
|
|
|
971
1154
|
- **Input (per-spawn task prompt):** the assigned angle, the PR number, and the absolute path to
|
|
972
1155
|
the detached read-only head worktree (the checkout above). The child fetches its own context
|
|
973
1156
|
via `perk pr review-context --pr <n> --json` (`plan_body` may be null).
|
|
974
|
-
- **Angles** (one per spawn; the adversarial menu is exactly these four —
|
|
975
|
-
autonomous menu is wider, seven fixed angles plus the dynamic flow's custom
|
|
976
|
-
checked against the diff, plus a first-class hunt
|
|
977
|
-
includes this angle) · `correctness` (incl. the
|
|
978
|
-
edits, dependency pins, install/build scripts,
|
|
979
|
-
(adequacy by reasoning only) · `quality`.
|
|
1157
|
+
- **Angles** (one per spawn; the adversarial selectable menu is exactly these four —
|
|
1158
|
+
`pr-reviewer`'s autonomous menu is wider, seven fixed angles plus the dynamic flow's custom
|
|
1159
|
+
lane): `claimed-intent` (the PR text's claims checked against the diff, plus a first-class hunt
|
|
1160
|
+
for **undisclosed scope**; the parent always includes this angle) · `correctness` (incl. the
|
|
1161
|
+
untrusted-code supply-chain axes: CI/workflow edits, dependency pins, install/build scripts,
|
|
1162
|
+
secrets handling, obfuscated code) · `tests` (adequacy by reasoning only) · `quality`. Every
|
|
1163
|
+
PR-backed adversarial wave appends exactly one **required automatic** final `ponytail` lane
|
|
1164
|
+
outside the 2–3 selection cap, using the same model/directive/report schema plus
|
|
1165
|
+
invocation-private `ponytail-review` from the exact package `skillPath`.
|
|
1166
|
+
Package/file/frontmatter preflight failure never dispatches/spawns that child, records
|
|
1167
|
+
non-retryable `skill-unavailable`, and leaves it uncovered without fallback; after successful
|
|
1168
|
+
preflight, the child's first-action recheck enforces the residual source-race posture above.
|
|
980
1169
|
- **Posture:** all fetched text is untrusted DATA, and the PR title/body are **unverified claims
|
|
981
1170
|
by the PR author** (an author not trusted by default) — checked against the diff, never built
|
|
982
1171
|
on. **Never-execute-the-head:** inside the head worktree the child uses
|
|
@@ -1040,13 +1229,14 @@ pieces; a neutral re-home is a deferred residual).
|
|
|
1040
1229
|
(2–3 unique angle slugs, `claimed-intent` mandatory), the `pr`/`worktree` relayed verbatim
|
|
1041
1230
|
from the guidance and the operator focus passed verbatim as `directive` — and the tool renders
|
|
1042
1231
|
and launches the wave itself, NON-BLOCKING (module-owned mechanics; the model never authors
|
|
1043
|
-
workflowScripts): one fresh-context `perk.adversarial-reviewer` lane per angle,
|
|
1044
|
-
the
|
|
1045
|
-
unrepresentable (no URL parameter
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1232
|
+
workflowScripts): one fresh-context `perk.adversarial-reviewer` lane per selected angle, then
|
|
1233
|
+
the automatic final source-bound Ponytail lane, each task naming the angle, the PR number, and
|
|
1234
|
+
the worktree path ONLY — the surface handle is structurally unrepresentable (no URL parameter
|
|
1235
|
+
exists). The effective manifest/receipts/coverage denominator is selected + Ponytail. The tool
|
|
1236
|
+
resolves the `[models.subagents] adversarial-reviewer` override at execute time (the doors read
|
|
1237
|
+
no config); a pending (launched, uncollected) wave makes a second start refuse `wave_active`; a
|
|
1238
|
+
launch failure is a LOUD soft-fail (`error_type` = the wave reason) with no retry — ZERO retries
|
|
1239
|
+
by design, honest incompleteness. The parent then holds the model-held
|
|
1050
1240
|
`subagent_wait({ timeoutMs })` relay loop — unchanged as the streaming cadence: progress
|
|
1051
1241
|
updates never wake `subagent_wait` and never enter pi-subagents' `pending` map — delivery is
|
|
1052
1242
|
an injected (`triggerTurn`-bearing) message when a tool call returns — so the timed wait loop
|
|
@@ -1374,8 +1564,8 @@ resolution fallback (and the `worktree wipe` guard).
|
|
|
1374
1564
|
|
|
1375
1565
|
The objective tier's full storage contract lives in **§8.24** (the `ObjectiveStore` seam); the
|
|
1376
1566
|
mechanics live in `src/perk/objective/` + `src/perk/backends/github/objectives.py`, the land-path
|
|
1377
|
-
handlers in `src/perk/cli/commands/pr/land_cmd.py`
|
|
1378
|
-
gateway-level facts:
|
|
1567
|
+
handlers in `src/perk/cli/commands/pr/land_cmd.py` (via `Delivery.land`) + the package-internal
|
|
1568
|
+
`src/perk/delivery/finalize.py`. The gateway-level facts:
|
|
1379
1569
|
|
|
1380
1570
|
- **Storage blocks (perk-namespaced, schema 1).** An objective is an issue + first comment:
|
|
1381
1571
|
`objective-header` (issue body — compact, queryable: `{ run_id, created, objective_comment_id,
|
|
@@ -1539,7 +1729,10 @@ pin + the report-only `cli-version` CLI-vs-repo-pin warning (warn, never fail) +
|
|
|
1539
1729
|
fail, no `--fix` arm — §8.6a) + the report-only `subagent-compat` pi-subagents surface probe
|
|
1540
1730
|
(installed version + source-file markers for the orchestration surfaces perk's guidance assumes;
|
|
1541
1731
|
`info` when not installed; warn on divergence, never fail; no `--fix` arm — the package stays
|
|
1542
|
-
unpinned) + the report-only `
|
|
1732
|
+
unpinned) + the report-only `ponytail-compat` exact package/`pi.skills`/two-skill-file/frontmatter
|
|
1733
|
+
probe (`info` when the lazy install is absent; warn on divergence, never fail; no `--fix` arm because
|
|
1734
|
+
operator pins are preserved; known-good remediation `npm:@dietrichgebert/ponytail@4.9.0` +
|
|
1735
|
+
`perk init` + session restart) + the report-only `subagent-bridge-config` check (reads
|
|
1543
1736
|
`subagents.intercomBridge.mode` from the project `.pi/settings.json` and the user-global
|
|
1544
1737
|
`~/.pi/agent/settings.json`; warns — never fails, no `--fix` arm — when either scope sets
|
|
1545
1738
|
`"off"` or `"fork-only"`, either of which silently disables the supervisor channel perk's
|
|
@@ -1567,9 +1760,15 @@ Keeping a consumer's pi-loaded perk extension runnable rests on two invariants:
|
|
|
1567
1760
|
own `packages` entry **in place** (list position preserved) when its `@mgiles/perk` identity already
|
|
1568
1761
|
exists but the full spec differs from the desired pin — so a stale `npm:@mgiles/perk@0.0.0` is
|
|
1569
1762
|
reconciled to `@{__version__}` (extra string duplicates of that identity collapse to one). Only
|
|
1570
|
-
perk's own npm identity is version-reconciled;
|
|
1571
|
-
(distinguished by `_npm_name` identity vs `_npm_name(NPM_PACKAGE)`), and a
|
|
1572
|
-
are never in the desired set so they stay untouched/append-only. The
|
|
1763
|
+
perk's own npm identity is version-reconciled; borrowed npm packages normally stay
|
|
1764
|
+
unpinned/append-only (distinguished by `_npm_name` identity vs `_npm_name(NPM_PACKAGE)`), and a
|
|
1765
|
+
user's other packages are never in the desired set so they stay untouched/append-only. The one
|
|
1766
|
+
managed-filter exception is `npm:@dietrichgebert/ponytail`: desired settings always carry one
|
|
1767
|
+
object entry with `extensions`/`skills`/`prompts`/`themes: []`. Reconciliation chooses the first
|
|
1768
|
+
object donor else first match, preserves its exact source pin, non-filter metadata, position
|
|
1769
|
+
relative to unrelated entries, forces all four filters empty, converts a string donor, and drops
|
|
1770
|
+
duplicate identities. Managed-state health canonicalizes a correctly filtered donor to npm
|
|
1771
|
+
identity while ignoring pin/metadata; any omitted or nonempty managed filter is drift. The in-body migration strips a
|
|
1573
1772
|
repo's legacy **`git:` perk** entry (any ref, **any entry form** — by
|
|
1574
1773
|
`_package_identity == GIT_PACKAGE`, covering a user-rewritten object-form entry) so the flip from
|
|
1575
1774
|
the old git wiring converges; a user's unrelated `git:` packages are preserved. **Presence is
|
|
@@ -1760,6 +1959,7 @@ perk's workflow skills are prompt-hidden; `transclude` exists for the user-bindi
|
|
|
1760
1959
|
| `command:learn-docs` | `perk-learn-docs` | `nudge` |
|
|
1761
1960
|
| `command:learn-code` | `perk-learn-code` | `nudge` |
|
|
1762
1961
|
| `command:learn-harvest` | `perk-learn-harvest` | `nudge` |
|
|
1962
|
+
| `command:learn-dream` | `perk-learn-dream` | `nudge` |
|
|
1763
1963
|
| `command:pr-review` | `perk-pr-review` | `nudge` |
|
|
1764
1964
|
| `command:pr-review-dynamic` | `perk-pr-review-dynamic` | `nudge` |
|
|
1765
1965
|
| `command:pr-review-terminal` | `perk-pr-review-terminal` | `nudge` |
|
|
@@ -1834,15 +2034,18 @@ The **cross-plane dedup marker is the render header itself** — `BINDING_HEADER
|
|
|
1834
2034
|
byte-for-byte to the cold `_HEADER` (Python) by a literal test in **both** planes. The cold door
|
|
1835
2035
|
already puts `stage:<id>` bindings in a cold-launched session's **initial prompt**, and
|
|
1836
2036
|
`before_agent_start` fires for that same session, so Mechanism A injects **iff** a launched `stage`
|
|
1837
|
-
exists, the resolved render is non-empty, no entry
|
|
2037
|
+
exists, the resolved render is non-empty, no entry in the branch's **compaction-active window**
|
|
1838
2038
|
already carries `BINDING_HEADER` (the cold prompt OR a prior warm inject), **and** the submitting
|
|
1839
|
-
turn's prompt (`event.prompt`) does not carry it either.
|
|
2039
|
+
turn's prompt (`event.prompt`) does not carry it either. Before compaction the active window is the
|
|
2040
|
+
full branch; after compaction it begins at the latest compaction's `firstKeptEntryId`, excludes
|
|
2041
|
+
compaction entries themselves, and includes later entries. This distinction is load-bearing because
|
|
2042
|
+
Pi's branch is append-only: historical entries remain readable after they leave model context, and a
|
|
2043
|
+
compaction summary quoting the header is not a live delivery. The prompt scan is load-bearing on the
|
|
1840
2044
|
launch turn: at `before_agent_start` the just-submitted prompt is **not yet** on the branch, so the
|
|
1841
2045
|
branch scan alone missed the cold seed on that turn and double-delivered (the fixed hole). The
|
|
1842
|
-
injected custom and the
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
carry the header, so the prompt scan stays inert there). Mechanism B is a one-shot
|
|
2046
|
+
injected custom and the cold prompt both carry the header → idempotent across turns/reloads; after
|
|
2047
|
+
compaction drops the original from the active window it **re-delivers** (its ongoing value — later
|
|
2048
|
+
prompts don't carry the header, so the prompt scan stays inert there). Mechanism B is a one-shot
|
|
1846
2049
|
`sendUserMessage` suffix at an invocation distinct from any cold launch, so it cannot auto-double. A
|
|
1847
2050
|
narrower-than-`planMode` `context` strip removes a **stale** `perk:binding-context` custom (stage
|
|
1848
2051
|
changed / overlay removed) while **never** stripping a user message that carries the header (a cold
|
|
@@ -2632,12 +2835,14 @@ so `init` writes them and `doctor` verifies/repairs them through the one shared
|
|
|
2632
2835
|
**`gh auth setup-git`** before invoking
|
|
2633
2836
|
`perk run-worker` — it installs `gh` as git's https credential helper using the step's
|
|
2634
2837
|
`GH_TOKEN` (= `PERK_GH_PAT`), so the skills CLI's sync during positioning (step 4 below) can
|
|
2635
|
-
clone private skill sources. A final **`Upload run diagnostics`** step (`actions/upload-artifact@v4`)
|
|
2636
|
-
`.perk/workflow/scratch/runs/<run_id>/` — the §8.12 durable run-event stream
|
|
2637
|
-
and friends), which is otherwise written into the runner's checkout and lost at
|
|
2638
|
-
artifact `perk-run-<run_id>` for **every real run, pass or fail
|
|
2639
|
-
|
|
2640
|
-
|
|
2838
|
+
clone private skill sources. A final **`Upload run diagnostics`** step (`actions/upload-artifact@v4`)
|
|
2839
|
+
uploads `.perk/workflow/scratch/runs/<run_id>/` — the §8.12 durable run-event stream
|
|
2840
|
+
(`events.ndjson` and friends), which is otherwise written into the runner's checkout and lost at
|
|
2841
|
+
teardown — as artifact `perk-run-<run_id>` for **every real run, pass or fail**. The upload sets
|
|
2842
|
+
`include-hidden-files: true` so the hidden `.perk` tree is actually retained, and its multiline
|
|
2843
|
+
path excludes `!.perk/workflow/scratch/runs/<run_id>/agent/**` so model-authored agent scratch is
|
|
2844
|
+
never retained remotely (`if: always() && inputs.smoke != 'true'`, `if-no-files-found: ignore`;
|
|
2845
|
+
smoke runs write nothing and upload nothing). An opt-out repo variable `PERK_ENABLED=false` disables the job without
|
|
2641
2846
|
removing the file. **Auth model:** checkout + push use the `PERK_GH_PAT` PAT, **not** `github.token` — a
|
|
2642
2847
|
PAT-pushed commit triggers downstream CI (the implement drive commits + `submit` pushes);
|
|
2643
2848
|
`GITHUB_TOKEN`-pushed commits do not. This is a stated decision Node 2.4 inherited (the
|
|
@@ -3483,6 +3688,32 @@ one-stop current shape.
|
|
|
3483
3688
|
(`ok:false` + `error`/`error_type` on unavailable / save-failed / bad_input / no_plan /
|
|
3484
3689
|
no_objective_draft; `ok:true` on verdicts and the sanctioned fail-open skips), so `tool_outcome`
|
|
3485
3690
|
run events classify it via `details.ok` rather than the `!isError` fallback.
|
|
3691
|
+
|
|
3692
|
+
**The launch chooser (the plannotator arms' wave opt-in).** The moment `plan_review`
|
|
3693
|
+
dispatches to a plannotator arm on an **eligible** round — the injected `WaveLaunch` deps
|
|
3694
|
+
present (the plannotator presence probe + the two door open cores, composed at the
|
|
3695
|
+
`registerPlanReview` call site in `extension/index.ts`; the factory imports nothing from door
|
|
3696
|
+
modules), plannotator loaded, and the review source a **validated draft artifact** (plan arm:
|
|
3697
|
+
`source === "plan-draft"`; objective arm: a non-null raw `objective-draft.json` baseline
|
|
3698
|
+
captured **before** the validated read — the door's fail-closed stale-guard ordering) — an
|
|
3699
|
+
in-TUI chooser is shown BEFORE anything launches: "Browser review + reviewer wave" vs
|
|
3700
|
+
"Browser review only". Shown **every** eligible round (no config key); Esc/dismiss selects the
|
|
3701
|
+
plain flavor (the chooser picks a flavor, never cancels the review); **abort outranks Esc,
|
|
3702
|
+
every dialog result, and the awaited opener** (`signal?.aborted` is checked at entry,
|
|
3703
|
+
re-checked immediately after each awaited dialog before interpreting it, and re-checked after
|
|
3704
|
+
the awaited open core — an interrupted turn never reports a launched wave). The wave choice collects an optional **trimmed**
|
|
3705
|
+
custom review angle (Esc/blank ⇒ none) and delegates to the door-shared open core
|
|
3706
|
+
(`openPlanReviewSurface` / `openObjectiveReviewSurface`), returning the **non-terminating**
|
|
3707
|
+
`details.status: "wave_launched"` result (`subject: "objective"` on the objective arm)
|
|
3708
|
+
carrying the door's guidance **verbatim** — the model launches the wave in the same turn, and
|
|
3709
|
+
the human's browser decision routes back through the door's background decision task (stale
|
|
3710
|
+
guards, degrade token, untrusted-feedback delimiting — all the door's existing code, shared
|
|
3711
|
+
not duplicated). A **synchronous port-pick failure** (a null core return, loudly reported
|
|
3712
|
+
there) falls **open** to the plain blocking review in the same call; an **asynchronous**
|
|
3713
|
+
readiness degrade follows the door's existing loud degrade path and recovers through the next
|
|
3714
|
+
`plan_review` round's chooser ("Browser review only" is one keystroke away — no forced-wave
|
|
3715
|
+
mode exists). Ineligible rounds — a param-tier source, plannotator absent, no injected deps,
|
|
3716
|
+
or the gist arm (no gist wave door exists) — keep today's plain blocking review silently.
|
|
3486
3717
|
- **The three backends.** All three speak review-first
|
|
3487
3718
|
(`plan_draft` → `plan_review` → auto-save on approval):
|
|
3488
3719
|
|
|
@@ -3492,6 +3723,11 @@ one-stop current shape.
|
|
|
3492
3723
|
| `plannotator-plan` | `PLAN_ADAPTER_PLANNOTATOR_CONTEXT` | browser bridge | present + `/plan-save` |
|
|
3493
3724
|
| `tombell-plan` | `PLAN_ADAPTER_TOMBELL_CONTEXT` (conditioned injection, Node 2.6) | first-party in-TUI review | present + `/plan-save` (incl. tombell's own interactive `/plan` `setActiveTools` restriction arm) |
|
|
3494
3725
|
|
|
3726
|
+
Under the plannotator selection the authoring context is **flavor-dispatched per stage** (one
|
|
3727
|
+
customType, three contents — §8.42's per-flavor marker dedup): the plan flavor by default, the
|
|
3728
|
+
**objective** flavor in **both** objective stages (`objective-author` **and** `objective-save`
|
|
3729
|
+
— matching `plan_review`'s objective-arm stage routing), and the gist flavor in `gist-author`.
|
|
3730
|
+
|
|
3495
3731
|
- **Plannotator "Direct Edits" (browser edits of the reviewed document).** Plannotator's
|
|
3496
3732
|
plan-review browser lets the reviewer edit the reviewed document directly; the edits arrive as
|
|
3497
3733
|
PROSE inside the existing `feedback` string, never a new envelope field — a `# Direct Edits`
|
|
@@ -3553,6 +3789,13 @@ one-stop current shape.
|
|
|
3553
3789
|
against a code-review server) → the prior env value ALWAYS restored when the poll ends. The
|
|
3554
3790
|
URL is deterministic the moment the port is picked, so the door primes its companions and
|
|
3555
3791
|
injects the guidance immediately, then ends its turn.
|
|
3792
|
+
- **The shared open core:** the door's whole open path is the exported guidance-RETURNING
|
|
3793
|
+
core `openPlanReviewSurface` (start the browser → prime both surfaces → the background
|
|
3794
|
+
readiness + decision tasks → return the composed guidance string, or null on the loudly
|
|
3795
|
+
reported port-pick failure); the command handler is a thin `sendUserMessage` wrapper, and
|
|
3796
|
+
`plan_review`'s wave arm consumes the SAME core — one open path, byte-identical semantics
|
|
3797
|
+
(the `applyPlannotatorDirectEdits` sharing precedent). The wave-arm guidance carries the
|
|
3798
|
+
same `command:plan-review-browser` skill-binding suffix.
|
|
3556
3799
|
- **The dual prime/clear lifecycle:** the moment the port is picked the door primes BOTH
|
|
3557
3800
|
companion surfaces — `primeAnnotationSurface({mode: "plan", url})` (the `push_annotations`
|
|
3558
3801
|
plan mode, §8.4) and `primeDraftReviewContext({draftType: "plan", draft, custom?})`
|
|
@@ -3569,12 +3812,21 @@ one-stop current shape.
|
|
|
3569
3812
|
construction. The **custom lane is the draft doors' user-input channel** (no `directive`
|
|
3570
3813
|
param exists), riding the primed context automatically as its own `custom` lane; the
|
|
3571
3814
|
surface handle is structurally unrepresentable in the wave (no URL parameter exists).
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
`
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3815
|
+
After selected lanes and the optional custom lane, the tool appends exactly one **required
|
|
3816
|
+
automatic** final `ponytail` lane outside both menus/caps. It uses the same
|
|
3817
|
+
`perk.draft-reviewer` model,
|
|
3818
|
+
timeout, and report schema with invocation-private `skill: "ponytail"` source-bound by the
|
|
3819
|
+
agent's exact package `skillPath`; package/file/frontmatter preflight failure omits only that
|
|
3820
|
+
child, records non-retryable `skill-unavailable`, and leaves it in the requested manifest but
|
|
3821
|
+
uncovered, with no same-name fallback. The child's first action rechecks the exact source;
|
|
3822
|
+
post-preflight instability therefore yields no report and remains incomplete under the
|
|
3823
|
+
residual source-race posture above. **Zero retries** — honest incompleteness surfaced to
|
|
3824
|
+
the human (the uncovered lane(s) named, never papered over); a pending wave makes a second
|
|
3825
|
+
start refuse `wave_active`; `collect_draft_review_wave` mirrors the PR pair (`no_wave` /
|
|
3826
|
+
grace-raced `wave_running` with the pending wave retained / the typed aggregate `{complete,
|
|
3827
|
+
covered, reports, failures}`, `covered` ordered across selected + optional custom + Ponytail).
|
|
3828
|
+
The `[models.subagents] draft-reviewer` override is resolved by the tool at execute time (the
|
|
3829
|
+
door reads no config); the per-call
|
|
3578
3830
|
signal is deliberately not threaded (the wave outlives the call; the module timeout is the
|
|
3579
3831
|
orphan insurance). Findings are the plan-mode `PlanFinding` shape (`{phrase, severity,
|
|
3580
3832
|
confidence, body}`), pushed phrase-byte-exact via `push_annotations` (streamed batches
|
|
@@ -3635,6 +3887,11 @@ one-stop current shape.
|
|
|
3635
3887
|
`plan-review` bridge action carries the rendered objective as `planContent` (arbitrary
|
|
3636
3888
|
markdown bytes; no plan-specific validation). The door primes its companions and injects
|
|
3637
3889
|
the guidance immediately, then ends its turn.
|
|
3890
|
+
- **The shared open core:** identical — the exported guidance-RETURNING core
|
|
3891
|
+
`openObjectiveReviewSurface` with the command handler as a thin `sendUserMessage` wrapper;
|
|
3892
|
+
`plan_review`'s objective wave arm consumes the SAME core — one open path, byte-identical
|
|
3893
|
+
semantics — and the wave-arm guidance carries the same `command:objective-review-browser`
|
|
3894
|
+
skill-binding suffix.
|
|
3638
3895
|
- **The dual prime/clear lifecycle:** identical — `primeAnnotationSurface({mode: "plan",
|
|
3639
3896
|
url})` + `primeDraftReviewContext({draftType: "objective", draft: rendered, custom?})`;
|
|
3640
3897
|
both cleared when the bridge settles AND on the readiness-degrade arm (idempotent).
|
|
@@ -4068,6 +4325,61 @@ Reconcilable region markers, the `Adopted-from` archive note, and the copyable c
|
|
|
4068
4325
|
plan-header)`, Linear the plan-header attachment), consumed by `plan from`'s `already_a_plan`
|
|
4069
4326
|
refusal.
|
|
4070
4327
|
|
|
4328
|
+
**Objective #1892 Node 1.1 amendment — objective `origin` + the open-by-origin lookup.**
|
|
4329
|
+
Additive, store-tier only (no CLI flag, no extension/TS change): machine-created objectives carry
|
|
4330
|
+
a provenance stamp, and the store tier can answer "is an open objective with this origin already
|
|
4331
|
+
live?" authoritatively — the foundation for the dream-launch guard and the save-time conflict
|
|
4332
|
+
re-check (the guard's first save-time consumer is the §8.64 dream save door).
|
|
4333
|
+
|
|
4334
|
+
- **The `origin` header field.** A **closed vocabulary** (`objective.ObjectiveOrigin`, a
|
|
4335
|
+
`StrEnum`; first value `learn-dream`), stored as a `str` in the `objective-header` (typed like
|
|
4336
|
+
`status`/`delivery`, the enum is the domain vocabulary). **Absence-compatible:** rendered only
|
|
4337
|
+
when set (the §8.42 additive-field rule) — every existing objective and every origin-less
|
|
4338
|
+
create/supersede renders byte-identically. **Launch-owned:** never model- or human-supplied —
|
|
4339
|
+
the launch flow injects it from claimed machine state; no interactive surface passes it.
|
|
4340
|
+
- **Write surface = create + the supersession carry, structurally enforced.** `origin` is stamped
|
|
4341
|
+
atomically into the INITIAL header by `create_objective(origin=…)` (never create-then-merge),
|
|
4342
|
+
and both live stores' `supersede_objective` automatically carry the predecessor's stored origin
|
|
4343
|
+
into the successor header (store-side, no parameter; validated — a junk stored value raises
|
|
4344
|
+
BEFORE the successor create; a header-less/sentinel-less predecessor carries nothing).
|
|
4345
|
+
`origin` is deliberately **excluded from `OBJECTIVE_HEADER_FIELDS`**, so the three
|
|
4346
|
+
`update_objective_header` writers reject any post-create origin merge via their existing LBYL
|
|
4347
|
+
check — while a stored origin round-trips untouched through unrelated merges (all three
|
|
4348
|
+
writers merge into the *parsed existing* mapping). The dormant issue-backed store's
|
|
4349
|
+
`create_objective` still stamps origin (it composes a real header); `adopt_source_as_objective`
|
|
4350
|
+
never stamps one (a machine-originated objective is never adopted-from).
|
|
4351
|
+
- **The `origin_value` validator** (`objective/parse.py`): `None` → `None`; a value in the closed
|
|
4352
|
+
vocabulary → the enum; **anything else raises `ValueError`** (fail-closed on junk/tampering,
|
|
4353
|
+
mirroring `delivery_policy`). Stores translate the `ValueError` into their native error at the
|
|
4354
|
+
boundary. (The header-dict origin classifier — the `delivery_policy` twin — is deferred to the
|
|
4355
|
+
first node that reads a specific objective's origin.)
|
|
4356
|
+
- **`find_open_objective_by_origin(origin, exclude_run_id=None) → ObjectiveRef | None`**
|
|
4357
|
+
(declared after `find_objective`): the first **open** match whose header `run_id` ≠
|
|
4358
|
+
`exclude_run_id` (`existed=True`); `None` means *authoritatively none in the exhaustively
|
|
4359
|
+
enumerated open population*. `exclude_run_id` is the caller-exclusion that makes a save-time
|
|
4360
|
+
re-check sound with a single-ref API (the consumer excludes its own run; any returned ref IS a
|
|
4361
|
+
conflict); a candidate with a missing/non-str `run_id` is never treated as excluded
|
|
4362
|
+
(fail-closed). **Exhaustive-or-raise:** an infra failure raises; a **present-but-malformed**
|
|
4363
|
+
header block/attachment raises (uncertainty about a real objective never reads as
|
|
4364
|
+
authoritatively-none); an origin outside the closed vocabulary raises (via `origin_value`);
|
|
4365
|
+
absent/different-known origins are non-matches (skip); the enumeration covers the full relevant
|
|
4366
|
+
open population — never one bounded page. A store that cannot answer authoritatively must
|
|
4367
|
+
RAISE, never return `None`.
|
|
4368
|
+
- **Per-store scope.** GitHub: **all pages** of the open `perk:objective` label population
|
|
4369
|
+
(a new paginated `_list_label_issues_all_pages` sibling; the bounded default-page reads are
|
|
4370
|
+
untouched). Linear project store: **team-scoped in v1** (a cross-team origin-stamped objective
|
|
4371
|
+
is invisible — documented limitation) — every team project swept via its metadata **sentinel**
|
|
4372
|
+
(the sentinel IS the identity; never the Reconcilable-marker heuristic); a sentinel-less
|
|
4373
|
+
project is skipped (the same accepted create-crash window as `find_objective`); a surviving
|
|
4374
|
+
match costs one project-state read (`completed`/`canceled` ⇒ closed, anything else including
|
|
4375
|
+
missing ⇒ open). The dormant issue-backed Linear store **raises**
|
|
4376
|
+
(`"the issue-backed Linear objective store does not support origin lookups"`) — deliberately
|
|
4377
|
+
OUTSIDE the `→ None`/`→ False` no-op family, which would falsely assert authoritatively-none
|
|
4378
|
+
and silently open a fail-closed guard.
|
|
4379
|
+
- **Closed under replan.** The supersession carry keeps the guarded origin population closed
|
|
4380
|
+
across re-authoring; during a deferred-close transfer window (§8.53) both predecessor and
|
|
4381
|
+
successor are visibly origin-stamped, so a concurrent origin-guarded launch correctly refuses.
|
|
4382
|
+
|
|
4071
4383
|
## §8.25 · The human-engagement read contract (Objective #682, Node 1.2)
|
|
4072
4384
|
|
|
4073
4385
|
A backend-neutral **READ** surface for human engagement — comments, description edits, and
|
|
@@ -4340,9 +4652,11 @@ are verbatim human content". A normally-authored plan leaves it `None`.
|
|
|
4340
4652
|
(mirrors `replan`/`resume`; `from` is a valid Click command string). It performs every Linear/GitHub
|
|
4341
4653
|
read up front (the read-only plan-mode session has no `gh`/Linear access), then re-launches the
|
|
4342
4654
|
`plan` stage seeded to author a plan over the materialized source. It **refuses** when: the issue is
|
|
4343
|
-
not found (`adopt_not_found`), not OPEN (`adopt_not_open`),
|
|
4655
|
+
not found (`adopt_not_found`), not OPEN (`adopt_not_open`), already a perk plan
|
|
4344
4656
|
(`AdoptableIssue.already_plan` — backend-decided: GitHub the body block, Linear the plan-header
|
|
4345
|
-
attachment → `already_a_plan`, hinting `perk plan replan <id>`)
|
|
4657
|
+
attachment → `already_a_plan`, hinting `perk plan replan <id>`), or a perk **objective**
|
|
4658
|
+
(`AdoptableIssue.already_objective` — presence-only, backend-decided like `already_plan` →
|
|
4659
|
+
`issue_kind_mismatch`; the GitHub message names the right door, `perk objective plan <N>`).
|
|
4346
4660
|
Engagement is read fail-soft (`render_adopted_engagement` → `<untrusted_adopted_issue_engagement>`;
|
|
4347
4661
|
`IssueBackendError` → omitted). The source is materialized to `scratch/adopt-<issue_id>.md` (title +
|
|
4348
4662
|
body wrapped in `<untrusted_adopted_issue>` + the optional engagement block). A **fresh** `run_id`
|
|
@@ -4365,6 +4679,17 @@ prints the header/body (now including `adopted_from`) without writes.
|
|
|
4365
4679
|
`adopted_from` plan-header field; `doctor` does **not** rewrite or validate the human prose/title —
|
|
4366
4680
|
the substantive deliverable is this contract section, not a new validating check.
|
|
4367
4681
|
|
|
4682
|
+
**Wrong-kind refusal at the writer (the mutation boundary).** `adopt_issue_as_plan` itself
|
|
4683
|
+
refuses an **objective-metadata carrier** before its first mutation, on both backends — GitHub:
|
|
4684
|
+
an `objective-header` body block on the source read it already performs; Linear: an
|
|
4685
|
+
`objective-header` attachment (one extra presence-only read on this rare mutation) — raising the
|
|
4686
|
+
backend error (“wrong kind for plan adoption”). This closes the `perk plan save --adopt-from`
|
|
4687
|
+
direct-invocation bypass and the door-gather→save race window; the door's `already_objective`
|
|
4688
|
+
refusal above is the friendlier typed UX layer over the same rule. **Gists are exempt** in both
|
|
4689
|
+
directions: a gist carries `gist-header`, so no refusal fires and the sanctioned
|
|
4690
|
+
plan-header-beside-gist-header stamp keeps working. Residual: the writer's read→PATCH race
|
|
4691
|
+
window is inherent to non-transactional backends (accepted).
|
|
4692
|
+
|
|
4368
4693
|
**Backend parity.** Honest on **both** GitHub and Linear (+ clean fake conformers). Live validation
|
|
4369
4694
|
is a preview-grade observation here (Mode 7); final live
|
|
4370
4695
|
proof is node 4.3.
|
|
@@ -4463,6 +4788,10 @@ reconcile. Mapped issues' titles/bodies are independently preserved verbatim by
|
|
|
4463
4788
|
`adopt_not_found` (source `None`); GitHub-only `adopt_not_open` (the source issue is CLOSED, via the
|
|
4464
4789
|
issue tier's `read_issue.state` — skipped for Linear projects, which have no OPEN/CLOSED);
|
|
4465
4790
|
`already_an_objective` (the source prose already carries an `objective-header` block);
|
|
4791
|
+
GitHub-only `already_a_plan` (the source issue carries perk's plan metadata —
|
|
4792
|
+
`read_issue.already_plan`, checked in the same issue-tier read arm as the OPEN check; the
|
|
4793
|
+
message points at `perk plan replan <id>` or a fresh objective; Linear sources are Projects
|
|
4794
|
+
with no issue-tier read — honestly skipped, matching the OPEN check's scoping);
|
|
4466
4795
|
`adopt_unsupported` (a `None` adoption return — in practice the resolver never returns the dormant
|
|
4467
4796
|
store). Project-level engagement is read fail-soft (`render_adopted_engagement(comments, ())` →
|
|
4468
4797
|
`<untrusted_adopted_issue_engagement>`; `ObjectiveStoreError` → omitted; per-issue engagement is
|
|
@@ -4483,6 +4812,14 @@ produces a fresh perk objective, `existed=False`). `--dry-run` falls through to
|
|
|
4483
4812
|
`create_objective(dry_run=True)` compose-preview (the writer returns `None` on dry-run). No
|
|
4484
4813
|
mutual-exclusion guard is needed (`objective create` has no `--node-id`).
|
|
4485
4814
|
|
|
4815
|
+
**Wrong-kind refusal at the writer (the mutation boundary).** GitHub
|
|
4816
|
+
`objectives.adopt_issue_as_objective` (the `adopt_source_as_objective` substrate) refuses a
|
|
4817
|
+
**plan-header carrier** before any mutation, keyed on the source body it already reads
|
|
4818
|
+
(“wrong kind for objective adoption”) — closing the `objective create --adopt-from` direct
|
|
4819
|
+
bypass, symmetric with §8.29's writer guard. The Linear **project** store adopts Projects (no
|
|
4820
|
+
issue-tier carrier — nothing to guard); the dormant issue-backed Linear store's adoption writer
|
|
4821
|
+
is out of scope. Gists stay exempt (a gist carries `gist-header`).
|
|
4822
|
+
|
|
4486
4823
|
**Backend parity.** Honest on **both** GitHub and Linear (+ clean fake conformers). Live validation
|
|
4487
4824
|
is preview-grade here (Mode 8); final live proof is Node
|
|
4488
4825
|
4.3 — no new config key, provider seam, or `EXPECTED_SURFACE` change (a flag, not a new
|
|
@@ -4523,8 +4860,9 @@ nothing, the subset being shared).
|
|
|
4523
4860
|
`prompts/contexts/adapters/` — with each module's identity marker passed as the `{{ marker }}`
|
|
4524
4861
|
render var (never a template literal), so the marker the strip handler scans for cannot drift
|
|
4525
4862
|
from the injected prose; the marker-as-render-var invariant now serves both the strip **and**
|
|
4526
|
-
the dedup key (plannotator's
|
|
4527
|
-
distinct
|
|
4863
|
+
the dedup key (plannotator's three flavors — plan / objective / gist, the objective flavor
|
|
4864
|
+
serving both objective stages — share one customType but dedup per-flavor on their distinct
|
|
4865
|
+
markers).
|
|
4528
4866
|
|
|
4529
4867
|
**Fail loudly on a missing var.** jinja2 uses `StrictUndefined` (raises `jinja2.UndefinedError`);
|
|
4530
4868
|
the vendored `miniJinja` renderer matches it — a referenced name that is **absent OR non-string**
|
|
@@ -4609,6 +4947,13 @@ Everything outside (1)–(4) is **outside the subset** — `{% for %}`/`{% endfo
|
|
|
4609
4947
|
with an allowlist posture (fail on any block matching no recognized construct):
|
|
4610
4948
|
`tests/test_prompt_grammar.py` (Python) and `extension/substrate/promptGrammar.test.ts` (TS).
|
|
4611
4949
|
`shared/contracts.md §8.31` is the SSOT for the shared scan algorithm; the two guards mirror it.
|
|
4950
|
+
The Python guard's scanner implementation lives in `perk_dev.prompt_grammar.scan_template`
|
|
4951
|
+
(consumed by both `tests/test_prompt_grammar.py` and the prose-review Assembly preview gate) and
|
|
4952
|
+
scans **whole-source** — unterminated, multiline, stray, nested, or partially matched delimiters
|
|
4953
|
+
are violations rather than unexamined text. That lexical completeness is a deliberate Python-side
|
|
4954
|
+
narrowing relative to the TS runtime tokenizer (`miniJinja.ts` accepts multiline tags and treats
|
|
4955
|
+
stray closers as literal text); the frozen construct set, the TS guard, and both runtime
|
|
4956
|
+
renderers are unchanged.
|
|
4612
4957
|
The guard checks **construct membership only**, not if/endif nesting balance — structural balance
|
|
4613
4958
|
is already proven by the golden harness rendering every real template. Widening the subset later
|
|
4614
4959
|
(e.g. a future template needing `in` or parentheses) is a deliberate decision that amends this
|
|
@@ -4632,9 +4977,12 @@ create-new shape sidesteps that gap. The structural siblings are §8.27 (replan
|
|
|
4632
4977
|
stage) that *borrows* the `objective-author` stage for launch (exactly like `plan replan` borrows
|
|
4633
4978
|
`plan` and `objective author --from` borrows `objective-author`). It mints a **fresh** `run_id`
|
|
4634
4979
|
(the new objective is net-new — no `run_id_override`), refuses `--remote` (objective-author is
|
|
4635
|
-
`cold_remote:false`), and
|
|
4636
|
-
|
|
4637
|
-
|
|
4980
|
+
`cold_remote:false`), and obtains its one-snapshot title/URL/nodes plus delivery constraints from
|
|
4981
|
+
`Delivery.prepare(PrepareRequest(kind="replan", objective_id=...))`. Prepare owns not-found,
|
|
4982
|
+
already-superseded, non-open, fail-closed policy, journal, train, and claimed-prefix checks; the
|
|
4983
|
+
command retains the objective store only for fail-soft engagement/prose reads and backend wording.
|
|
4984
|
+
Refusals remain `objective_not_found` / `objective_not_open` and the §8.53 codes. `("replan", ())`
|
|
4985
|
+
joins the `objective` group in the parity-smoke `EXPECTED_SURFACE`.
|
|
4638
4986
|
|
|
4639
4987
|
**The carry model.** Only the **unfinished** nodes carry forward (status ∈ {`pending`, `planning`,
|
|
4640
4988
|
`in_progress`, `blocked`}); `done`/`skipped` nodes stay as **history on the closed old objective**
|
|
@@ -4676,11 +5024,12 @@ node-issues), `False` from the dormant issue-backed store (the no-op-family sign
|
|
|
4676
5024
|
`supersede_objective(close_predecessor=True)` wraps it fail-open — one implementation, two
|
|
4677
5025
|
postures.
|
|
4678
5026
|
|
|
4679
|
-
**Transfer-aware routing.**
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
5027
|
+
**Transfer-aware routing.** Every real `--supersedes` save submits one immutable
|
|
5028
|
+
`TransferRequest` to `Delivery.transfer`. The façade acquires the shared operation lock before its
|
|
5029
|
+
single authoritative D1 predecessor read/classification and holds it through the selected
|
|
5030
|
+
mutation. Only incremental→incremental takes the plain mutation above; a stacked predecessor —
|
|
5031
|
+
or an incremental→stacked conversion — routes through the §8.53 transfer protocol (which owns
|
|
5032
|
+
lineage copy-or-mint, prefix preservation, ownership transfer, deferred close, and verification).
|
|
4684
5033
|
|
|
4685
5034
|
**Backend-specific carry-forward.**
|
|
4686
5035
|
- **GitHub** (a node is a row in one objective issue body): the new objective's roadmap rows are
|
|
@@ -4701,8 +5050,10 @@ close, and verification).
|
|
|
4701
5050
|
`--adopt-from`: a `--supersedes` worker flag (recovered from the handoff via
|
|
4702
5051
|
`_supersedes_from_handoff`; explicit flag wins) parses the carry map via the reused
|
|
4703
5052
|
`objective.parse_adopt_mapping(raw_roadmap)` (the node→issue side-map, interpreted as **move**
|
|
4704
|
-
semantics here)
|
|
4705
|
-
`
|
|
5053
|
+
semantics here), constructs exactly one `TransferRequest` from raw intent, and consumes only
|
|
5054
|
+
`TransferResult.successor`. Provider filtering, D1 classification, route choice, writer probes,
|
|
5055
|
+
and mutation stay behind Delivery; a `None` store result becomes `supersede_unsupported` there.
|
|
5056
|
+
`--supersedes` and `--adopt-from` are **mutually exclusive** (`invalid_input`).
|
|
4706
5057
|
|
|
4707
5058
|
**Binding + skill.** `command:objective-replan → perk-objective-replan` (nudge) joins
|
|
4708
5059
|
`shared/bindings.yaml` (mirroring `command:objective-reconcile`) and `DELIVERABLE_COMMAND_TARGETS`
|
|
@@ -5091,6 +5442,17 @@ text), derives `manifest.json` itself (`bad_input` when absent), and resolves th
|
|
|
5091
5442
|
from `[models.subagents] learn-analyst` at execute time (the wave's workflow-level `model`
|
|
5092
5443
|
default). The manifest write rule above and the DECISION vocabulary are unchanged.
|
|
5093
5444
|
|
|
5445
|
+
**Streaming launch manifests.** `startReportWave` returns one preflight-derived
|
|
5446
|
+
`WaveLaunchManifest = {requested, runnable, preflightFailures}` on both result arms. `requested`
|
|
5447
|
+
preserves the declared lane order; `runnable` is the ordered subset eligible for the rendered
|
|
5448
|
+
workflow after required-skill preflight; `preflightFailures` contains one ordered keyed
|
|
5449
|
+
`skill-unavailable` row per omission. The streaming adversarial/draft start tools expose this
|
|
5450
|
+
nested `launch` shape and say only runnable lanes launched; pending collection still keeps the full
|
|
5451
|
+
requested denominator. If every lane is skipped, no workflow is spawned: the start is unavailable,
|
|
5452
|
+
its receipt has no children, and the same keyed failures appear in the manifest/result without a
|
|
5453
|
+
synthetic wave-level failure. `WaveAttemptReceipt.requestedKeys` remains the pre-launch logical
|
|
5454
|
+
manifest and is not narrowed by this launch vocabulary.
|
|
5455
|
+
|
|
5094
5456
|
**Attempt receipts (flow-generic).** Every code-owned wave flow records an **output-free**
|
|
5095
5457
|
`WaveAttemptReceipt` per top-level workflow launch when the completion payload carries the
|
|
5096
5458
|
projection (pi-subagents ≥ 0.45.0): the child lane key ↔ child `runId` ↔ artifact paths —
|
|
@@ -5099,7 +5461,7 @@ attempt (a failed lane and its relaunch stay distinguishable). `status.json.work
|
|
|
5099
5461
|
remains the SOLE authority for reports and completeness; receipt absence (an identity-only
|
|
5100
5462
|
completion) never changes a verdict, completeness, retry selection, or mutation decision —
|
|
5101
5463
|
receipts are write-only correlation telemetry. The flow tools (`run_learn_wave`,
|
|
5102
|
-
`run_harvest_wave`, `run_pr_review_wave`, `run_pr_review_dynamic_wave`, and the single-lane
|
|
5464
|
+
`run_harvest_wave`, `run_dream_wave`, `run_pr_review_wave`, `run_pr_review_dynamic_wave`, and the single-lane
|
|
5103
5465
|
`classify_review_feedback` / `explore_objective_node`) persist `attempts` in their structured
|
|
5104
5466
|
tool-result details only (never the model-facing prose); a wave-level soft-failure retains any
|
|
5105
5467
|
receipt known before the failure in its fail details.
|
|
@@ -5263,7 +5625,7 @@ identity).
|
|
|
5263
5625
|
| 2 | prompt generation (local vs worker) | canonical templates `prompts/stages/*` via the §8.31 render seam; `_implement_prompt`/`_address_prompt` ↔ `initialPromptFor` ↔ `implementHandoffPrompt`/`addressGuidance` | `tests/test_prompt_parity.py` (live cross-engine byte parity) + goldens; reciprocal substring suites `tests/test_worker_prompt_parity.py` ↔ `extension/worker/worker.test.ts`; binding-content byte parity `tests/test_binding_render_parity.py` (via `extension/testing/renderBindingsLive.ts`) |
|
|
5264
5626
|
| 3 | submit side effects | one Python door, `perk pr submit --json`; the warm `submit` tool/`/submit` command delegate via `submitPr` (`extension/doors/submit.ts`), and the remote worker drives that same registered tool | `extension/worker/workerE2e.test.ts` (implement HAPPY drives the real tool through the real extension into a stubbed `PERK_BIN` router), `extension/doors/submit.test.ts`, `tests/test_pr_submit.py` |
|
|
5265
5627
|
| 4 | address terminal criteria | `finalize_address` (`extension/doors/address.ts`) runs submit first, delegates its internal resolve half to `perk pr resolve-threads --json`, and appends `last_review_batch`; the worker requires finalizer success + that write + successful effective submit evidence with `mergeable !== false` | `workerE2e.test.ts` (address HAPPY binds both real door writes to classification), `worker.test.ts` `evaluateTerminal` matrix; post-address the supervisor re-classifies via row 1 |
|
|
5266
|
-
| 5 | plan-ref reconstruction + positioning | one function, `resume.reconstruct_plan_ref` — all reconstruction sites converge on it (`cli/plan_selection.py::select_plan` for the explicit-id doors, `plan/resume_cmd.py`, `objective/run_cmd.py`, `run/run_worker.py`, the positioner's restore arm); one validating selector/positioner, `launch.resolve_worktree` (see the positioning semantics below) used by every cold door needing a plan checkout (`implement`/`submit`/`address`/`land`/`learn`/`plan watch`); `run_worker.position_worktree` mirrors `launch_stage`'s positioning, and stacked
|
|
5628
|
+
| 5 | plan-ref reconstruction + positioning | one function, `resume.reconstruct_plan_ref` — all reconstruction sites converge on it (`cli/plan_selection.py::select_plan` for the explicit-id doors, `plan/resume_cmd.py`, `objective/run_cmd.py`, `run/run_worker.py`, the positioner's restore arm); one validating selector/positioner, `launch.resolve_worktree` (see the positioning semantics below) used by every cold door needing a plan checkout (`implement`/`submit`/`address`/`land`/`learn`/`plan watch`); `run_worker.position_worktree` mirrors `launch_stage`'s positioning, and fresh stacked starts independently call the same execution `Delivery.prepare(PrepareRequest(kind="layer_start", mode="execution", …))` boundary (§8.46) from `resolve_worktree` and `run_worker.position_branch` | `tests/test_plan_ref_parity.py` (the save→reconstruct round trip + the `PlanRef` field census), `tests/test_plan_selection.py`, `tests/test_resume.py`, `tests/test_launch_restore.py` (the non-destructive restore matrix), `tests/test_run_worker.py::test_positioning_parity_local_launch_vs_remote_worker` (artifact byte parity, `run_id` excepted; the explicit-ref twin pins the direct-ref arm), `tests/test_run_worker.py::test_positioning_parity_stacked_local_create_vs_remote_position` (same start SHA + `layer-context.json` parity, timestamps excepted) |
|
|
5267
5629
|
| 6 | run reporting | **remote-only by design**: `perk/run/run_report.py` derives the §8.15 plan-issue comments + job summary solely from the §8.12 events stream + exit code | `tests/test_run_report.py` (incl. the `RunOutcome` lockstep literals) ↔ `worker.test.ts` (the frozen `assembleOutcome` shapes) |
|
|
5268
5630
|
|
|
5269
5631
|
### Positioning semantics (`launch.resolve_worktree` — the one selector/positioner)
|
|
@@ -5350,9 +5712,9 @@ asymmetry). Selection precedence + the two roots are §8.1. The rest of the post
|
|
|
5350
5712
|
scoping applies there. Warm sessions and bare interactive `pi` are likewise untouched.
|
|
5351
5713
|
8. **The stacked branch-creation gesture differs; the prepared start does not.** A fresh
|
|
5352
5714
|
stacked layer starts locally via `git worktree add … <parent_sha>` and remotely via an
|
|
5353
|
-
in-place `git checkout -b plan-<N> <parent_sha>` (§8.46) — both
|
|
5354
|
-
`LayerContext` + `
|
|
5355
|
-
the same `layer-context.json` (timestamps excepted).
|
|
5715
|
+
in-place `git checkout -b plan-<N> <parent_sha>` (§8.46) — both independently call the same
|
|
5716
|
+
execution Prepare variant, consume its internal `LayerContext` + verified `parent_sha`, land
|
|
5717
|
+
on the same commit, and write the same `layer-context.json` (timestamps excepted).
|
|
5356
5718
|
9. **The resume prior-work advisory is `plan resume`-cold-local-only.** When `perk plan resume`
|
|
5357
5719
|
resolves to `implement` and the plan worktree already exists locally (the D4 reuse arm), the
|
|
5358
5720
|
door appends `prompts/common/resume-advisory.md` to the implement primer via `launch_stage`'s
|
|
@@ -5438,8 +5800,10 @@ an extra user `--skill` stays additive — pi merges explicit skill paths even u
|
|
|
5438
5800
|
1. `--no-skills`;
|
|
5439
5801
|
2. the `include_dirs` whitelist entries (absolute `--skill <dir>`, config order);
|
|
5440
5802
|
3. the **npm-package skills** (unless `include_packages = false`): from `.pi/settings.json`
|
|
5441
|
-
`packages` (strings or `{source}` rows),
|
|
5442
|
-
|
|
5803
|
+
`packages` (strings or `{source}` rows), except an object-form package whose `skills` key is
|
|
5804
|
+
exactly `[]` is excluded before package-directory probing and cannot trigger the package-tier
|
|
5805
|
+
wholesale fail-open. Remaining **`npm:` sources only** → `.pi/npm/node_modules/<name>`.
|
|
5806
|
+
Local-path sources (the self-repo's `".."`) and `git:` sources
|
|
5443
5807
|
are deliberately **not** enumerated — first-party skills come from `.agents/skills` full stop
|
|
5444
5808
|
(no committed-`skills/` fallback); enumerating the self-repo's local-path (`..`) package would
|
|
5445
5809
|
also re-import the committed-`skills/` vs `.agents/skills` name-collision noise (the 16-way
|
|
@@ -5495,7 +5859,8 @@ stage (§8.50) carries `ask_user_question` + `run_audit_wave` + the research fam
|
|
|
5495
5859
|
sessions run GATED (read-only mode), where the gate-ON set governs, so the row exists for the
|
|
5496
5860
|
keys≡registry pin and the defensive gate-off arm; `run_audit_wave` also joins `PERK_TOOLS`
|
|
5497
5861
|
and `READ_ONLY_TOOLS` (§8.3's carve-in — the write target is handoff-bound, never
|
|
5498
|
-
caller-supplied).
|
|
5862
|
+
caller-supplied). `run_harvest_wave` and `run_dream_wave` likewise join `PERK_TOOLS` +
|
|
5863
|
+
`READ_ONLY_TOOLS` with NO stage row at all (cold-only, gate-on — §8.48/§8.61). **Scoped universe:
|
|
5499
5864
|
`PERK_TOOLS ∪ BORROWED_TOOLS`** — perk's own name-keyed census plus the enumerated
|
|
5500
5865
|
borrowed-package census (the web-provider union, pi-mono-linear's 25 tools, pi-subagents'
|
|
5501
5866
|
delegation four, pi-fff's search names — both mode name-sets, `fffind`/`ffgrep`/`fff-multi-grep`
|
|
@@ -5611,7 +5976,10 @@ lenient read — an unknown stored scope parses to `None`). Per-backend storage:
|
|
|
5611
5976
|
`find_gist_issue{run_id}` (label + header-key scoped — cannot return a plan/learn issue),
|
|
5612
5977
|
`create_gist_issue{title, body, run_id, scope, dry_run}` (idempotent via the finder; stamps
|
|
5613
5978
|
`scope` into the header), and `list_gist_issues{} -> GistSummary[]{id, title, url, body, scope,
|
|
5614
|
-
adopted}` — every OPEN `perk:gist` issue; raises on infra failure, never masks as empty.
|
|
5979
|
+
adopted}` — every OPEN `perk:gist` issue; raises on infra failure, never masks as empty. On
|
|
5980
|
+
GitHub `find_gist_issue` / `list_gist_issues` ride the same exhaustive full-open-set read as the
|
|
5981
|
+
plan/learn ops (`gh api --paginate --slurp`, per_page=100; fail-closed on an unexpected page
|
|
5982
|
+
shape). The
|
|
5615
5983
|
`ObjectiveStore` grows the project-tier pair in the no-op-return family:
|
|
5616
5984
|
`create_gist_source{title, prose, run_id, dry_run} -> ObjectiveRef | None` (`None` = "no project
|
|
5617
5985
|
surface" — the CLI falls back to the issue tier; the Linear project store creates/finds the gist
|
|
@@ -5715,9 +6083,13 @@ fields are now **written** by stacked authoring: the `objective create` cold doo
|
|
|
5715
6083
|
capability preflight; the TS plane carries the reviewed choice
|
|
5716
6084
|
end-to-end (`objective_draft`/`objective_save` → `--delivery`). The **layer-identity trio**
|
|
5717
6085
|
(`objective_node_id`/`delivery_lineage`/`predecessor_plan_id`) is now written at node-linked
|
|
5718
|
-
plan save (§8.46)
|
|
5719
|
-
|
|
5720
|
-
|
|
6086
|
+
plan save through `PrepareRequest(kind="plan_identity", mode=…)` (§8.46): Prepare owns the
|
|
6087
|
+
single objective read and returns the independently optional objective base plus nested
|
|
6088
|
+
`PlanIdentity {objective_node_id, delivery_lineage, predecessor_plan_id}`. Every header write arm
|
|
6089
|
+
receives all three identity fields; `PlanRef` remains deliberately narrower and stores **only**
|
|
6090
|
+
`delivery_lineage` as its routing field (no node/predecessor/schema growth). The checkpoint pair
|
|
6091
|
+
(`parent_checkpoint_sha`/`published_head_sha`) is written by the §8.47 publish operation —
|
|
6092
|
+
together, in one write, only after publication verification.
|
|
5721
6093
|
|
|
5722
6094
|
## §8.45 · Stacked authoring: the reviewed delivery choice + capability preflight
|
|
5723
6095
|
|
|
@@ -5746,18 +6118,24 @@ facts and the seed instructs `delivery: stacked` without re-asking — the save
|
|
|
5746
6118
|
`validate_stacked_roadmap` (errors verbatim under the existing `invalid_roadmap` error_type —
|
|
5747
6119
|
including the one-node "save it as a standalone plan" message; a fan-out/fan-in DAG passes) →
|
|
5748
6120
|
the `--adopt-from` refusal (`invalid_input`: in-place adoption of a stacked objective is
|
|
5749
|
-
deferred — no roadmap node owns it) →
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
6121
|
+
deferred — no roadmap node owns it) → authoring Prepare (**skipped on `--dry-run`**, which stays
|
|
6122
|
+
offline and still validates the bounds) → the write gate (retired below) → the store mutation.
|
|
6123
|
+
Stored `base` semantics are unchanged. The CLI passes the stored base intent — explicit `--base`
|
|
6124
|
+
→ `[workflow] base` → `None` — to Prepare; optional-base trunk fallback now belongs to the
|
|
6125
|
+
façade and still yields the same effective probe base.
|
|
6126
|
+
|
|
6127
|
+
**The capability boundary** is
|
|
6128
|
+
`resolve_delivery(repo_root).prepare(PrepareRequest(kind="authoring",
|
|
6129
|
+
base=<stored-base-or-null>))` → `PrepareResult(kind="authoring", base=<effective-base>)`.
|
|
6130
|
+
`resolve_delivery` remains zero-I/O; `Delivery.prepare` resolves a null base through
|
|
6131
|
+
`DeliveryGit.trunk_branch`, invokes the authorities in the order below, and returns only after
|
|
6132
|
+
every check passes. Capability rows are private implementation details: a complete failed-row set
|
|
6133
|
+
raises one `DeliveryError(error_type="capability_unsupported")` whose message is
|
|
6134
|
+
`This repository cannot take a stacked delivery train against base <repr>:` followed by every
|
|
6135
|
+
`- <name>: <detail>` row in observation order. Import direction stays §8.44's (delivery →
|
|
6136
|
+
github/substrate; the CLI imports delivery). A stacked `--dry-run` does not resolve a `Delivery`
|
|
6137
|
+
or invoke Prepare at all, so its compose-preview remains fully offline. The checks, each with
|
|
6138
|
+
honest expected-vs-observed detail:
|
|
5761
6139
|
|
|
5762
6140
|
- **`native-stack`** — `stacks.stack_capability(repo_root)`: a GraphQL schema-introspection
|
|
5763
6141
|
read (`__type(name: "PullRequest")` → a `stack` field exists). **Fail closed** (introspection
|
|
@@ -5766,8 +6144,9 @@ expected-vs-observed detail:
|
|
|
5766
6144
|
- **`merge-rules`** — `stacks.base_merge_rules(repo_root, base)` → `MergeRules
|
|
5767
6145
|
{squash_allowed, merge_queue_required}`: GraphQL `repository { squashMergeAllowed }` (squash
|
|
5768
6146
|
direct merge must be allowed) + REST `GET repos/{owner}/{repo}/rules/branches/{base}` (any
|
|
5769
|
-
effective `merge_queue` rule ⇒ reject). Strict reads —
|
|
5770
|
-
`GitHubError
|
|
6147
|
+
effective `merge_queue` rule ⇒ reject). Strict reads — the real authority adapter converts an
|
|
6148
|
+
expected `GitHubError` into its frozen `ProbeError`; Prepare records that discriminant as a
|
|
6149
|
+
failed check and continues (can't verify ⇒ don't promise).
|
|
5771
6150
|
- **`remote-base`** — `git.remote_branch_head`: the observed remote base SHA. An absent remote
|
|
5772
6151
|
base branch is a **failed check** (honest message), never a crash; without it the push
|
|
5773
6152
|
probes are skipped.
|
|
@@ -5778,10 +6157,10 @@ expected-vs-observed detail:
|
|
|
5778
6157
|
failure). The detail states — on success AND failure — that the probe proves **server
|
|
5779
6158
|
capability and authentication, not branch write permission**.
|
|
5780
6159
|
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
objectives live on Linear).
|
|
6160
|
+
Prepare aggregates independent failures: native stack, merge rules, and remote base are always
|
|
6161
|
+
observed in order; push-URL resolution and one atomic probe per URL run only after a positive
|
|
6162
|
+
remote-base SHA, and all URLs must pass. The probes run against the Git/GitHub plane **regardless
|
|
6163
|
+
of issue backend** (GitHub is the universal PR plane even when objectives live on Linear).
|
|
5785
6164
|
|
|
5786
6165
|
**The write gate (retired).** Stacked authoring shipped behind a development-only environment
|
|
5787
6166
|
opt-in (a missing opt-in failed the save with a typed gated refusal) until the publication
|
|
@@ -5901,14 +6280,15 @@ ambiguous** — it proves neither presence nor absence, so it raises `JournalApp
|
|
|
5901
6280
|
without a retry (only a rescan that proved absence earns the retry). The remote effect an event
|
|
5902
6281
|
guards must not proceed past an ambiguous append.
|
|
5903
6282
|
|
|
5904
|
-
**The rest of the stored train state**
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
`
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
`
|
|
5911
|
-
|
|
6283
|
+
**The rest of the stored train state** uses the §8.42 merge-write seams. `TrainPersistence`
|
|
6284
|
+
retains the reusable typed writers: `write_checkpoints` writes the `parent_checkpoint_sha` +
|
|
6285
|
+
`published_head_sha` pair in ONE `update_plan_header` call, and `write_delivery_lineage` delegates
|
|
6286
|
+
to `update_objective_header` (lineage *minting* stays the authoring node's concern). Transfer's
|
|
6287
|
+
private roll-forward core instead uses its issue seam's generic `update_plan_header` once per
|
|
6288
|
+
atomic group: ownership (`objective_id` + `objective_node_id`), stacked identity
|
|
6289
|
+
(`delivery_lineage` + `predecessor_plan_id`, whose bottom value may be null), or all four explicit
|
|
6290
|
+
nulls when clearing stacked metadata. Read-before-write checks preserve idempotence; no
|
|
6291
|
+
transfer-only persistence wrappers remain.
|
|
5912
6292
|
|
|
5913
6293
|
**Engagement exclusion.** Journal comments carry a `perk:*` sentinel in either encoding, so the
|
|
5914
6294
|
existing engagement classifier (§8.25) classifies them `perk` and the engagement renderers drop
|
|
@@ -5923,20 +6303,155 @@ deliberately untouched (no cross-plane consumer yet — mirroring §8.42's postu
|
|
|
5923
6303
|
|
|
5924
6304
|
## §8.44 · The DeliveryTrain projection + stack status (read path)
|
|
5925
6305
|
|
|
5926
|
-
**The projection.** `perk.delivery.train.reconstruct_train`
|
|
5927
|
-
|
|
5928
|
-
store (policy, lineage, roadmap),
|
|
5929
|
-
fold (§8.43), Git refs, and GitHub PR + native-stack state.
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
(
|
|
5933
|
-
`
|
|
5934
|
-
|
|
5935
|
-
`
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
6306
|
+
**The projection and public façade boundary.** `perk.delivery.train.reconstruct_train` remains the
|
|
6307
|
+
internal pure core that rebuilds one **immutable** `DeliveryTrain` projection from the durable
|
|
6308
|
+
authorities — the objective store (policy, lineage, roadmap), plan issues (layer identity +
|
|
6309
|
+
checkpoints), the journal fold (§8.43), Git refs, and GitHub PR + native-stack state. The canonical
|
|
6310
|
+
repository-scoped APIs are `resolve_delivery(repo_root).status(StatusRequest(objective_id))`,
|
|
6311
|
+
one flat frozen `Delivery.prepare(PrepareRequest(...))` family,
|
|
6312
|
+
`Delivery.transfer(TransferRequest(...))`, `Delivery.publish(PublishRequest(...))`, and
|
|
6313
|
+
`Delivery.sync(SyncRequest(...), consent=...)`.
|
|
6314
|
+
The sync keyword is required at the call
|
|
6315
|
+
boundary: a caller must explicitly pass a callback or deliberately pass `None` for automation's
|
|
6316
|
+
auto-approval policy; omission can never silently select mutation consent. `resolve_delivery`
|
|
6317
|
+
performs ZERO I/O. `Delivery.status` returns a `StatusResult` whose invariant is exactly one
|
|
6318
|
+
non-null branch:
|
|
6319
|
+
`train` OR the successful incremental `no_train_reason`, alongside `objective_id`, `objective_url`,
|
|
6320
|
+
and `redirected_from`.
|
|
6321
|
+
|
|
6322
|
+
Prepare's legal request/result variants are closed shapes (an unknown `kind` retains the exact
|
|
6323
|
+
`unknown prepare kind: <repr>` diagnostic; every other illegal shape raises `ValueError`):
|
|
6324
|
+
|
|
6325
|
+
- `authoring`: optional request `base`, no mode/ids; result carries only effective `base`;
|
|
6326
|
+
- `replan`: required nonblank objective, no other fields; result carries one nested
|
|
6327
|
+
`ReplanContext` from the single objective snapshot (`objective_id`/URL/title/nodes, policy,
|
|
6328
|
+
base/lineage) plus claimed `ReplanClaim` rows and open-PR plan pairs in delivery order. An
|
|
6329
|
+
incremental objective has empty claimed/open-PR facts; stacked preparation additionally gates
|
|
6330
|
+
the journal, bound status projection, structural blockers, and claimed prefix;
|
|
6331
|
+
- `plan_identity`: explicit `mode=strict|best_effort`, required objective, optional node (strict
|
|
6332
|
+
requires it), no base/plan request fields; result carries independently optional objective
|
|
6333
|
+
`base` and nested `PlanIdentity`, or — only for a best-effort expected read failure —
|
|
6334
|
+
`notice=str(exc)` with base/identity absent. The empty string is a valid notice and
|
|
6335
|
+
`notice is not None` is the failure discriminator;
|
|
6336
|
+
- planning `layer_start`: `mode=planning`, required objective, optional requested node; result
|
|
6337
|
+
carries exactly one internally-valid nested `PlanningDecision`; and
|
|
6338
|
+
- execution `layer_start`: `mode=execution`, required plan, nullable objective solely for the
|
|
6339
|
+
defensive missing-objective refusal; result carries the internal `layer.LayerContext` plus a
|
|
6340
|
+
nonblank verified `parent_sha`.
|
|
6341
|
+
|
|
6342
|
+
`TransferRequest` is frozen intent only: predecessor id, run id, title, prose, nullable base,
|
|
6343
|
+
immutable roadmap nodes, ordered immutable raw `(node_id, issue_id)` carries, and successor
|
|
6344
|
+
`delivery=incremental|stacked`. It deliberately performs no constructor normalization or
|
|
6345
|
+
validation. `Delivery.transfer` applies the pure recoverability predicate before lock/I/O: closed
|
|
6346
|
+
delivery value, non-empty unique-node roadmap, known dependencies, acyclic delivery order,
|
|
6347
|
+
unique/nonblank/subset carry keys, and nonblank string carry identities. `TransferResult` preserves
|
|
6348
|
+
the existing `{predecessor_id, successor, operation_id, abandoned_operation_id, rolled_forward,
|
|
6349
|
+
journaled}` shape without a constructor matrix; plain incremental→incremental returns the
|
|
6350
|
+
null-operation arm.
|
|
6351
|
+
|
|
6352
|
+
`SyncRequest` is the other closed flat family. It carries `mode=cascade|continue|abort` plus a
|
|
6353
|
+
required nonblank objective. Cascade requires a nonblank journal `run_id`; optional `include_base`,
|
|
6354
|
+
`dry_run`, `adopt_node`, `trigger_plan_id`, and raw `trigger_run_id` obey this matrix: base and
|
|
6355
|
+
adoption are exclusive; a trigger plan composes with neither; trigger run requires trigger plan;
|
|
6356
|
+
dry run may compose with base, adoption, or trigger. Continue/abort carry no cascade field.
|
|
6357
|
+
`SyncResult` moves the existing flat operation result unchanged and deliberately has no new
|
|
6358
|
+
constructor combination guards: its additive historically reachable arms are enforced by the
|
|
6359
|
+
operation protocol, not a partial discriminator. Its frozen nested records are `Layer`, `Cascade`
|
|
6360
|
+
(the consent preview), and `AbortPreview`; these add no separate package-root names.
|
|
6361
|
+
|
|
6362
|
+
`PublishRequest` is the closed flat publish/ready family:
|
|
6363
|
+
|
|
6364
|
+
- common fields are `kind=layer|ready`, required nonblank `plan_id` (accepted with or without one
|
|
6365
|
+
leading `#`), and `dry_run=false`;
|
|
6366
|
+
- either dry-run kind accepts only those common fields and returns before route classification or
|
|
6367
|
+
any authority call;
|
|
6368
|
+
- a real layer requires nonblank journal `run_id`, permits one optional nonblank raw
|
|
6369
|
+
`trigger_run_id`, and rejects delivery/objective fields; and
|
|
6370
|
+
- a real ready requires `delivery=incremental|stacked`, rejects run fields, requires no objective
|
|
6371
|
+
for incremental, and permits a null stacked objective solely for the defensive `not_stacked`
|
|
6372
|
+
refusal (a present objective is nonblank).
|
|
6373
|
+
|
|
6374
|
+
`PublishResult {kind, plan_id, dry_run, layer?, ready?}` carries the canonical bare plan id and
|
|
6375
|
+
exactly one matching nested detail. `Ready {pr, was_draft}` preserves the gateway PR and its
|
|
6376
|
+
pre-mutation draft fact. `Layer` is exactly `{pr, branch, header_update, plan_embedded, pr_checked,
|
|
6377
|
+
parent_branch, operation_id, stack_number, stack_size, stack_position, parent_checkpoint_sha,
|
|
6378
|
+
published_head_sha, resumed, converged_noop, cascade}`. The stack triple is all-null or all-positive
|
|
6379
|
+
with position within size. A real layer has nonblank branches/checkpoints, a non-dry-run header
|
|
6380
|
+
update, and `pr_checked=true`; a direct result has no operation id exactly on a converged no-op. A
|
|
6381
|
+
cascade carries `SyncResult` directly, mirrors its operation/resume/no-op fields, and carries no
|
|
6382
|
+
stack triple. There is no replacement operation wrapper. The dry-run layer and ready sentinels,
|
|
6383
|
+
including PR number/url/draft/state/existed facts and the layer's exact synthetic header update,
|
|
6384
|
+
are constructor-validated; malformed combinations raise `ValueError`.
|
|
6385
|
+
|
|
6386
|
+
The nested detail vocabulary adds no package-root exports: `PlanIdentity`; `PlanningNode`;
|
|
6387
|
+
`PlanningContext {position, layer_count, delivery_lineage, base, predecessor_node_id,
|
|
6388
|
+
predecessor_plan_id, parent_branch, observed_parent_head_sha}` (one-based position/count); and
|
|
6389
|
+
`PlanningDecision {kind, objective_id, objective_title, objective_url, requested_node_id, node,
|
|
6390
|
+
reason, skipped_claim_ids, context}`. Decision `kind` is exactly `ready | build_blocked |
|
|
6391
|
+
in_flight | wrong_candidate | complete | node_not_found | terminal | blocked | no_actionable`;
|
|
6392
|
+
node/reason/skipped/context presence is shape-validated (context only on train-derived `ready`; the
|
|
6393
|
+
all-layers-published graph fallback may be `ready` without context). The package root exports
|
|
6394
|
+
`PublishRequest`, `PublishResult`, `TransferRequest`, and `TransferResult` but no publication or
|
|
6395
|
+
transfer core/runtime/error. Removing `DeliveryOperationFacts`, `LayerBodyFacts`,
|
|
6396
|
+
`PublicationError`, `PublicationResult`, `TrainRowFacts`, and `publish_layer` plus adding the two
|
|
6397
|
+
Transfer values left the exact 61-name `perk.delivery.__all__` of that era; the atomic
|
|
6398
|
+
objective-landing migration has since completed the cut to the **canonical 20-name surface**
|
|
6399
|
+
(`Delivery`, `resolve_delivery`, `DeliveryError`, the three authority ABCs, and the seven
|
|
6400
|
+
request/result families) — every operation module, the journal/persistence machinery, and
|
|
6401
|
+
the landing readiness/mutation internals are module-path-internal only.
|
|
6402
|
+
|
|
6403
|
+
The façade composes three nominal ABC authorities, with each interface, real adapter, and owned
|
|
6404
|
+
constructor-configured fake moving in lockstep. `DeliveryPersistence` aggregates objective, plan,
|
|
6405
|
+
and journal reads plus `get_plan_body(*, issue_id)`, `update_plan_header(*, issue_id, fields)`,
|
|
6406
|
+
prepared/outcome appends, checkpoint-pair writes, transfer carry normalization, objective lookup
|
|
6407
|
+
by run id, supersede creation, and supersession finalization. `DeliveryGit` exposes its bound `repo_root`,
|
|
6408
|
+
aggregates trunk/fetch/exact-ref-fetch/local-commit-resolution/ref/ancestry/worktree/base reads,
|
|
6409
|
+
adds `push_with_exact_lease(branch, *, expected_remote_sha)`, plus Prepare's push-URL resolution and
|
|
6410
|
+
no-op atomic probe, and owns
|
|
6411
|
+
the genuine sync Git behavior: exact-leased atomic push; temp-ref update/delete/list; detached
|
|
6412
|
+
worktree add/remove/prune; detached checkout/rebase; retained-worktree rebase/dirty state; and
|
|
6413
|
+
worktree-scoped commit resolution. It reuses substrate `RefUpdate`, rebase results, `GitError`, and
|
|
6414
|
+
`PushRejectedError` unchanged. `DeliveryGitHub` aggregates stable PR and tolerant native-stack
|
|
6415
|
+
reads plus Prepare's host stack-capability/base merge-rule facts. Its existing branch lookup is the
|
|
6416
|
+
rich all-state `pr_for_branch(branch) -> PullRequest|null`; its strict read is
|
|
6417
|
+
`strict_stack(number) -> StackRestFacts|null`. Publication/ready add the distinct `get_pr`,
|
|
6418
|
+
`create_pr`, `update_pr_body`, `update_pr_base`, `reopen_pr`, `mark_pr_ready`, `create_stack`, and
|
|
6419
|
+
`append_stack` effects; sync adds active-writer plan observation. No duplicate branch/stack/full-PR
|
|
6420
|
+
endpoint exists. The production
|
|
6421
|
+
`RepoDeliveryPersistence`, `RepoDeliveryGit`, and
|
|
6422
|
+
`RepoDeliveryGitHub` adapters live in `perk/delivery/observe.py`. Persistence resolves the
|
|
6423
|
+
backend-aligned objective store, issue backend, and `TrainPersistence` only on its first read,
|
|
6424
|
+
caches them only after backend identities agree, and keeps no partial cache after a failed attempt.
|
|
6425
|
+
The Git and GitHub adapters likewise store only constructor data and observe on method calls.
|
|
6426
|
+
Publication binds these same instances plus bound `status`/`sync` into private `_PublishContext`;
|
|
6427
|
+
private immutable `_PublishRuntime` owns only operation-id minting, clock, sleep, and PR-body
|
|
6428
|
+
validation. Transfer binds the same instances into private `TransferSeams` plus aggregate Git and
|
|
6429
|
+
GitHub authorities; recovery continues to compose `TransferSeams` directly. Its private runtime
|
|
6430
|
+
owns only the operation lock, operation-id minting, and clock. The bound-status bridge unwraps only
|
|
6431
|
+
the exact reconstruction/store/issue/persistence causes expected by the shared transfer core.
|
|
6432
|
+
Cause-aware bridges around the status-oriented Git/GitHub reads unwrap only a
|
|
6433
|
+
`TrainReconstructionError` whose direct cause is the matching raw `GitError`/`GitHubError`; every
|
|
6434
|
+
other typed failure stays typed and fails closed. Both Publish dry-run arms return before every
|
|
6435
|
+
context method, so assignment-only resolution remains offline under GitHub and Linear config.
|
|
6436
|
+
|
|
6437
|
+
For a new authority call whose consumer must record a failure and continue independent checks, the
|
|
6438
|
+
aggregate ABC owns frozen nested success/error discriminants: Git `PushUrlsResult`,
|
|
6439
|
+
`AtomicPushResult`, and `ProbeError`; GitHub `MergeRules` and `ProbeError`. Only the real adapter
|
|
6440
|
+
catches the expected substrate/gateway exception and converts it; fakes return constructor-seeded
|
|
6441
|
+
discriminants without catching programming errors. The existing terminal status methods retain
|
|
6442
|
+
their typed exceptions. No speculative subgateway split is warranted: each capability belongs to
|
|
6443
|
+
one existing aggregate authority, while narrow pure-core reader roles remain internal. There is no
|
|
6444
|
+
dry-run authority implementation because objective-create dry run omits Prepare entirely.
|
|
6445
|
+
|
|
6446
|
+
Import direction stays §8.43's: nothing in `perk/backends/` or `perk/github/` imports
|
|
6447
|
+
`perk.delivery`. The projection is read-only and works from a **fresh clone** — no local worktree
|
|
6448
|
+
or branch is authoritative, and local absence is never an error. Branch-sensitive laziness is
|
|
6449
|
+
load-bearing: status reads objective policy before fallback trunk resolution, so an incremental
|
|
6450
|
+
objective returns the no-train branch without Git or GitHub work; authoring Prepare never resolves
|
|
6451
|
+
persistence, issue backends, credentials, or configuration and touches only its Git/GitHub
|
|
6452
|
+
authorities. Identity Prepare performs exactly one objective read and no Git/GitHub call; planning
|
|
6453
|
+
delegates to exactly one status reconstruction; execution delegates to status and then exact
|
|
6454
|
+
parent-ref verification.
|
|
5940
6455
|
|
|
5941
6456
|
**Layer axes.** Layer state is orthogonal, never one lossy enum: `intent`
|
|
5942
6457
|
(`skipped|unplanned|planned|canceled` — skipped nodes contract out of the rendered layers;
|
|
@@ -6031,25 +6546,68 @@ projection-only PENDING ordering surrogates, never returned/persisted) → roadm
|
|
|
6031
6546
|
`delivery_order` over the effective nodes → the node↔plan join (idempotent per layer — a plan
|
|
6032
6547
|
preloaded by the cancellation proof is never re-joined, so canonical findings emit once) →
|
|
6033
6548
|
PUBLISH coverage + checkpoint topology → predecessors → Git observation (reusing the fetch) →
|
|
6034
|
-
PRs → publication → membership → prefix → base → readiness.
|
|
6549
|
+
PRs → publication → membership → prefix → base → readiness. Production reconstruction also
|
|
6550
|
+
captures `objective_title` and the exact active-state `objective_nodes` tuple on the immutable
|
|
6551
|
+
train for planning Prepare; both are defaulted internal projection inputs and are deliberately
|
|
6552
|
+
absent from `TrainOut`, so status human/JSON bytes do not grow. `DeliveryTrain` additionally
|
|
6035
6553
|
carries the §8.54 projection facts `projected_canceled_nodes` /
|
|
6036
6554
|
`repairable_canceled_nodes` (default-empty tuples of `ProjectedCancellation(node_id,
|
|
6037
6555
|
persisted_status)`).
|
|
6038
6556
|
|
|
6039
6557
|
**Failure-posture split.** Stable authorities hard-fail: a failed objective read, plan join,
|
|
6040
|
-
journal **carrier** read, or `git fetch` is a
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6558
|
+
journal **carrier** read, or `git fetch` is a status failure. At the pure-core seam this remains
|
|
6559
|
+
`TrainReconstructionError`; `Delivery.status` translates ONLY the declared status codes into
|
|
6560
|
+
`DeliveryError` with the same message and stable `error_type` (`objective_not_found |
|
|
6561
|
+
invalid_delivery_policy | invalid_train | git_error | github_error |
|
|
6562
|
+
supersession_corruption`). The private status allowlist remains exactly those six even though
|
|
6563
|
+
`DeliveryError`'s façade-wide vocabulary is the bounded union of status, Prepare, Transfer,
|
|
6564
|
+
Publish, and sync codes.
|
|
6565
|
+
Prepare adds `capability_unsupported | invalid_input | missing_lineage |
|
|
6566
|
+
stacked_predecessor_missing | unknown_layer | node_not_build_ready | parent_missing |
|
|
6567
|
+
parent_unverified`. Expected
|
|
6568
|
+
objective-store, issue-backend, and train-persistence exceptions normalize to `github_error`.
|
|
6569
|
+
`DeliveryError` rejects unknown codes, and a status error outside its six-code subset propagates
|
|
6570
|
+
rather than silently widening status. Prepare reuses the status-owned
|
|
6571
|
+
trunk and remote-branch methods without changing their status messages: for a typed `git_error`
|
|
6572
|
+
wrapper it preserves the chained substrate `GitError` text when that guarded cause exists, else the
|
|
6573
|
+
wrapper text; a non-`git_error` reconstruction failure remains unexpected and propagates.
|
|
6574
|
+
`DeliveryError` additionally owns sync's bounded operation codes (`not_stacked`,
|
|
6575
|
+
`unresolved_operation`, `sync_conflict_pending`, `claimed_prefix_malformed`, `active_writer`,
|
|
6576
|
+
`dirty_worktree`, `writer_observation_unavailable`, `remote_drift`, `pr_drift`,
|
|
6577
|
+
`membership_drift`, `stale_parent`, `base_unobserved`, `multiple_push_urls`,
|
|
6578
|
+
`atomic_push_unsupported`, `rebase_conflict`, `push_rejected`, `sync_drift`,
|
|
6579
|
+
`postcondition_unverified`, `adopt_blocked`, `no_continuation`, `continuation_stale`,
|
|
6580
|
+
`continuation_invalid`, `rebase_in_progress`, `operation_in_progress`) and boundary codes
|
|
6581
|
+
`journal_corruption`, `journal_record_too_large`, and `invalid_config`. Publish/ready add only
|
|
6582
|
+
`delivery_error`, `stack_capability_lost`, `pr_already_merged`, `remote_settling_timeout`,
|
|
6583
|
+
`stack_registration_drift`, `stack_registration_failed`, `publication_drift`, `no_pr`,
|
|
6584
|
+
`pr_not_open`, `layer_not_published`, and `structural_blockers`; status's private subset remains
|
|
6585
|
+
exactly the six codes above.
|
|
6586
|
+
|
|
6587
|
+
Every `DeliveryError` emitted by `Delivery.publish` carries a jointly-present constrained
|
|
6588
|
+
`phase=layer|cascade|ready` and `origin=domain|git|github|delivery`; existing operations may leave
|
|
6589
|
+
both absent. Package-internal `PublicationError` and pure layer refusals become domain errors with
|
|
6590
|
+
their original code/message. Bound sync errors become `(cascade,delivery)`; bound layer status,
|
|
6591
|
+
objective-store/persistence/journal/reconstruction failures become `delivery_error/(layer,delivery)` except
|
|
6592
|
+
record-size, which preserves `journal_record_too_large`; raw exact-lease rejection and Git failures
|
|
6593
|
+
become `(layer,git)`; raw GitHub/issue failures become `(layer,github)`. Ready's pure selection and
|
|
6594
|
+
reviewability refusals become `(ready,domain)`; a status reconstruction cause preserves its domain
|
|
6595
|
+
code/message, while backend/objective/persistence/journal and raw GitHub failures become
|
|
6596
|
+
`github_error/(ready,github)`. Unexpected exceptions are not wrapped. CLI presentation maps these
|
|
6597
|
+
facts back to the established submit/ready prefixes and bytes.
|
|
6598
|
+
|
|
6599
|
+
Only two reads
|
|
6600
|
+
degrade: the **preview** native-stack read (membership `unknown` + information
|
|
6601
|
+
`stack_read_unavailable`, never a blocker — but unverifiable membership still declassifies the
|
|
6602
|
+
affected layers' publication to drift: the information posture governs the *finding*, not the
|
|
6603
|
+
verification bar) and journal **corruption** (`JournalCorruptionError` → the
|
|
6604
|
+
`journal_corruption` blocker; unresolved-operation facts report unknown). A superseded objective
|
|
6605
|
+
**redirects forward** along `superseded_by` (cycle guard + depth cap 50; breach ⇒
|
|
6606
|
+
`supersession_corruption`) to the active objective and reports `redirected_from`. An incremental
|
|
6607
|
+
objective short-circuits before fallback trunk detection, fetch, or any GitHub work into the
|
|
6608
|
+
successful `StatusResult.no_train_reason` branch; a junk `delivery` value fails closed
|
|
6609
|
+
(`invalid_delivery_policy`). `delivery: stacked` without a lineage renders the train with the
|
|
6610
|
+
`missing_lineage` blocker and skips the journal fold (report, don't abort).
|
|
6053
6611
|
|
|
6054
6612
|
**The GitHub-native read adapter.** `perk/github/stacks.py` splits the reads by schema
|
|
6055
6613
|
stability: `pr_delivery_facts(number, repo_root)` reads the **stable** GraphQL surface
|
|
@@ -6096,7 +6654,10 @@ reports no pending continuation, and an unparseable manifest file reports a
|
|
|
6096
6654
|
than being hidden; (b) the orphaned-sync-residue observation through recover's shared
|
|
6097
6655
|
classifier (§8.51), **fail-honest**: a Config-load or git/fs read failure — and the
|
|
6098
6656
|
classifier's own unparseable-manifest skip — reports `observed: false` plus the reason;
|
|
6099
|
-
`observed: true` with empty lists means *genuinely clean*.
|
|
6657
|
+
`observed: true` with empty lists means *genuinely clean*. This status-only path calls the
|
|
6658
|
+
package-internal read-only `recover.observe_orphans` classifier directly; it is deliberately not a
|
|
6659
|
+
second `RecoverRequest` variant, takes no operation lock, and performs no cleanup. The reported
|
|
6660
|
+
worktrees include
|
|
6100
6661
|
the stale worktree-admin entries (directory gone, inventory record left) beside the on-disk
|
|
6101
6662
|
ones — both are would-be sweep targets. An unobserved state is never
|
|
6102
6663
|
serialized as clean empty lists (the Config load is tolerant only in that it degrades the
|
|
@@ -6106,9 +6667,12 @@ observation, never the command).
|
|
|
6106
6667
|
(`commands/objective/stack/`, the recursive group-dir template; the group carries `status` +
|
|
6107
6668
|
`sync` (§8.49) + `recover` (§8.51) + `land` (§8.55 dry-run readiness + §8.56 the landing
|
|
6108
6669
|
mutation); the shared objective resolution and run-id resolution live in
|
|
6109
|
-
`stack/shared.py`).
|
|
6110
|
-
|
|
6111
|
-
|
|
6670
|
+
`stack/shared.py`). It is a migrated façade consumer: after objective-id resolution it constructs
|
|
6671
|
+
one lazy repository service and makes one `Delivery.status(StatusRequest(...))` call; it never
|
|
6672
|
+
imports or assembles the pure reconstruction readers. Resolution: explicit argument → the plan
|
|
6673
|
+
worktree's `cache.plan-ref` `objective_id` → a typed `no_objective` refusal. A `DeliveryError`
|
|
6674
|
+
preserves the declared status error type/message at the command boundary. Envelope
|
|
6675
|
+
(`ObjectiveStackStatusOut`, snapshotted at
|
|
6112
6676
|
`shared/schemas/outputs/objective-stack-status.schema.json`): `{success, error_type,
|
|
6113
6677
|
objective{id,url,redirected_from}, delivery: incremental|stacked, train|null, no_train|null,
|
|
6114
6678
|
operations[], continuation|null, orphaned_residue}` — the last three are the additive
|
|
@@ -6134,13 +6698,14 @@ remain read-only (the dry-run push is a no-op); build-ready derivation now rides
|
|
|
6134
6698
|
projection (§8.46); suffix synchronization has since landed as its own operation (§8.49);
|
|
6135
6699
|
recovery and the warm stack surface (`/objective-stack` + the typed stack tools) have since
|
|
6136
6700
|
landed (§8.51); atomic landing has since landed (§8.55 readiness + §8.56 the mutation);
|
|
6137
|
-
doctor findings
|
|
6701
|
+
doctor findings have since landed (§8.54, the unified drift-finding policy).
|
|
6138
6702
|
|
|
6139
6703
|
## §8.46 · Stacked build readiness + parent-aware execution
|
|
6140
6704
|
|
|
6141
|
-
**Build readiness is a derived fact of the projection — never a node status.**
|
|
6142
|
-
|
|
6143
|
-
|
|
6705
|
+
**Build readiness is a derived fact of the projection — never a node status.** The internal pure
|
|
6706
|
+
projection computes a frozen `BuildReadiness {next_node_id, ready, reason}` at the end of the
|
|
6707
|
+
pipeline and carries it on `DeliveryTrain.build_readiness`; repository consumers obtain that
|
|
6708
|
+
projection through `Delivery.status`, never by assembling the pure readers. `next_node_id` is the first
|
|
6144
6709
|
layer in delivery order whose `publication` is not `PUBLISHED` (`None` when every layer is
|
|
6145
6710
|
published, or the layer list is empty/all-skipped — the contiguous-prefix invariant makes the
|
|
6146
6711
|
candidate's predecessor published by construction; a violation is already a `prefix_gap`
|
|
@@ -6153,57 +6718,68 @@ untouched. `perk objective stack status` surfaces it additively: the `--json` `t
|
|
|
6153
6718
|
gains `next_build_ready {node_id, ready, reason}` (schema snapshot regenerated) and the human
|
|
6154
6719
|
render one line (`next build-ready: <id>` / `build blocked: <reason>`).
|
|
6155
6720
|
|
|
6156
|
-
**Stacked
|
|
6157
|
-
planning candidate is the readiness-derived next node — roadmap
|
|
6158
|
-
`delivery_order` but stop acting as a separate terminal-status planning gate,
|
|
6159
|
-
permits planning layer k+1 while layer k is published-but-unmerged.
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
`
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6721
|
+
**Stacked planning replaces dep-terminal gating with one planning Prepare snapshot.** For a
|
|
6722
|
+
stacked objective the single live planning candidate is the readiness-derived next node — roadmap
|
|
6723
|
+
DAG deps still shape `delivery_order` but stop acting as a separate terminal-status planning gate,
|
|
6724
|
+
which permits planning layer k+1 while layer k is published-but-unmerged. After the plan door's
|
|
6725
|
+
initial objective read chooses stacked versus incremental, a real stacked launch calls exactly
|
|
6726
|
+
one `Delivery.prepare(PrepareRequest(kind="layer_start", mode="planning", objective_id,
|
|
6727
|
+
node_id?))`; that operation calls `Delivery.status` exactly once and performs no extra persistence
|
|
6728
|
+
read. The returned train is the sole post-Prepare authority: title/URL/node presentation, graph
|
|
6729
|
+
fallback, and resumable claims come from its captured objective snapshot; base/lineage/order,
|
|
6730
|
+
position/count, predecessor branch/head, blockers, and readiness come from the same immutable
|
|
6731
|
+
projection. The initial read is never reused for those facts. Incremental planning stays
|
|
6732
|
+
byte-identical; stacked `--dry-run` remains offline on the existing graph path and reports
|
|
6733
|
+
`"build_readiness": "unchecked (dry-run)"`.
|
|
6734
|
+
|
|
6735
|
+
Planning classification returns the nested decision vocabulary from §8.44. A non-ready train is
|
|
6736
|
+
`build_blocked`; a pending or planning-without-plan candidate is `ready` unless an explicit other
|
|
6737
|
+
node was requested (`wrong_candidate`); in-progress or planning-with-plan is `in_flight` before
|
|
6738
|
+
that comparison; another candidate status is `build_blocked`. Without a readiness candidate, the
|
|
6739
|
+
captured dependency graph yields explicit `ready | in_flight | node_not_found | terminal |
|
|
6740
|
+
blocked`, or automatic `ready | in_flight | complete | no_actionable`; a graph-fallback `ready`
|
|
6741
|
+
contains no train context. `skipped_claim_ids` comes from that same graph and excludes the selected
|
|
6742
|
+
node. The CLI mapper preserves every existing refusal code/message (`node_not_build_ready`,
|
|
6743
|
+
`objective_in_flight`, `no_actionable_node`) and consumes the decision's title/URL/node/context for
|
|
6744
|
+
seed, lookup completion, mark, engagement, notes, and handoff.
|
|
6745
|
+
|
|
6746
|
+
Hard failures are distinct from decisions and preserve exact precedence: `redirected_from` (the
|
|
6747
|
+
only supersession signal — provider normalization such as `007`→`7` with no redirect is accepted)
|
|
6748
|
+
is checked before train/no-train; no train is `invalid_train`; a readiness candidate absent from
|
|
6749
|
+
`train.layers` is `unknown_layer`; and a ready child whose predecessor has no plan/branch is
|
|
6750
|
+
`stacked_predecessor_missing`. Planning never exact-fetches a parent or returns a parent SHA.
|
|
6751
|
+
Prepare runs before the existing `update_objective_node(..., planning)` write, but this is an
|
|
6752
|
+
**observation followed by a non-CAS mark, not a lease or atomic claim**: concurrent edits,
|
|
6753
|
+
supersession after observation, and duplicate launches remain possible and are outside this
|
|
6754
|
+
contract.
|
|
6755
|
+
|
|
6756
|
+
`stacked_selection(repo_root, state)` remains unchanged for **`perk objective next`** and the
|
|
6757
|
+
**`objective run` supervisor**: it still returns `StackedSelection {kind, node, ready, reason,
|
|
6758
|
+
train}`, calls status once, supplies `build_ready {ready, reason}` to next, and drives the
|
|
6759
|
+
supervisor's honest `action: "build_blocked"`/remediation arms. The run supervisor's dry-run status
|
|
6760
|
+
omission and repair/lower-address prioritization remain §8.52 behavior.
|
|
6179
6761
|
|
|
6180
6762
|
**Predecessor context seeds stacked planning.** The plan door's seed gains a stacked-only DATA
|
|
6181
|
-
block (`_layer_context_block`
|
|
6763
|
+
block (`_layer_context_block` is pure presentation over `PlanningContext`; incremental seeds stay
|
|
6182
6764
|
byte-identical): the layer's position in the delivery order; for a child layer the predecessor
|
|
6183
|
-
node/plan, its branch, and the
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
child layer down the incremental path), and a
|
|
6202
|
-
non-bottom save whose predecessor node has no linked plan is
|
|
6203
|
-
**`stacked_predecessor_missing`**. `parent_checkpoint_sha`/
|
|
6204
|
-
`published_head_sha` stay unwritten — the durable checkpoint pair is publication-owned.
|
|
6205
|
-
Incremental saves stay byte-identical. A dry run composes the trio best-effort from the same
|
|
6206
|
-
read and omits it when the objective is unreadable.
|
|
6765
|
+
node/plan, its branch, and the status-observed remote head (the objective base for the bottom
|
|
6766
|
+
layer); a note that `origin/<parent branch>` is already fetched and locally inspectable; and the
|
|
6767
|
+
explicit statement that perk records **no planning-time parent SHA** — later movement of the
|
|
6768
|
+
predecessor/codebase is a normal implementation danger.
|
|
6769
|
+
|
|
6770
|
+
**Save-time layer identity.** `perk plan save` delegates its one objective read and identity
|
|
6771
|
+
policy to `PrepareRequest(kind="plan_identity")`: `mode=strict` only for a real node-linked save;
|
|
6772
|
+
objective-only real saves and every dry run use `best_effort`. Strict expected read failures map
|
|
6773
|
+
to the established `github_error`, and proven absence to `objective_not_found`. Best-effort
|
|
6774
|
+
returns `notice=str(exc)` (including the empty string) for an expected read failure and otherwise
|
|
6775
|
+
treats absence silently; unexpected exceptions propagate. The independently optional normalized
|
|
6776
|
+
objective base and nested `PlanIdentity` come from the same snapshot. A no-node request returns
|
|
6777
|
+
the base without policy/lineage/order validation. A node request applies the existing pure policy:
|
|
6778
|
+
incremental has no identity; stacked requires a nonblank lineage, target membership, and a linked
|
|
6779
|
+
predecessor for non-bottom layers (`missing_lineage`, `invalid_input`, or
|
|
6780
|
+
`stacked_predecessor_missing` before any write). Every initial/re-save/unification arm receives all
|
|
6781
|
+
three identity fields. `PlanRef` persists only lineage, and the publication-owned checkpoint pair
|
|
6782
|
+
stays unwritten. Dry-run output prints a best-effort notice when one exists and omits identity.
|
|
6207
6783
|
|
|
6208
6784
|
**The `PlanRef` routing field.** `PlanRef`/`PlanRefModel`/`PlanRefOut` grow one nullable field,
|
|
6209
6785
|
`delivery_lineage` (amending §8.42's deliberate-non-growth note): stamped at save, recovered by
|
|
@@ -6211,47 +6787,33 @@ read and omits it when the objective is unreadable.
|
|
|
6211
6787
|
reconstructor in lockstep). It only **routes** a launch into the stacked path — every decision
|
|
6212
6788
|
still reconstructs the train fresh; the ref is never train-authoritative.
|
|
6213
6789
|
|
|
6214
|
-
**`LayerContext`
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
**Local
|
|
6234
|
-
`
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
offline.
|
|
6242
|
-
|
|
6243
|
-
**Remote positioning relocates into `run_worker`.** The managed workflow's "Check out the plan
|
|
6244
|
-
branch" shell step is **deleted** (§8.14 amended; the §8.13 input contract and `gh auth
|
|
6245
|
-
setup-git` are unchanged); `run_worker` now calls `position_branch(repo_root, plan_ref, base)`
|
|
6246
|
-
before `position_worktree`: an existing remote `plan-<N>` (either policy) → checkout +
|
|
6247
|
-
`reset --hard origin/plan-<N>`; absent + incremental → create from `origin/<base>`
|
|
6248
|
-
(behavior-equivalent to the removed shell arms; `base` = the dispatched input, else the plan's
|
|
6249
|
-
pinned base, else the detected trunk — the `base` param is now consumed); absent + stacked →
|
|
6250
|
-
the same readiness gate + `prepare_layer_start`, then `git checkout -b plan-<N> <parent_sha>`
|
|
6251
|
-
and `layer-context.json`. The branch-creation **gesture** intentionally differs (local
|
|
6252
|
-
`worktree add` vs remote in-place `checkout -b`) — a named §8.38 difference; both consume the
|
|
6253
|
-
same `LayerContext` + `prepare_layer_start`, and local/remote parity is proven for fresh
|
|
6254
|
-
creation (same start SHA, byte-identical `layer-context.json`, timestamps excepted).
|
|
6790
|
+
**`LayerContext` is internal; execution Prepare is the sole fresh-start proof.** The frozen
|
|
6791
|
+
internal `LayerContext {objective_id, node_id, plan_id, delivery_lineage, predecessor_plan_id,
|
|
6792
|
+
base, parent_branch, branch}` and pure `derive_layer_context`/`require_ready_layer` core remain in
|
|
6793
|
+
`perk.delivery.layer`, but none is exported from `perk.delivery`. Both launch paths independently
|
|
6794
|
+
call `Delivery.prepare(PrepareRequest(kind="layer_start", mode="execution", plan_id,
|
|
6795
|
+
objective_id?))`; there is no launch wrapper. It refuses a missing objective id before lookup,
|
|
6796
|
+
performs one status reconstruction, rejects no-train, locates the plan layer, proves it is
|
|
6797
|
+
build-ready, then derives the context. Execution does not inspect `redirected_from`; the freshly
|
|
6798
|
+
reconstructed active train remains authoritative. The bottom layer uses objective base; a child
|
|
6799
|
+
requires predecessor identity/plan/branch.
|
|
6800
|
+
|
|
6801
|
+
Prepare next calls `DeliveryGit.fetch_refs` once for the exact parent branch,
|
|
6802
|
+
`remote_branch_sha(parent_branch)` once, then `resolve_commit(observed_sha)` once. A fetch or remote
|
|
6803
|
+
observation failure is `git_error`; an absent remote branch is `parent_missing`; an observed SHA
|
|
6804
|
+
that does not resolve locally is `parent_unverified`. No fallback is permitted: the result's
|
|
6805
|
+
nonblank `parent_sha` is the verified latest remote head, never a stored checkpoint. Deferred
|
|
6806
|
+
publication still calls callback-only `prepare_layer_start` internally; that core has no
|
|
6807
|
+
repository/global defaults.
|
|
6808
|
+
|
|
6809
|
+
**Local and remote creation consume the same result.** Local `resolve_worktree` runs execution
|
|
6810
|
+
Prepare immediately before `git worktree add … <parent_sha>`; remote `position_branch` runs it
|
|
6811
|
+
immediately before `git checkout -b plan-<N> <parent_sha>`. Neither path fetches or derives the
|
|
6812
|
+
parent independently. Both write the returned context and SHA to `layer-context.json`; commit
|
|
6813
|
+
start and artifact match (timestamps and path gesture excepted). `DeliveryError` remains a typed
|
|
6814
|
+
CLI refusal, and an explicit local `--base` remains `invalid_input`. Existing branch/worktree reuse,
|
|
6815
|
+
`worktree: none`, resume, incremental creation, and stacked dry-run behavior are unchanged; the
|
|
6816
|
+
boundary governs fresh creation and never resets an active layer.
|
|
6255
6817
|
|
|
6256
6818
|
**Status.** Everything here is inert for incremental objectives; stacked authoring and layer
|
|
6257
6819
|
publication are supported (the §8.45 dogfood gate passed —
|
|
@@ -6273,20 +6835,29 @@ reconstruction, byte-identical behavior (the additive envelope fields serialize
|
|
|
6273
6835
|
(The §8.45 development write gate, which this route reused while stacked delivery was under
|
|
6274
6836
|
development, is retired — the dogfood gate passed.)
|
|
6275
6837
|
|
|
6276
|
-
**The publish operation** is
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
`
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6838
|
+
**The publish operation** is
|
|
6839
|
+
`resolve_delivery(repo_root).publish(PublishRequest(kind="layer", ...))`; the private
|
|
6840
|
+
`perk.delivery.publish._dispatch` engine is bound to the façade's three aggregate authorities and
|
|
6841
|
+
bound `status`/`sync` methods. Its immutable private runtime contains only clock/sleep/id minting
|
|
6842
|
+
and PR-body validation. Publication acquires no stack-operation lock. Automatic cascade calls the
|
|
6843
|
+
same façade's bound `sync`, whose dispatcher acquires the non-reentrant lock exactly once; there is
|
|
6844
|
+
no resolver or callback bundle inside publish.
|
|
6845
|
+
|
|
6846
|
+
For a real layer, the façade re-reads the current plan, requires only presence plus nonblank
|
|
6847
|
+
`objective_id` (never a fresh lineage-header gate), and best-effort reads its body (`IssueBackendError`
|
|
6848
|
+
means no embed). That one fresh plan owns title, body truth, header merge, and route facts. The
|
|
6849
|
+
operation composes the exact stacked PR body and identity fields internally: canonical branch, PR
|
|
6850
|
+
id, lifecycle stage, and explicit-trigger-only order-preserving `impl_run_ids` merge via
|
|
6851
|
+
`plan.merge_untrusted_str_list`. Submit retains route selection, eager config validation,
|
|
6852
|
+
resolved journal/raw trigger ids, Linear emission, mergeability probing, envelope, and rendering;
|
|
6853
|
+
it performs no stacked body/header/operation inference. `PreparedRecord.run_id` resolves
|
|
6854
|
+
`--run-id` → the plan header's `run_id`; both absent is a typed `invalid_input` refusal (a defensive
|
|
6855
|
+
arm — the header run id is stamped at save). The protocol, in order:
|
|
6856
|
+
|
|
6857
|
+
1. **Reconstruct** the train fresh through the bound `Delivery.status` using the plan header's
|
|
6858
|
+
`objective_id`; no train / plan not a layer → `not_stacked`.
|
|
6288
6859
|
2. **Route on the checkpoint-claimed prefix before reading the publish journal fold.** Derive it
|
|
6289
|
-
through §8.49's
|
|
6860
|
+
through §8.49's package-internal `derive_claimed_prefix` helper. If this plan is claimed and a claimed
|
|
6290
6861
|
successor exists, delegate immediately to trigger-scoped synchronization (§8.52); sync owns the
|
|
6291
6862
|
journal routing for this arm, including unresolved SYNC/ADOPT and pending continuations. Never
|
|
6292
6863
|
route on `published_prefix_len`: operational drift may declassify a claimed successor and must
|
|
@@ -6388,7 +6959,18 @@ before) and prepare FRESH in the same invocation. Anything mixed/unrelated →
|
|
|
6388
6959
|
`stack_registration_failed`, `postcondition_unverified`, `publication_drift`, `git_error`,
|
|
6389
6960
|
`github_error` — plus the §8.46 layer codes passed through verbatim (`parent_missing`,
|
|
6390
6961
|
`parent_unverified`, `stacked_predecessor_missing`): honest self-describing preparation
|
|
6391
|
-
failures, deliberately not folded into a vaguer code.
|
|
6962
|
+
failures, deliberately not folded into a vaguer code. It remains package-internal and is
|
|
6963
|
+
translated once to `(phase=layer, origin=domain)`; aggregate infrastructure and bound-status/sync
|
|
6964
|
+
failures follow §8.44's contextual table.
|
|
6965
|
+
|
|
6966
|
+
**Result and caller boundary.** Direct publication returns the backend's real identity
|
|
6967
|
+
`PlanHeaderUpdate`; direct convergence returns an exact empty synthetic update. Cascade returns the
|
|
6968
|
+
bound `SyncResult` directly and performs only the explicit-trigger `impl_run_ids` merge-write (or an
|
|
6969
|
+
empty update without a trigger). `PublishResult.Layer` carries those facts plus embed/body-check,
|
|
6970
|
+
stack, checkpoint, resume/no-op, and parent-target facts. Submit's local mergeability probe targets
|
|
6971
|
+
the returned parent branch and published head; Linear PR-opened emission is suppressed exactly when
|
|
6972
|
+
`cascade` is present. JSON and human bytes remain unchanged because serialization supplies the fixed
|
|
6973
|
+
operation `kind="sync"` around the nested result.
|
|
6392
6974
|
|
|
6393
6975
|
**Surface changes.** The github tier gains the `stack` state key; the submit stage reads
|
|
6394
6976
|
`[cache.plan-ref, github.plan, github.objective, github.stack]` and writes
|
|
@@ -6510,21 +7092,23 @@ evidence report).
|
|
|
6510
7092
|
|
|
6511
7093
|
## §8.49 · Published-suffix synchronization (the sync operation + `perk objective stack sync`)
|
|
6512
7094
|
|
|
6513
|
-
**The operation** is `
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
7095
|
+
**The operation** is the public `Delivery.sync(SyncRequest(...), consent=...)` façade over the
|
|
7096
|
+
private `perk.delivery.sync` transactional engine: change a published stacked layer — or re-anchor
|
|
7097
|
+
the whole train onto an advanced objective base (`--base`) — and move every published successor
|
|
7098
|
+
with it as one transaction. `Delivery.sync` uses one documented local import, binds its three
|
|
7099
|
+
aggregate authorities plus `self.status`, and reads the immutable private `_DEFAULT_SYNC_RUNTIME`
|
|
7100
|
+
at invocation time. The runtime owns only worktree-root configuration, the operation lock,
|
|
7101
|
+
continuation-manifest/containment/path helpers, clock, sleep, and operation-id minting; no
|
|
7102
|
+
persistence/Git/GitHub behavior or public constructor seam lives there. Tests replace the whole
|
|
7103
|
+
frozen runtime symbol in a scoped monkeypatch, never mutate it.
|
|
6520
7104
|
|
|
6521
7105
|
**The operation universe is the checkpoint-claimed prefix — never `published_prefix_len`.**
|
|
6522
7106
|
The train classifier truncates its verified prefix on exactly the discrepancies sync exists
|
|
6523
7107
|
to diagnose (publication drift, membership divergence), which would make the drift refusals
|
|
6524
7108
|
unreachable — a drifted bottom layer would read as a false no-op; a drifted upper layer would
|
|
6525
7109
|
silently shrink a lower-layer cascade. `published_prefix_len` stays a status fact only. The
|
|
6526
|
-
|
|
6527
|
-
publish routing. The claimed prefix: the maximal contiguous run, from the bottom of delivery order, of layers
|
|
7110
|
+
package-internal `derive_claimed_prefix(train)` helper remains the single derivation consumed by
|
|
7111
|
+
sync, publish routing, transfer, and deferred recovery. The claimed prefix: the maximal contiguous run, from the bottom of delivery order, of layers
|
|
6528
7112
|
carrying plan identity, a branch, a PR number, and the FULL checkpoint pair — **starting
|
|
6529
7113
|
above the bottom-contiguous LANDED run** (§8.44): landed layers are terminal, never claimed,
|
|
6530
7114
|
so a partially-landed train's remainder cascades over the advanced base (`claimed[0]` expects
|
|
@@ -6535,9 +7119,11 @@ deletion/retarget). Malformed claims
|
|
|
6535
7119
|
a LANDED layer above a non-landed claimed layer)
|
|
6536
7120
|
are the typed refusal `claimed_prefix_malformed`.
|
|
6537
7121
|
|
|
6538
|
-
**The operation lock.** Every
|
|
6539
|
-
|
|
6540
|
-
|
|
7122
|
+
**The operation lock.** Every `Delivery.sync` request (cascade, continue, or abort) acquires the
|
|
7123
|
+
private runtime's operation lock exactly ONCE around dispatch; recover (§8.51), transfer (§8.53),
|
|
7124
|
+
and land (§8.56) use the same lock in their own dispatches (land's is runtime-bound inside
|
|
7125
|
+
`Delivery.land`'s objective mutation arm; its dry-run preview is lock-free). It is a machine-local non-blocking
|
|
7126
|
+
`flock` at
|
|
6541
7127
|
the main checkout (`.perk/workflow/stack-operation.lock`,
|
|
6542
7128
|
`perk/delivery/oplock.py::stack_operation_lock`); a busy lock is the typed refusal
|
|
6543
7129
|
`operation_in_progress` (never a wait — concurrent invocations are an operator error to
|
|
@@ -6559,7 +7145,9 @@ Post-push arms never need the temp refs (an applied push holds the candidates re
|
|
|
6559
7145
|
unapplied push's resume arm abandons and recomputes fresh). Orphaned (process-killed,
|
|
6560
7146
|
manifest-less) `sync-*` residue is inert until `recover`'s orphan sweep (§8.51) collects it.
|
|
6561
7147
|
|
|
6562
|
-
1. **Reconstruct fresh**
|
|
7148
|
+
1. **Reconstruct fresh** through the context's bound `Delivery.status(StatusRequest(...))`; every
|
|
7149
|
+
fresh/re-entry reconstruction uses that same service and authority instances. A successful
|
|
7150
|
+
no-train result / no lineage → `not_stacked`; a status `DeliveryError` propagates unchanged.
|
|
6563
7151
|
2. **Continuation gate**: any manifest for this lineage → `sync_conflict_pending` (the
|
|
6564
7152
|
message names the manifest path and the retained worktree; clearing is manual until the
|
|
6565
7153
|
continue/abort surface). An unparseable manifest is treated as PRESENT — fail closed,
|
|
@@ -6578,7 +7166,8 @@ manifest-less) `sync-*` residue is inert until `recover`'s orphan sweep (§8.51)
|
|
|
6578
7166
|
`missing_lineage`; that member is unreachable here because step 1 already classifies a
|
|
6579
7167
|
lineage-less train as `not_stacked`, but the shared set is context-free for §8.52 consumers.
|
|
6580
7168
|
5. **Preflight every claimed layer** (all refusals before any candidate work): remote head ==
|
|
6581
|
-
the `published_head_sha` checkpoint → else `remote_drift` (
|
|
7169
|
+
the `published_head_sha` checkpoint → else `remote_drift` (the `--adopt` arm below
|
|
7170
|
+
re-anchors exactly one such layer); a
|
|
6582
7171
|
fresh strict PR-facts read per layer — OPEN, base == the expected predecessor branch (the
|
|
6583
7172
|
objective base for the bottom layer), head == the checkpoint → else `pr_drift`; native
|
|
6584
7173
|
membership exactly the claimed PRs (`not_applicable` below two) → else `membership_drift`;
|
|
@@ -6602,11 +7191,12 @@ manifest-less) `sync-*` residue is inert until `recover`'s orphan sweep (§8.51)
|
|
|
6602
7191
|
locally-changed head that lacks it → `stale_parent` (the actionable rebase-first arm); an
|
|
6603
7192
|
UNCHANGED claimed layer whose published head lacks it → `claimed_prefix_malformed`
|
|
6604
7193
|
(an internally inconsistent stored pair — broken stored state).
|
|
6605
|
-
7. **Capability**:
|
|
6606
|
-
ONE receiving repository — no pretended
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
7194
|
+
7. **Capability**: the Git authority resolves configured push URLs; >1 URL →
|
|
7195
|
+
`multiple_push_urls` (`--atomic` is atomic within ONE receiving repository — no pretended
|
|
7196
|
+
distributed atomicity). The same authority runs the no-op atomic probe against the sole URL,
|
|
7197
|
+
pinned to the bottom affected layer's branch at its verified remote head; private capability
|
|
7198
|
+
formatters preserve §8.45's caveat strings. Failure is `atomic_push_unsupported`. This
|
|
7199
|
+
discharges §8.47's recorded deviation without a public probe helper.
|
|
6610
7200
|
8. **Candidate calculation** in ONE isolated worktree (`<worktree_root>/sync-<operation_id>`;
|
|
6611
7201
|
temp refs `refs/perk/sync/<operation_id>/<branch>`; the freshly minted operation ULID
|
|
6612
7202
|
names all residue). Bottom-up over the affected set: source = the local head when locally
|
|
@@ -6620,7 +7210,7 @@ manifest-less) `sync-*` residue is inert until `recover`'s orphan sweep (§8.51)
|
|
|
6620
7210
|
A manifest WRITE failure keeps the guard armed — residue is cleaned, nothing is retained
|
|
6621
7211
|
— and still classifies as `rebase_conflict` inside the typed boundary (the message says
|
|
6622
7212
|
retention failed and why).
|
|
6623
|
-
9. **Approval gate**: the ordered `
|
|
7213
|
+
9. **Approval gate**: the ordered `SyncResult.Cascade` (per-ref before→after, node ids, PR numbers,
|
|
6624
7214
|
base facts) → the `approve` callback (`None` = auto-approve). Declined → the guard
|
|
6625
7215
|
cleans; the declined result returns — no journal record, nothing mutated.
|
|
6626
7216
|
10. **Post-approval re-observation** (closing the arbitrary-pause race before the journal
|
|
@@ -6636,15 +7226,18 @@ manifest-less) `sync-*` residue is inert until `recover`'s orphan sweep (§8.51)
|
|
|
6636
7226
|
`after: {branches: [{ref, sha}], prs: [{number, head_sha, base}], base_parent:
|
|
6637
7227
|
sha|null}` — the candidates; PR bases unchanged by construction (sync moves heads,
|
|
6638
7228
|
never branch names).
|
|
6639
|
-
12. **
|
|
7229
|
+
12. **Zero or one atomic push.** Build the update set by excluding every affected ref whose
|
|
7230
|
+
candidate equals its observed before SHA. An empty set (including checkpoint-only adoption)
|
|
7231
|
+
issues **zero pushes**; otherwise `git.push_atomic_with_leases` issues ONE
|
|
6640
7232
|
`push --atomic --porcelain --no-verify --no-signed --no-follow-tags
|
|
6641
|
-
--recurse-submodules=no` to `origin` with `-c push.pushOption=` cleared, each ref
|
|
6642
|
-
`--force-with-lease=refs/heads/<branch>:<exact before sha>` (never an absence lease —
|
|
7233
|
+
--recurse-submodules=no` to `origin` with `-c push.pushOption=` cleared, each included ref
|
|
7234
|
+
under `--force-with-lease=refs/heads/<branch>:<exact before sha>` (never an absence lease —
|
|
6643
7235
|
sync never pushes creations). A rejection → refetch and classify: all-at-before →
|
|
6644
7236
|
append `abandoned` (observed = the all-before proof) + `push_rejected` (retry = rerun
|
|
6645
7237
|
sync); an unreadable refetch → `postcondition_unverified` (unresolved); mixed →
|
|
6646
7238
|
`sync_drift` (unresolved, fail closed). Individual refs are NEVER retried.
|
|
6647
|
-
13. **Verify postconditions**: refetch every affected branch
|
|
7239
|
+
13. **Verify postconditions**: refetch **every affected branch**, including refs excluded as
|
|
7240
|
+
no-op updates — head == candidate (else
|
|
6648
7241
|
`sync_drift`); PR facts through the bounded settle poll (up to five observations through
|
|
6649
7242
|
the injectable observe/sleep seam — GitHub's PR-head propagation lags a push) before a
|
|
6650
7243
|
mismatch classifies as `pr_drift`; an unreadable read → `postcondition_unverified`;
|
|
@@ -6656,10 +7249,12 @@ manifest-less) `sync-*` residue is inert until `recover`'s orphan sweep (§8.51)
|
|
|
6656
7249
|
verified branches + PR heads). A crash between the checkpoint writes and completion
|
|
6657
7250
|
reconstructs as roll-forward — merge-writes + idempotent byte-identical appends.
|
|
6658
7251
|
|
|
6659
|
-
**`--dry-run` (the
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
push,
|
|
7252
|
+
**`--dry-run` (the pre-consent preview).** Runs the full protocol up to the approval boundary —
|
|
7253
|
+
preflight, capability, candidate calculation in the isolated worktree — then stops and returns
|
|
7254
|
+
the would-be cascade as the `dry_run: true` result arm: **no consent call, journal record, remote
|
|
7255
|
+
push, checkpoint write, or continuation manifest**. Candidate calculation can create local temp
|
|
7256
|
+
refs/worktree residue; cleanup is best-effort, each failure is a loud result note, and noted residue
|
|
7257
|
+
is valid orphan-sweep input — “dry run” never falsely promises no local side effects.
|
|
6663
7258
|
The conflict arm is retention-free: a dry-run rebase conflict writes NO manifest (the guard
|
|
6664
7259
|
stays armed, residue is cleaned) and classifies as `rebase_conflict` with a message naming the
|
|
6665
7260
|
dry run. A pending unresolved operation still routes per step 3 — but the unresolved-kind
|
|
@@ -6825,7 +7420,10 @@ operation simply never happened. Like `--continue`, no cascade flags compose.
|
|
|
6825
7420
|
**The result arms** (`SyncResult`; invariant: `operation_id` non-null ⟺ a prepared record was
|
|
6826
7421
|
journaled by, or resumed by, this invocation). Identity fields (`objective_id`,
|
|
6827
7422
|
`objective_url`, `redirected_from`) ride the result so the CLI never re-reconstructs;
|
|
6828
|
-
`base_advanced` is the §8.44 status notice, independent of `base_cascaded`.
|
|
7423
|
+
`base_advanced` is the §8.44 status notice, independent of `base_cascaded`. The moved frozen data
|
|
7424
|
+
is `SyncResult` plus nested `Layer`, `Cascade`, and `AbortPreview`; only `SyncRequest` and
|
|
7425
|
+
`SyncResult` are package-root sync-family exports. No `SyncResult.__post_init__` combination matrix
|
|
7426
|
+
is added: the table below and operation protocol remain the authority for additive reachable arms.
|
|
6829
7427
|
|
|
6830
7428
|
| arm | operation_id | abandoned_operation_id | no_op | declined | resumed | base_cascaded | affected |
|
|
6831
7429
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
@@ -6849,39 +7447,38 @@ non-refusal notes, e.g. failed cleanup or manifest retirement). The JSON envelop
|
|
|
6849
7447
|
`notes` verbatim; both the cold human renderer and the warm TypeScript tools render every
|
|
6850
7448
|
one, so machine-routed success can never hide leftover residue.
|
|
6851
7449
|
|
|
6852
|
-
**The error vocabulary is bounded.** `SyncError
|
|
6853
|
-
|
|
6854
|
-
`
|
|
6855
|
-
`
|
|
6856
|
-
`
|
|
6857
|
-
`
|
|
6858
|
-
`
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
`
|
|
6862
|
-
own boundary codes (`confirmation_required`, `journal_corruption`, `no_objective`,
|
|
6863
|
-
`not_a_repo`) — the full envelope vocabulary is the union of those layers.
|
|
7450
|
+
**The error vocabulary is bounded.** Private `SyncError` is a named `DeliveryError` subclass
|
|
7451
|
+
retained for recover/transfer compatibility; it accepts the same façade-wide bounded vocabulary.
|
|
7452
|
+
`Delivery.sync` preserves any `DeliveryError` unchanged and maps only expected boundary failures:
|
|
7453
|
+
raw `GitError` → `git_error`; `GitHubError` and backend/store/persistence failures →
|
|
7454
|
+
`github_error`; an allowed `TrainReconstructionError` code/message passes through;
|
|
7455
|
+
`JournalCorruptionError` → `journal_corruption`; `JournalRecordTooLarge` →
|
|
7456
|
+
`journal_record_too_large` with the exact cap detail (an outcome-append failure may leave the
|
|
7457
|
+
prepared operation unresolved); and private config failure → `invalid_config`. Unexpected
|
|
7458
|
+
exceptions propagate. The command adds only its separately-owned `confirmation_required`,
|
|
7459
|
+
`no_objective`, and `not_a_repo` arms.
|
|
6864
7460
|
|
|
6865
7461
|
**The cold worker.** `perk objective stack sync [OBJECTIVE] [--base] [--dry-run]
|
|
6866
7462
|
[--adopt NODE] [--continue] [--abort] [--run-id RUN_ID] [--yes] [--json]`
|
|
6867
7463
|
(`commands/objective/stack/sync_cmd.py`). The control-flag matrix is validated FIRST as
|
|
6868
7464
|
typed `invalid_input`: `--continue`/`--abort` are mutually exclusive with each other and
|
|
6869
7465
|
with every cascade flag; `--adopt` × `--base` is refused; `--adopt`/`--base` × `--dry-run`
|
|
6870
|
-
compose.
|
|
6871
|
-
`
|
|
7466
|
+
compose. After repo resolution the command keeps its existing eager validation-only
|
|
7467
|
+
`require_config(ctx)` call (same `invalid_input` precedence and malformed-TOML wording), then
|
|
7468
|
+
objective resolution mirrors `status`'s exactly. It resolves one zero-I/O `Delivery`, builds one
|
|
7469
|
+
`SyncRequest`, and calls `Delivery.sync` exactly once. Cascade `run_id` resolves `--run-id` → the **ACTIVE** objective header's
|
|
6872
7470
|
`run_id` (the fallback follows `superseded_by` forward, the same walk the reconstruction
|
|
6873
7471
|
performs — syncing through a superseded objective never journals the predecessor's run
|
|
6874
|
-
identity), both absent → `invalid_input`.
|
|
6875
|
-
|
|
6876
|
-
(queued + in-progress, one call each —
|
|
6877
|
-
never be displaced off a newest-first page by completed runs; the existing 100-cap
|
|
6878
|
-
*simultaneously active* runs) and matches plan ids via the managed run-name convention; any
|
|
6879
|
-
listing failure
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
skipped because its already-committed work is the trigger; every other active writer still blocks.
|
|
7472
|
+
identity), both absent → `invalid_input`. Continue/abort set no cascade fields and never resolve a
|
|
7473
|
+
run id. `RepoDeliveryGitHub.active_writer_plan_ids` owns the production observation. It queries the
|
|
7474
|
+
gateway run listing with a **server-side status filter** (queued + in-progress, one call each —
|
|
7475
|
+
active runs can never be displaced off a newest-first page by completed runs; the existing 100-cap
|
|
7476
|
+
bounds *simultaneously active* runs) and matches plan ids via the managed run-name convention; any
|
|
7477
|
+
listing failure becomes `WriterObservationError` → `writer_observation_unavailable`. Explicit sync
|
|
7478
|
+
passes no trigger context. Automatic submit supplies raw `(trigger_plan_id, trigger_run_id)`; the
|
|
7479
|
+
adapter excludes a writer only after `PERK_RUN_ID`, a consumed implement/address handoff, and the
|
|
7480
|
+
active plan-ref corroborate that exact pair. Neither field excludes alone; uncorroborated ids
|
|
7481
|
+
exclude nothing. Only the exact run+plan pair is skipped; every other active writer still blocks.
|
|
6885
7482
|
Confirmation: the `approve` callback renders the cascade to **stderr** and confirms via
|
|
6886
7483
|
`click.confirm(..., err=True)` — interactive `--json` never contaminates stdout; `--yes`
|
|
6887
7484
|
auto-approves; non-interactive without `--yes` → the typed `confirmation_required` refusal
|
|
@@ -6889,19 +7486,17 @@ auto-approves; non-interactive without `--yes` → the typed `confirmation_requi
|
|
|
6889
7486
|
`--abort` gets its own confirmation render (the preview: operation id, conflict node,
|
|
6890
7487
|
retained worktree, and — on the uncontained/unparseable arms — that ONLY the manifest file
|
|
6891
7488
|
will be deleted) under the same `--yes`/non-interactive discipline; `--dry-run` needs no
|
|
6892
|
-
confirmation (it stops before the approval boundary); `--continue`
|
|
6893
|
-
`
|
|
7489
|
+
confirmation (it stops before the approval boundary); `--continue` uses
|
|
7490
|
+
`SyncRequest(mode="continue")` and journals under the manifest's captured run identity (`--run-id`
|
|
6894
7491
|
is ignored). The `--json` envelope `ObjectiveStackSyncOut` (snapshotted at
|
|
6895
7492
|
`shared/schemas/outputs/objective-stack-sync.schema.json`), declaration order pinned:
|
|
6896
7493
|
`{success, objective{id,url,redirected_from}, operation_id|null,
|
|
6897
7494
|
abandoned_operation_id|null, no_op, declined, resumed, base_cascaded, base_advanced,
|
|
6898
|
-
affected: [{node_id, plan_id, branch, pr_number, before_sha, after_sha}], dry_run,
|
|
7495
|
+
affected: [{node_id, plan_id, branch, pr_number, before_sha, after_sha}], notes:[str], dry_run,
|
|
6899
7496
|
adopted_node|null, continued, aborted}` (the last four are the additive control-surface
|
|
6900
|
-
growth); failures use the
|
|
6901
|
-
`
|
|
6902
|
-
|
|
6903
|
-
same way (the stack-status convention), and a corrupt journal read fails as
|
|
6904
|
-
`journal_corruption`. Exit
|
|
7497
|
+
growth); failures use the `{success, error_type, message}` fail shape with one caught
|
|
7498
|
+
`DeliveryError`'s code/message verbatim; command-owned `UserFacingCliError` still covers
|
|
7499
|
+
flag/config/confirmation/no-objective validation. Exit
|
|
6905
7500
|
discipline: 0 = success (incl. no-op, declined, dry-run, continued, aborted), 1 = typed
|
|
6906
7501
|
operation failures, 2 = not-a-repo. The explicit command remains the owner of base advancement,
|
|
6907
7502
|
adoption, conflict continuation, abort, and preview; ordinary submit/address propagation delegates
|
|
@@ -7037,13 +7632,68 @@ exits 2.
|
|
|
7037
7632
|
|
|
7038
7633
|
## §8.51 · Stack recovery (`perk objective stack recover`) + the warm stack surface
|
|
7039
7634
|
|
|
7040
|
-
**The operation** is `
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7635
|
+
**The operation** is `Delivery.recover(RecoverRequest(...), consent=...)` behind the
|
|
7636
|
+
repository-scoped delivery façade. The closed request family is a strict TWO-kind
|
|
7637
|
+
discriminator: `operation_conclusion` (conclude-only recovery) and `cancellation_metadata`
|
|
7638
|
+
(the §8.54 metadata repair), each with the flat fields `{objective_id, action, dry_run,
|
|
7639
|
+
operation_id}`. For `operation_conclusion`, `action ∈ {report, abandon, accept_prefix}`
|
|
7640
|
+
replaces the old pair of service booleans, and a supplied operation id is carried verbatim
|
|
7641
|
+
(including `""`) so target selection remains the authority and returns `operation_not_found`
|
|
7642
|
+
for a nonmatch. `cancellation_metadata` accepts only a nonblank `objective_id` plus optional
|
|
7643
|
+
`dry_run`: an acting action or ANY operation id (even `""`) is rejected at construction, and
|
|
7644
|
+
a non-`None` consent callback is rejected with `ValueError` before dispatch or authority
|
|
7645
|
+
access — the variant has no operation target, no generic action verb, and no confirmation
|
|
7646
|
+
boundary.
|
|
7647
|
+
|
|
7648
|
+
The frozen `RecoverResult` is the matching strict wrapper: `kind` plus exactly the one
|
|
7649
|
+
detail matching it (`operation_conclusion: OperationConclusion | None`,
|
|
7650
|
+
`cancellation_metadata: CancellationMetadata | None` — one kind↔detail constructor guard,
|
|
7651
|
+
no forwarding properties, no cross-variant "must stay empty" matrix). Nested
|
|
7652
|
+
`OperationConclusion` carries the complete operation report (`objective_id, objective_url,
|
|
7653
|
+
redirected_from, dry_run, selection_required, operations, swept_worktrees, swept_refs,
|
|
7654
|
+
sweep_failures, sweep_skipped, landed_layers, objective_closed, reconcile_evidence, notes`)
|
|
7655
|
+
over the existing nested `Operation`, `MergedPrefix`, `RemainderPr`, `LandedLayer`,
|
|
7656
|
+
`SweepFailure`, `AbandonPreview`, and `AcceptPrefixPreview` records, with additive
|
|
7657
|
+
operation-produced combinations and no new guards; its `reconcile_evidence` annotation stays
|
|
7658
|
+
deferred/type-only to avoid a façade↔landing import cycle. Nested `CancellationMetadata`
|
|
7659
|
+
carries `{objective_id, actions, failed, aborted, dry_run, unavailable}` over
|
|
7660
|
+
`CancellationAction{code, node_id, outcome, error}` — the §8.54 repair pass without exposing
|
|
7661
|
+
the internal diagnostics vocabulary (`failed` stays separate from `actions`).
|
|
7662
|
+
|
|
7663
|
+
**The `cancellation_metadata` lifecycle** is pinned against operation-conclusion
|
|
7664
|
+
**machinery**: dispatched before worktree-root/config resolution and before the operation
|
|
7665
|
+
lock, it resolves no worktree config, takes no stack-operation lock, appends no journal
|
|
7666
|
+
event and writes no checkpoint, classifies/concludes no operation, asks for no consent, runs
|
|
7667
|
+
no finalization/convergence/close, and sweeps no residue. Read-only train reconstruction is
|
|
7668
|
+
explicitly retained and required — it IS the repair's fresh safety proof, and it inherently
|
|
7669
|
+
reads the journal fold, runs `git fetch`, and observes branches/PRs/stack membership through
|
|
7670
|
+
the façade's reconstruction bridge; the only mutation is the conditional attachment write
|
|
7671
|
+
through the §8.54 writer capability. A backend whose persistence authority answers no writer
|
|
7672
|
+
is a successful empty pass before any reconstruction. `perk objective stack recover` remains
|
|
7673
|
+
an operation-conclusion-only command — the repair's sole production caller is doctor's
|
|
7674
|
+
`--fix` (§8.54).
|
|
7675
|
+
|
|
7676
|
+
The operation-conclusion variant classifies every unresolved stack operation against fresh
|
|
7677
|
+
authority, concludes the
|
|
7678
|
+
one selected target (deterministic roll-forward, a consented abandon-with-proof, or a consented
|
|
7679
|
+
accept-prefix breach), runs the LAND finalization-convergence pass, then sweeps orphaned
|
|
7680
|
+
machine-local sync residue. `consent` receives either preview type; `None` auto-approves an
|
|
7681
|
+
explicitly requested library action, while the cold command always supplies its
|
|
7682
|
+
interactive/headless callback. Retry is never recover's verb — the report's detail names the owning command (`stack
|
|
7683
|
+
sync`, `/submit`, `stack land`). Runs under the shared operation lock (§8.49); `--dry-run` reports
|
|
7684
|
+
everything and mutates nothing.
|
|
7685
|
+
|
|
7686
|
+
The private engine receives one `_RecoverContext` bound to the same three aggregate authorities as
|
|
7687
|
+
all other façade operations plus `_RecoverRuntime` for worktree-root loading (the existing sync
|
|
7688
|
+
config helper), lock, continuation/on-disk enumeration, per-layer finalization, sleep, and clock.
|
|
7689
|
+
The aggregate growth is exact: persistence adds `close_objective`; Git adds
|
|
7690
|
+
`worktree_admin_paths`; GitHub adds `merge_async_probe` and `merged_evidence`. Every other
|
|
7691
|
+
journal/objective/plan/ref/PR/stack effect reuses an existing authority method. Runtime and
|
|
7692
|
+
aggregate adapters are package-internal; no repo path, config, backend, lock, clock, factory, or
|
|
7693
|
+
probe callback crosses `RecoverRequest`. Config resolves before one lock acquisition, and that
|
|
7694
|
+
single non-reentrant lock stays held through classification, consent, from-scratch
|
|
7695
|
+
reclassification, conclusion/convergence, result metadata reads, and the final sweep (the
|
|
7696
|
+
cancellation variant branches away before both, above).
|
|
7047
7697
|
|
|
7048
7698
|
**The phased protocol.** (0) **Fold-first TRANSFER routing (§8.53)**: read the REQUESTED
|
|
7049
7699
|
objective's succession journal before any train gate — a sole unresolved TRANSFER dispatches
|
|
@@ -7056,10 +7706,16 @@ recorded manifest + the `run_id` successor lookup: successor found + corroborate
|
|
|
7056
7706
|
supersedes/lineage corroboration) → `all_after`, rolled forward automatically through
|
|
7057
7707
|
`transfer.roll_forward_transfer` under the same held lock; absent → `all_before`, abandonable
|
|
7058
7708
|
with the `successor_absent` proof under `--abandon` (confirmed + re-classified); an
|
|
7059
|
-
undecodable manifest or a corroboration mismatch → a report-only `mixed` row.
|
|
7709
|
+
undecodable manifest or a corroboration mismatch → a report-only `mixed` row. `accept_prefix` is
|
|
7710
|
+
LAND-only: fold-first TRANSFER rejects it as `accept_blocked` before successor classification,
|
|
7711
|
+
mutation, finalization, or sweep, including the otherwise-automatic all-after arm. Hints name the
|
|
7060
7712
|
predecessor id (the documented recovery entry for an interrupted transfer is
|
|
7061
|
-
`recover <predecessor-id>`)
|
|
7062
|
-
|
|
7713
|
+
`recover <predecessor-id>`). Recovery binds `TransferSeams` from the same aggregate persistence
|
|
7714
|
+
authority plus the façade's cause-aware reconstruction bridge; `TransferError` remains a
|
|
7715
|
+
package-internal `DeliveryError` subtype and passes through unchanged. After the selected TRANSFER
|
|
7716
|
+
report/conclusion, the result objective URL/state is read before `_sweep`; a typed metadata-read
|
|
7717
|
+
failure therefore leaves every orphan untouched and cleanup is truly the final authority/effect
|
|
7718
|
+
phase.
|
|
7063
7719
|
(0b) **Fold-first sole-PUBLISH routing (§8.54)**: when the ACTIVE train's fold — read after
|
|
7064
7720
|
reconstruction (which follows supersession forward), the SAME snapshot the classifier
|
|
7065
7721
|
consumes; the requested fold walks predecessors only, so a successor-recorded PUBLISH is
|
|
@@ -7226,7 +7882,7 @@ still classified), except under `--abandon`/`--accept-prefix`, where acting ambi
|
|
|
7226
7882
|
LAND rolls forward automatically through `landing.roll_forward_land` (above); `all_after`
|
|
7227
7883
|
PUBLISH reports (its roll-forward already lives in `/submit`'s own resume — the report says
|
|
7228
7884
|
so). `--abandon` requires `all_before` (else `abandon_blocked`), renders the
|
|
7229
|
-
`AbandonPreview` through the `
|
|
7885
|
+
`AbandonPreview` through the union `consent` callback, and **re-classifies after confirmation**
|
|
7230
7886
|
(the human may pause arbitrarily long on the prompt; a post-confirmation observation change
|
|
7231
7887
|
is `abandon_blocked`, nothing journaled) before appending the `abandoned` outcome (observed
|
|
7232
7888
|
= the all-before proof; for LAND, reason `recovered_before_state` + the reobserved rows).
|
|
@@ -7254,12 +7910,14 @@ are collected as explicit
|
|
|
7254
7910
|
`sweep_failures: [{target, error}]`, never silent, never aborting the remaining sweep.
|
|
7255
7911
|
Typed refusals never sweep (the sweep runs only after a successful conclude/report phase).
|
|
7256
7912
|
|
|
7257
|
-
**Errors.**
|
|
7258
|
-
`
|
|
7259
|
-
`not_stacked`,
|
|
7260
|
-
`invalid_input
|
|
7261
|
-
|
|
7262
|
-
|
|
7913
|
+
**Errors.** Recovery uses the single bounded `DeliveryError`; its recovery-specific additions are
|
|
7914
|
+
`operation_ambiguous`, `operation_not_found`, `abandon_blocked`, `accept_blocked`, and
|
|
7915
|
+
`unsupported_operation_kind`, while it reuses `operation_in_progress`, `not_stacked`,
|
|
7916
|
+
`invalid_input`, the sync/transfer tail codes, and journal/config/infra codes already in the façade
|
|
7917
|
+
vocabulary. Existing `DeliveryError`s pass unchanged. The façade boundary translates allowed
|
|
7918
|
+
reconstruction codes, journal corruption/oversize, raw Git, GitHub/backend/persistence, and sync
|
|
7919
|
+
config failures; unexpected programming/filesystem exceptions propagate. The cold worker catches
|
|
7920
|
+
that one error family without changing code/message.
|
|
7263
7921
|
|
|
7264
7922
|
**The cold worker.** `perk objective stack recover [OBJECTIVE] [--dry-run]
|
|
7265
7923
|
[--operation ULID] [--abandon] [--accept-prefix] [--yes] [--json]`
|
|
@@ -7268,7 +7926,11 @@ conclude-only recovery needs no run identity. `--dry-run` × `--abandon`/`--acce
|
|
|
7268
7926
|
and `--abandon` × `--accept-prefix` are `invalid_input`
|
|
7269
7927
|
(preview first, then act; one conclusion per invocation). Both confirmations follow sync's
|
|
7270
7928
|
discipline (stderr
|
|
7271
|
-
render, `--yes` auto-approve, non-interactive without `--yes` → `confirmation_required`).
|
|
7929
|
+
render, `--yes` auto-approve, non-interactive without `--yes` → `confirmation_required`). After
|
|
7930
|
+
flag-first validation and the retained eager validation-only config read, the command resolves one
|
|
7931
|
+
Delivery, maps the booleans to one closed action, constructs one `RecoverRequest`, calls
|
|
7932
|
+
`Delivery.recover` once with the union consent callback, and renders the existing DTO. It catches
|
|
7933
|
+
one `DeliveryError`; no low-level recovery/Git/GitHub/backend error ladder remains.
|
|
7272
7934
|
Envelope `ObjectiveStackRecoverOut` (snapshotted at
|
|
7273
7935
|
`shared/schemas/outputs/objective-stack-recover.schema.json`), declaration order pinned:
|
|
7274
7936
|
`{success, objective{id,url,redirected_from}, dry_run, selection_required, operations:
|
|
@@ -7282,7 +7944,9 @@ objective_closed, reconcile_evidence|null (§8.56's shape), notes[]}` with `clas
|
|
|
7282
7944
|
{reported, rolled_forward,
|
|
7283
7945
|
abandoned, accepted_prefix, declined}`; the LAND fields are trailing additive growth
|
|
7284
7946
|
(`merged_layers`/`remainder` are the external-prefix structured preview, dry-run included —
|
|
7285
|
-
empty on other rows);
|
|
7947
|
+
empty on other rows); the DTO serializes the unwrapped `OperationConclusion` detail — the
|
|
7948
|
+
wrapper discriminator never reaches the envelope; under `dry_run` the swept
|
|
7949
|
+
lists carry the WOULD-BE targets. The human
|
|
7286
7950
|
render prints the landed rows, the close line, and the copyable `/objective-reconcile <id>`
|
|
7287
7951
|
hint on close-with-evidence. Exit
|
|
7288
7952
|
discipline: 0 = successful classification/report/no-op/actions (including declined and
|
|
@@ -7343,16 +8007,19 @@ since §8.54, the non-recoverable cancellation/checkpoint-topology/journal-histo
|
|
|
7343
8007
|
`checkpoint_after_abandoned_publish`); only the two PENDING codes (`publish_outcome_pending`,
|
|
7344
8008
|
`canceled_publication_pending`) are excluded — a live unresolved PUBLISH concludes via
|
|
7345
8009
|
recover / the owning `/submit` (§8.51's sole-PUBLISH route), never as identity corruption.
|
|
7346
|
-
Sync's
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
**Automatic lower-layer submit.** After reconstructing,
|
|
7353
|
-
*before* reading publish's journal fold. A claimed plan with a claimed successor
|
|
7354
|
-
|
|
7355
|
-
|
|
8010
|
+
Sync's structural gate, supervisor veto classification, and reviewability consume this same
|
|
8011
|
+
public set; no caller maintains a context-specific copy. Package-internal `ClaimedLayer` facts plus
|
|
8012
|
+
`derive_claimed_prefix(train) -> tuple[ClaimedLayer, ...]` remain the one
|
|
8013
|
+
checkpoint-claimed-universe contract shared by publish routing, sync mutation, transfer, and
|
|
8014
|
+
recovery; they are no longer package-root API.
|
|
8015
|
+
|
|
8016
|
+
**Automatic lower-layer submit.** After reconstructing, the bound Publish context derives the
|
|
8017
|
+
claimed prefix *before* reading publish's journal fold. A claimed plan with a claimed successor
|
|
8018
|
+
calls the same façade instance's `sync(SyncRequest(mode="cascade", objective_id=<objective>,
|
|
8019
|
+
run_id=<resolved journal id>, trigger_plan_id=<plan>, trigger_run_id=<raw invoking id>),
|
|
8020
|
+
consent=None)`; the submit gesture is the consent, so there is no second prompt and no
|
|
8021
|
+
warm/headless split. Publish takes no operation lock; the nested sync dispatcher takes its ordinary
|
|
8022
|
+
lock exactly once, so a non-reentrant lock is never self-entered. Sync therefore owns unresolved-operation
|
|
7356
8023
|
and pending-continuation routing for this arm. `published_prefix_len` never selects it: a successor
|
|
7357
8024
|
declassified by PR/membership/remote drift remains checkpoint-claimed and reaches sync's typed
|
|
7358
8025
|
preflight instead of turning a lower submit into a top republish. The automatic path never includes
|
|
@@ -7377,16 +8044,15 @@ null on no-op), with a note naming the concluded id: `concluded unresolved opera
|
|
|
7377
8044
|
(roll-forward) before cascading`. A completed older operation can therefore never report that it
|
|
7378
8045
|
published a newer trigger head.
|
|
7379
8046
|
|
|
7380
|
-
**Writer exclusion and result contract.**
|
|
7381
|
-
`
|
|
7382
|
-
|
|
7383
|
-
implement/address handoff, and this
|
|
7384
|
-
|
|
7385
|
-
excludes nothing, and every other active writer (including
|
|
7386
|
-
Explicit sync passes no
|
|
7387
|
-
`
|
|
7388
|
-
|
|
7389
|
-
non-cascade publish/republish/converge arms keep `operation=None`. Publish reconstructs after sync
|
|
8047
|
+
**Writer exclusion and result contract.** `RepoDeliveryGitHub` owns the private
|
|
8048
|
+
`_corroborated_remote_run_id` proof and active-writer observation. Automatic submit passes the raw
|
|
8049
|
+
caller id as trigger context, distinct from the separately resolved journal run id; the adapter
|
|
8050
|
+
excludes only when inherited `PERK_RUN_ID`, a consumed implement/address handoff, and this
|
|
8051
|
+
worktree's active plan-ref corroborate the exact `(run_id, plan_id)` pair. Neither field excludes
|
|
8052
|
+
alone, an arbitrary/header-derived id excludes nothing, and every other active writer (including
|
|
8053
|
+
one on the same plan) still blocks. Explicit sync passes no trigger context. Cascade success returns the exact frozen `SyncResult` in
|
|
8054
|
+
`PublishResult.Layer.cascade`; there is no copied operation record. Non-cascade
|
|
8055
|
+
publish/republish/converge arms keep `cascade=None`. Publish reconstructs after sync
|
|
7390
8056
|
so a roll-forward-then-fresh-no-op cannot return the pre-roll-forward checkpoint snapshot. The
|
|
7391
8057
|
target layer's PR is fetched from that fresh projection-correlated number, and its published
|
|
7392
8058
|
checkpoint must agree with the affected after-row (or the fresh checkpoint on a sync no-op);
|
|
@@ -7397,10 +8063,12 @@ affected:[{node_id, plan_id, branch, pr_number, before_sha, after_sha}], notes:[
|
|
|
7397
8063
|
`operation_id` remains the compatibility alias and carries the sync id. The warm submit decoder
|
|
7398
8064
|
reduces a valid block to `{kind, operation_id, no_op, affected_count, notes}`, drops the whole block
|
|
7399
8065
|
when malformed without sinking submit, renders cascade/no-op suffixes, and reports every note.
|
|
7400
|
-
`
|
|
7401
|
-
`--run-id` merges into `impl_run_ids`; a header-derived run id is not newly stamped, and
|
|
7402
|
-
already-existing PR emits no duplicate Linear PR-opened event.
|
|
7403
|
-
|
|
8066
|
+
`DeliveryError.error_type` passes through the submit fail envelope verbatim. On cascades an
|
|
8067
|
+
explicit `--run-id` merges into `impl_run_ids`; a header-derived run id is not newly stamped, and
|
|
8068
|
+
the already-existing PR emits no duplicate Linear PR-opened event. Stacked submit preserves its
|
|
8069
|
+
eager config validation and `invalid_config` envelope but passes no worktree root or writer probe;
|
|
8070
|
+
the private runtime may read config again only when sync candidate/continuation work needs it.
|
|
8071
|
+
Incremental submit remains independent of config.
|
|
7404
8072
|
|
|
7405
8073
|
**`finalize_address` is the only model-facing address finalizer.** Parameters remain
|
|
7406
8074
|
`{threads:[{thread_id, comment?}], pr?, counts?}`. After the parent commits its own fixes, the tool
|
|
@@ -7439,24 +8107,31 @@ fetch feedback. `objective run` renders the additive `repair_required` action wi
|
|
|
7439
8107
|
copyable owning remedy, but never auto-runs sync/recover. Dry-run remains the existing offline graph
|
|
7440
8108
|
classification.
|
|
7441
8109
|
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
8110
|
+
**`/ready` delegates selected intent to Publish.** Selection remains command policy. Explicit
|
|
8111
|
+
input is parse-normalized even offline and a real run uses `select_plan(main_repo_root(...))` with
|
|
8112
|
+
its one canonical read; the no-argument form reads the invoking checkout's `cache.plan-ref` and
|
|
8113
|
+
performs its existing one plan read there. The command never writes a selector. From that selected
|
|
8114
|
+
snapshot it derives only plan id, header-wins `delivery=stacked|incremental`, and the stacked
|
|
8115
|
+
objective id, then calls one `Delivery.publish(PublishRequest(kind="ready", ...))`. Dry-run passes
|
|
8116
|
+
only kind/id/dry-run and returns through the façade before backend/config/credential/GitHub calls.
|
|
8117
|
+
|
|
8118
|
+
Incremental ready derives canonical `plan-<id>` internally and uses the all-state
|
|
8119
|
+
`DeliveryGitHub.pr_for_branch`: absence is `no_pr`; any returned draft is sent to `mark_pr_ready`
|
|
8120
|
+
regardless of OPEN/CLOSED/MERGED and a gateway rejection remains `github_error/(ready,github)`;
|
|
8121
|
+
any non-draft is the idempotent already-ready result regardless of state. This intentionally
|
|
8122
|
+
preserves the pre-migration all-state edge rather than adding an OPEN gate.
|
|
8123
|
+
|
|
8124
|
+
Stacked ready calls bound status once, derives the target, and fetches the full `PullRequest` from
|
|
8125
|
+
the projection-correlated number before any gate/mutation (`no_pr` on absent number/object).
|
|
8126
|
+
`require_reviewable_layer(train, plan_id, mutating=false)` first requires a known target whose
|
|
8127
|
+
publication axis is exactly `PUBLISHED`; failure is `layer_not_published` with axes/findings and
|
|
8128
|
+
therefore precedes interpretation of a fresh close race. A fetched non-OPEN PR is then
|
|
8129
|
+
`pr_not_open`. Only an OPEN draft enters `mutating=true`, which additionally requires no unresolved
|
|
7445
8130
|
operation (`unresolved_operation`) and no train-wide structural blocker (`structural_blockers`,
|
|
7446
|
-
including `missing_lineage`)
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
The worker keeps dry-run offline and first, then reads the plan and applies submit's header-wins
|
|
7451
|
-
lineage discriminator. Incremental behavior is unchanged. Stacked ready reconstructs the train,
|
|
7452
|
-
locates the layer, and fetches the full `PullRequest` from the projection-correlated number before
|
|
7453
|
-
any gate/mutation (`no_pr` on absent number/object). The validation-only target gate then preserves
|
|
7454
|
-
`layer_not_published` for a projection-classified merged/closed/wrong-base or otherwise drifted
|
|
7455
|
-
target. Only when the projection still says `PUBLISHED` does the fetched state apply: a non-OPEN PR
|
|
7456
|
-
(the post-reconstruction close race) refuses as `pr_not_open`; an OPEN draft enters the mutating gate
|
|
7457
|
-
then `mark_pr_ready`; OPEN already-ready succeeds without a write after the validation-only gate.
|
|
7458
|
-
`LayerError`/`TrainReconstructionError` codes pass through; backend/persistence failures map to
|
|
7459
|
-
`github_error`. The fetched PR supplies the unchanged output envelope's number and URL.
|
|
8131
|
+
including `missing_lineage`), before `mark_pr_ready`. OPEN already-ready succeeds without a write
|
|
8132
|
+
after the validation-only gate, even when a later global veto exists. Operational drift on
|
|
8133
|
+
unrelated layers does not block review. Publish returns the fetched PR plus its original
|
|
8134
|
+
`was_draft`; contextual errors map back to the unchanged ready envelope and bytes.
|
|
7460
8135
|
|
|
7461
8136
|
**Status.** Ordinary `/submit` and `/address` now converge published suffixes automatically;
|
|
7462
8137
|
explicit sync remains the owner of base advancement, adoption, continuation/abort, preview, and
|
|
@@ -7465,19 +8140,21 @@ Atomic landing and ordinary-land stacked refusal remain separate later contracts
|
|
|
7465
8140
|
|
|
7466
8141
|
## §8.53 · Objective replan transfer (stacked supersession — the convergence protocol)
|
|
7467
8142
|
|
|
7468
|
-
**
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
8143
|
+
**The public boundary and D1 routing matrix.** `objective create --supersedes` submits one frozen
|
|
8144
|
+
intent-only `TransferRequest` to `Delivery.transfer`; no predecessor snapshot, policy, provider,
|
|
8145
|
+
store, probe, callback, clock, lock, or factory crosses the boundary. Transfer validates the
|
|
8146
|
+
roadmap/dependency/carry shape before any authority call, canonicalizes the predecessor id, then
|
|
8147
|
+
acquires the shared operation lock. While holding it, the façade performs exactly ONE fail-closed
|
|
8148
|
+
authoritative predecessor read/classification — `get_objective(old)` →
|
|
8149
|
+
`objective.delivery_policy(header)`: not-found → `objective_not_found`; a classifier `ValueError`
|
|
8150
|
+
→ `invalid_delivery_policy`; an infra failure fails the save — and completes the selected
|
|
8151
|
+
mutation without a second read. Routing keys on the **authoritative policy classifier**, never on
|
|
8152
|
+
lineage presence:
|
|
7476
8153
|
|
|
7477
8154
|
| predecessor → successor | path |
|
|
7478
8155
|
| --- | --- |
|
|
7479
|
-
| incremental → incremental | the plain §8.32 store mutation
|
|
7480
|
-
| stacked → any | the full **journaled** transfer protocol
|
|
8156
|
+
| incremental → incremental | the plain §8.32 store mutation under the same lock; no journal/Git/GitHub/status work |
|
|
8157
|
+
| stacked → any | the full **journaled** transfer protocol behind `Delivery.transfer` |
|
|
7481
8158
|
| incremental → stacked | the transfer orchestration **minus the journal** (the §8.43 append gate requires stored-lineage equality and the predecessor stores none); interruption tolerance is by-construction — run_id-keyed convergent creation + idempotent merge-writes + close-last. Residual: cross-session abandonment of this arm is not journal-discoverable (drift-diagnostic territory). |
|
|
7482
8159
|
|
|
7483
8160
|
A stacked-policy predecessor with a missing/blank/junk `delivery_lineage` refuses fail-closed
|
|
@@ -7486,12 +8163,14 @@ A stacked-policy predecessor with a missing/blank/junk `delivery_lineage` refuse
|
|
|
7486
8163
|
the completed TRANSFER) stays readable via the predecessor id. `--dry-run` stays offline (the
|
|
7487
8164
|
transfer never engages).
|
|
7488
8165
|
|
|
7489
|
-
**The protocol** (lock
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
`
|
|
7493
|
-
|
|
7494
|
-
the
|
|
8166
|
+
**The protocol** (the lock is acquired before D1, journal fold, planning, and probes, and held
|
|
8167
|
+
through completion): fold → rerun routing → plan → **prepare → create → stamp → verify → finalize
|
|
8168
|
+
→ complete**. Fresh dispatch is private `perk/delivery/transfer.py` machinery reached only through
|
|
8169
|
+
`Delivery.transfer`; its `_FreshTransfer` carries `TransferSeams`, aggregate Git/GitHub
|
|
8170
|
+
authorities, and the explicitly typed carry-normalization callable required only by fresh
|
|
8171
|
+
dispatch. `_TransferRuntime` is the whole private clock/id/lock test seam. There is no
|
|
8172
|
+
`run_transfer` compatibility entrypoint. `roll_forward_transfer(seams, record)` remains the
|
|
8173
|
+
lock-ASSUMED conclusion core shared by same-run rerun and recover's all-after arm.
|
|
7495
8174
|
|
|
7496
8175
|
**Planning (the preflight, split by predecessor policy — D13).** A stacked predecessor:
|
|
7497
8176
|
reconstruct the train, `refuse_structural_blockers`, `derive_claimed_prefix` (**"published" for
|
|
@@ -7513,10 +8192,10 @@ detail, **nothing written when planning raises**:
|
|
|
7513
8192
|
the K claimed plans **in exact order, each exactly once, none dropped** → `prefix_mismatch`
|
|
7514
8193
|
(also: a duplicate carry, or a cited plan that does not exist on the predecessor). A node's
|
|
7515
8194
|
carried plan identity is its `carry_map` entry (Linear — the plan IS the node-issue) else its
|
|
7516
|
-
`pr` backlink (GitHub), bare-normalized. The
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
(ownership writes the NEW node id).
|
|
8195
|
+
`pr` backlink (GitHub), bare-normalized. The persistence authority normalizes raw request
|
|
8196
|
+
carries behind the façade: Linear preserves insertion order; GitHub returns an empty map because
|
|
8197
|
+
its store contract ignores `adopt_issue`. No caller or transfer core reads a provider id. Node
|
|
8198
|
+
ids/descriptions may change freely (ownership writes the NEW node id).
|
|
7520
8199
|
- **Suffix reshaping + the open-PR guards**: below the prefix, reshaping is arbitrary — except
|
|
7521
8200
|
every predecessor plan with an OPEN PR is **mandatory-carry** (dropping one → `dropped_open_pr`
|
|
7522
8201
|
until the PR closes), and a policy-**changing** replan (stacked↔incremental, either direction)
|
|
@@ -7567,16 +8246,17 @@ fingerprint (target project + node-id-prefixed title + clean description + phase
|
|
|
7567
8246
|
label); if its later objective-node attachment write was interrupted, the found-arm resumes the
|
|
7568
8247
|
unique matching issue, refuses ambiguity/conflict, and only mints when no match exists. Each write
|
|
7569
8248
|
is idempotent. Stamp: per carried plan, derived from the
|
|
7570
|
-
manifest alone — claimed-prefix plans
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
null when the layer below is unplanned);
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
effects)
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
8249
|
+
manifest alone — claimed-prefix plans receive one generic grouped `update_plan_header` ownership
|
|
8250
|
+
write (`objective_id` + NEW `objective_node_id`); carried-unpublished plans under a stacked
|
|
8251
|
+
successor additionally receive one grouped identity write (`delivery_lineage` + the
|
|
8252
|
+
successor-delivery-order `predecessor_plan_id`, explicit null when the layer below is unplanned);
|
|
8253
|
+
carried plans under an incremental successor instead receive the four stacked fields as explicit
|
|
8254
|
+
nulls in ONE generic write. Every group is skipped when stored values already match (idempotent
|
|
8255
|
+
rerun, no duplicate header effects); the former three transfer-only persistence wrappers do not
|
|
8256
|
+
exist. Verify (**before finalize**; failure → `transfer_unverified`, journal unresolved,
|
|
8257
|
+
predecessor open, no auto-abandon): a stacked successor requires a fresh train reconstruction
|
|
8258
|
+
through the seams' bound `reconstruct` (the façade's cause-preserving bridge) whose full
|
|
8259
|
+
`(node_id, plan_id|null)` projection equals the recorded manifest projection exactly (a never-materialized carried node fails here), zero structural
|
|
7580
8260
|
blockers, and `derive_claimed_prefix` equal to `before.claimed_prefix` (plan ids, branches,
|
|
7581
8261
|
checkpoint pairs, order); an incremental successor verifies by direct reads (roadmap rows match
|
|
7582
8262
|
the projection; every carried plan re-reads with the successor ownership pair and all four
|
|
@@ -7599,26 +8279,37 @@ stamped by THIS run's successor re-finalizes idempotently and returns success (t
|
|
|
7599
8279
|
interrupted-finalize tail and the idempotent re-save); any other stamp → `objective_not_open`.
|
|
7600
8280
|
The incremental→stacked arm's lineage resolution is likewise rerun-convergent: a same-run
|
|
7601
8281
|
successor's stored lineage wins over copy-or-mint (a fresh mint mid-convergence would fork the
|
|
7602
|
-
train identity). `perk objective stack recover` owns cross-session conclusion
|
|
7603
|
-
arm).
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
8282
|
+
train identity). `perk objective stack recover` owns cross-session conclusion through
|
|
8283
|
+
`Delivery.recover(RecoverRequest(kind="operation_conclusion", ...))` (§8.51's TRANSFER arm).
|
|
8284
|
+
That arm binds `TransferSeams` directly from the façade's aggregate persistence and cause-aware
|
|
8285
|
+
status bridge—never from a caller factory—and rejects the LAND-only `accept_prefix` action before
|
|
8286
|
+
successor observation or any effect.
|
|
8287
|
+
|
|
8288
|
+
**The door posture** (`objective replan`, §8.32). The door calls
|
|
8289
|
+
`Delivery.prepare(PrepareRequest(kind="replan", objective_id=...))` once. Prepare reads the
|
|
8290
|
+
objective once, classifies policy fail-closed and, for a stacked predecessor, refuses on any
|
|
8291
|
+
unresolved journal operation (TRANSFER → `transfer_incomplete` + the recover hint; other kinds →
|
|
8292
|
+
`unresolved_operation`), calls bound `Delivery.status`, applies the same structural
|
|
8293
|
+
identity/topology blocker gate as save, and returns only the facts needed to render a
|
|
8294
|
+
`<stacked_delivery_facts>` scratch block: the
|
|
7610
8295
|
claimed-prefix MUST-carry listing (exact order), the mandatory-carry open-PR plans, and the
|
|
7611
8296
|
immutability facts. The seed's delivery re-ask is **pre-publication only** (§8.45): a published
|
|
7612
8297
|
predecessor's seed instructs `delivery: stacked` without re-asking.
|
|
7613
8298
|
|
|
7614
|
-
**Errors.** `TransferError
|
|
7615
|
-
`
|
|
7616
|
-
`
|
|
7617
|
-
`
|
|
7618
|
-
`
|
|
7619
|
-
`
|
|
7620
|
-
|
|
7621
|
-
|
|
8299
|
+
**Errors.** Package-internal `TransferError` subclasses `DeliveryError` for recovery reuse;
|
|
8300
|
+
`Delivery.transfer` is the sole fresh boundary. Its bounded domain codes are
|
|
8301
|
+
{`policy_immutable`, `base_immutable`, `prefix_mismatch`, `dropped_open_pr`, `pr_exists`,
|
|
8302
|
+
`missing_lineage`, `transfer_incomplete`, `transfer_unverified`, `transfer_manifest_oversize`,
|
|
8303
|
+
`unresolved_operation`, `dirty_worktree`, `active_writer`, `writer_observation_unavailable`,
|
|
8304
|
+
`claimed_prefix_malformed`, `operation_in_progress`, `objective_not_found`,
|
|
8305
|
+
`objective_not_open`, `invalid_delivery_policy`, `invalid_roadmap`, `supersede_unsupported`,
|
|
8306
|
+
`invalid_input`}. Existing Delivery/Transfer errors pass unchanged; reconstruction keeps its
|
|
8307
|
+
bounded code/message; journal corruption → `journal_corruption`; raw Git → `git_error`; raw
|
|
8308
|
+
GitHub/issue/train-persistence → `github_error`; objective-store failure → `github_error` with
|
|
8309
|
+
`objective create failed\n…`. A cause-aware bound-status bridge restores the original
|
|
8310
|
+
reconstruction/store/issue/persistence exception to the shared core, preserving domain versus
|
|
8311
|
+
infrastructure verification behavior. Unexpected programming errors propagate. The CLI consumes
|
|
8312
|
+
one `DeliveryError` envelope; any prepared failure remains unresolved and recoverable.
|
|
7622
8313
|
|
|
7623
8314
|
**Residuals (flagged).** The **D14 Linear creation window**: Linear cannot make its first write
|
|
7624
8315
|
run-id-discoverable (discovery IS the sentinel header attachment), so a crash inside the
|
|
@@ -7627,11 +8318,15 @@ sentinel ⇒ invisible to `find_objective`/journal/train, and no predecessor-tou
|
|
|
7627
8318
|
happened (carried moves run only after the sentinel — the pinned ordering invariant), so the
|
|
7628
8319
|
rerun's all-before proof stays safe; re-creation may strand the residue project (inherited from
|
|
7629
8320
|
the plain create path; GitHub has no such window — its issue POST carries the run-id header
|
|
7630
|
-
atomically). This is the ONLY accepted Linear materialization window
|
|
7631
|
-
fresh node issue whose create succeeded before its attachment is
|
|
7632
|
-
create-time fingerprint described above. The non-journaled
|
|
7633
|
-
|
|
7634
|
-
|
|
8321
|
+
atomically). This is the ONLY accepted Linear materialization window for the journaled route:
|
|
8322
|
+
after the sentinel, even a fresh node issue whose create succeeded before its attachment is
|
|
8323
|
+
recoverable through the atomic create-time fingerprint described above. The non-journaled
|
|
8324
|
+
incremental→stacked arm's cross-session abandonment is not journal-discoverable. In particular,
|
|
8325
|
+
real Linear process death after a carried node MOVE but before plan ownership/finalization is
|
|
8326
|
+
**not proven** by this refactor: no durable operation record binds a later run to the preflighted
|
|
8327
|
+
request. Designing that recovery posture is a separately reviewed behavior change, not a
|
|
8328
|
+
permissive inference from partial state. An oversize manifest refuses rather than truncating. The
|
|
8329
|
+
journaled Linear transfer path is fake-proven; live proof belongs to the Linear smoke gate.
|
|
7635
8330
|
|
|
7636
8331
|
## §8.54 · Native cancellation projection + unified train drift diagnostics
|
|
7637
8332
|
|
|
@@ -7720,6 +8415,17 @@ Protocol (declared in `diagnostics.py`, implemented only by `LinearProjectObject
|
|
|
7720
8415
|
`write_node_cancellation_status`) is the ONE narrow train repair: a conditional, ATTACHMENT-ONLY
|
|
7721
8416
|
compare-and-write (`expected_status`/`new_status`, `require_native_canceled: bool|None`,
|
|
7722
8417
|
`require_no_raw_publish_claims`, `dry_run`) returning `APPLIED | ALREADY_CONVERGED | STALE`.
|
|
8418
|
+
Production ownership lives behind `Delivery.recover(RecoverRequest(kind=
|
|
8419
|
+
"cancellation_metadata", objective_id, dry_run))` (§8.51): the persistence authority exposes
|
|
8420
|
+
the writer through the optional capability `DeliveryPersistence.
|
|
8421
|
+
native_cancellation_metadata_writer()` — a concrete default-`None` method (the
|
|
8422
|
+
unsupported-backend posture) with a quoted type-only annotation, overridden only by the lazy
|
|
8423
|
+
production adapter (which returns the resolved objective store exactly when it structurally
|
|
8424
|
+
satisfies the Protocol, through its one aligned resolution, with no extra objective read and
|
|
8425
|
+
failed-resolution non-caching) and the owned fake. `None` is a successful empty pass before
|
|
8426
|
+
any reconstruction; only the Recover engine consumes the Protocol and
|
|
8427
|
+
`repair_projected_cancellations` in production — the Protocol is never a package-root export
|
|
8428
|
+
or a fourth aggregate authority.
|
|
7723
8429
|
The writer performs a FRESH state-bearing read at the effect boundary, compares the attachment
|
|
7724
8430
|
status, requires native canceled for the forward write, rechecks raw PR/checkpoint claims, and
|
|
7725
8431
|
upserts ONLY the `objective-node` attachment — never the generic status update, never a
|
|
@@ -7739,15 +8445,33 @@ conditional validation with no write/compensation. This is not distributed atomi
|
|
|
7739
8445
|
prevents stale snapshots from writing and compensates observed drift. Doctor never repairs plan
|
|
7740
8446
|
identity, checkpoints, journal history, branches, PRs, or native stack membership.
|
|
7741
8447
|
|
|
7742
|
-
**The two-part doctor.**
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
`
|
|
7749
|
-
|
|
7750
|
-
|
|
8448
|
+
**The two-part doctor.** Both report diagnosis and the train repair are façade consumers. One
|
|
8449
|
+
zero-I/O `Delivery`
|
|
8450
|
+
is constructed per command and reused for initial diagnosis, post-manifest re-diagnosis, the
|
|
8451
|
+
cancellation Recover pass, and the
|
|
8452
|
+
final remaining-findings read. Each diagnosis calls
|
|
8453
|
+
`Delivery.status(StatusRequest(objective_id=active_id))`; its bounded `DeliveryError` becomes the
|
|
8454
|
+
modeled `unavailable` state with the same error type/message, so a routine plan/journal/store
|
|
8455
|
+
outage is never an escape. The cancellation repair's effect-boundary proof lives inside the
|
|
8456
|
+
Recover engine: a private reconstruction closure over the façade's cause-aware bridge, pinned
|
|
8457
|
+
to the request objective on every call, normalizing expected
|
|
8458
|
+
issue/objective/train-persistence read failures to
|
|
8459
|
+
`TrainReconstructionError(error_type="github_error")` so the repair core answers its modeled
|
|
8460
|
+
unavailable arm; no public read API is reintroduced. Doctor itself is a thin request/result
|
|
8461
|
+
mapper: for a currently stacked diagnosis it constructs exactly
|
|
8462
|
+
`RecoverRequest(kind="cancellation_metadata", objective_id=active_id, dry_run=dry_run)`,
|
|
8463
|
+
calls the shared `Delivery.recover` once with no consent, and maps the strict
|
|
8464
|
+
`CancellationMetadata` detail into `_TrainFixOut`; a bounded `DeliveryError` raised before a
|
|
8465
|
+
modeled detail exists (e.g. lazy persistence capability resolution failed) is treated as an
|
|
8466
|
+
unavailable/aborted repair pass — the final diagnosis still runs and the assembled report
|
|
8467
|
+
keeps `success: true` with the exit-1 posture. Current incremental/unavailable diagnoses
|
|
8468
|
+
short-circuit before any Recover call, exactly as before.
|
|
8469
|
+
|
|
8470
|
+
`perk objective doctor` resolves the requested objective through `train.resolve_active_objective`
|
|
8471
|
+
ONCE — manifest detection/repair and train reconstruction/repair all target the ACTIVE id
|
|
8472
|
+
(`objective` reports it; additive `redirected_from` preserves the requested id; a predecessor is
|
|
8473
|
+
never mutated by `doctor OLD --fix`). The report is two parts: the existing Linear manifest drift
|
|
8474
|
+
plus the exact `DeliveryTrain` findings on every backend, each annotated with the diagnosis policy
|
|
7751
8475
|
(`TrainFindingOut: code, severity, node_id, plan_id, message, repairable, remediation`).
|
|
7752
8476
|
`TrainDiagnosisOut` (field order load-bearing): `state: stacked|incremental|unavailable,
|
|
7753
8477
|
objective_id, redirected_from, error_type, message, blockers[], information[]` — stacked
|
|
@@ -7764,12 +8488,31 @@ repair aborted first — no train action, initial diagnosis remains), `unavailab
|
|
|
7764
8488
|
records the verification failure)). Sequence: initial manifest/train reports → existing
|
|
7765
8489
|
manifest repair → reconstruct if the manifest changed → per-action fresh conditional repair →
|
|
7766
8490
|
final diagnosis in `remaining`. Top-level payload order stays
|
|
7767
|
-
`success,error_type,objective,drift,fix` then appends `redirected_from,train,train_fix
|
|
7768
|
-
(without `--fix`, `train_fix` is null). An assembled report keeps `success:
|
|
7769
|
-
the EXIT code conveys unavailability/aborted repair: detect-with-findings 0;
|
|
7770
|
-
1; manifest-abort 1; current-train-unavailable 1; write/verification abort
|
|
7771
|
-
(report-only drift remaining) 0; not-a-repo the fail envelope 2;
|
|
7772
|
-
failure the fail envelope 1.
|
|
8491
|
+
`success,error_type,objective,drift,fix` then appends `redirected_from,train,train_fix,
|
|
8492
|
+
corruption` (without `--fix`, `train_fix` is null). An assembled report keeps `success:
|
|
8493
|
+
true`/null error; the EXIT code conveys unavailability/aborted repair: detect-with-findings 0;
|
|
8494
|
+
detect-unavailable 1; manifest-abort 1; current-train-unavailable 1; write/verification abort
|
|
8495
|
+
1; fix-succeeded (report-only drift remaining) 0; not-a-repo the fail envelope 2;
|
|
8496
|
+
active-resolution/store failure the fail envelope 1.
|
|
8497
|
+
|
|
8498
|
+
**The both-headers corruption signature (report-only).** A third check rides every doctor
|
|
8499
|
+
report: `corruption: [_CorruptionFindingOut{code, carrier, message, remediation}]` (appended
|
|
8500
|
+
last — additive). The check resolves the ACTIVE objective's **issue-tier carrier** via the
|
|
8501
|
+
§8.43 `journal_carrier_id` (GitHub → the objective issue; Linear project store → the metadata
|
|
8502
|
+
**sentinel** issue's identifier — so a sentinel bearing both attachments is detected) and reads
|
|
8503
|
+
it **presence-only** via `IssueBackend.read_issue` (never `get_plan`, whose header-`pr` chase
|
|
8504
|
+
could abort the whole report on a PR-lookup infra failure). A carrier whose read shows
|
|
8505
|
+
`already_plan AND already_objective` yields exactly one `both_headers` finding (its `carrier`
|
|
8506
|
+
field = the resolved carrier id); a healthy or unresolvable carrier yields `[]`. Cost: up to
|
|
8507
|
+
two bounded reads per report. Semantics: **report-only** (`--fix` never touches it — no repair
|
|
8508
|
+
code path exists), **direction-neutral** (the signature cannot prove which header is the
|
|
8509
|
+
stray one; remediation is provenance inspection — issue history, each header's `run_id` — with
|
|
8510
|
+
manual removal, or supersession via `perk objective replan <active>` when the objective side is
|
|
8511
|
+
live), **active-objective-targeted** (a superseded predecessor is redirected away — the
|
|
8512
|
+
supersession IS the worked remediation), **exit-0** (a detected finding is still a clean
|
|
8513
|
+
report), and the human render prints the `Corruption:` part only when detected (clean runs'
|
|
8514
|
+
output is byte-unchanged). An `IssueBackendError` from the check fails the report as
|
|
8515
|
+
`github_error` (the assembly boundary's posture).
|
|
7773
8516
|
|
|
7774
8517
|
**Compatibility.** No provenance ⇒ byte-existing behavior; the stack-status JSON shape is
|
|
7775
8518
|
unchanged (new findings and `intent: canceled` are values inside existing string fields);
|
|
@@ -7795,10 +8538,14 @@ wires *this* module and `sync.py` imports `observe`); it owns its observation vi
|
|
|
7795
8538
|
`LandObservationError`) and never sees `perk.github` types. The shared remote-writer seam
|
|
7796
8539
|
(`RemoteWriterProbe` + `WriterObservationError`) lives in the leaf `perk/delivery/writers.py`
|
|
7797
8540
|
(moved from `sync.py`, which re-exports both names) so mutating and readiness preflights share
|
|
7798
|
-
one fail-closed observation contract. Production wiring:
|
|
7799
|
-
`observe.GatewayLandObservations(
|
|
7800
|
-
(`pr_land_facts` / `base_merge_rules` / `stack_capability`), wrapping
|
|
7801
|
-
`
|
|
8541
|
+
one fail-closed observation contract. Production wiring is aggregate-backed: the landing
|
|
8542
|
+
engine constructs `observe.GatewayLandObservations(github, base=…)` over the bound
|
|
8543
|
+
`DeliveryGitHub` (`pr_land_facts` / `base_merge_rules` / `stack_capability`), wrapping the
|
|
8544
|
+
aggregate's failures (`GitHubError` on the readiness read; the frozen merge-rules
|
|
8545
|
+
`ProbeError`, whose message is the same `str(exc)` bytes) into `LandObservationError` —
|
|
8546
|
+
except the capability bool, which passes through (below) — plus the fail-closed
|
|
8547
|
+
`observe._AggregateWriterProbe` over `DeliveryGitHub.active_writer_plan_ids` (exact
|
|
8548
|
+
forwarding, no trigger exclusions).
|
|
7802
8549
|
|
|
7803
8550
|
**Dispositions.** `READY | BLOCKED | NOTHING_TO_LAND`. READY iff ≥1 **non-landed** layer and ZERO blockers
|
|
7804
8551
|
(information never vetoes — advisory threads, failed optional checks, and a clean ACTIVE
|
|
@@ -7932,9 +8679,13 @@ GitHub's own `mergeStateStatus: BLOCKED` is the covering authority for that case
|
|
|
7932
8679
|
(`commands/objective/stack/land_cmd.py`; the group's land verb). **No remote mutation
|
|
7933
8680
|
anywhere in this section**: bare `land` (no `--dry-run`) is the §8.56 landing mutation on
|
|
7934
8681
|
this same argv shape (it replaced the historical `land_unimplemented` refusal).
|
|
7935
|
-
`--dry-run` resolves the objective (explicit arg → worktree plan-ref → `no_objective`)
|
|
7936
|
-
|
|
7937
|
-
|
|
8682
|
+
`--dry-run` resolves the objective (explicit arg → worktree plan-ref → `no_objective`) and
|
|
8683
|
+
makes exactly one `Delivery.land(LandRequest(kind="objective", objective_id, dry_run=True))`
|
|
8684
|
+
call — lock-free, consent-free, run-id-free; the engine reconstructs the train (a
|
|
8685
|
+
train-less objective is the typed `not_stacked` with the `Objective #N: <reason>` dry-run
|
|
8686
|
+
message shape, exit 1; reconstruction failures keep exactly `stack status`'s typed
|
|
8687
|
+
envelope mapping through the façade's land boundary), assesses, and returns the
|
|
8688
|
+
readiness-only detail the command maps onto the envelope. Envelope
|
|
7938
8689
|
(`ObjectiveStackLandOut`, snapshotted at
|
|
7939
8690
|
`shared/schemas/outputs/objective-stack-land.schema.json`; field order load-bearing):
|
|
7940
8691
|
`{success, error_type, objective{id,url,redirected_from}, dry_run, disposition, base,
|
|
@@ -7968,21 +8719,27 @@ train is only the `NOTHING_TO_LAND` disposition.
|
|
|
7968
8719
|
|
|
7969
8720
|
## §8.56 · Objective landing (the journaled atomic merge)
|
|
7970
8721
|
|
|
7971
|
-
**The operation.** `
|
|
7972
|
-
|
|
7973
|
-
`perk objective stack land` —
|
|
7974
|
-
|
|
7975
|
-
(`
|
|
7976
|
-
(`
|
|
7977
|
-
(
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
`
|
|
8722
|
+
**The operation.** `Delivery.land(LandRequest(kind="objective", objective_id, run_id,
|
|
8723
|
+
dry_run=False), consent=…)` is the landing **mutation** behind bare
|
|
8724
|
+
`perk objective stack land` — the façade-bound engine (`perk/delivery/landing.py`, no
|
|
8725
|
+
public entry) is a thin consumer of the §8.55 readiness projection
|
|
8726
|
+
(`assess_land_readiness`, consumed as-is, never re-derived) plus the §8.43 journal through
|
|
8727
|
+
the aggregate persistence (`append_prepared`/`append_outcome`), the per-layer finalize seam
|
|
8728
|
+
(`perk.delivery.finalize.finalize_landed_plan`, bound through the private landing runtime),
|
|
8729
|
+
and the machine-local operation lock (oplock scope grows to sync + recover + **land**; a
|
|
8730
|
+
busy lock is the typed `operation_in_progress`; acquired before reconstruction and held
|
|
8731
|
+
through consent, merge, verification, finalization, and close — the dry-run preview is
|
|
8732
|
+
lock-free). `consent` is the Land family's confirmation callback over the composed
|
|
8733
|
+
`LandReadiness` (`None` auto-approves); it fires for both the READY land plan and the
|
|
8734
|
+
NOTHING_TO_LAND completion preview, and the façade rejects a callback on the non-mutating
|
|
8735
|
+
shapes (`kind="plan"` and the objective dry-run) with `ValueError` — never
|
|
8736
|
+
accept-and-ignore. The request always carries a nonblank `run_id` on the mutation and never
|
|
8737
|
+
on the preview (construction-guarded). No extra merge modes, no queue emulation, no
|
|
8738
|
+
generalized landing abstraction. `land.py` stays the pure readiness core (its import
|
|
8739
|
+
direction forbids `observe`/gateway imports). The squash-message helper stays the pure
|
|
7983
8740
|
`landing.squash_commit_message(*, issue, url, backend_id, title)` (byte-identical to the
|
|
7984
8741
|
incremental `pr land` footer format — GitHub `Closes #N`, non-github `Plan: <id> — <url>`;
|
|
7985
|
-
|
|
8742
|
+
one implementation, no drift) — module-path internal, no root export.
|
|
7986
8743
|
|
|
7987
8744
|
**The wire contract (GitHub's stacked-PR merge API, public preview).** Submit:
|
|
7988
8745
|
`PUT /repos/{owner}/{repo}/pulls/{top}/merge-async` with EXACTLY
|
|
@@ -8005,14 +8762,29 @@ must land even where merge-async preview enrollment is absent). Gateway surface
|
|
|
8005
8762
|
(`perk/github/stacks.py`): `submit_merge_async` / `merge_pr_direct` are **total** (the
|
|
8006
8763
|
`--include` status + `Retry-After` classification; a spawn failure folds into the ambiguous
|
|
8007
8764
|
`status=None` arm; an unparseable 2xx/409 body leaves `state=None` — ambiguous, never a
|
|
8008
|
-
guessed success); `
|
|
8765
|
+
guessed success); `pr_merged_evidence` (per-PR
|
|
8009
8766
|
`state + baseRefName + headRefName + headRefOid + mergeCommit.oid` — the identity fields
|
|
8010
8767
|
the verification corroborates; a zero-exit reply carrying an explicitly-null PR node is the
|
|
8011
|
-
ordinary lookup miss, `None`)
|
|
8012
|
-
appended — junk raises, never degrades).
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
(
|
|
8768
|
+
ordinary lookup miss, `None`) is **strict** (it decides whether a journal outcome may be
|
|
8769
|
+
appended — junk raises, never degrades). The landing engine reaches every landing effect
|
|
8770
|
+
and observation through the aggregate `DeliveryGitHub`: `submit_merge_async` /
|
|
8771
|
+
`merge_pr_direct` (the two mutations above), the **total** `merge_async_probe` as the poll
|
|
8772
|
+
(the four live states pass through — `merged` carries `details.sha`; an `expired`/
|
|
8773
|
+
`unreadable` probe consumes the tick exactly like the historical tolerated per-tick
|
|
8774
|
+
strict-reader failure), and `merged_evidence` for the post-approval re-observation, per-PR
|
|
8775
|
+
verification, and abandon proof (`merge_commit_sha` is ignored pre-merge; the raw
|
|
8776
|
+
`GitHubError` posture and drift/unproven message bytes are unchanged).
|
|
8777
|
+
Operation-conclusion recovery reaches the same handle probe and strict merged evidence
|
|
8778
|
+
through `DeliveryGitHub.merge_async_probe` and
|
|
8779
|
+
`DeliveryGitHub.merged_evidence`; objective close likewise uses
|
|
8780
|
+
`DeliveryPersistence.close_objective`. Recovery binds LAND issue reads only to the existing
|
|
8781
|
+
`train.PlanReader` shape; the landing mutation additionally reads the provider identity
|
|
8782
|
+
through `DeliveryPersistence.backend_id()`. The package-internal
|
|
8783
|
+
per-layer `finalize_landed_plan(close_objective_on_complete=False)` calls are private
|
|
8784
|
+
`_RecoverRuntime`/`_LandingRuntime` machinery, not a fourth public authority or request field.
|
|
8785
|
+
|
|
8786
|
+
**The protocol, in order.** (1) The operation lock. (2) Reconstruct (the façade's
|
|
8787
|
+
cause-preserving train-reconstruction bridge); `NoDeliveryTrain` or a null
|
|
8016
8788
|
`delivery_lineage` → typed `not_stacked`. (3) Assess (§8.55). (4) **NOTHING_TO_LAND** →
|
|
8017
8789
|
`approve` with the completion preview ("nothing to merge; close objective #N") — declined ⇒
|
|
8018
8790
|
`outcome: declined`; approved ⇒ the **state-aware close** (`landing.state_aware_close`,
|
|
@@ -8023,12 +8795,16 @@ failure is a typed error, never fail-open) ⇒ `outcome: completed_without_merge
|
|
|
8023
8795
|
note). The approval pause is a race boundary: node terminality is REVALIDATED on the fresh
|
|
8024
8796
|
fetch — a node added/reopened during the pause ⇒ typed `land_drift`, nothing closed (a
|
|
8025
8797
|
stale NOTHING_TO_LAND snapshot never closes an incomplete objective). **No journal** (no remote train mutation to guard; the close is
|
|
8026
|
-
idempotent/convergent). (5) **BLOCKED** → the
|
|
8027
|
-
|
|
8798
|
+
idempotent/convergent). (5) **BLOCKED** → the **in-band refusal detail**: the mutation arm
|
|
8799
|
+
returns the readiness-only `LandResult.Objective` (`outcome: null`, the full composed
|
|
8800
|
+
readiness embedded) before consent — no exception, `DeliveryError` stays payload-free; the
|
|
8801
|
+
CLI maps it to its exit-1 `land_blocked` envelope (below). (6) **READY**: for `singleton_squash` the load-bearing pre-merge
|
|
8028
8802
|
`get_plan` read happens NOW (missing ⇒ typed `plan_not_found`; it supplies the squash
|
|
8029
8803
|
title/url + the tolerantly-parsed `consumed_learn`); then `approve(readiness)` — the
|
|
8030
8804
|
rendered land plan; declined ⇒ `outcome: declined`, nothing journaled. (7) **Re-observe**
|
|
8031
|
-
every layer PR after the arbitrary approval pause (
|
|
8805
|
+
every layer PR after the arbitrary approval pause (the strict aggregate
|
|
8806
|
+
`DeliveryGitHub.merged_evidence` — `PrMergedEvidence` carries every inspected identity
|
|
8807
|
+
fact, `merge_commit_sha` ignored pre-merge: OPEN, head ==
|
|
8032
8808
|
plan `head_sha`, base == expected base ref, head ref == branch); any mismatch/read failure →
|
|
8033
8809
|
typed `land_drift`, nothing journaled. (8) **Prepared** (journal-first, read back; the
|
|
8034
8810
|
one-unresolved gate and `JournalAppendAmbiguous` propagate typed). (9) **Submit** — classification
|
|
@@ -8055,10 +8831,11 @@ one identical retry, with the same preserved-ambiguity rule — only a `merged`
|
|
|
8055
8831
|
(including the already-merged idempotent arm, which recovers an applied-but-unconfirmed
|
|
8056
8832
|
first attempt) concludes it; anything else leaves `pending`. **No `accepted` event ever on
|
|
8057
8833
|
the singleton** — there is no handle. (10) **Poll** (async
|
|
8058
|
-
arm): up to 60 ticks, injected `sleep(1)`; `pending`
|
|
8834
|
+
arm): up to 60 ticks, injected `sleep(1)`, over the total `merge_async_probe`; `pending`
|
|
8835
|
+
continues; `merged` ⇒ verification (its `sha` is the journaled `reported_sha`);
|
|
8059
8836
|
`failed` ⇒ abandon-with-proof then `land_failed`; `enqueued` ⇒ stop immediately,
|
|
8060
|
-
`outcome: unexpected_enqueued` (unresolved);
|
|
8061
|
-
|
|
8837
|
+
`outcome: unexpected_enqueued` (unresolved); an `expired`/`unreadable` probe consumes the
|
|
8838
|
+
tick (the historical tolerated per-tick read failure); exhaustion ⇒ `outcome: pending`. (11) **Abandon-with-proof** (terminal
|
|
8062
8839
|
non-application only): every layer PR re-observed OPEN at its exact expected head ⇒ append
|
|
8063
8840
|
`abandoned` and let the typed failure propagate (retry is legal — the operation is
|
|
8064
8841
|
resolved); ANY contradiction or read failure ⇒ NO outcome append, `outcome: pending` (never
|
|
@@ -8135,24 +8912,44 @@ pre-merge objects reachable) / Git objects.
|
|
|
8135
8912
|
`unexpected_enqueued` mean the LAND operation stays **unresolved** — never success, never
|
|
8136
8913
|
failure (§8.51's `stack recover` concludes it once the merge settles or expires — the
|
|
8137
8914
|
recorded operation identity, the journaled `accepted` UUID handle or the prepared `mode`,
|
|
8138
|
-
is the recovery probe's input).
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
`
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
`
|
|
8915
|
+
is the recovery probe's input). Failures are the bounded `DeliveryError` vocabulary with
|
|
8916
|
+
`phase="land"` (exit 1). Engine refusals/protocol classifications — `not_stacked` (both
|
|
8917
|
+
message shapes preserved: the dry-run's `Objective #N: <reason>` and the mutation's
|
|
8918
|
+
`objective N has no delivery train (<reason>)`), `plan_not_found`, `land_drift`,
|
|
8919
|
+
`merge_request_conflict`, `merge_async_unavailable`, `land_failed`,
|
|
8920
|
+
`operation_in_progress` — carry `origin="domain"`. The boundary origin rule:
|
|
8921
|
+
`train.TrainReconstructionError` passes its code through when it is a known delivery code
|
|
8922
|
+
(else normalizes to `github_error`), with origin derived from the final code (`git_error` →
|
|
8923
|
+
`git`; `github_error`, including the unknown-code fallback → `github`; every other
|
|
8924
|
+
recognized code → `domain`); `GitHubError`/`IssueBackendError`/`ObjectiveStoreError`/
|
|
8925
|
+
`TrainPersistenceError` (including `JournalAppendAmbiguous`) → `github_error`/`github`;
|
|
8926
|
+
`JournalCorruptionError` → `journal_corruption`/`delivery`; a defensive `GitError` →
|
|
8927
|
+
`git_error`/`git` (the engine makes no Git authority calls). `JournalRecordTooLarge` is
|
|
8928
|
+
deliberately **not** translated — an oversize append propagates as the unexpected
|
|
8929
|
+
programming error it always was (both phases; a typed mapping would be a behavior change
|
|
8930
|
+
and a façade-only translation would break invariant 20 post-verification). A consent
|
|
8931
|
+
callback raising (the CLI's typed `confirmation_required` refusal) propagates
|
|
8932
|
+
untranslated. Exit 2 = not-a-repo. `land_blocked` is a **CLI-authored envelope code** (like
|
|
8933
|
+
`confirmation_required`/`no_objective`): the CLI maps the in-band BLOCKED detail to the
|
|
8934
|
+
exit-1 fail envelope carrying the verbatim message
|
|
8935
|
+
`objective <id> is not ready to land: <"[code] message"-joined blockers or 'blocked'>`,
|
|
8936
|
+
renders the full readiness report to stderr (the shared human renderer with a "landing
|
|
8937
|
+
readiness" heading), and attaches the dry-run-shaped readiness payload to the JSON fail
|
|
8938
|
+
envelope under the `readiness` key.
|
|
8146
8939
|
|
|
8147
8940
|
**The cold worker.** `perk objective stack land [OBJECTIVE] [--dry-run] [--run-id ID]
|
|
8148
|
-
[--yes] [--json]` —
|
|
8941
|
+
[--yes] [--json]` — a thin request→façade→map on **both arms** (envelope/exit semantics
|
|
8942
|
+
unchanged). `--dry-run` maps the §8.55 read-only preview (behavior unchanged; the
|
|
8149
8943
|
envelope preserves the §8.55 field prefix/order with the §8.56 mutation fields appended as
|
|
8150
8944
|
trailing nulls/empties; no consent, `--yes`/`--run-id` ignored). Bare
|
|
8151
8945
|
`land` requires GitHub auth (`require_github`), resolves the run id (explicit `--run-id` →
|
|
8152
8946
|
the ACTIVE objective header's `run_id` → typed `invalid_input`; `stack/shared.py::
|
|
8153
|
-
resolve_run_id`, shared with sync
|
|
8947
|
+
resolve_run_id`, shared with sync — caller intent reconstruction stays CLI-side, and its
|
|
8948
|
+
store-walk failures keep their typed envelopes at the command boundary), passes the
|
|
8949
|
+
consent callback to `Delivery.land`, and confirms: `--yes` auto-approves (still rendering
|
|
8154
8950
|
what it approved); a non-interactive session without `--yes` is the typed
|
|
8155
|
-
`confirmation_required` refusal BEFORE any prompt
|
|
8951
|
+
`confirmation_required` refusal BEFORE any prompt (raised inside the callback,
|
|
8952
|
+
propagating through the façade untranslated). The success envelope grows the trailing
|
|
8156
8953
|
fields `{outcome, operation_id, merge_async_uuid, landed_layers: [{node_id, plan_id,
|
|
8157
8954
|
pr_number, merge_commit_sha, learn_state, plan_issue_closed, nodes_marked, finalized,
|
|
8158
8955
|
base_sha, head_sha}],
|
|
@@ -8283,6 +9080,12 @@ suites that pin the edited prose in the same change (prompt-prose edits touch no
|
|
|
8283
9080
|
fixture — §8.31's Tier B is engine-vs-engine — but extension context/factory tests and binding
|
|
8284
9081
|
guards may pin strings).
|
|
8285
9082
|
|
|
9083
|
+
**Review-flow application.** The automated, adversarial, and draft-review launch statements own
|
|
9084
|
+
only flow choreography and compact labels: they name Ponytail as required automatic coverage and
|
|
9085
|
+
teach the requested/runnable launch vocabulary, but never restate its full rubric or source-check
|
|
9086
|
+
procedure. The bound review skills and reviewer agent definitions remain the canonical detail and
|
|
9087
|
+
judgment carriers for ownership, exact-source recheck, and the residual filesystem race.
|
|
9088
|
+
|
|
8286
9089
|
**Scope.** The objective's migration nodes cover the named stage families; all other perk-owned
|
|
8287
9090
|
stage prose (e.g. the `skills` door family) is held to this rule as ordinary maintenance going
|
|
8288
9091
|
forward.
|
|
@@ -8462,3 +9265,876 @@ construction site — it ships standalone into the wheel and cannot import the c
|
|
|
8462
9265
|
two sites are pinned together by a path-parity test). The family is disposable local cache:
|
|
8463
9266
|
ignored by run GC, retained for the worktree's life, removed with the worktree; never copied to
|
|
8464
9267
|
GitHub/Linear.
|
|
9268
|
+
|
|
9269
|
+
## §8.59 · The learn-dream gather core (manifest contract)
|
|
9270
|
+
|
|
9271
|
+
The pure exterior gather core for the `perk learn dream` factory (`perk/learn/dream.py`);
|
|
9272
|
+
the public door is `perk learn dream` (§8.65 — the §8.48-style door text), and the TS
|
|
9273
|
+
analyst wave is the decoder side (§8.60 pins this same schema version). `commit_sha` and
|
|
9274
|
+
`run_id` are **door-supplied parameters** — the core never captures HEAD, syncs, or
|
|
9275
|
+
preflights clean-tree/origin (the door owns all of that, §8.65).
|
|
9276
|
+
|
|
9277
|
+
**The manifest.** Versioned JSON (schema_version the string `"1"` — dream's own version line,
|
|
9278
|
+
independent of harvest's), written run-scoped at
|
|
9279
|
+
`.perk/workflow/scratch/runs/<run_id>/dream-manifest.json` (`DREAM_MANIFEST_FILENAME`):
|
|
9280
|
+
|
|
9281
|
+
```json
|
|
9282
|
+
{ "schema_version": "1",
|
|
9283
|
+
"commit_sha": "<door-supplied>",
|
|
9284
|
+
"registry_mode": "clusters",
|
|
9285
|
+
"doc_count": 63,
|
|
9286
|
+
"total_bytes": 1160620,
|
|
9287
|
+
"findings": {
|
|
9288
|
+
"structural": {
|
|
9289
|
+
"stale_pointers": [ { "doc": "docs/learned/pi/context-injection.md",
|
|
9290
|
+
"pointer": "perk/run/launch.py::_gone",
|
|
9291
|
+
"reason": "missing-symbol" } ],
|
|
9292
|
+
"broken_doc_paths": [ { "doc": "docs/learned/pi/context-injection.md",
|
|
9293
|
+
"target": "../workflow/renamed.md" } ],
|
|
9294
|
+
"duplicate_cues": [ { "key": "when touching the extension api.",
|
|
9295
|
+
"docs": [ "docs/learned/pi/context-injection.md",
|
|
9296
|
+
"docs/learned/pi/extension-api.md" ] } ],
|
|
9297
|
+
"missing_frontmatter": [ "docs/learned/pi/untitled.md" ] },
|
|
9298
|
+
"advisory": {
|
|
9299
|
+
"distillation_issues": [ { "doc": "docs/learned/pi/context-system.md",
|
|
9300
|
+
"problem": "missing" } ],
|
|
9301
|
+
"source_code_blocks": [ { "doc": "docs/learned/pi/extension-api.md",
|
|
9302
|
+
"language": "ts", "lines": 14 } ],
|
|
9303
|
+
"overlong_cues": [ { "doc": "docs/learned/pi/tui-surfaces.md", "length": 214 } ],
|
|
9304
|
+
"cue_hazards": [ { "doc": "docs/learned/pi/subagents.md",
|
|
9305
|
+
"hazard": "space-hash" } ],
|
|
9306
|
+
"empty_clusters": [ "prose-governance" ] } },
|
|
9307
|
+
"lanes": [
|
|
9308
|
+
{ "id": "pi-extension-1",
|
|
9309
|
+
"rollup": "Pi SDK/extension substrate craft — …",
|
|
9310
|
+
"docs": [ { "path": "docs/learned/pi/context-injection.md",
|
|
9311
|
+
"title": "…", "read_when": "…",
|
|
9312
|
+
"cluster": "pi-extension", "bytes": 12345 } ] } ] }
|
|
9313
|
+
```
|
|
9314
|
+
|
|
9315
|
+
`registry_mode` ∈ `"clusters" | "categories"`; per-doc `bytes` is the raw file byte size;
|
|
9316
|
+
`doc_count`/`total_bytes` are the corpus count and the per-doc-bytes sum; the per-doc cue field
|
|
9317
|
+
is named `read_when` (matching the harvest manifest and the frontmatter key); `None`
|
|
9318
|
+
title/cue/cluster/rollup values are carried as JSON `null`, never dropped.
|
|
9319
|
+
|
|
9320
|
+
**The two-source partition.** Lanes join the committed cluster registry
|
|
9321
|
+
(`docs/learned/clusters.yaml` — ids + rollups + **file order**, the presentation SSOT matching
|
|
9322
|
+
the `docs-sync` rendering) with each doc's `cluster` frontmatter: per registry cluster, members
|
|
9323
|
+
= the docs whose `cluster` matches, path-sorted, chunked sequentially at `MAX_LANE_DOCS` (the
|
|
9324
|
+
shared harvest cap, 8); lane ids `<cluster>-<n>`, 1-based per cluster; **every chunk lane of a
|
|
9325
|
+
cluster carries that cluster's `rollup`**; an empty cluster emits no lane. The **category
|
|
9326
|
+
fallback** applies only to a **truly absent** registry (`load_cluster_registry` → `None`):
|
|
9327
|
+
`partition_lanes` semantics — `<category>-<n>` ids in sorted-group order — with `rollup: null`
|
|
9328
|
+
and `registry_mode: "categories"`.
|
|
9329
|
+
|
|
9330
|
+
**The refusal vocabulary** (all `UserFacingCliError`s): `no_learned_docs` (empty corpus);
|
|
9331
|
+
`invalid_registry` (a present-but-broken registry — the loader's precise reason relayed);
|
|
9332
|
+
`incomplete_registry` (any doc whose `cluster` is undeclared or names no registry id — every
|
|
9333
|
+
offending doc listed; the docs-sync posture, never a silent fallback); `invalid_input` (the
|
|
9334
|
+
symlinked corpus root, escaping docs, and unreadable doc bytes — snapshot honesty, never a
|
|
9335
|
+
silent 0). Byte measurement runs **before** the partition, so an unreadable doc is refused
|
|
9336
|
+
`invalid_input`, never misnamed `incomplete_registry` (the never-raising scan degrades an
|
|
9337
|
+
unreadable doc's frontmatter — its `cluster` included — to `null`): readability precedes
|
|
9338
|
+
membership. **Dream refuses where harvest filters**: an enumerated doc whose resolved path
|
|
9339
|
+
escapes `docs/learned/` is a refusal naming every escaping doc — a complete-corpus audit never
|
|
9340
|
+
silently narrows the corpus, so a completed gather's doc set is exactly the
|
|
9341
|
+
`read_learned_docs` enumeration.
|
|
9342
|
+
|
|
9343
|
+
**Findings.** One `check_docs` call mapped into the pinned **closed** sets above (the field
|
|
9344
|
+
vocabularies are `docs_sync`/`docs_scan`'s, by reference — dream only *reads* the existing
|
|
9345
|
+
scanners, never widens the docs-check report). Every family is filtered by its **owner-doc
|
|
9346
|
+
field only** against the manifest path set: a `broken_doc_paths` row's `target` (and a
|
|
9347
|
+
`stale_pointers` row's `pointer`) never participates in filtering — a broken target is by
|
|
9348
|
+
definition not a corpus member; it IS the finding. `duplicate_cues` comes from
|
|
9349
|
+
`duplicate_read_when` (learned-docs-only by construction; groups kept when all `docs` are in
|
|
9350
|
+
the path set — degenerate post-refusal, pinned for determinism); `empty_clusters` carries
|
|
9351
|
+
cluster ids untouched (registry mode only; `[]` in fallback). Deliberately excluded: artifact
|
|
9352
|
+
freshness/`stale_files` and `ambient_routing_bytes` (generated-artifact mechanics `docs-sync`
|
|
9353
|
+
repairs), `oversize_docs` (derivable from per-doc `bytes`), `registry_error`/`cluster_issues`
|
|
9354
|
+
(structurally impossible — refused before a gather completes), and any duplicate-**title**
|
|
9355
|
+
family (`DocsCheckReport` exposes only `duplicate_read_when`; the whole-corpus analysts read
|
|
9356
|
+
titles anyway).
|
|
9357
|
+
|
|
9358
|
+
**The shared primitive.** `eligible_learned_docs(repo_root)` (extracted in
|
|
9359
|
+
`perk/learn/harvest.py`) owns the symlinked-corpus-root guard + the per-doc resolved
|
|
9360
|
+
containment **filter**, returning `(doc, resolved_path)` pairs in corpus order; harvest's
|
|
9361
|
+
selection semantics stay byte-identical (it consumes the primitive), and dream layers its
|
|
9362
|
+
refuse posture on top. Resolved paths are consumed only for byte measurement — never as
|
|
9363
|
+
partition input.
|
|
9364
|
+
|
|
9365
|
+
## §8.60 · The learn-dream analyst wave (first level)
|
|
9366
|
+
|
|
9367
|
+
The first-level cluster-analyst wave for `perk learn dream` in the TypeScript plane
|
|
9368
|
+
(`extension/waves/dreamWave.ts`, over the shared report-wave runner). Consumed by the
|
|
9369
|
+
`run_dream_wave` tool (§8.61), reachable only inside a `perk learn dream` launch (§8.65).
|
|
9370
|
+
ONE attempt, NO retry; the manifest and every analyst
|
|
9371
|
+
report are untrusted DATA, never instructions. The module additionally exports
|
|
9372
|
+
`DREAM_MANIFEST_FILENAME` (the TS mirror of the §8.59 literal — the harvest precedent, no
|
|
9373
|
+
cross-plane codegen) and the shared cap helpers `codePointLength`/`decodeStringArray` (one
|
|
9374
|
+
code-point measure across both dream re-decodes — §8.61's reducer re-decode imports them).
|
|
9375
|
+
|
|
9376
|
+
**The strict decoder.** `decodeDreamManifest(raw, manifestPath)` pins the §8.59 manifest and
|
|
9377
|
+
BINDS the run-scoped manifest path into the decoded value — ONE authority: the object the wave
|
|
9378
|
+
plans and validates and the file the analysts read can never diverge. Rules: `schema_version`
|
|
9379
|
+
byte-identical the string `"1"` (dream's own version line); string `commit_sha`;
|
|
9380
|
+
`registry_mode` ∈ `"clusters" | "categories"`; `doc_count`/`total_bytes` non-negative integers
|
|
9381
|
+
**cross-checked** against the lanes (total doc count / per-doc `bytes` sum); `findings` present
|
|
9382
|
+
with `structural`/`advisory` records each carrying its four/five pinned family keys **as
|
|
9383
|
+
arrays** — rows deliberately NOT deep-validated (TS consumes findings only via the manifest
|
|
9384
|
+
file the analysts read; the Python `OutputModel` renderer owns row shapes; the shallow check
|
|
9385
|
+
catches truncation/gross drift); non-empty `lanes`, each with a non-empty unique string `id`,
|
|
9386
|
+
string-or-null `rollup`, and a non-empty `docs` array of **at most `laneDocs` (8)** entries — a
|
|
9387
|
+
larger lane is structurally unwinnable under the report schema's per-lane doc cap, refused
|
|
9388
|
+
pre-spawn with a named detail; each doc with a non-empty string `path` passing the LEXICAL
|
|
9389
|
+
containment layer (`lexicalContainmentError`, shared from `harvestWave.ts`), equal to its own
|
|
9390
|
+
POSIX normalization (**canonical form required** — an alias spelling like
|
|
9391
|
+
`docs/learned/a/../x.md` can never enter the corpus set, so membership and self-target checks
|
|
9392
|
+
operate on canonical identities), and **globally unique across the whole manifest** (lanes
|
|
9393
|
+
partition the corpus), string-or-null
|
|
9394
|
+
`title`/`read_when`/`cluster`, and a non-negative-integer `bytes`. Any deviation refuses the
|
|
9395
|
+
whole wave pre-spawn with a named detail; unknown extra keys are ignored (forward-compat rides
|
|
9396
|
+
`schema_version`).
|
|
9397
|
+
|
|
9398
|
+
**Code-owned orchestration lane keys** (the §8.50 audit-wave pattern): the run key is
|
|
9399
|
+
`<sanitized lane id>.<ordinal>` (invalid chars collapsed to `-`, leading non-alnum stripped,
|
|
9400
|
+
stem clamped, global 1-based ordinal); the SEMANTIC manifest lane id rides the lane `label`,
|
|
9401
|
+
the module-private lane plan, and the task text — producer lane ids are deliberately NOT
|
|
9402
|
+
run-key-bounded (category-fallback and long-cluster ids never fail the run-key contract), so
|
|
9403
|
+
the decoder performs no run-key conformance check. Lane planning is module-private: callers
|
|
9404
|
+
see only the entrypoint's typed outcome, never orchestration keys or the plan shape.
|
|
9405
|
+
|
|
9406
|
+
**The closed report schema.** `DREAM_ANALYST_REPORT_SCHEMA`: `additionalProperties: false` at
|
|
9407
|
+
every level, all fields required, no if/then conditionals, no `pattern` constraints on
|
|
9408
|
+
path/pointer fields. Every `maxItems`/`maxLength` reads from the ONE exported
|
|
9409
|
+
`DREAM_ANALYST_CAPS` SSOT — `laneDocs: 8` (also the decoder's lane bound and `docs.maxItems`),
|
|
9410
|
+
`rationaleChars: 500`, `preserveItems: 4`, `preserveItemChars: 300`, `evidenceItems: 6`,
|
|
9411
|
+
`evidenceItemChars: 250`, `overlapSignals: 8`, `overlapNoteChars: 250`, `harvestFollowups: 5`,
|
|
9412
|
+
`followupTitleChars: 150`, `followupEvidenceChars: 250`, `uncertainties: 6`,
|
|
9413
|
+
`uncertaintyChars: 300` — consumed by the schema, the decoder, and the re-decode alike. String
|
|
9414
|
+
caps are measured in **Unicode code points** (JSON Schema `maxLength` semantics; UTF-16
|
|
9415
|
+
`.length` would reject engine-valid astral strings). Per-doc rows carry
|
|
9416
|
+
`{path, disposition (keep|revise|merge-into|retire), merge_target (string|null), rationale,
|
|
9417
|
+
preserve[], evidence_checked[], confidence (high|medium|low)}`; report-level fields are
|
|
9418
|
+
`overlap_signals[] {doc, counterpart, note}`, `harvest_followups[] {title, pointer, evidence}`,
|
|
9419
|
+
`uncertainties[]`, and the three required omission counters `overlap_signals_omitted`/
|
|
9420
|
+
`harvest_followups_omitted`/`uncertainties_omitted` (non-negative integers — omission
|
|
9421
|
+
accounting is report-level only).
|
|
9422
|
+
|
|
9423
|
+
**The composed defensive re-decode.** `decodeDreamAnalystReport(report, laneDocPaths,
|
|
9424
|
+
corpusDocPaths)` — whitelisted construction (an extra input key never survives; every miss a
|
|
9425
|
+
named detail): `docs` rows' path set EXACTLY the lane's doc set (no duplicates/extras/missing),
|
|
9426
|
+
normalized to **manifest lane-doc order** (deterministic downstream bundles); the
|
|
9427
|
+
**merge-target rule** — `merge-into` ⇒ `merge_target` a **byte-exact member of the manifest's
|
|
9428
|
+
corpus path set** (membership in the canonical producer-written set subsumes containment and
|
|
9429
|
+
defeats `docs/learned/a/../x.md` aliases) and ≠ the row's own path, any other disposition ⇒
|
|
9430
|
+
`merge_target === null`; overlap `counterpart` follows the same membership + ≠ rule (`doc` ∈
|
|
9431
|
+
the lane's docs); follow-up `pointer` non-empty, no pointer stamping (destination survival is
|
|
9432
|
+
the dream-report node's validation); every cap re-checked in code points.
|
|
9433
|
+
|
|
9434
|
+
**Strict completeness.** `runDreamAnalystWave(adapter, {manifest, model?}, signal?)` (the
|
|
9435
|
+
manifest carries its decode-time-bound `manifestPath`) runs `flow: "dream-analyst"` under
|
|
9436
|
+
`completeness: "strict"`, forwarding the caller's `signal` (cancellation at the glue boundary)
|
|
9437
|
+
— one failed/undecodable lane ⇒ `complete: false`; a schema-valid report failing the re-decode
|
|
9438
|
+
is a `malformed-report` failure. Failures surface in the dream-specific
|
|
9439
|
+
`DreamLaneFailure {lane, reason, detail}` shape — `lane` is the SEMANTIC manifest lane id, or
|
|
9440
|
+
`null` for wave-level failures and the defensive unplanned-key arm (a raw orchestration key is
|
|
9441
|
+
named only in `detail`, never surfaced as a lane identity). Decoded analyses are RETAINED even
|
|
9442
|
+
when incomplete — honest coverage for the tool's refusal and the incomplete-analysis outcome.
|
|
9443
|
+
The outcome additionally carries `requestedKeys` — the code-owned orchestration keys in launch
|
|
9444
|
+
order, receipt-correlation telemetry ONLY (they correlate with `receipt.children[*].key`; the
|
|
9445
|
+
semantic lane identity stays `lane`) — the additive widening the §8.61 attempt receipts build
|
|
9446
|
+
from. **Single-lane manifests are valid** — dream has NO direct-analysis path (the harvest
|
|
9447
|
+
single-lane refusal is deliberately not mirrored).
|
|
9448
|
+
|
|
9449
|
+
**Containment posture.** Lexical containment lives in the decoder (per doc path); the resolved
|
|
9450
|
+
layer is the existing shared `verifyDocContainment` (`harvestWave.ts` — `DreamManifest` is
|
|
9451
|
+
structurally assignable to its manifest parameter, pinned by test), invoked pre-spawn by the
|
|
9452
|
+
`run_dream_wave` tool (§8.61) exactly as `harvestWaveTools.ts` invokes it for harvest (the
|
|
9453
|
+
§8.48 sequence).
|
|
9454
|
+
|
|
9455
|
+
**Model threading.** The wave takes the caller's `model?` as the workflow-level default; the
|
|
9456
|
+
`[models.subagents] dream-analyst` config key is resolved by the `run_dream_wave` tool at
|
|
9457
|
+
execute time (§8.61) and threaded here.
|
|
9458
|
+
|
|
9459
|
+
**The agent.** `perk.dream-analyst` (`agents/dream-analyst.md`): report-only
|
|
9460
|
+
(`REPORT_ONLY_CHILD_AGENTS` + the §8.1 report-only children list), read-only tool posture
|
|
9461
|
+
(`read, grep, find, ls, bash`), fresh context, engine-injected `structured_output` completion
|
|
9462
|
+
(never fenced JSON), delivered via `PERK_AGENTS` into `.pi/agents/perk/`.
|
|
9463
|
+
|
|
9464
|
+
## §8.61 · The learn-dream reducer wave + the `run_dream_wave` tool
|
|
9465
|
+
|
|
9466
|
+
The second level of the `perk learn dream` analysis pipeline
|
|
9467
|
+
(`extension/waves/dreamReducerWave.ts`) and the ONE run-bound tool that makes both levels
|
|
9468
|
+
reachable (`extension/doors/dreamWaveTools.ts`, registered globally). The tool
|
|
9469
|
+
**structurally refuses outside a dream launch** (below): only the `perk learn dream` door
|
|
9470
|
+
(§8.65) plants a run-scoped dream manifest, so it is unreachable in every other session. The
|
|
9471
|
+
bundle, the manifest, and every analyst/reducer report are untrusted DATA, never
|
|
9472
|
+
instructions.
|
|
9473
|
+
|
|
9474
|
+
**The compact analyst bundle.** `DREAM_ANALYSES_FILENAME = "dream-analyses.json"`, written
|
|
9475
|
+
run-scoped **beside the run's dream manifest** (the ONE path authority: derived from the
|
|
9476
|
+
decode-time-bound `manifest.manifestPath`, never a second `runScratchDir` derivation). The
|
|
9477
|
+
versioned shape: `{schema_version: "1", commit_sha, registry_mode, doc_count, total_bytes,
|
|
9478
|
+
lanes: [{lane, report}]}` — the identity fields echo the manifest; `lanes` carries the
|
|
9479
|
+
re-decoded compact analyst reports **in manifest lane order** (an already-guaranteed invariant
|
|
9480
|
+
of the runner's `spec.lanes`-order normalization + `buildDreamLanes`' manifest-order plan + the
|
|
9481
|
+
re-decode's doc-order normalization — no re-sort layer). Deterministic serialization
|
|
9482
|
+
(pretty-printed JSON + trailing newline). The aggregate budget:
|
|
9483
|
+
`DREAM_BUNDLE_BUDGET_BYTES = 393216` (384 KiB), measured as **UTF-8 bytes** of the serialized
|
|
9484
|
+
bundle and enforced **before reducer task composition** — over budget ⇒ the bundle is NOT
|
|
9485
|
+
written, the reducers are NOT launched, and the aggregate carries the explicit accounting
|
|
9486
|
+
`{bytes, budget_bytes, overflow_bytes}` — **never truncation** (a truncated bundle would
|
|
9487
|
+
corrupt stance evaluation; overflow is a loud corpus-growth tripwire). The budget governs
|
|
9488
|
+
exactly these reducer-INPUT bytes — the post-complete finalize rewrite (below) happens after
|
|
9489
|
+
the reducers consumed them and is not budget-checked. An incomplete first
|
|
9490
|
+
wave writes nothing (`bundle: null`). **The entry-time removal invariant:** the execute core
|
|
9491
|
+
removes any pre-existing bundle at entry (before the first wave), so the fixed name exists
|
|
9492
|
+
**iff the current call wrote it** — the incomplete/over-budget arms can never leave a stale
|
|
9493
|
+
prior bundle contradicting the returned aggregate, and after an `io_error` the target is
|
|
9494
|
+
absent (entry removal ran; the atomic temp+rename never landed). A repeat call (the
|
|
9495
|
+
blocking-tool retry — no guard state, the audit/harvest posture) is therefore always
|
|
9496
|
+
self-consistent. A **failed entry-time removal** refuses `io_error` BEFORE any spawn — a
|
|
9497
|
+
typed refusal with empty `{analyses, attempts}` extras, never an uncaught throw (launching
|
|
9498
|
+
over an irremovable stale bundle would break the invariant); the digest marker (below) was
|
|
9499
|
+
already cleared, so whatever files the failed cleanup left behind are refused by the
|
|
9500
|
+
dream-report recovery.
|
|
9501
|
+
|
|
9502
|
+
**The finalize-in-place rewrite + the `dream_bundle_digest` marker (the reviewed §8.61
|
|
9503
|
+
widening).** Reducer stances were previously never persisted (reducer reports lived only in
|
|
9504
|
+
the tool result); the dream-report draft path (§8.63) needs them to survive pi restarts, so
|
|
9505
|
+
after a **fully complete** two-level wave — and only after the **post-wave revalidation
|
|
9506
|
+
bracket** (§8.65) passes: evaluated only when BOTH waves completed, BEFORE the finalize
|
|
9507
|
+
write; drift ⇒ NO finalize, NO marker set (the entry clear stands — the analyses-only shape
|
|
9508
|
+
is left behind and recovery refuses it, so a drifted wave is structurally undraftable), the
|
|
9509
|
+
aggregate records the drift and `complete: false` — the execute core atomically REWRITES the
|
|
9510
|
+
existing `dream-analyses.json` via
|
|
9511
|
+
`finalizeDreamBundle(manifest, analyses, reducers, manifestDigest)`:
|
|
9512
|
+
the same wrapper fields (`schema_version` stays `"1"`) plus `manifest_digest` — the
|
|
9513
|
+
`sha256:<hex>` digest of the on-disk manifest BYTES the wave read and decoded, extending the
|
|
9514
|
+
marker's bundle-byte authentication to the manifest itself (an at-rest manifest edit that
|
|
9515
|
+
preserves the echoed identity fields still refuses at recovery) — plus `reducers`, an array
|
|
9516
|
+
in the fixed `DREAM_REDUCER_ANGLES` order, each entry the **raw echo shape**
|
|
9517
|
+
`{angle, ...report}` (exactly the shape `decodeDreamReducerReport` accepts, the angle
|
|
9518
|
+
echoed). Deliberately NOT a second
|
|
9519
|
+
file: one fixed name means the analyses-only mid-wave shape and the finalized shape are
|
|
9520
|
+
mutually exclusive states of one path — a cross-attempt MIXED state is structurally
|
|
9521
|
+
impossible; the `reducers` key is present **iff** finalized, and an incomplete reducer wave
|
|
9522
|
+
naturally leaves the analyses-only shape behind (recovery refuses it). The recovery-side
|
|
9523
|
+
decode is `decodeFinalizedDreamBundle(raw, manifest, manifestDigest)` — strict, fail-closed,
|
|
9524
|
+
every miss a named detail: **the pinned unknown-key policy** — the persisted format is CLOSED
|
|
9525
|
+
at every level this decoder authors (the wrapper: exactly `{schema_version, commit_sha,
|
|
9526
|
+
registry_mode, doc_count, total_bytes, manifest_digest, lanes, reducers}`; each lane entry:
|
|
9527
|
+
`{lane, report}`; each reducer entry: the raw echo keys) with unknown keys refusing; a
|
|
9528
|
+
missing `reducers` key refuses as not-finalized; the identity fields must equal the
|
|
9529
|
+
manifest's; `manifest_digest` must equal the caller's digest of the manifest bytes just read
|
|
9530
|
+
(the manifest-authentication link); `lanes` must pair the
|
|
9531
|
+
manifest's lanes EXACTLY (same ids, same order); `reducers` must carry exactly the three
|
|
9532
|
+
angles in fixed order (the byte-exact angle echo refuses duplicates/reorders); INSIDE a row
|
|
9533
|
+
the reused row decoders (`decodeDreamAnalystReport` over the manifest-derived lane/corpus
|
|
9534
|
+
path sets, `decodeDreamReducerReport` over `nonKeepProposals(analyses)`) stay the **single
|
|
9535
|
+
row authorities** — whitelisted construction means an extra row-level key is IGNORED and
|
|
9536
|
+
never survives into typed values (no fork of the row decoders). This
|
|
9537
|
+
closed-wrapper/whitelist-projected-row split is the decided policy, pinned by tests on both
|
|
9538
|
+
sides. **The marker lifecycle:** the freshness authority is the `dream_bundle_digest`
|
|
9539
|
+
workflow-state field (§8.3) — a bare run-scratch file is never trusted by the recovery
|
|
9540
|
+
consumer (the session-artifacts digest-pointer doctrine). The execute clears it (`""`)
|
|
9541
|
+
unconditionally at entry BEFORE the stale-bundle removal attempt — the invalidation record
|
|
9542
|
+
that keeps the removal `io_error` refusal fail-closed for downstream consumers — and sets it
|
|
9543
|
+
to the sha256 of the finalized bytes (`digestSessionData`, the `sha256:<hex>` convention)
|
|
9544
|
+
only after the finalize write succeeds. The entry clear is **verified**: `markers.clear()`
|
|
9545
|
+
returns the append+read-back result, and an UNVERIFIED clear refuses `io_error` before ANY
|
|
9546
|
+
filesystem work or spawn — with the old digest possibly still live, proceeding into a failed
|
|
9547
|
+
removal would leave the prior bundle + prior digest PAIR recoverable as fresh, so the wave
|
|
9548
|
+
stops instead (no mutation happens, and the untouched prior finalized state remains exactly
|
|
9549
|
+
what it was). The marker seam is injected into the execute core (`markers: {clear, set}`;
|
|
9550
|
+
the registered tool wires the production `appendWorkflowState` pair); `set` stays
|
|
9551
|
+
loud-but-non-fatal — a failed set warns via the read-back check and leaves the marker
|
|
9552
|
+
cleared by the entry clear, so recovery refuses; re-running the wave repairs it. A
|
|
9553
|
+
finalize-write throw is the SECOND post-launch `io_error` fail arm, mirroring the
|
|
9554
|
+
analyst-bundle arm's `{analyses, attempts}` extras retention (the message names the
|
|
9555
|
+
finalize).
|
|
9556
|
+
|
|
9557
|
+
**The three fixed reducer angles** (`DREAM_REDUCER_ANGLES`, fixed order everywhere — lanes,
|
|
9558
|
+
normalized reports, and the vocabulary the dream-report validation's disagreement rule
|
|
9559
|
+
references): `consolidation-preservation` (reconcile merge/retire proposals, detect
|
|
9560
|
+
cross-cluster redundancy, ensure unique durable content has a surviving home, reject merge
|
|
9561
|
+
cycles and retiring merge targets), `currency-accuracy` (challenge claims against current
|
|
9562
|
+
repository truth, distinguish obsolete knowledge from still-valid rationale, prioritize
|
|
9563
|
+
misleading guidance), `knowledge-architecture` (document boundaries, clusters, routing cues,
|
|
9564
|
+
distillation/read cost, harvest-follow-up quality). **The agent:** `perk.dream-reducer`
|
|
9565
|
+
(`agents/dream-reducer.md`): report-only (`REPORT_ONLY_CHILD_AGENTS` + the §8.1 report-only
|
|
9566
|
+
children list), read-only tool posture (`read, grep, find, ls, bash`), fresh context,
|
|
9567
|
+
engine-injected `structured_output` completion (never fenced JSON), stronger-tier default
|
|
9568
|
+
model (`anthropic/claude-fable-5`, fallback `anthropic/claude-sonnet-4-5` — the reducers are
|
|
9569
|
+
the judgment-heaviest lanes), delivered via `PERK_AGENTS` into `.pi/agents/perk/`.
|
|
9570
|
+
|
|
9571
|
+
**The closed reducer schema.** `DREAM_REDUCER_REPORT_SCHEMA`: `additionalProperties: false`
|
|
9572
|
+
at every level, all fields required, no if/then, no `pattern`; every `maxItems`/`maxLength`
|
|
9573
|
+
reads from the ONE `DREAM_REDUCER_CAPS` SSOT — `stances: 120`, `stanceReasonChars: 300`,
|
|
9574
|
+
`stanceEvidenceItems: 4`, `stanceEvidenceItemChars: 250`, `angleFindings: 8`,
|
|
9575
|
+
`angleFindingChars: 400`, `uncertainties: 6`, `uncertaintyChars: 300` (string caps in Unicode
|
|
9576
|
+
code points, the shared `codePointLength`). Fields: `angle` (the echoed identity, enum =
|
|
9577
|
+
the three slugs), `stances[]` `{doc, disposition ∈ revise|merge-into|retire, stance ∈
|
|
9578
|
+
endorse|challenge, reason, evidence_checked[]}`, `angle_findings[]`, `uncertainties[]`, and
|
|
9579
|
+
the three non-negative-integer omission counters `stances_omitted`/`angle_findings_omitted`/
|
|
9580
|
+
`uncertainties_omitted`.
|
|
9581
|
+
|
|
9582
|
+
**The stance vocabulary.** A stance is `endorse` or `challenge` with a required non-empty
|
|
9583
|
+
`reason` — there is deliberately **no abstain value**; `disposition` is a **defensive echo**
|
|
9584
|
+
of the analyst proposal being stanced (mismatch = malformed lane — the audit echoed-identity
|
|
9585
|
+
precedent); `evidence_checked` records what the selective verification actually touched (the
|
|
9586
|
+
dream-report node's destructive evidence bar consumes it). **Silence counts as
|
|
9587
|
+
non-endorsement**: the re-decode never requires stance coverage — empty `stances` is valid —
|
|
9588
|
+
and the consumption rule (an unstanced destructive proposal cannot proceed) is the
|
|
9589
|
+
dream-report node's evidence bar, not this decode. **The destructive-first priority** (an
|
|
9590
|
+
instruction-layer obligation bounded by the schema cap, pinned in the def prose): the two gate
|
|
9591
|
+
angles (consolidation-preservation, currency-accuracy) stance every `merge-into`/`retire`
|
|
9592
|
+
proposal FIRST and only then `revise` proposals; if the cap truncates, the overflow is counted
|
|
9593
|
+
in `stances_omitted` and the resulting silence is **explicitly conservative** — a documented,
|
|
9594
|
+
accounted, fail-safe form of incomplete stance coverage (no pre-wave refusal on proposal
|
|
9595
|
+
count; `stances: 120` ≥ the non-keep proposal count of any plausible corpus). **The
|
|
9596
|
+
selective-evidence posture** (def prose): verify cited evidence — follow the analysts'
|
|
9597
|
+
`evidence_checked` pointers and read the specific named docs/code sites, read-only — never
|
|
9598
|
+
broadly rescan the corpus, never re-run gather commands, never read docs beyond the
|
|
9599
|
+
cited/named ones.
|
|
9600
|
+
|
|
9601
|
+
**The re-decode + the wave.** `decodeDreamReducerReport(report, angle, proposals)` —
|
|
9602
|
+
whitelisted construction, named details, code-point caps via the shared helpers: the echoed
|
|
9603
|
+
`angle` must equal the assigned angle byte-exact and the typed result OMITS it (the aggregate
|
|
9604
|
+
names the angle once, on `DreamReducerAnalysis.angle`); each stance row's `doc` must be a
|
|
9605
|
+
member of the ordered non-keep proposal universe (`nonKeepProposals(analyses)` — a flat-map
|
|
9606
|
+
over the analyses' docs filtered to `revise`/`merge-into`/`retire`, inheriting the manifest
|
|
9607
|
+
ordering) with the disposition echo rule above, no duplicate rows, stances normalized to the
|
|
9608
|
+
proposal order. `runDreamReducerWave(adapter, {manifestPath, bundlePath, proposals, model?},
|
|
9609
|
+
signal?)` runs `flow: "dream-reducer"` under `completeness: "strict"`, ONE attempt, NO retry,
|
|
9610
|
+
three fixed lanes — key = label = the angle slug (code-owned, run-key-safe by construction),
|
|
9611
|
+
agent `perk.dream-reducer`, short code-composed task text (the angle, the bundle path read
|
|
9612
|
+
FIRST, the manifest path, the untrusted-DATA + structured_output lines — the judgment rubric
|
|
9613
|
+
lives in the def). Failures surface in the angle-named `DreamReducerFailure {angle, reason,
|
|
9614
|
+
detail}` shape (`null` = wave-level); a schema-valid report failing the re-decode is a
|
|
9615
|
+
`malformed-report` failure with the angle identity; `complete` = runner complete AND zero
|
|
9616
|
+
decode failures; decoded reports retained when incomplete, normalized to the fixed angle
|
|
9617
|
+
order. `DreamReducerOutcome` carries `requestedKeys` (= the three slugs) from birth — the
|
|
9618
|
+
receipt-correlation twin of §8.60's outcome field. Reducers launch even when the proposal
|
|
9619
|
+
universe is EMPTY (a keep-heavy corpus still gets angle findings/uncertainties).
|
|
9620
|
+
|
|
9621
|
+
**The tool binding (`run_dream_wave`).** NO parameters (the §8.50 no-param shape,
|
|
9622
|
+
`additionalProperties: false`, empty `properties`): the execute recovers the session's claimed
|
|
9623
|
+
`run_id` from the rebuilt workflow-state and derives the ONE manifest path
|
|
9624
|
+
`runScratchDir(run_id)/dream-manifest.json` (`DREAM_MANIFEST_FILENAME`) — the manifest read
|
|
9625
|
+
AND the bundle write are both derived from the claimed run, so no caller-supplied path exists
|
|
9626
|
+
(the `run_audit_wave` no-aimable-writer posture, BOTH sides) — the structural boundary
|
|
9627
|
+
justifying the `READ_ONLY_TOOLS` carve-in (§8.3), beside `PERK_TOOLS`; deliberately NO
|
|
9628
|
+
`STAGE_TOOLS`/drive coverage (cold-only, gate-on — the harvest census posture). The pre-launch
|
|
9629
|
+
refusal ladder (each arm before any spawn): no claimed run ⇒ `bad_state`; no run-scoped dream
|
|
9630
|
+
manifest ⇒ `bad_state` (the structural refusal outside a dream launch); unparseable JSON ⇒
|
|
9631
|
+
`bad_input`; `decodeDreamManifest(raw, manifestPath)` refusal ⇒ `bad_input`;
|
|
9632
|
+
`verifyDocContainment` refusal ⇒ `bad_input` (the §8.48 sequence). Both `[models.subagents]`
|
|
9633
|
+
keys (`dream-analyst`, `dream-reducer`) are resolved at execute time via `subagentModel` and
|
|
9634
|
+
threaded as each wave's workflow-level `model?` default; production runs the RPC adapter,
|
|
9635
|
+
tests the in-memory adapter.
|
|
9636
|
+
|
|
9637
|
+
**The result posture.** Every post-launch outcome — with the exception of the two write
|
|
9638
|
+
`io_error` fail arms below — is **ok** with the full typed normalized
|
|
9639
|
+
aggregate — `{complete, analysis: {complete, analyses, failures}, bracket, bundle, reducers:
|
|
9640
|
+
{launched, skip_reason, complete, reports, failures}, attempts}` — `complete` = both waves
|
|
9641
|
+
complete AND the bracket ok; `bracket` is `{ok, detail}` when evaluated and `null` when an
|
|
9642
|
+
earlier incomplete **ok** arm skipped it (incomplete analysis, budget-exceeded, incomplete
|
|
9643
|
+
reducers — the bracket fn is never invoked on those arms; the `io_error` **fail** arms return
|
|
9644
|
+
the failure-details shape below — error fields plus `{analyses, attempts}` — which carries no
|
|
9645
|
+
`bracket` field at all);
|
|
9646
|
+
the `skip_reason` vocabulary is `incomplete-analysis` (strict first wave failed —
|
|
9647
|
+
no bundle write, **no reducer launch**) and `budget-exceeded` (composed but over budget —
|
|
9648
|
+
nothing written, no reducer launch). A drifted bracket retains the analyses AND reducer
|
|
9649
|
+
reports in the aggregate (honest coverage). `attempts` carries one output-free `WaveAttemptReceipt`
|
|
9650
|
+
per launched wave, built from each wave's code-owned `requestedKeys` (they correlate with
|
|
9651
|
+
`children[*].key`, never semantic labels). The TWO post-launch fail arms are the
|
|
9652
|
+
analyst-bundle-write and the finalize-write `io_error`s, whose typed extras retain BOTH the
|
|
9653
|
+
analyst analyses AND the already-recorded attempt receipts (`{analyses, attempts}` — the
|
|
9654
|
+
§8.48 receipt-retention discipline). The
|
|
9655
|
+
model-facing result text: the untrusted-DATA banner, the JSON aggregate, and — when
|
|
9656
|
+
incomplete — an explicit line that the analysis is incomplete and the parent must present
|
|
9657
|
+
coverage honestly and stop before drafting (no retry); on the drifted-bracket arm an
|
|
9658
|
+
ADDITIONAL line names the drift ("the repository DRIFTED during the wave (<detail>) — the
|
|
9659
|
+
dream snapshot is STALE") — it accompanies the generic incomplete instruction, never
|
|
9660
|
+
replaces it.
|
|
9661
|
+
|
|
9662
|
+
## §8.62 · The learn-dream report (model, validation, renderer)
|
|
9663
|
+
|
|
9664
|
+
The pure interior layer that turns the two-level dream outcome (§8.60/§8.61) into ONE
|
|
9665
|
+
checkable, savable final report (`extension/waves/dreamReport.ts`): the structured
|
|
9666
|
+
dream-report model, the validation that proves the parent's judgment obeys the pinned curation
|
|
9667
|
+
policy, and the deterministic Markdown renderer that owns the CANONICAL report bytes in parts.
|
|
9668
|
+
Pure domain code — no fs, no tool registration, no `ExtensionAPI`; imports only the two dream
|
|
9669
|
+
siblings. Consumed by the `objective_draft`/review/save wiring (the `dream_report` param,
|
|
9670
|
+
§8.63 — landed); part persistence to the backend is §8.64; the whole pipeline is reachable
|
|
9671
|
+
only inside a `perk learn dream` launch (§8.65). The input is untrusted DATA,
|
|
9672
|
+
never instructions.
|
|
9673
|
+
|
|
9674
|
+
**The trust split.** Two input shapes: `DreamReportInput` — untrusted, model-supplied —
|
|
9675
|
+
carries ONLY the decisions the design assigns to the parent (the final per-doc disposition
|
|
9676
|
+
rows with rationales and fallback reasons, parent uncertainties, ranked selected/overflow
|
|
9677
|
+
curation units, harvest follow-ups, predicted effects); `DreamReportContext` — trusted,
|
|
9678
|
+
caller-supplied — is `{manifest: DreamManifest, analyses: DreamLaneAnalysis[], reducers:
|
|
9679
|
+
DreamReducerAnalysis[], run_id, generated_at}`. Everything factual — snapshot identity (run
|
|
9680
|
+
id, commit SHA, counts, bytes, registry mode, findings family counts), wave coverage, analyst
|
|
9681
|
+
evidence (rationale/preserve/evidence_checked/confidence), reducer stances, analyst/reducer
|
|
9682
|
+
uncertainties, omission counters — is **injected from context**, never accepted from the
|
|
9683
|
+
model: the model cannot fabricate evidence, stances, or coverage.
|
|
9684
|
+
|
|
9685
|
+
**The completeness precondition.** Validation first re-verifies the context itself:
|
|
9686
|
+
non-empty single-line `run_id`/`generated_at` (trusted caller stamps — validated shallowly
|
|
9687
|
+
only), `analyses` covering the manifest's lanes exactly (one per lane, manifest order, each
|
|
9688
|
+
covering its lane's docs exactly) and `reducers` carrying exactly the three
|
|
9689
|
+
`DREAM_REDUCER_ANGLES` in fixed order — a report can only be built from COMPLETE waves;
|
|
9690
|
+
anything else refuses (incomplete coverage is never described as complete).
|
|
9691
|
+
|
|
9692
|
+
**The input schema + caps SSOT.** `DREAM_REPORT_INPUT_SCHEMA` (exported for the review
|
|
9693
|
+
wiring's param embedding) mirrors the §8.60 schema discipline: closed shape at every level,
|
|
9694
|
+
all fields required (optional semantics via `null`), enums, no if/then, no `pattern`; every
|
|
9695
|
+
`maxItems`/`maxLength` reads from the ONE `DREAM_REPORT_CAPS` SSOT — `rows: 512`,
|
|
9696
|
+
`rowRationaleChars: 300`, `fallbackReasonChars: 300`, `uncertainties: 12`,
|
|
9697
|
+
`uncertaintyChars: 300`, `selectedUnits: 64`, `overflowUnits: 64`, `unitTitleChars: 150`,
|
|
9698
|
+
`unitDocs: 32`, `unitRationaleChars: 400`, `unitNodeChars: 32`, `harvestFollowups: 12`,
|
|
9699
|
+
`followupTitleChars: 150`, `followupPointerChars: 250`, `followupEvidenceChars: 250`,
|
|
9700
|
+
`followupDestinationChars: 400`, `predictedNoteChars: 300` (string caps in Unicode code
|
|
9701
|
+
points, the shared `codePointLength`; `rows`/`selectedUnits`/`overflowUnits` are static
|
|
9702
|
+
schema bounds — the real gates are the exact path-set equality, the ≤12-distinct-node cap,
|
|
9703
|
+
and the exact partition). **The single-line rule:** every model-supplied string field refuses
|
|
9704
|
+
`\r`/`\n` and other C0 control characters with a named detail — the renderer places these
|
|
9705
|
+
strings in table cells and bullets, so line structure stays renderer-owned.
|
|
9706
|
+
|
|
9707
|
+
**Per-doc validation (downgrade-only).** `rows` path set = the manifest's authored-doc path
|
|
9708
|
+
set EXACTLY (no missing/extra/duplicate rows; byte comparison over the §8.60 canonical
|
|
9709
|
+
identities), normalized to manifest lane/doc order in the composed report. Exactly one
|
|
9710
|
+
disposition per doc from `DREAM_DISPOSITIONS`; `merge_target` non-null iff
|
|
9711
|
+
`disposition === "merge-into"`; `rationale` required non-empty on every row.
|
|
9712
|
+
**Downgrade-only against the analyst proposal** (destructiveness order `keep(0) < revise(1) <
|
|
9713
|
+
merge-into/retire(2)`, the two destructive dispositions incomparable): the final level must
|
|
9714
|
+
be ≤ the analyst level; a final destructive row must match the analyst proposal EXACTLY —
|
|
9715
|
+
same disposition AND byte-identical `merge_target` (a different target or a merge↔retire swap
|
|
9716
|
+
is an unendorsed new action, refused); `keep → revise` is an escalation and refuses.
|
|
9717
|
+
`fallback_reason` is REQUIRED non-empty exactly when the final disposition differs from the
|
|
9718
|
+
analyst proposal and must be `null` when it doesn't.
|
|
9719
|
+
|
|
9720
|
+
**The destructive evidence bar.** For every row whose FINAL disposition is `merge-into` or
|
|
9721
|
+
`retire`, over the INJECTED reducer stances for that doc's proposal: an explicit `endorse`
|
|
9722
|
+
from `consolidation-preservation` AND from `currency-accuracy`, and NO `challenge` from ANY
|
|
9723
|
+
of the three reducers (knowledge-architecture included); silence counts as non-endorsement (a
|
|
9724
|
+
missing gate-angle stance blocks eligibility). Anything else refuses with a named detail
|
|
9725
|
+
naming the only legal moves — downgrade to `revise`/`keep` with a `fallback_reason`; the
|
|
9726
|
+
parent never resolves upward. Eligibility is computed only from context stances, never from
|
|
9727
|
+
model input; the bar is necessary, not sufficient (an eligible proposal MAY still be
|
|
9728
|
+
downgraded).
|
|
9729
|
+
|
|
9730
|
+
**Merge-target survival.** Over FINAL dispositions: every `merge_target` must be a member of
|
|
9731
|
+
the manifest corpus path set (existence — revalidated even though analyst-matching rows
|
|
9732
|
+
already guarantee it) and must have a final disposition of `keep` or `revise` (survival).
|
|
9733
|
+
Survival structurally forbids merge chains and cycles — a merge-into doc can never be a
|
|
9734
|
+
target — so acyclicity is subsumed (2-cycles and chains refuse via the survival detail).
|
|
9735
|
+
|
|
9736
|
+
**The unit partition.** A curation unit is `{title, docs, rationale}`; selected units
|
|
9737
|
+
additionally carry a non-empty `roadmap_node`. **Many-to-one node mapping**: several selected
|
|
9738
|
+
units MAY name the same node; the cap is `DREAM_REPORT_MAX_ROADMAP_NODES = 12` DISTINCT
|
|
9739
|
+
`roadmap_node` values across the selected units. **Exact partition**: the union of all units'
|
|
9740
|
+
`docs` (selected + overflow) must equal EXACTLY the set of docs whose final disposition is
|
|
9741
|
+
non-keep — no doc in two units, no empty unit, no final-keep doc in any unit, every unit doc
|
|
9742
|
+
a corpus member (accepted work can neither vanish silently nor double-count; final-`keep`
|
|
9743
|
+
merge targets are named by the row's `merge_target`, not re-listed). Rank is positional
|
|
9744
|
+
(input order), selected and overflow ranked independently; overflow units carry no node.
|
|
9745
|
+
Unit atomicity is execution-time guidance owned by the activation prose — the validator
|
|
9746
|
+
checks only mapping shape, cap, and partition.
|
|
9747
|
+
|
|
9748
|
+
**Harvest follow-ups.** Each `destination` must be a corpus doc path whose final disposition
|
|
9749
|
+
is `keep`/`revise`, or a cluster id (the manifest's per-doc `cluster` values) named by at
|
|
9750
|
+
least one final-`keep`/`revise` doc — a destination pointing at a merged-away or retired doc
|
|
9751
|
+
refuses (repoint at the survivor). `pointer` non-empty; `pointer`/`evidence` capped strings.
|
|
9752
|
+
|
|
9753
|
+
**Predicted effects.** Model-supplied `docs_after`/`bytes_after` (non-negative integers) plus
|
|
9754
|
+
an optional single-line `note`; `docs_before`/`bytes_before` are injected from the manifest.
|
|
9755
|
+
TYPE sanity only — deliberately NO directional/quota rule (a growth prediction is valid,
|
|
9756
|
+
pinned by a vacuity-proof test); the renderer states “predictions are not quotas”.
|
|
9757
|
+
|
|
9758
|
+
**Two-stage bounded error collection** (the deliberate deviation from the fail-fast decoder
|
|
9759
|
+
posture, justified by the interactive redraft loop this validator gates): **structural decode
|
|
9760
|
+
fails fast** (context re-verification, non-object input, schema-shape misses including caps
|
|
9761
|
+
and the single-line rule — whitelisted construction cannot proceed over malformed input;
|
|
9762
|
+
first named detail wins, a one-element `details`); **semantic rules collect** (the per-doc,
|
|
9763
|
+
evidence-bar, survival, partition, and destination rules plus the renderer's defensive arm)
|
|
9764
|
+
up to `DREAM_REPORT_MAX_VALIDATION_DETAILS = 25` named details in deterministic order
|
|
9765
|
+
(validation phase order, then manifest doc order within a phase), overflow appending one
|
|
9766
|
+
final synthetic detail counting the omitted violations.
|
|
9767
|
+
`validateDreamReport(input, context)` returns `{ok: true, report}` (the composed
|
|
9768
|
+
JSON-serializable `DreamReport` — snapshot, findings counts, coverage, rows joined with
|
|
9769
|
+
analyst evidence + stances, uncertainties by source, reducer findings, units, follow-ups,
|
|
9770
|
+
effects) or `{ok: false, details}`.
|
|
9771
|
+
|
|
9772
|
+
**The deterministic renderer.** `renderDreamReport(report)` → `{ok: true, parts: string[]}`
|
|
9773
|
+
or `{ok: false, detail}` — a pure function of the composed report (no clock, no locale, no
|
|
9774
|
+
environment); the renderer owns the CANONICAL report bytes. Constants:
|
|
9775
|
+
`DREAM_REPORT_PART_MAX_CHARS = 60_000` — the per-part cap measured in Unicode CODE POINTS
|
|
9776
|
+
(the `journal.py` `JOURNAL_EVENT_MAX_CHARS` precedent — that precedent is a single-body
|
|
9777
|
+
refusal cap; the part-splitting semantics are new here), under GitHub's 65,536-char comment
|
|
9778
|
+
limit with margin for the persistence-side storage markers (the renderer never emits marker
|
|
9779
|
+
HTML) — and `DREAM_REPORT_PART_HEADER_RESERVE = 200`, the fixed per-part packing allowance
|
|
9780
|
+
for the part header. Pipeline: the report renders to an ordered stream of Markdown blocks;
|
|
9781
|
+
blocks are greedily packed into parts under `cap − reserve`; splits happen only at block
|
|
9782
|
+
boundaries; a table split re-emits the table header row in the next part; bullet-list
|
|
9783
|
+
sections (§7 Uncertainties, §8 Reducer findings) pack per bullet line — a block group splits
|
|
9784
|
+
at line boundaries, header re-emission applying only to tables — keeping the single-block
|
|
9785
|
+
defensive refusal structurally unreachable under the caps arithmetic; after packing, each
|
|
9786
|
+
part is prefixed with its header — part 1 `# Dream report — <run_id>`, continuations
|
|
9787
|
+
`# Dream report — <run_id> (continued, part <i> of <n>)`. A single block exceeding the budget
|
|
9788
|
+
is a defensive refusal (named, never truncated). Fixed section order: 1 Snapshot (run id, `DREAM_REPORT_SCHEMA_VERSION = "1"`,
|
|
9789
|
+
commit SHA, generated-at, registry mode, doc count, total bytes) · 2 Findings summary
|
|
9790
|
+
(per-family counts) · 3 Wave coverage (analyst lanes + reducer angles tables with omission
|
|
9791
|
+
counters) · 4 Dispositions (ONE table, manifest order: path, cluster, analyst proposal, final,
|
|
9792
|
+
merge target, analyst confidence, rationale) · 5 Non-keep evidence (per FINAL non-keep doc:
|
|
9793
|
+
injected analyst rationale/preserve/evidence_checked + every injected reducer stance) ·
|
|
9794
|
+
6 Fallbacks (rendered directly from the rows carrying a non-null `fallback_reason`, manifest
|
|
9795
|
+
order: doc, analyst proposal, final, reason — a final-`keep` fallback renders its stances
|
|
9796
|
+
here, so every reducer stance renders exactly once, §5 or §6) · 7 Uncertainties
|
|
9797
|
+
(parent, then analyst by lane, reducer by angle, labeled) · 8 Reducer findings (the injected
|
|
9798
|
+
`angle_findings` — a deliberate minor addition beyond the node's section list) · 9 Selected
|
|
9799
|
+
curation units (rank-ordered) · 10 Overflow · 11 Harvest follow-ups · 12 Predicted effects
|
|
9800
|
+
(with the explicit not-quotas line). Rendering hygiene: model strings are single-line by
|
|
9801
|
+
validation; INJECTED strings may carry newlines/pipes — in a table cell or bullet, `|` is
|
|
9802
|
+
escaped and internal newline runs collapse to a single space (deterministic sanitization; the
|
|
9803
|
+
typed report retains exact strings).
|
|
9804
|
+
|
|
9805
|
+
**The entry point.** `buildDreamReport(input, context)` — validate, compose, render, and
|
|
9806
|
+
enforce the part budget in ONE call, returning `{ok: true, report, parts}` or
|
|
9807
|
+
`{ok: false, details}` (the renderer's single-detail defensive arm wrapped into a one-element
|
|
9808
|
+
`details`) — so the draft path validates BEFORE review and an approved report is always
|
|
9809
|
+
savable.
|
|
9810
|
+
|
|
9811
|
+
## §8.63 · The `dream_report` objective draft/review wiring
|
|
9812
|
+
|
|
9813
|
+
The concrete `dream_report` field on the objective draft/review/save path — deliberately NOT
|
|
9814
|
+
a generic companion abstraction (one consumer, one kind). The dream arms are structurally
|
|
9815
|
+
reachable only inside a `perk learn dream` launch (§8.65 — a session outside one has no
|
|
9816
|
+
run-scoped `dream-manifest.json`). Absence-compatible by construction: every existing
|
|
9817
|
+
objective path stays **byte-identical** without the field.
|
|
9818
|
+
|
|
9819
|
+
**The shared param vocabulary.** `objective_draft` and `objective_save` both carry an
|
|
9820
|
+
optional `dream_report` parameter embedding the §8.62 `DREAM_REPORT_INPUT_SCHEMA` by
|
|
9821
|
+
identifier as `DREAM_REPORT_PARAM_SCHEMA` (`extension/factories/objectiveDraft.ts` — the leaf
|
|
9822
|
+
owning the shared vocabulary, the `DELIVERY_PARAM_SCHEMA`/`ROADMAP_PARAM_SCHEMA` pattern)
|
|
9823
|
+
plus the gate description ("required inside a dream session, refused outside one"). The
|
|
9824
|
+
shared `decodeObjectiveSaveParams` decodes it as a tri-state plain object (absent →
|
|
9825
|
+
`undefined`, present-but-not-a-plain-object → strict-fail); deep validation stays with the
|
|
9826
|
+
gate resolver.
|
|
9827
|
+
|
|
9828
|
+
**The ONE gate resolver.** `resolveDreamReportGate(ctx, input, generatedAt)`
|
|
9829
|
+
(`extension/factories/objectiveDreamReport.ts`) implements the whole matrix ONCE — both
|
|
9830
|
+
`writeObjectiveDraft` and `saveObjective` consume its typed outcome
|
|
9831
|
+
(`absent` | `block` | `refuse{errorType, detail}`); no parallel branch/message
|
|
9832
|
+
implementations. "Dream session" is detected structurally, exactly like `run_dream_wave`: the
|
|
9833
|
+
session's claimed `run_id` + the existence of `runScratchDir(run_id)/dream-manifest.json` (no
|
|
9834
|
+
claimed run counts as non-dream). The matrix (identical at draft-write and save): non-dream +
|
|
9835
|
+
absent → `absent` (unchanged, byte-identical behavior); non-dream + present → refuse
|
|
9836
|
+
`invalid_input` (refusing rather than silently dropping it); dream + absent → refuse
|
|
9837
|
+
`invalid_input` (the objective and its report review as ONE bundle — draft-time enforcement
|
|
9838
|
+
means a report-less dream bundle can never reach review, so an approval is always savable,
|
|
9839
|
+
the §8.62 "validates BEFORE review" promise); dream + present → recover trusted context →
|
|
9840
|
+
**the revalidation-bracket re-check** (§8.65 — after context recovery authenticates the
|
|
9841
|
+
manifest, `bracket(ctx.cwd, manifest.commit_sha)` runs at draft-write AND save, both
|
|
9842
|
+
consumers flowing through this one resolver; drift refuses `bad_state` — "the repository
|
|
9843
|
+
moved since the dream snapshot (<detail>) — the analysis is stale; re-run perk learn dream";
|
|
9844
|
+
non-dream paths never reach the bracket; the resolver's optional fourth parameter defaults to
|
|
9845
|
+
the production `revalidationBracket`, injected only by tests) →
|
|
9846
|
+
`buildDreamReport(input, context)` → refuse on any failure, else yield the block. The gate
|
|
9847
|
+
reads ONE workflow-state snapshot with error distinction: an UNREADABLE state (a throwing
|
|
9848
|
+
branch read) refuses `bad_state` BEFORE the matrix — never conflated with a confirmed
|
|
9849
|
+
non-dream session (a transient read failure must not surface as `absent`). Failure
|
|
9850
|
+
taxonomy (soft results, never throws): gate violations and `buildDreamReport` validation
|
|
9851
|
+
refusals → `invalid_input` (the bounded ≤25 named details ride the message, newline-joined);
|
|
9852
|
+
an unreadable workflow state, context-recovery failures (missing/stale/tampered/undecodable
|
|
9853
|
+
run-scratch state — "re-run the dream wave"), and the save-time stored-parts mismatch →
|
|
9854
|
+
`bad_state`.
|
|
9855
|
+
|
|
9856
|
+
**Trusted-context recovery** (module-internal, fail-closed, every arm a named detail):
|
|
9857
|
+
(1) read + parse the run-scoped manifest and `decodeDreamManifest(raw, manifestPath)` (the
|
|
9858
|
+
strict §8.60 decoder, path bound at decode time; no `verifyDocContainment` — the report path
|
|
9859
|
+
reads no doc files, so the lexical decode suffices; resolved containment stays the wave
|
|
9860
|
+
tool's pre-spawn concern); (2) **the freshness check** — the `dream_bundle_digest` marker
|
|
9861
|
+
(§8.3/§8.61, read from the gate's one workflow-state snapshot) must be present, non-empty,
|
|
9862
|
+
and equal the digest of the bundle bytes just read (missing/empty/mismatch refuses); (3)
|
|
9863
|
+
`decodeFinalizedDreamBundle(parsedBundle, manifest, digest-of-manifest-bytes-just-read)`
|
|
9864
|
+
(§8.61 — the analyses-only mid-wave shape refuses here, and the bundle's bound
|
|
9865
|
+
`manifest_digest` authenticates the manifest itself: the marker covers the bundle bytes and
|
|
9866
|
+
the bundle covers the manifest bytes, so an at-rest manifest edit refuses); the recovered
|
|
9867
|
+
context is `{manifest, analyses, reducers, run_id, generated_at}`.
|
|
9868
|
+
|
|
9869
|
+
**The artifact block.** A valid dream draft stores `dream_report: {input, generated_at,
|
|
9870
|
+
parts}` in `objective-draft.json` — **tool-written only** (the model never writes the
|
|
9871
|
+
artifact): `writeObjectiveDraft` runs the gate, stamps `generated_at` ONCE
|
|
9872
|
+
(`new Date().toISOString()`), and stores the validated input beside the rendered CANONICAL
|
|
9873
|
+
parts. `readObjectiveDraft` validates the block via `decodeDreamReportBlock` (a plain-object
|
|
9874
|
+
`input`, a non-blank `generated_at`, a non-empty all-string `parts`) and refuses the WHOLE
|
|
9875
|
+
draft on a malformed block (warn + `null`) — deliberately stricter than the lenient
|
|
9876
|
+
junk→absent handling of `base`/`delivery` (§8.1).
|
|
9877
|
+
|
|
9878
|
+
**One approval bundle.** `renderObjectiveDraft` appends the stored parts as the final section
|
|
9879
|
+
(`trimEnd()` + `"\n\n"` + `parts.join("\n\n")` + `"\n"`; the parts carry their own
|
|
9880
|
+
`# Dream report — <run_id>` headers), so the review surfaces need ZERO plumbing:
|
|
9881
|
+
`plan_review`'s objective arm and the browser door both review via
|
|
9882
|
+
`readObjectiveDraft` + `renderObjectiveDraft`, the browser's stale-draft guard covers the
|
|
9883
|
+
report bytes for free (it compares raw artifact bytes), and DENY routes the ordinary
|
|
9884
|
+
full-redraft `objective_draft` loop (no new machinery).
|
|
9885
|
+
|
|
9886
|
+
**Save-time re-validation.** `saveObjective` accepts `dream_report` as ONE carrier with two
|
|
9887
|
+
sources: the direct tool path wraps only a PRESENT decoded value as `{input}` (the save
|
|
9888
|
+
stamps `generated_at`; an `{input: undefined}` carrier is never constructed — presence is the
|
|
9889
|
+
`opts.dream_report === undefined` boundary); the approval path (`objectiveApprovalSave`)
|
|
9890
|
+
passes the artifact block through whole — stored stamp AND stored parts. Before the cold-door
|
|
9891
|
+
call the gate re-runs against freshly recovered context, and when stored parts are present
|
|
9892
|
+
they are byte-compared (`JSON.stringify` equality) against the re-rendered parts — a mismatch
|
|
9893
|
+
(run-scratch drift or artifact tamper between draft-write and save) refuses `bad_state` with
|
|
9894
|
+
nothing saved and the read-only gate left on. The one `generated_at` stamp is what keeps the
|
|
9895
|
+
re-render deterministic. On success the parts cross to the Python save door through the
|
|
9896
|
+
run-scoped transfer file and are durably persisted as the report companion — **§8.64** (which
|
|
9897
|
+
superseded the original §8.63 deferral); no new cold-door flag exists.
|
|
9898
|
+
No new ok-details fields on either tool (the visible review bundle and the normal ok results
|
|
9899
|
+
already carry the signal). `STAGE_TOOLS`/`READ_ONLY_TOOLS` are untouched: `objective_draft`
|
|
9900
|
+
stays read-only-safe (the dream arm only READS run scratch; the marker rides the ordinary
|
|
9901
|
+
session-entry channel), `objective_save` stays gate-excluded.
|
|
9902
|
+
|
|
9903
|
+
## §8.64 · Dream companion persistence + convergent save ordering
|
|
9904
|
+
|
|
9905
|
+
The dream save's durable half (Objective #1892 Node 4.3): the run-scoped **transfer file**
|
|
9906
|
+
carries the reviewed CANONICAL parts from the extension to the `perk objective create` save
|
|
9907
|
+
door; the door stamps `origin` (§8.24), re-checks the open-by-origin conflict, persists the
|
|
9908
|
+
parts as the immutable **dream report companion** on the objective's **report carrier**, and
|
|
9909
|
+
publishes the per-backend human artifact — all BEFORE activation (activation stays LAST, cold-
|
|
9910
|
+
door success only). The producing session is the `perk learn dream` launch (§8.65).
|
|
9911
|
+
|
|
9912
|
+
**The transfer file.** `dream-report-transfer.json`, run-scoped scratch
|
|
9913
|
+
(`run_scratch_dir(root, run_id)`), filename constant mirrored in both planes
|
|
9914
|
+
(`perk.learn.dream_companion.DREAM_REPORT_TRANSFER_FILENAME` ↔
|
|
9915
|
+
`extension/factories/objectiveSave.ts` — parity-pinned): `{schema_version: "1", run_id, parts}`.
|
|
9916
|
+
Written atomically by `saveObjective` on the dream arm only — after the gate yields `block`
|
|
9917
|
+
(and after the approval-path byte-compare), BEFORE the cold door; a write throw is the soft
|
|
9918
|
+
`errorType: "scratch_failed"` failure (the `runColdDoor` stdin-staging precedent) — the cold
|
|
9919
|
+
door is NOT invoked, nothing activates, the read-only gate stays on. Non-dream saves write
|
|
9920
|
+
nothing (byte-identical). **No `origin` field**: the transfer has one producer and one meaning,
|
|
9921
|
+
so the door derives `ObjectiveOrigin.LEARN_DREAM` on the validated dream arm itself — origin
|
|
9922
|
+
stays launch-owned (no `--origin` flag exists; manual/direct saves have no transfer file and
|
|
9923
|
+
never stamp it; a manual retry with the same `--run-id` IS convergence). `run_id` is the
|
|
9924
|
+
cross-run mismatch guard. Door-side decode is a `StrictInputModel`
|
|
9925
|
+
(`DreamReportTransferModel`): `schema_version` literal `"1"`, `run_id` must equal the door's
|
|
9926
|
+
resolved run id, `parts` a non-empty list of non-empty strings — malformed refuses
|
|
9927
|
+
`invalid_input` (never ignored). **Structural launch evidence:** a present transfer requires
|
|
9928
|
+
the run-scoped `dream-manifest.json`; transfer-without-manifest refuses `invalid_input`. A
|
|
9929
|
+
transfer combined with `--supersedes`/`--adopt-from` refuses `invalid_input`. `--dry-run`
|
|
9930
|
+
stays fully offline and byte-identical (the transfer arc, the guard, and the companion are all
|
|
9931
|
+
skipped; payload unchanged).
|
|
9932
|
+
|
|
9933
|
+
**Save-door ordering (the dream arm, strictly).** (1) transfer decode + the shared
|
|
9934
|
+
part-invariance rule — before ANY network, the GitHub auth probe included (`require_github`
|
|
9935
|
+
runs only after the offline refusal ladder, so a malformed transfer refuses `invalid_input`
|
|
9936
|
+
even unauthed — never masked by `github_unauthed`); an unreadable / invalid-UTF-8 transfer
|
|
9937
|
+
file refuses the same `invalid_input`, never a raw crash; (2) the existing stacked block stays byte-positioned (strict train validation +
|
|
9938
|
+
`Delivery.prepare` exactly where they run today — a dream+stacked save keeps the
|
|
9939
|
+
pre-persistence capability gate); (3) the **origin conflict re-check** immediately before
|
|
9940
|
+
create: `find_open_objective_by_origin(origin=LEARN_DREAM, exclude_run_id=<run id>)` — a
|
|
9941
|
+
returned ref refuses `error_type="origin_conflict"` naming the existing objective id + url;
|
|
9942
|
+
exhaustive-or-raise means a lookup failure fails closed; the residual re-check→create race is
|
|
9943
|
+
documented (below), not closed — guard adjacency to create minimizes the window; (4)
|
|
9944
|
+
`create_objective(…, origin=LEARN_DREAM)` — origin stamped atomically at initial creation;
|
|
9945
|
+
the find-then-return `run_id` idempotency recovers an interrupted dream save's later steps;
|
|
9946
|
+
(5) companion convergence: carrier = `journal_carrier_id(objective_id)` (`None` → raise) →
|
|
9947
|
+
`persist_parts` (create-once, byte-compared) → `publish_dream_artifact` (Linear real / GitHub
|
|
9948
|
+
no-op) → `update_objective_header({"dream_report": <carrier_id>})` LAST; (6)
|
|
9949
|
+
`post_status_update` stays byte-positioned (after the try-block, fresh-create only) — the
|
|
9950
|
+
accepted consequence: a companion failure after create skips it, and the converging retry
|
|
9951
|
+
(`existed=True`) skips it permanently (bookkeeping, never load-bearing); (7) the `--json`
|
|
9952
|
+
payload is byte-identical (no new machine fields — the header field is the durable reference);
|
|
9953
|
+
one `user_output` narration line reports the converged part count + carrier; (8) activation
|
|
9954
|
+
LAST: `saveObjective` appends `active_objective` + the budget marker only after cold-door
|
|
9955
|
+
success — any failure in 1–5 exits non-zero, nothing activates, the gate stays on, retry
|
|
9956
|
+
converges. Failure mapping: `CompanionConflictError` → `error_type="companion_conflict"`;
|
|
9957
|
+
`CompanionAppendAmbiguous` → `"companion_ambiguous"`.
|
|
9958
|
+
|
|
9959
|
+
**The companion core** (`perk/learn/dream_companion.py` — backend contracts only; mirrors the
|
|
9960
|
+
§8.43 journal disciplines without reusing the journal). The **report carrier** is
|
|
9961
|
+
`ObjectiveStore.journal_carrier_id` — GitHub: the objective issue itself (normalized id);
|
|
9962
|
+
Linear: the Project metadata sentinel's identifier. **Marker grammar** (dual-encoding):
|
|
9963
|
+
canonical HTML `<!-- perk:learn-dream-report:<run_id>:<i> -->`; the parser also accepts the
|
|
9964
|
+
inline-code rewrite `` `perk:learn-dream-report:<run_id>:<i>` ``. `<i>` is a canonical 1-based
|
|
9965
|
+
decimal (no leading zeros/signs — any other spelling in a marked comment is corruption).
|
|
9966
|
+
Marker-text detection is substring-based; a comment carrying the marker text MUST parse
|
|
9967
|
+
strictly (marker on the PHYSICAL first line — leading blank lines/indentation are corruption,
|
|
9968
|
+
never normalized away, for foreign-run comments identically; identity from the first line;
|
|
9969
|
+
marker-text count > 1 in one body = corruption; an edited marked comment = corruption); an
|
|
9970
|
+
unmarked comment is unrelated untrusted DATA. A comment body is `marker + blank line + part`. **Dual-candidate
|
|
9971
|
+
byte-identity:** a stored body converges iff byte-equal to the verbatim render OR the local
|
|
9972
|
+
transcode candidate (the marker-line inline-code rewrite derived by the same rule as
|
|
9973
|
+
`to_linear_markdown`, never imported from the Linear backend — with invariant content the only
|
|
9974
|
+
rewritten line is perk's own marker line, so the candidate is exact). **`persist_parts`**: one
|
|
9975
|
+
complete `read_comments` scan (foreign-run companion comments parse strictly but never
|
|
9976
|
+
participate; an index outside `1..N` for this run is corruption — a stale longer render never
|
|
9977
|
+
silently tolerated; conflicting duplicates under one key are corruption); per index, present +
|
|
9978
|
+
byte-equal → idempotent skip, differing → loud `CompanionConflictError`; absent → POST with
|
|
9979
|
+
the rescan-one-retry ambiguity policy (a raised POST is AMBIGUOUS; the complete rescan
|
|
9980
|
+
decides; only proven absence earns the one retry; a failed rescan or a still-unproven write
|
|
9981
|
+
raises `CompanionAppendAmbiguous` — never a blind re-POST).
|
|
9982
|
+
|
|
9983
|
+
**The three-point invariance rule** (ONE shared rule, parity-pinned fixtures across both
|
|
9984
|
+
planes — `tests/parity/dream_report_invariance.json`): parts must be **transcode-invariant** —
|
|
9985
|
+
non-empty; no perk HTML-comment marker; no literal `perk:learn-dream-report` marker text; no
|
|
9986
|
+
exact `<details><summary><code>…</code></summary>` / `</details>` wrapper line (the shapes
|
|
9987
|
+
`to_linear_markdown` rewrites/drops); no line boundary other than `\n` (`\r`, VT, FF, FS/GS/RS,
|
|
9988
|
+
NEL, U+2028/U+2029 — the transcoder's `splitlines()` + `"\n".join` normalizes every other form,
|
|
9989
|
+
which would defeat the dual-candidate byte comparison forever); full comment body ≤ 65,000 chars
|
|
9990
|
+
(`COMPANION_COMMENT_MAX_CHARS`, the backstop under GitHub's 65,536; §8.62 already caps parts
|
|
9991
|
+
at 60,000 code points). Enforced at THREE points: TS-side in the gate resolver (draft-write
|
|
9992
|
+
AND save — approved ⇒ savable), door-side at transfer decode (before anything durable), and as
|
|
9993
|
+
the `persist_parts` pre-POST backstop.
|
|
9994
|
+
|
|
9995
|
+
**The per-backend human artifact** (`publish_dream_artifact(repo_root, …)` in
|
|
9996
|
+
`perk/backends/resolve.py`, ONE function keyed off the committed `[issues]` selection — no
|
|
9997
|
+
strategy objects). GitHub → an **immediate return** (the marker-keyed parts on the objective
|
|
9998
|
+
issue ARE the visible artifact). Linear → the `perk/backends/linear/dream_report.py`
|
|
9999
|
+
`publish_dream_artifact` flow: presence probe first (the new `project_external_links` read —
|
|
10000
|
+
skip when a Resources link labeled `Dream report (<run_id>)` exists), else
|
|
10001
|
+
`LinearClient.upload_file(filename="dream-report-<run_id>.md", content_type="text/markdown",
|
|
10002
|
+
content)` — one call owning the whole `fileUpload` reservation (sized by the actual bytes) +
|
|
10003
|
+
signed-PUT choreography (the PUT rides the same injectable transport, propagating the
|
|
10004
|
+
reservation headers + `Content-Type`) and returning the asset URL — → the existing
|
|
10005
|
+
`create_entity_external_link(project_id, label, url=<assetUrl>)`. The uploaded bytes are the
|
|
10006
|
+
canonical parts joined `"\n\n"` — verbatim, never transcoded (a file asset, not a comment).
|
|
10007
|
+
**Fail-loud** (part of the convergent sequence, never fail-open bookkeeping) — each boundary
|
|
10008
|
+
failure fails the save; retry converges. Uploaded assets are workspace-auth-gated (fine — the
|
|
10009
|
+
artifact serves workspace humans). Non-binding recorded intent: once `gh` grows general-file
|
|
10010
|
+
issue attachment (cli/cli#14194; today's preview `--attach` is images/video only), the GitHub
|
|
10011
|
+
arm may attach the rendered report file — a desire, not a promise.
|
|
10012
|
+
|
|
10013
|
+
**The `dream_report` header field.** `OBJECTIVE_HEADER_FIELDS` gains `dream_report` —
|
|
10014
|
+
merge-writable by design (recorded AFTER create per the activation-last ordering; `origin`
|
|
10015
|
+
stays excluded). Value = the carrier id only (GitHub: the objective issue's own normalized
|
|
10016
|
+
number; Linear: the sentinel identifier) — the header already carries the run's `run_id`,
|
|
10017
|
+
parts are keyed `(run_id, index)`, and discovery is the marker scan (no comment-id list). No
|
|
10018
|
+
`ObjectiveHeader` dataclass field; a superseding successor deliberately does not carry it (the
|
|
10019
|
+
report stays with the run that produced it).
|
|
10020
|
+
|
|
10021
|
+
**Documented residuals** (accepted, not closed): (1) the origin-guard re-check→create race
|
|
10022
|
+
(non-atomic by design; adjacency minimizes the window); (2) the store-tier interrupted-create
|
|
10023
|
+
posture — `create_objective`'s find-then-return on a `run_id` hit does not converge a
|
|
10024
|
+
partially created objective (a pre-existing window shared by every objective save; this node's
|
|
10025
|
+
OWN steps are each convergent); (3) the Linear pre-sentinel orphan window — a project created
|
|
10026
|
+
but its sentinel create crashed is invisible to `find_objective` AND
|
|
10027
|
+
`find_open_objective_by_origin`; a dream retry creates a fresh project and the orphan lingers.
|
|
10028
|
+
Guidance: an orphan is a project with no `Perk: objective metadata` issue and no perk header
|
|
10029
|
+
attachment — delete it manually; it carries no perk state and no report parts; (4) the Linear
|
|
10030
|
+
orphan-asset window — a crash after `fileUpload`/PUT but before the link write leaves an
|
|
10031
|
+
uploaded asset with no discoverable run key; the retry uploads a fresh asset and links it;
|
|
10032
|
+
unreferenced workspace assets are inert.
|
|
10033
|
+
|
|
10034
|
+
## §8.65 · The learn-dream activation (door + preflight + revalidation bracket)
|
|
10035
|
+
|
|
10036
|
+
**The door.** `perk learn dream` — a seeded cold door (the seeded-door pipeline,
|
|
10037
|
+
`perk/cli/commands/learn/dream_cmd.py`) borrowing the `objective-author` stage descriptor via
|
|
10038
|
+
`prompt_override` (no new registry stage), `binding_trigger="command:learn-dream"`, options =
|
|
10039
|
+
exactly the shared seeded-door family (`--worktree`, `--dry-run`, `--remote` local-only,
|
|
10040
|
+
`--json`, `--no-sync` phrased for the pre-gather sync, trailing `pi_args`). **No `--from`
|
|
10041
|
+
exists, and the door actively rejects the spelling**: the gather closure scans `pi_args`
|
|
10042
|
+
FIRST — before the banner, the sync, or any side effect — and any token `== "--from"` or
|
|
10043
|
+
starting with `--from=` refuses `invalid_input` naming `perk learn harvest --from` as the
|
|
10044
|
+
partial-corpus door; every other unknown token keeps the family's pi passthrough semantics.
|
|
10045
|
+
Cold-only (no warm `/learn-dream` door — an objective non-goal) and backend-light: no
|
|
10046
|
+
`require_github`, `backend_errors=()` — the only backend read is the origin guard below,
|
|
10047
|
+
wrapped explicitly.
|
|
10048
|
+
|
|
10049
|
+
**The preflight (ordering, exact).** (1) the `--from` rejection above; (2)
|
|
10050
|
+
`launch.resolve_target(stage, remote)` — the local-only rejection before any side effect;
|
|
10051
|
+
(3) the gated launch banner; (4) the **`GitError → git_error` fail-closed boundary** opens —
|
|
10052
|
+
one `try/except git.GitError` around every git probe below (the `create_cmd.py` precedent):
|
|
10053
|
+
an unprovable probe (e.g. `git status` cannot run) becomes a typed `git_error` refusal, never
|
|
10054
|
+
a traceback and never an assumed-clean snapshot; (4a) the ONE pre-gather guarded fast-forward
|
|
10055
|
+
(`_sync_main_checkout`, only when `not dry_run and not no_sync`; `run_seeded_door` gets
|
|
10056
|
+
`no_sync=True` unconditionally so the in-launch sync never fires — the harvest
|
|
10057
|
+
one-revision-boundary discipline); (4b) the **single SHA capture** — the STRICT resolver
|
|
10058
|
+
`git.head_commit` runs exactly ONCE per invocation: an UNBORN head (a `--verify --quiet`
|
|
10059
|
+
verification failure) refuses `invalid_input` ("commit once"), while any other probe failure
|
|
10060
|
+
raises `GitError` into the boundary's `git_error` arm — a broken probe never misreports as
|
|
10061
|
+
"commit once"; (4c) the **clean-checkout requirement** — `git.is_dirty` (untracked included)
|
|
10062
|
+
refuses the distinct type `dirty_checkout` (a distinct repair action: commit/stash); runs on
|
|
10063
|
+
`--dry-run` too; (4d) the **index-flag refusal** — `git.index_flagged_paths` (any
|
|
10064
|
+
assume-unchanged or skip-worktree entry, the `ls-files -v` lowercase/`S` tags) refuses
|
|
10065
|
+
`invalid_input` naming the flagged paths (≤10 shown): either bit hides edits from
|
|
10066
|
+
`git status`, so 4c's proof would not be a proof (a sparse checkout is the common producer);
|
|
10067
|
+
(5) the gather (`gather_dream`) — its §8.59 refusals pass through the door envelope
|
|
10068
|
+
unchanged; (6) the **tracked-corpus rule** (still inside the GitError boundary), BOTH
|
|
10069
|
+
directions — the gathered doc-path set must EQUAL the tracked learned corpus (the tracked
|
|
10070
|
+
`docs/learned/**/*.md` set minus the generated `index.md` — the `read_learned_docs`
|
|
10071
|
+
enumeration rule): gathered ⊆ tracked because `git status --porcelain` omits gitignored
|
|
10072
|
+
files, so an IGNORED doc could be gathered while the tree reports clean (refused
|
|
10073
|
+
`invalid_input` naming every offender as not reproducible from the stamped commit;
|
|
10074
|
+
plain-untracked docs already refused at 4c); tracked ⊆ gathered because the gather
|
|
10075
|
+
enumerates the FILESYSTEM, so a tracked doc absent from disk (a sparse/skip-worktree
|
|
10076
|
+
checkout) would silently narrow a "whole-corpus" audit (refused `invalid_input` naming every
|
|
10077
|
+
missing doc); (7)
|
|
10078
|
+
the **pre-launch active-origin guard** (real launch only — skipped entirely on `--dry-run`,
|
|
10079
|
+
which stays offline): `resolve_objective_store(repo_root)` +
|
|
10080
|
+
`find_open_objective_by_origin(origin=LEARN_DREAM, exclude_run_id=None)` wrapped in ONE
|
|
10081
|
+
`try/except (ObjectiveStoreError, IssueBackendError)` → `origin_lookup_failed` (**fail-closed**
|
|
10082
|
+
— the wrap covers the store resolution AND the lookup); a returned ref refuses
|
|
10083
|
+
`origin_conflict` naming `#<id>` + url ("complete or close it before dreaming again"); the
|
|
10084
|
+
guard runs BEFORE the run id is minted and before any scratch write (`exclude_run_id=None`
|
|
10085
|
+
because a freshly minted run can have no stored objective; the §8.64 save-time re-check owns
|
|
10086
|
+
the current-run exclusion); (8) mint + `write_manifest` (`OSError` → `manifest_write_failed`);
|
|
10087
|
+
(9) the seed render — `stages/learn-dream.md` with exactly three string vars
|
|
10088
|
+
(`manifest_path`, `doc_count`, `lane_count`; lane ids/cluster names stay DATA in the
|
|
10089
|
+
manifest, never interpolated into instruction text).
|
|
10090
|
+
|
|
10091
|
+
**The `--dry-run` posture.** Offline (the origin guard is never evaluated), side-effect-free
|
|
10092
|
+
outside run scratch, validates ALL local preconditions (the `--from` rejection, HEAD, the
|
|
10093
|
+
clean check, the gather refusals, the tracked-corpus rule), and **materializes on dry-run**
|
|
10094
|
+
(the manifest is written — the harvest posture). The full `--json` dry-run payload keys,
|
|
10095
|
+
exactly: `{success, error_type, manifest_path, commit_sha, registry_mode, doc_count,
|
|
10096
|
+
lane_count, lane_ids, total_bytes, origin_guard: "not-evaluated", launched: false}`.
|
|
10097
|
+
|
|
10098
|
+
**The error vocabulary** (one envelope): `remote_blocked`, `invalid_input` (the `--from`
|
|
10099
|
+
spelling, an unborn HEAD, the index-flag refusal, the gather's §8.59 `invalid_input` arms,
|
|
10100
|
+
both tracked-corpus arms), `dirty_checkout`, `git_error`, `no_learned_docs`, `invalid_registry`, `incomplete_registry`,
|
|
10101
|
+
`origin_conflict`, `origin_lookup_failed`, `manifest_write_failed`, `not_a_repo`. Stable
|
|
10102
|
+
exits: `0` ok · `1` op-failure/refusal · `2` not-a-repo.
|
|
10103
|
+
|
|
10104
|
+
**The carrier map** (§8.57): the seed (`prompts/stages/learn-dream.md`) is the launch-flow
|
|
10105
|
+
carrier — the manifest read, the ONE no-argument `run_dream_wave` call (single-lane included
|
|
10106
|
+
— dream has no direct-analysis path), the uniform incomplete rule (ANY tool failure — a
|
|
10107
|
+
pre-spawn `bad_state`/`bad_input` refusal, any `io_error` arm, or an ok aggregate with
|
|
10108
|
+
`complete: false`, the drifted bracket included — is an INCOMPLETE audit: report honestly,
|
|
10109
|
+
STOP before `objective_draft`, no retry, never a direct corpus read), the clean-audit stop,
|
|
10110
|
+
and the review-first authoring loop. The seed hardcodes NO skill pointer; the
|
|
10111
|
+
`perk-learn-dream` skill's read path rides the `command:learn-dream` nudge binding (§8.9),
|
|
10112
|
+
and the skill carries the judgment detail only: the closed dispositions, the destructive
|
|
10113
|
+
evidence bar + disagreement rule (downgrade-only), the truth-then-leverage ranking, the
|
|
10114
|
+
unit/≤12-distinct-node selection shape, the report-only harvest follow-ups, the
|
|
10115
|
+
`dream_report` param fields, and the `perk-objective-author` cross-reference.
|
|
10116
|
+
|
|
10117
|
+
**The revalidation bracket.** `revalidationBracket(cwd, expectedSha, probes?)`
|
|
10118
|
+
(`extension/substrate/git.ts`) — the module's ONE deliberately **fail-closed** composition:
|
|
10119
|
+
drift when HEAD cannot be resolved, when HEAD ≠ `expectedSha` (naming both SHAs), when
|
|
10120
|
+
cleanliness cannot be verified, when the tree is dirty, when the index-flag state cannot be
|
|
10121
|
+
verified, or when the index carries assume-unchanged/skip-worktree flags
|
|
10122
|
+
(`indexHidesChanges` — either bit hides edits from the status probe, the same hazard the
|
|
10123
|
+
door's 4d refusal closes at launch time); ok otherwise. `/.perk/workflow/`
|
|
10124
|
+
is gitignored, so run-scratch writes (the manifest, `dream-analyses.json`) never trip the
|
|
10125
|
+
tree-clean check. Two wiring points, by reference: the post-wave check inside
|
|
10126
|
+
`executeDreamWave` (§8.61 — after both waves complete, BEFORE the finalize write; drift skips
|
|
10127
|
+
the finalize AND the marker set, so a drifted wave is structurally undraftable) and the
|
|
10128
|
+
before-drafting/save re-check inside `resolveDreamReportGate` (§8.63 — after context
|
|
10129
|
+
recovery, at draft-write AND save; drift refuses `bad_state` with "re-run perk learn dream").
|
|
10130
|
+
|
|
10131
|
+
**The claim, narrowed honestly.** The bracket proves **end-state equality** — HEAD unchanged
|
|
10132
|
+
and tree clean at each check against the stamped `commit_sha` — never mid-wave byte
|
|
10133
|
+
immutability. Accepted residuals (documented, not closed): (1) a transient
|
|
10134
|
+
modify-and-restore during the wave window is invisible (a revalidation bracket, not a frozen
|
|
10135
|
+
checkout — no physically frozen/materialized-commit snapshot in v1, the objective's stated
|
|
10136
|
+
non-goal — a transient flag-edit-unflag inside the window is the same class); (2) an
|
|
10137
|
+
ignored `docs/learned` file appearing MID-session is invisible to the
|
|
10138
|
+
tree-clean check (launch-time trackedness is door-enforced; the mid-session blind spot is the
|
|
10139
|
+
same window class); (3) the §8.64 gate-check→create race window is unchanged (the save-time
|
|
10140
|
+
origin re-check + adjacency own it).
|