froid-loop 0.11.1__py3-none-any.whl

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.
Files changed (116) hide show
  1. froid_loop/__init__.py +11 -0
  2. froid_loop/__main__.py +12 -0
  3. froid_loop/adapters/__init__.py +3 -0
  4. froid_loop/adapters/base.py +254 -0
  5. froid_loop/adapters/entrypoints.py +63 -0
  6. froid_loop/adapters/env_fault.py +290 -0
  7. froid_loop/adapters/generic.py +2013 -0
  8. froid_loop/adapters/mock.py +49 -0
  9. froid_loop/adapters/multiplexer.py +914 -0
  10. froid_loop/adapters/opencode_http.py +1687 -0
  11. froid_loop/adapters/profile.py +650 -0
  12. froid_loop/adapters/psmux_backend.py +1428 -0
  13. froid_loop/adapters/registry.py +322 -0
  14. froid_loop/adapters/tmux_backend.py +35 -0
  15. froid_loop/adapters/tmux_base.py +630 -0
  16. froid_loop/checks.py +187 -0
  17. froid_loop/cli.py +5041 -0
  18. froid_loop/data/__init__.py +0 -0
  19. froid_loop/data/froid_loop_hook.py +228 -0
  20. froid_loop/data/froid_loop_probe_hook.py +88 -0
  21. froid_loop/data/plugins/example/plugin.toml +21 -0
  22. froid_loop/data/plugins/tea/plugin.toml +184 -0
  23. froid_loop/data/plugins/tea/tea_plugin.py +258 -0
  24. froid_loop/data/plugins/unity/plugin.toml +140 -0
  25. froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef +16 -0
  26. froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef.meta +7 -0
  27. froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs +221 -0
  28. froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs.meta +11 -0
  29. froid_loop/data/plugins/unity/unity_assets/_folders/Editor.meta +8 -0
  30. froid_loop/data/plugins/unity/unity_assets/_folders/FroidLoop.meta +8 -0
  31. froid_loop/data/plugins/unity/unity_cleanup.py +125 -0
  32. froid_loop/data/plugins/unity/unity_dialog_probe.py +239 -0
  33. froid_loop/data/plugins/unity/unity_facts.md +17 -0
  34. froid_loop/data/plugins/unity/unity_plugin.py +415 -0
  35. froid_loop/data/plugins/unity/unity_quiesce.py +234 -0
  36. froid_loop/data/plugins/unity/unity_ready.py +230 -0
  37. froid_loop/data/plugins/unity/unity_seed_assets.py +298 -0
  38. froid_loop/data/plugins/unity/unity_setup.py +551 -0
  39. froid_loop/data/plugins/unity/unity_teardown.py +362 -0
  40. froid_loop/data/profiles/antigravity.toml +52 -0
  41. froid_loop/data/profiles/claude.toml +85 -0
  42. froid_loop/data/profiles/codex.toml +22 -0
  43. froid_loop/data/profiles/copilot.toml +52 -0
  44. froid_loop/data/profiles/gemini.toml +26 -0
  45. froid_loop/data/profiles/opencode.toml +54 -0
  46. froid_loop/data/settings/core.toml +458 -0
  47. froid_loop/data/skills/README.md +93 -0
  48. froid_loop/data/skills/froid-loop-resolve/SKILL.md +288 -0
  49. froid_loop/data/skills/froid-loop-setup/SKILL.md +161 -0
  50. froid_loop/data/skills/froid-loop-setup/assets/module-help.csv +3 -0
  51. froid_loop/data/skills/froid-loop-setup/assets/module.yaml +19 -0
  52. froid_loop/data/skills/froid-loop-sweep/SKILL.md +100 -0
  53. froid_loop/data/skills/froid-loop-sweep/automation-mode.md +127 -0
  54. froid_loop/data/skills/froid-loop-sweep/deferred-work-format.md +302 -0
  55. froid_loop/data/skills/froid-loop-sweep/migration-mode.md +86 -0
  56. froid_loop/decisions.py +202 -0
  57. froid_loop/deferredwork.py +2282 -0
  58. froid_loop/devcontract.py +892 -0
  59. froid_loop/diagnostics.py +1104 -0
  60. froid_loop/documents.py +532 -0
  61. froid_loop/engine.py +7732 -0
  62. froid_loop/envvars.py +111 -0
  63. froid_loop/escalation.py +225 -0
  64. froid_loop/events.py +266 -0
  65. froid_loop/fences.py +103 -0
  66. froid_loop/froidconfig.py +226 -0
  67. froid_loop/frontmatter.py +526 -0
  68. froid_loop/gates.py +133 -0
  69. froid_loop/install.py +2936 -0
  70. froid_loop/journal.py +178 -0
  71. froid_loop/machine.py +148 -0
  72. froid_loop/model.py +898 -0
  73. froid_loop/operatoractions.py +474 -0
  74. froid_loop/platform_util.py +1490 -0
  75. froid_loop/plugins/__init__.py +64 -0
  76. froid_loop/plugins/bus.py +259 -0
  77. froid_loop/plugins/context.py +319 -0
  78. froid_loop/plugins/loader.py +145 -0
  79. froid_loop/plugins/manifest.py +279 -0
  80. froid_loop/plugins/model.py +296 -0
  81. froid_loop/plugins/registry.py +245 -0
  82. froid_loop/plugins/trust.py +75 -0
  83. froid_loop/policy.py +1569 -0
  84. froid_loop/probe.py +1044 -0
  85. froid_loop/process_host.py +408 -0
  86. froid_loop/recovery_flow.py +1561 -0
  87. froid_loop/resolve.py +283 -0
  88. froid_loop/runs.py +4715 -0
  89. froid_loop/runsetup.py +1293 -0
  90. froid_loop/sanitize.py +593 -0
  91. froid_loop/settings_schema.py +276 -0
  92. froid_loop/signals.py +160 -0
  93. froid_loop/sprintstatus.py +609 -0
  94. froid_loop/statemachine.py +57 -0
  95. froid_loop/stories.py +615 -0
  96. froid_loop/stories_engine.py +796 -0
  97. froid_loop/sweep.py +1892 -0
  98. froid_loop/tokens.py +196 -0
  99. froid_loop/tui/__init__.py +11 -0
  100. froid_loop/tui/app.py +1584 -0
  101. froid_loop/tui/data.py +840 -0
  102. froid_loop/tui/launch.py +1003 -0
  103. froid_loop/tui/screens/__init__.py +1 -0
  104. froid_loop/tui/screens/dashboard.py +1071 -0
  105. froid_loop/tui/screens/modals.py +943 -0
  106. froid_loop/tui/screens/settings_screen.py +477 -0
  107. froid_loop/tui/settings.py +135 -0
  108. froid_loop/tui/widgets.py +981 -0
  109. froid_loop/verify.py +4545 -0
  110. froid_loop/workspace.py +320 -0
  111. froid_loop/worktree_flow.py +2301 -0
  112. froid_loop-0.11.1.dist-info/METADATA +728 -0
  113. froid_loop-0.11.1.dist-info/RECORD +116 -0
  114. froid_loop-0.11.1.dist-info/WHEEL +4 -0
  115. froid_loop-0.11.1.dist-info/entry_points.txt +2 -0
  116. froid_loop-0.11.1.dist-info/licenses/LICENSE +30 -0
@@ -0,0 +1,127 @@
1
+ # Automation Mode
2
+
3
+ You are running unattended inside a `froid-loop` sweep session. No human is
4
+ watching; a deterministic program spawned you, will validate your result.json
5
+ field-by-field, and will kill this session after your final turn.
6
+
7
+ ## Identity & I/O contract
8
+
9
+ - `$FROID_LOOP_RUN_DIR` and `$FROID_LOOP_TASK_ID` are set in your environment.
10
+ - Your **result file** is `$FROID_LOOP_RUN_DIR/tasks/$FROID_LOOP_TASK_ID/result.json`.
11
+ Writing it is the LAST action of the run. Schema:
12
+
13
+ ```json
14
+ {
15
+ "workflow": "deferred-sweep-triage",
16
+ "open_ids": ["DW-1", "DW-3", "..."],
17
+ "already_resolved": [{ "id": "DW-1", "evidence": "<file:line or commit that resolved it>" }],
18
+ "bundles": [
19
+ {
20
+ "name": "<kebab-case-name>",
21
+ "dw_ids": ["DW-3"],
22
+ "intent": "<2-6 sentences: the one cohesive goal>"
23
+ }
24
+ ],
25
+ "blocked": [{ "id": "DW-4", "blocker": "<named story/epic that must land first>" }],
26
+ "skip": [{ "id": "DW-9", "reason": "<why this is moot/superseded>" }],
27
+ "decisions": [
28
+ {
29
+ "id": "DW-7",
30
+ "question": "<the choice the human must make>",
31
+ "context": "<2-4 sentences of code-grounded context>",
32
+ "options": [
33
+ {
34
+ "key": "1",
35
+ "label": "<short label>",
36
+ "effect": "build",
37
+ "intent": "<what a dev session would implement>"
38
+ },
39
+ {
40
+ "key": "2",
41
+ "label": "<short label>",
42
+ "effect": "close",
43
+ "resolution": "<optional: why closing is fine>"
44
+ },
45
+ { "key": "3", "label": "<short label>", "effect": "keep-open" }
46
+ ],
47
+ "recommendation": "1"
48
+ }
49
+ ],
50
+ "escalations": []
51
+ }
52
+ ```
53
+
54
+ - Validation rules the orchestrator enforces (a violation fails the whole
55
+ result and burns a retry):
56
+ - `open_ids` must list exactly the ledger's `status: open` entries — the
57
+ orchestrator parses the ledger itself and compares.
58
+ - Every open id appears in exactly ONE of already_resolved / bundles /
59
+ blocked / skip / decisions. No misses, no duplicates, no invented ids.
60
+ - Bundle names: `^[a-z0-9][a-z0-9-]{1,39}\Z`, unique, non-empty `dw_ids`,
61
+ non-empty `intent`. An otherwise-valid overlong bundle name or decision
62
+ option `bundle_name` is truncated to 40 characters and journaled before
63
+ validation; post-truncation name collisions still fail validation.
64
+ - Every `already_resolved` entry needs non-empty `evidence`; every
65
+ `blocked` a `blocker`; every `skip` a `reason`.
66
+ - Decisions: >= 2 options with unique keys, `effect` one of
67
+ `build|close|keep-open`, `intent` required when effect is `build`,
68
+ `recommendation` must be one of the option keys.
69
+
70
+ - Write `already_resolved[].evidence` and an option's `label` and `resolution`
71
+ as a **single line** — each is copied onto one line of the line-oriented
72
+ deferred-work ledger. This is guidance, not a validation rule: a break is
73
+ collapsed to a space rather than rejected, so it costs nothing but reads
74
+ worse. Both `intent` fields are exempt — keep them at the length the schema
75
+ asks above (2-6 sentences for a bundle), newlines and all.
76
+
77
+ - **Migration sessions** (`--migrate`, see `./migration-mode.md`) use this
78
+ result schema instead:
79
+
80
+ ```json
81
+ {
82
+ "workflow": "deferred-sweep-migrate",
83
+ "mapping": [{ "key": "<manifest key>", "dw_id": "DW-12" }],
84
+ "escalations": []
85
+ }
86
+ ```
87
+
88
+ Validation rules: the rewritten ledger parses with ZERO legacy items;
89
+ pre-existing `### DW-<n>:` entries keep their ids and status; new entries
90
+ continue numbering past the highest existing number with `status: open` or
91
+ `status: done <date>`; `mapping` covers every manifest key exactly once and
92
+ each `dw_id` exists with the manifest's open/done state (two keys may share
93
+ a `dw_id` when merging duplicates of equal done-ness).
94
+
95
+ - Your **escalation file** is `$FROID_LOOP_RUN_DIR/tasks/$FROID_LOOP_TASK_ID/escalation.json`.
96
+ Use it only for blockers no rule resolves (e.g. the ledger is missing or
97
+ unreadable: `type: missing-ledger`, severity `CRITICAL`), then include the
98
+ same entries in result.json `escalations` and end your turn. Schema:
99
+
100
+ ```json
101
+ {
102
+ "escalations": [
103
+ {
104
+ "type": "<short-kebab-kind>",
105
+ "severity": "CRITICAL|PREFERENCE",
106
+ "detail": "<one or two sentences>"
107
+ }
108
+ ]
109
+ }
110
+ ```
111
+
112
+ ## Behavior rules
113
+
114
+ 1. **Never HALT for input. Never ask the user anything.** No greeting, no
115
+ menus, no "what next" offers.
116
+ 2. **Read-only — except migration mode.** In triage you never edit the
117
+ ledger, code, specs, or sprint-status; the orchestrator performs all
118
+ ledger edits deterministically and runs the bundles you propose through
119
+ separate dev/review sessions. A `--migrate` session edits exactly one
120
+ file: the ledger (still never code/specs/sprint-status, never commits).
121
+ 3. **Verify before classifying.** Never trust an entry's own status or wording;
122
+ check the code. An entry that says "open" but is fixed goes to
123
+ already_resolved with evidence.
124
+ 4. **Conservative on human territory.** Frozen-block renegotiations, scope
125
+ reversals, and API-shape changes are decisions, not bundles (see SKILL.md
126
+ Step 3). When in doubt between bundle and decision, choose decision.
127
+ 5. **Never commit, never push, never open an editor.**
@@ -0,0 +1,302 @@
1
+ # Deferred Work Format
2
+
3
+ Canonical entry format for `{implementation_artifacts}/deferred-work.md`. The
4
+ orchestrator owns this file, and two eras of dev session feed it:
5
+
6
+ - **Current (Froid Plane 6.10.1-next.33+).** The unattended primitive
7
+ `froid-build-auto` writes nothing here: it records defer-triaged review findings
8
+ in its spec's frontmatter `deferred:` list, and after the session the
9
+ orchestrator harvests those into canonical entries below, carrying a
10
+ fingerprinted `origin:` that starts with `spec-deferred`, plus `source_spec:`.
11
+ - **Legacy and attended.** Pre-rename primitives (`froid-dev-auto`) and the
12
+ attended `froid-build` append flat `- source_spec:` blocks directly into this
13
+ file; a `froid-loop sweep --migrate` session normalizes them into the canonical
14
+ form, and rewrites freeform pre-DW-format content from older projects wholesale
15
+ (see `./migration-mode.md`; the TUI shows such legacy items read-only until
16
+ then).
17
+
18
+ Either way this file stays the sweep's sole read surface. Multi-goal and token
19
+ splits are a legacy/attended source only — the current unattended primitive does
20
+ not split a multi-goal spec, it records a `multiple-goals` warning in the spec's
21
+ `warnings:` and proceeds.
22
+
23
+ The file is append-only — never rewrite or delete existing entries. The one
24
+ sanctioned rewrite is operator-run archiving (below): closed entries move to
25
+ `deferred-work-archive.md` — body preserved, with an `archived: <date>` marker
26
+ appended after the status line — leaving a stub behind.
27
+
28
+ ## Archiving (`froid-loop sweep --archive`)
29
+
30
+ The operator may periodically move closed entries
31
+ (`status: done <ISO date>`) to the sibling `deferred-work-archive.md`, leaving
32
+ a stub in this file:
33
+
34
+ ```markdown
35
+ ### DW-7: Old closed item
36
+
37
+ status: done 2026-05-25
38
+ archived: 2026-08-24
39
+ ```
40
+
41
+ Rules for sessions reading the ledger:
42
+
43
+ - The stub's `status: done` line means what it always meant — the entry is
44
+ closed and not open work.
45
+ - An `archived:` line marks content that lives in `deferred-work-archive.md`.
46
+ The full body (evidence, resolution, dates) is there, keyed by the same
47
+ DW- id — read the archive file for anything beyond the stub. An id may own
48
+ more than one block there once a reopened entry has been archived twice.
49
+ Narrow by the date on this line; when several blocks share it — one entry
50
+ closed, archived, reopened and archived again inside a single day — take the
51
+ **last** of them. The archive file is append-only, so for one id a later
52
+ block is a later closure.
53
+ - An `archived-body:` line is that same pointer carried by an entry that was
54
+ archived and then **reopened** — the orchestrator writes it in place of the
55
+ `archived:` stamp, which would otherwise claim a live entry's body is
56
+ elsewhere. The entry is open work again and its `status:` says so, but the
57
+ body it carried before that close is still in the archive file — resolve
58
+ this line exactly as an `archived:` stamp above: narrow by its date, and take
59
+ the last of the blocks that share it. Reopening does not bring the body back, and a stub keeps neither `location:` nor `reason:`, so read that block
60
+ before triaging the entry. Never edit or drop the line.
61
+ - The archive may be absent even when a stub or an `archived-body:` line
62
+ references it: only the ledger is seeded into an isolated unit worktree. That
63
+ is not an error — the fields the stub preserves are sufficient for dedupe.
64
+ - Stubs keep load-bearing field lines (`gate:`, `origin:`/`source_spec:`,
65
+ resolution undo markers) — treat them exactly as if the entry were still
66
+ whole. Never edit or drop them when touching a stub.
67
+ - When deduping against existing entries, a stub still counts: match on the
68
+ id and preserved `origin:`/`source_spec:` lines, and check the archive for
69
+ the full substance before appending.
70
+
71
+ ## Before appending: dedupe check
72
+
73
+ Scan the existing file for an entry describing the same issue or goal (same
74
+ location and same substance, even if worded differently). If one exists, do
75
+ NOT append a duplicate — add a `seen-again:` line to the existing entry
76
+ instead:
77
+
78
+ ```markdown
79
+ seen-again: 2026-06-12 (code review of spec-3-3-export.md)
80
+ ```
81
+
82
+ ## Entry format
83
+
84
+ Number entries sequentially (`DW-1`, `DW-2`, …) by scanning the file for the
85
+ highest existing number. One entry per deferred item:
86
+
87
+ ```markdown
88
+ ### DW-<seq>: <one-line title>
89
+
90
+ origin: <workflow + artifact + date, e.g. "code review of spec-3-2-digest.md, 2026-06-12">
91
+ location: <file:line or component, or "n/a" for deferred goals>
92
+ severity: <critical | high | medium | low — how much it matters if never done>
93
+ reason: <why this was deferred rather than done now, one or two sentences>
94
+ status: open
95
+ ```
96
+
97
+ `location:` is always written. Use `n/a` whenever there is nothing to open — a
98
+ deferred goal, but equally a finding whose reporter recorded no place. The field
99
+ says "no location was recorded", not "this item has none": a reader that finds
100
+ `n/a` should fall back to `reason:`, which often names the file even when
101
+ `location:` is empty. Entries written before this rule may omit the line; read
102
+ an absent `location:` as `n/a`, never as "not yet known".
103
+
104
+ Entries the orchestrator harvests from a spec carry one extra line,
105
+ `source_spec:`, directly after `location:` — the spec the deferral came from. It
106
+ is half the dedupe key (with `origin:`), so never edit or drop it when touching
107
+ an entry; entries written by hand do not need it.
108
+
109
+ **Every field line is exactly one line, and so is the title.** The format is
110
+ line-oriented: readers find each field by scanning for `<name>:` at the start of
111
+ a line, and an entry ends at whichever comes first — the next `### DW-<n>`
112
+ entry, any other `#` .. `######` heading (indented up to three spaces, and
113
+ followed by a space, a tab or the end of the line; four spaces or a leading tab
114
+ makes an indented code block, which ends nothing), or a `- source_spec:`
115
+ flat-append bullet. A value carrying a line break therefore does not wrap; it
116
+ becomes new ledger content, and three things can follow:
117
+
118
+ - a break followed by `### ` mints an entry nobody filed;
119
+ - a break before a `status:` line leaves one entry carrying two, so the ledger
120
+ no longer says one thing about it;
121
+ - a break followed by `- source_spec:` cuts the entry short at that bullet, and
122
+ everything after it re-surfaces as a phantom _legacy_ item.
123
+
124
+ Keep breaks out of field values, along with `### ` and a leading
125
+ `- source_spec:`. If a reason needs two sentences, write them on one line.
126
+
127
+ `severity:` is optional — entries written before this field existed have none
128
+ and that is fine; readers must treat a missing or unrecognized value as
129
+ "unspecified". Use `critical` for correctness/security issues, `high` for
130
+ likely user-visible problems, `medium` for quality and robustness gaps, `low`
131
+ for polish and nice-to-haves.
132
+
133
+ When a deferred item is later completed, set its `status:` to `done` with the
134
+ date (e.g. `status: done 2026-06-20`) — do not delete the entry.
135
+
136
+ ## Hard gates: `gate:`
137
+
138
+ Some entries are not merely deferred — they **block** specific stories. An
139
+ infrastructure leg nobody has wired yet is not a nice-to-have for the first story
140
+ that consumes it; that story must not run at all until the entry lands. Say so
141
+ with a `gate:` line naming the blocked story keys:
142
+
143
+ ```markdown
144
+ ### DW-1: wire the blob-storage credentials
145
+
146
+ status: open
147
+ gate: 3-2, 3-3
148
+ ```
149
+
150
+ `gate:` is optional and most entries have none. Its value is a **comma-separated**
151
+ list of story-key tokens; several `gate:` lines in one entry union, so an entry
152
+ blocking three stories may list them on one line or on three. A token matches a
153
+ story key when it **is** that key, or is its prefix at a key boundary — either a
154
+ `-`, or the split-story suffix (one lowercase letter then `-`). So `3-2` gates the
155
+ sprint key `3-2-invite-link-student-surface`, the stories-mode id `3-2`, and both
156
+ halves of a split (`3-2a-…` / `3-2b-…`), but never `3-20-later-story`. The split
157
+ arm matters because breakdown can split a story _after_ the gate was written, and
158
+ a gate that quietly stops matching is worse than one that was never there. The
159
+ prefix must end at a story **number** for the split arm to apply, so a word id
160
+ like `auth` does not gate `authz-login`.
161
+
162
+ Like `source_spec:`, a `gate:` line is never edited or dropped when an entry is
163
+ otherwise touched: removing it un-gates the story silently, which is the exact
164
+ failure this field exists to prevent. During a `froid-loop sweep --migrate` that
165
+ is enforced mechanically — the orchestrator refuses a rewrite that drops a token
166
+ a pre-existing entry declared (#519); every other ledger edit is still held by
167
+ this instruction alone.
168
+
169
+ Until the entry lands, the gate is enforced twice. `froid-loop validate` **fails**
170
+ (`deferred.hard-gate`) for every story a token matches that the queue would
171
+ actually dispatch — sprint-status stories at `backlog` / `ready-for-dev`, or
172
+ manifest entries whose spec is not yet written or sits at `draft` /
173
+ `ready-for-dev` / `in-progress` / `in-review`. A `blocked` manifest entry is not
174
+ gated, nor is one the scheduler would stop on anyway (two specs matching one id,
175
+ or a skeletal sentinel from a failed planning halt): the queue cannot reach that
176
+ story, so a gate refusing it would report work held back that was never going to
177
+ run. A `run` that never called `validate` **pauses** (`story-gate`) rather
178
+ than dispatch a gated story. Two things clear it: closing the entry
179
+ (`status: done <date>`), or removing the token because it no longer blocks that
180
+ work. This is the one deferred-work check that gates rather than advises:
181
+ everything else here is traceability that may be wrong, while this is work that
182
+ must not start.
183
+
184
+ **Only an explicit `done` retires a gate.** A status the format cannot read —
185
+ `status: opne`, or an entry with no `status:` line — is not evidence the work
186
+ landed, so the gate still holds. Write the status word exactly.
187
+
188
+ A sweep is never gated by the ledger it is draining, whatever any entry's `gate:`
189
+ says: closing the gating entry is what a sweep is for, so gating it would
190
+ deadlock the gate against its own remedy.
191
+
192
+ **Quoting the field is safe.** A `gate:` line inside a fenced code block is an
193
+ example, not a declaration, so an entry that documents this convention gates
194
+ nothing. This holds for a whole quoted entry too — heading, `status:` and `gate:`
195
+ inside one fence, the shape shown above: the fenced heading starts no entry, and
196
+ a quoted heading or bullet does not end the entry that quotes it, so a real
197
+ `gate:` below the example keeps gating. One exception worth knowing when you
198
+ write an entry: a fence you open and never close is not treated as a fence at
199
+ all, because swallowing the rest of the entry could silently disable a real
200
+ `gate:` line below it — and, at file scope, hide every entry after it. Close your
201
+ fences.
202
+
203
+ Four shapes declare a gate that nothing can enforce, and all four are reported as
204
+ `deferred.hard-gate-unstructured` while the entry is unlanded:
205
+
206
+ - a token nothing can match. It must look like a story key
207
+ (`[A-Za-z0-9][A-Za-z0-9._-]*`, no spaces) **and** be a shape a key can actually
208
+ take — alphanumeric segments joined by `-`, or a full sprint key. So a
209
+ space-separated `gate: 3-2 3-3` is one bad token rather than two good ones, and
210
+ `gate: 3.2` / `gate: 3_2` are rejected: no key spells its numbers that way.
211
+ Inside a sprint slug those characters are fine — `gate: 3-2-a_b` is a real gate;
212
+ - a `gate:` line with nothing usable after the colon (`gate:`, `gate: ,`) — each
213
+ such line is reported, including one sitting beside a line that does name a
214
+ story, since the half that names nothing is the half you are wrong about;
215
+ - a `gate:` that is not lowercase at the very start of its line — `Gate: 3-2`, or
216
+ a line that indents `gate: 3-2`. These are reported rather than read as
217
+ declarations: the field is a fixed spelling, and guessing at near-misses is how
218
+ a line that was never meant to gate ends up refusing a story;
219
+ - prose declaring `HARD GATE:` — the convention that predates this field —
220
+ anywhere on a line of an entry that carries no `gate:` line. It is matched
221
+ mid-line because `reason:` prose is hard-wrapped, but never directly after a
222
+ quote character (`"`, `'`, `` ` ``, `«`, or a curly quote): an entry that merely
223
+ _cites_ the phrase stays silent, as does one that writes it without the colon.
224
+
225
+ Each reads like a gate already in force while holding nothing back. Add or repair
226
+ the `gate:` line to make it enforceable.
227
+
228
+ ## Sweep annotations
229
+
230
+ `froid-loop sweep` runs (the orchestrator and its bundle dev sessions) add two
231
+ optional field lines to existing entries — both directly after `status:`:
232
+
233
+ ```markdown
234
+ resolution: <one line: what was built or why the entry was closed>
235
+ decision: <date> <chosen option label> — <detail>
236
+ ```
237
+
238
+ - `resolution:` accompanies every sweep close (`status: done <date>`). Bundle
239
+ dev sessions write it when finishing a bundle's entries; the orchestrator
240
+ writes it when closing entries triage proved already resolved.
241
+ - `decision:` records a human's sweep-time choice on an entry. It does not by
242
+ itself change `status:` — a `keep-open` decision leaves the entry open.
243
+
244
+ ## Closure declared by a story
245
+
246
+ A sweep bundle is not the only thing that closes an entry. A regular story may
247
+ declare the entries its work closes — on its `stories.yaml` entry (stories mode),
248
+ or in its story spec's frontmatter. The two are unioned:
249
+
250
+ ```yaml
251
+ closes_deferred: [DW-5, DW-6] # DW-<n> ids this story closes
252
+ ```
253
+
254
+ Both are written by a human, and breakdown time — with this file open — is where
255
+ it belongs, though not a deadline: the declaration is read when the story
256
+ commits, so one added to a spec's frontmatter mid-run still counts. No upstream
257
+ skill emits the field yet, and re-deriving `stories.yaml` will drop it unless the
258
+ intent is recorded in `.memlog.md` first.
259
+
260
+ When the story commits, the orchestrator annotates each declared id exactly as a
261
+ bundle close does — `status: done <date>` plus a `resolution:` line naming the
262
+ story:
263
+
264
+ ```markdown
265
+ status: done 2026-07-23
266
+ resolution: resolved by story 3-2-export
267
+ ```
268
+
269
+ The rules that keep this safe:
270
+
271
+ - **Declared, never inferred.** Closure comes only from this field; the
272
+ orchestrator does not guess it from a diff.
273
+ - **Only once the story actually lands.** The annotation is written at the
274
+ commit boundary — after verification, the review loop and every checkpoint,
275
+ and just before the story's commit is squashed. A story that fails, blocks, is
276
+ rejected by review, or escalates closes nothing; a commit that then fails
277
+ takes the annotation back with it, restored to the pre-close text.
278
+ - **In the story's own commit**, when this file lives inside the repo —
279
+ worktree isolation included: the unit's copy rides the unit commit and
280
+ reaches the target branch with the merge. If the artifacts dir is configured
281
+ outside the repo, the file is shared between worktrees and no commit can
282
+ carry it; the annotation is written all the same and the run journals
283
+ `deferred-close-external-ledger`. A location that cannot be read or written
284
+ when the write comes due closes nothing and is journaled — the entries stay
285
+ `open` for a sweep to re-verify, and an outage is never read as "no such
286
+ entries", never allowed to fail the story or crash the run.
287
+ - **Idempotent.** An id already `done` is left untouched, so a resumed run
288
+ re-driving the same close neither doubles the `resolution:` line nor warns.
289
+ - **Never a gate.** An id that matches no entry, an entry whose `status:` reads
290
+ as neither `open` nor `done`, and a story spec declaring a bare
291
+ `closes_deferred: DW-5` where a list belongs are each journaled and dropped —
292
+ none can fail the story. `froid-loop validate` reports the same mismatches as
293
+ warnings before the run starts. The one exception is that same wrong container
294
+ in `stories.yaml`: the manifest is a schema the parser owns, so it fails to
295
+ load there like any other field of the wrong type — before any story runs, and
296
+ reported by `validate` up front.
297
+ - **Read at the commit.** The declaration that counts is the one on disk when the
298
+ story commits, not the one it was implemented from — edit it late and the edit
299
+ is honored, in both directions.
300
+
301
+ Keep the ids stable when editing this file: a reworded title is fine, but
302
+ renumbering an entry orphans any declaration that already references it.
@@ -0,0 +1,86 @@
1
+ # Migration Mode
2
+
3
+ Projects started before the DW entry format carry a freeform
4
+ `deferred-work.md` — "## Deferred from: ..." sections with bullets,
5
+ strikethrough done-markers, `### D-1.2-003: title — RESOLVED` headings,
6
+ topic sections suffixed "(... — DONE)". The orchestrator cannot sweep those:
7
+ `status:` lines don't exist to flip, and open items are invisible to its
8
+ parser. Your job is a one-time rewrite of every legacy item into a canonical
9
+ `### DW-<n>:` entry, after which the normal triage flow takes over.
10
+
11
+ This is the ONE workflow mode that edits a file: exactly the ledger at
12
+ `{implementation_artifacts}/deferred-work.md`. Never any other file, never
13
+ code, never specs, never sprint-status. Never commit — the orchestrator
14
+ commits the migrated ledger after validating it.
15
+
16
+ ## Inputs
17
+
18
+ - `--migrate <manifest-path>`: a JSON array — the orchestrator's parse of the
19
+ legacy items. Each element:
20
+
21
+ ```json
22
+ {
23
+ "key": "<stable identity — echo it back in your mapping>",
24
+ "id": "<native id like W2 / D-CAP-001, or empty>",
25
+ "title": "<cleaned one-line title>",
26
+ "section": "<enclosing heading text>",
27
+ "done": true,
28
+ "severity": "high"
29
+ }
30
+ ```
31
+
32
+ The manifest is authoritative for WHAT to convert and each item's
33
+ open/done state. Do not re-interpret done-ness from the prose; the
34
+ orchestrator validates your output against the manifest's `done` flags.
35
+
36
+ - `--feedback <path>` (retry only): the deterministic validation errors your
37
+ previous attempt failed on, including any items that still parse as legacy.
38
+ Read it FIRST and fix exactly those defects.
39
+
40
+ ## The rewrite
41
+
42
+ 1. Read the manifest and the full ledger.
43
+ 2. Keep every existing `### DW-<n>:` entry **byte-identical** — the
44
+ orchestrator fails the migration if a pre-existing entry's status changes,
45
+ an entry disappears, or a `gate:` token an entry declared is no longer
46
+ present.
47
+ 3. Replace all legacy content with canonical entries per
48
+ `./deferred-work-format.md`. Number new entries continuing
49
+ from the highest existing `DW-<n>` (start at DW-1 when none exist), in
50
+ the items' original file order. Per item:
51
+ - `### DW-<n>: <title>` — the manifest title, refined from the original
52
+ bullet when it improves clarity.
53
+ - `origin: migrated from legacy ledger ("<section>"), <today>` — keep the
54
+ original review/date context from the section text.
55
+ - `location:` — extract a file/component from the original text, else `n/a`.
56
+ - `severity:` — the manifest severity; omit the line when null.
57
+ - `reason:` — the substance of the original bullet, condensed but lossless
58
+ enough that a future dev session can act on it. Long forensic detail may
59
+ follow as body lines under the fields.
60
+ - `status: open`, or for done items `status: done <date>` using the
61
+ original completion date when recoverable (else today), plus a
62
+ `resolution:` line carrying the original resolution text when one
63
+ exists (e.g. the text after `→` or a `**Resolution:**` field).
64
+ 4. Two manifest items describing the same underlying issue (e.g. a duplicate
65
+ `W1` re-raised in a later review) may merge into ONE DW entry — map both
66
+ keys to the same `dw_id`. Merge only when their `done` flags match.
67
+ 5. The finished file must contain only the `# Deferred Work` title line and
68
+ canonical `### DW-<n>:` entries. Any leftover freeform section, bullet
69
+ list, or strikethrough item fails the orchestrator's zero-legacy check
70
+ and burns a retry.
71
+
72
+ ## Result
73
+
74
+ Write the result file per automation-mode.md with the migration schema:
75
+
76
+ ```json
77
+ {
78
+ "workflow": "deferred-sweep-migrate",
79
+ "mapping": [{ "key": "<manifest key>", "dw_id": "DW-12" }],
80
+ "escalations": []
81
+ }
82
+ ```
83
+
84
+ Every manifest key appears exactly once; every `dw_id` must exist in the
85
+ rewritten ledger with the manifest's open/done state. State in one line how
86
+ many items you converted (and how many merged), then end your turn.