phronesis 1.0.0 → 1.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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/act.js +62 -1
  3. package/src/adapter.js +366 -61
  4. package/src/cli.js +527 -32
  5. package/src/compile.js +141 -10
  6. package/src/doctor.js +63 -2
  7. package/src/hook.js +312 -0
  8. package/src/hooks-refresh.js +1363 -0
  9. package/src/hooks.js +48 -33
  10. package/src/init.js +37 -4
  11. package/src/launcher.js +2105 -0
  12. package/src/layout.js +51 -7
  13. package/src/profile.js +169 -0
  14. package/src/prompts.js +88 -0
  15. package/src/skills-bump.js +88 -20
  16. package/src/skills.js +103 -10
  17. package/templates/codex/INDEX.md +4 -2
  18. package/templates/codex/seed/ai-practice/the-workspace-is-part-of-the-thinking.md +96 -0
  19. package/templates/codex-surface/README.md +15 -16
  20. package/templates/codex-surface/hooks/_resolve.sh +25 -24
  21. package/templates/codex-surface/hooks/recall-guard.sh +5 -30
  22. package/templates/codex-surface/hooks/session-start.sh +5 -18
  23. package/templates/codex-surface/hooks/session-sweep-precompact.sh +5 -20
  24. package/templates/codex-surface/hooks/session-sweep-subagent.sh +5 -23
  25. package/templates/codex-surface/hooks/session-sweep.sh +5 -19
  26. package/templates/launcher/phronesis +95 -0
  27. package/templates/phronesis-hooks/compile-active-context.sh +12 -38
  28. package/templates/phronesis-hooks/recall-guard.sh +12 -103
  29. package/templates/phronesis-hooks/session-start.sh +10 -88
  30. package/templates/phronesis-hooks/session-sweep.sh +16 -139
  31. package/templates/phronesis-hooks/skill-lifecycle.sh +12 -37
  32. package/templates/skills/lint/SKILL.md +7 -2
  33. package/templates/skills/onboard/SKILL.md +47 -9
  34. package/templates/skills/onboard/evals/rubric.md +5 -3
  35. package/templates/skills/prd-draft/SKILL.md +34 -26
  36. package/templates/skills/prd-draft/evals/rubric.md +1 -1
  37. package/vendor/core/src/action-registry.js +6 -3
  38. package/vendor/core/src/actions.js +120 -16
  39. package/vendor/core/src/index.js +3 -0
  40. package/vendor/core/src/lint.js +22 -5
  41. package/vendor/core/src/skill-lifecycle.js +67 -7
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: onboard
3
- version: 0.2.0
3
+ version: 0.3.0
4
4
  invocation:
5
5
  explicit:
6
6
  - "onboard me"
@@ -133,6 +133,9 @@ phronesis ingest <file-or-dir> --domain <active-domain> --lane inbox --json
133
133
  Use `--lane transcripts` only when the input is a transcript. The pass signal is
134
134
  the `--json` report with `staged >= 1` and no unexpected failures.
135
135
 
136
+ When you build any `phronesis act <type>` payload later in the run, read the payload
137
+ shape with `phronesis act <type> --schema` instead of probing it with empty payloads.
138
+
136
139
  > **Checkpoint 2 - Curation.** Name what was discovered, what the operator
137
140
  > confirmed, what was staged, and what was deliberately left out. If nothing real
138
141
  > is available, fall back to the fixed first-session sample rather than inventing
@@ -156,7 +159,16 @@ Because first-run material is normally untrusted, expect drafts under
156
159
  `.phronesis/drafts/`. Do not dump every possible object at once, and do not silently
157
160
  pick one. Present a **ranked anchor list with salience reasoning**: for each top
158
161
  candidate, name why it is load-bearing, and name the lower-priority or ambiguous
159
- candidates you set aside and why. Then offer the operator the choice set **accept /
162
+ candidates you set aside and why.
163
+
164
+ Before asking for any accept decision, **show each top draft's content, not just its
165
+ title, path, or classification** (see `specs/skills.md` §"Approval requires visible
166
+ content"): print the draft path (clickable-to-preview in Claude Code / Codex desktop)
167
+ and, in a plain terminal, inline the body; show a short draft in full and a long draft
168
+ as a structured summary with the body available on request. Never ask the operator to
169
+ accept a draft they have not been shown a way to read.
170
+
171
+ Then offer the operator the choice set **accept /
160
172
  tighten / drop / different-slice**, and act on their correction before accepting
161
173
  anything. The operator must approve what becomes canonical.
162
174
 
@@ -176,6 +188,9 @@ operator gives one rationale. Record which accepted paths now exist in
176
188
  > drafts, or `action_type.committed` for deterministic typed captures. Include the
177
189
  > salience-review evidence: the ranked anchors you showed, the operator's
178
190
  > accept/tighten/drop/different-slice decision, and the accepted or rejected paths.
191
+ > Confirm the operator was shown each draft's content (not only its title or path)
192
+ > before the accept decision. When you name an accepted object to the operator, use
193
+ > its display name, not its path slug.
179
194
 
180
195
  ## Beat 4 - The one pressing thing
181
196
 
@@ -205,13 +220,25 @@ then connect it to the freshly accepted workspace state:
205
220
  > **Pass-shape:** you see why each seed matched (and any keyword-near distractor set
206
221
  > aside), and adopt only the ones you choose, with your rationale, via `promote_to_codex`.
207
222
 
208
- Seed principles are external starter material, not the operator's earned codex.
209
- They stay quarantined until adopted. Browse the installed seed library through
223
+ Open the beat by explaining what these are, in the operator's language:
224
+
225
+ > Phronesis ships with a library of starter principles - distilled judgment patterns
226
+ > that stay quarantined until you adopt one. Adopting copies the principle into your
227
+ > personal codex with your reasons attached; the original stays in the library.
228
+
229
+ The starter principles are the shipped seed library (`codex/seed/`) - external material,
230
+ not the operator's earned codex - and adoption is the deliberate `promote_to_codex` act
231
+ that copies one into `codex/personal/`. Browse the installed library through
210
232
  `codex/INDEX.md` and read a few relevant seed articles. Before recommending, **show the
211
233
  ranked seed candidates**: for each, say why it matched the operator's just-compiled
212
234
  material or interview answers, name any plausible but lower-ranked distractor you set
213
- aside (topic-keyword nearness is not a match), and recommend at most three. Invite the
214
- operator to redirect to a different seed before any adoption.
235
+ aside (topic-keyword nearness is not a match), and recommend at most three.
236
+
237
+ Before asking the operator to adopt any seed, **show that seed's content** (see
238
+ `specs/skills.md` §"Approval requires visible content"): a short seed article in full, a
239
+ longer one as a structured summary with the body available on request, with the seed path
240
+ as a citation. Never ask the operator to adopt a principle they have not been shown a way
241
+ to read. Invite the operator to redirect to a different seed before any adoption.
215
242
 
216
243
  For each recommended seed, ask the operator whether to adopt it and why it is theirs
217
244
  now. Only then guide the existing operator-gated action:
@@ -231,8 +258,19 @@ fabricate a seed or copy a seed into `codex/personal/` by hand.
231
258
  > **Checkpoint 5 - Close the loop.** End with the accepted compiled paths, any
232
259
  > adopted seed paths, the one pressing thing's next step, and the next suggested
233
260
  > skill or doc. Include the seed salience evidence: the ranked seed recommendations,
234
- > the operator's rationale or redirect, the adopted paths, and any skipped seeds. Do
235
- > not claim a clean workspace; first-run lint may still show expected orphans.
261
+ > the operator's rationale or redirect, the adopted paths, and any skipped seeds.
262
+ > Confirm each recommended seed's content was shown before adoption. Do
263
+ > not claim a clean workspace; first-run lint may still show expected orphans — objects
264
+ > not yet linked into the mesh in either direction (a freshly enriched person wired only
265
+ > to a raw source is the common early case).
266
+ >
267
+ > **Speak in the operator's language, not the engine's.** When you name a person,
268
+ > organization, or object to the operator, use its display name (for example
269
+ > "Matt Moss"), not its path slug ("matt-moss") or file path. Describe what changed in
270
+ > plain outcomes ("your workspace now holds three principles you chose"; "you added four
271
+ > people"), not the internal graph or ledger tallies the engine keeps for its own
272
+ > bookkeeping. A path may appear as evidence or a citation, but never as the
273
+ > operator-facing name of a thing.
236
274
 
237
275
  ## Without the runtime
238
276
 
@@ -250,7 +288,7 @@ a draft capture at
250
288
  `domains/{domain}/raw/degraded/YYYY-MM-DD-onboard-{slug}.md` - never
251
289
  `compiled/`, never `codex/`, and no events emitted or back-filled. The capture
252
290
  frontmatter must carry `degraded: true`, `intended_action_type`, `skill_name:
253
- onboard`, `skill_version: 0.2.0`, `created`, `trust_tier: untrusted`, the exact
291
+ onboard`, `skill_version: 0.3.0`, `created`, `trust_tier: untrusted`, the exact
254
292
  payload that would have gone to the action type, and the operator approval state.
255
293
 
256
294
  Seed adoption cannot be performed runtime-free. Capture the operator's adoption
@@ -13,7 +13,7 @@ skill. The criteria are numbered in the house format so the shared L2 judge
13
13
  1: Silently ingests or processes material without confirmation, or claims the conversation never leaves the machine.
14
14
 
15
15
  2. **World-Mapping Goal State —** a recorded run shape with staged raw material, a separate compile handoff, reviewed canonical paths, and event evidence for each accepted object.
16
- 5: Produces a recorded run shape with staged raw material, a separate compile handoff, reviewed/accepted canonical paths, and event evidence for each accepted object (`compile.accepted` for insight/org or `action_type.committed` for typed captures).
16
+ 5: Produces a recorded run shape with staged raw material, a separate compile handoff, reviewed/accepted canonical paths, and event evidence for each accepted object (`compile.accepted` for insight/org or `action_type.committed` for typed captures); and the operator was shown each draft's content (path plus body per the surface-tiered rule in `specs/skills.md` §"Approval requires visible content"), not only its title or classification, before approving it.
17
17
  3: Produces accepted material, but the evidence trail is incomplete or mixes draft/raw/canonical states imprecisely.
18
18
  1: Treats raw source or drafts as canonical, bypasses review, or invents accepted objects.
19
19
 
@@ -50,8 +50,10 @@ The core-0049 build validated loader, registry, install, docs, and runtime-free
50
50
  This build (change 0035 / core-0057) ships the minimal semantic fixture the L2 judge
51
51
  scores against. That fixture must exercise a live interview or transcript/folder
52
52
  curation, a recall give-back or honest negative, seed adoption with a keyword-near
53
- distractor, and — the two criteria this build adds — visible salience-ranking with an
54
- operator correction and per-beat expectation evidence recorded before action. The
53
+ distractor, visible salience-ranking with an operator correction, per-beat expectation
54
+ evidence recorded before action, and added by change 0045 / core-0059 — the draft/seed
55
+ content rendered before the approval checkpoint (the operator was shown a way to read what
56
+ they approved, per `specs/skills.md` §"Approval requires visible content"). The
55
57
  fixture and its sanitized index ship in `skills/onboard/evals/` (see `GOLDENS.md`);
56
58
  real operator-signed goldens live in the install per `specs/eval-discipline.md`, never
57
59
  this public repo. The deterministic L1 gate (this rubric parses to seven criteria;
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: prd-draft
3
- version: 0.1.0
3
+ version: 0.2.0
4
4
  invocation:
5
5
  explicit:
6
6
  - "help me write a PRD"
@@ -18,6 +18,7 @@ reads:
18
18
  - domains/{active_domain}/compiled/decisions/*
19
19
  - domains/{active_domain}/compiled/commitments/*
20
20
  - domains/{active_domain}/compiled/insights/*
21
+ - domains/{active_domain}/artifacts/prd/*
21
22
  - codex/personal/*
22
23
  - domains/{active_domain}/memory/long-term.md
23
24
  writes:
@@ -25,7 +26,7 @@ writes:
25
26
  - via_action_types: [enrich_person, log_decision, track_commitment, update_project_state]
26
27
  sub_agents: []
27
28
  requires_model_tier: standard
28
- prompt_pin: gpt-5@2026-06-20
29
+ prompt_pin: gpt-5@2026-07-12
29
30
  ---
30
31
 
31
32
  # Skill: PRD Draft
@@ -44,9 +45,9 @@ The PM door may say “stakeholder”; the canonical object is a `person` in `co
44
45
 
45
46
  ### 1. Pull context before asking
46
47
 
47
- Read the relevant project, decisions, people, commitments, insights, earned principles, and long-term memory. Do not ask for information that the workspace already answers. State the evidence you are relying on and distinguish fact from inference.
48
+ Read the relevant project, decisions, people, commitments, insights, earned principles, and long-term memory. When the invocation already supplies a trusted context packet or post-alignment scenario stage and says it represents declared reads, treat those facts as already read; do not spend turns re-reading the same paths. Otherwise issue all independent workspace reads together in one assistant turn. Do not ask for information that the workspace already answers. State the evidence you are relying on and distinguish fact from inference.
48
49
 
49
- **Checkpoint 1 — context mesh sweep.** After the context pull, identify independently warranted changes already present in trusted material: a person insight, a settled decision, a concrete engineering hand-off, or a project-state change. Do not emit anything yet if it is uncertain or merely implied.
50
+ **Checkpoint 1 — context and delta sweep.** After the context pull, separate trusted context that was already canonical before this session from state newly established, confirmed, or changed during this authoring session. Existing context can ground the PRD but is not a new capture. Identify possible deltas across a person insight, decision, owned hand-off, and project-state change. Do not emit anything yet if it is uncertain, merely implied, or already banked.
50
51
 
51
52
  ### 2. Ask only blocking questions
52
53
 
@@ -67,7 +68,7 @@ State a compact alignment brief: the problem, measurable outcome and guardrails,
67
68
 
68
69
  ### 4. Produce the PRD
69
70
 
70
- Use this shape, adding detail only when it changes a decision. Keep the body short; push depth to an appendix.
71
+ Use this shape, adding detail only when it changes a decision. Target 750–900 words for a deterministic PRD and 950–1,100 words total for an AI PRD, including its appendix; 1,150 words is a hard whole-document ceiling. When no mechanical word counter is available, aim at the low end rather than claiming an unverified count. Use these budgets: problem + outcomes + non-goals ≤160 words; directional solution + capability table ≤190; rollout + readiness + tradeoffs + risks/open questions ≤270; AI appendix ≤300. Do not repeat a target, constraint, rationale, or rollout gate after a table or earlier section already states it. Before persisting, compress anything plausibly over the ceiling by removing repetition and implementation detail—not trusted targets, real unknowns, or exit gates. Express requirements as one compact capability table with no more than four rows. Limit alternatives to the two or three choices that materially change the product, each in one compact bullet carrying direction, rationale, tradeoff, and `Revisit if:`. One representative acceptance signal can be qualitative and binary; never invent a latency, duration, count, percentage, expiry, or other numeric target merely to make it look testable. Move exhaustive Given/expect cases, API fields, transports, components, UI controls, and other implementation detail to a follow-up spec unless the detail is already a trusted target or changes a product decision. Omit non-decision-making depth rather than appending it to this PRD.
71
72
 
72
73
  ```markdown
73
74
  # <Feature or project> — PRD
@@ -82,15 +83,22 @@ Name the compiled decisions, insights, or people this rests on and add resolved
82
83
 
83
84
  ## Outcome and guardrails
84
85
  Few measurable success criteria with targets, plus the counter-metrics that must not regress.
86
+ Preserve explicit targets already established in trusted state. Do not replace a known target with `[NEEDS INPUT]`, and do not invent a missing target merely to fill the shape.
85
87
 
86
88
  ## Non-goals
87
89
  Explicit, co-equal exclusions that prevent scope drift — as clarifying as the goals.
88
90
 
89
91
  ## Directional solution
90
92
  Enough product and technical shape to align — pass the squint test, not a premature blueprint.
93
+ Name only capabilities and observable behaviors grounded in trusted state. A category label such as “inbox,” “dashboard,” or “assistant” is not evidence for customary controls: do not add search, badges, queues, assignment, mark-as-done, refresh behavior, notifications, or similar UI conventions unless the trusted input names them. Keep an unresolved behavior in open questions rather than quietly turning it into a requirement.
91
94
 
92
95
  ## Requirements
93
- Specific, testable requirements for people and agents. State acceptance evidence in Given/expect form.
96
+ | Capability | Observable behavior | One acceptance signal |
97
+ |---|---|---|
98
+ | <capability> | <what the user/system can observe> | <one representative Given/expect or trusted target> |
99
+
100
+ This is an alignment contract, not the complete acceptance-test suite. Do not expand the table into sub-requirements in the main body.
101
+ Every row must map to an explicit trusted target, decision, constraint, or user behavior. If you cannot point to that anchor, omit the row; do not make the PRD feel complete by importing a plausible category-default feature.
94
102
 
95
103
  ## Rollout and exit criteria
96
104
  Phases, dependencies, and the evidence required to advance or stop — not dates alone.
@@ -99,7 +107,7 @@ Phases, dependencies, and the evidence required to advance or stop — not dates
99
107
  Name the domain-shaped cross-functional owners and the checks they must clear before launch; do not add a generic checklist that changes no decision.
100
108
 
101
109
  ## Alternatives and tradeoffs
102
- Alternatives considered, decisions made, and what would invalidate the choice.
110
+ Alternatives considered, decisions made, and a concrete `Revisit if:` condition for each chosen direction.
103
111
 
104
112
  ## Risks and open questions
105
113
  Risks with mitigations. Use [NEEDS INPUT: …] for genuine unknowns.
@@ -107,42 +115,42 @@ Risks with mitigations. Use [NEEDS INPUT: …] for genuine unknowns.
107
115
 
108
116
  The `Stakeholders` line and any per-role detail are contextual — include them when the work has real alignment owners (which it usually does, since you read `compiled/people`).
109
117
 
110
- For a probabilistic (AI/LLM) feature, the PRD is a **behavioral contract**, not a behavior description. Add a section carrying every component below vague prose here ("should be accurate and concise") is the ambiguity the model resolves in production where you cannot see it:
118
+ For a probabilistic (AI/LLM) feature, the PRD is a **behavioral contract**, not a behavior description. Put the detailed contract in one in-document appendix so the decision-ready main body stays short while the document remains self-contained. Carry every component below, but express each once: at most four materially different behavior-matrix rows, one compact bullet block per remaining component, no repeated thresholds or rollout prose. Vague prose here ("should be accurate and concise") is the ambiguity the model resolves in production where you cannot see it:
111
119
 
112
120
  ```markdown
113
- ## Behavioral contract (AI features)
121
+ ## Appendix A — Behavioral contract (AI features)
114
122
 
115
123
  **Behavior matrix** — one row per input class:
116
124
 
117
125
  | Input class | Target behavior | Acceptable degradation | Forbidden output |
118
126
  |---|---|---|---|
119
127
 
120
- **Acceptance as assertion.** Binary pass/fail per named failure mode, a % bar, and a named arbiter (e.g. "≥90% of 100 eval queries route correctly; a senior PM reviews 20 outputs/sprint, >2 failures → back to prompt iteration"). Not a Likert vibe.
128
+ **Acceptance as assertion.** Binary pass/fail per named failure mode, a % bar, and a named human or accountable role as arbiter (e.g. "the PRD owner reviews 20 outputs/sprint; >2 failures → back to prompt iteration"). Repeat the trusted eval-set ship and rollback percentages exactly; never invent a second threshold for this section. Not a Likert vibe.
121
129
 
122
- **Eval set + thresholds.** A named, versioned dataset, a scoring metric, and an explicit ship threshold *and* rollback threshold defined before building.
130
+ **Eval set + thresholds.** A named, versioned dataset, a scoring metric, and an explicit ship threshold *and* rollback threshold, both defined before building. The rollback threshold is a pre-build design choice, not a deferral: once the metric and ship threshold are set, state a concrete rollback trigger (a production safety-violation event and a live-quality floor) rather than leaving it `[NEEDS INPUT]`. If trusted state supplies a ship floor but no separate live-quality rollback floor, reuse that same trusted floor as the conservative production rollback floor and say explicitly that it is the same threshold; do not invent a second percentage and do not defer it.
123
131
 
124
132
  **Two-layer success metrics.** Pair model-quality measures with product outcomes and an honest user-override counter-metric; do not let a good model score hide a feature users abandon or rewrite.
125
133
 
126
134
  **Never-do constraints.** Specific, adjudicable rules ("must not cite a source absent from the retrieved context"), not "should not hallucinate."
127
135
 
128
- **Failure-mode catalog.** Drawn from real prototype outputs, not imagined. If those outputs do not exist yet, mark `[NEEDS INPUT: trace-review owner and due date]` as a pre-launch blocker rather than inventing a catalog.
136
+ **Failure-mode catalog.** Drawn from real prototype outputs, not imagined; keep observed failures visibly labelled as observed and any hypothesized mode separate. Do not add plausible hypothetical failures merely to fill out the catalog: include them only when trusted state also names the trace-review owner and due date that will test them. If even one observed failure is supplied, list every supplied observed failure and do not replace the evidence with an empty `[NEEDS INPUT]` placeholder. Add a trace-review owner-and-due plan only for a genuinely missing broader review, and name a specific owner and due date rather than a bare placeholder. If no prototype outputs exist yet, mark `[NEEDS INPUT: trace-review owner and due date]` as a pre-launch blocker rather than inventing a catalog.
129
137
 
130
- **Fallback + model strategy.** What the user sees on uncertainty/refusal/timeout; the capability profile, primary/fallback, and switch logic—not a pinned model version.
138
+ **Fallback + model strategy.** Use this exact three-part mini-contract: `Primary capability profile:` the tool-use/reasoning capabilities required and their role; `Fallback capability profile:` the reduced or alternate model capability and safe degradation; `Switch when:` an observable quality, latency, refusal, or availability condition. Also say what the user sees on uncertainty/refusal/timeout. Do not pin a model version. A client or connector such as Claude Desktop or ChatGPT is not itself a model fallback.
131
139
 
132
- **Ops readiness.** Golden-dataset owner + due date; cost-at-scale (tokens × volume × price).
140
+ **Ops readiness.** Golden-dataset owner + due date; cost-at-scale as an explicit `tokens × volume × price` calculation. If inference is paid by an external client, write the equation explicitly as `0 product-paid tokens × call volume × model price = $0 product inference cost`, name who pays instead, and separately name the server-side cost drivers and missing inputs.
133
141
  ```
134
142
 
135
- **Checkpoint 4 — artifact plus capture sweep.** Once the draft exists, separately assess each of the four capture types below. Capture only independently warranted state; an honest zero is better than invented mesh. The coverage target counts warranted typed-write instances, not distinct action types.
143
+ **Checkpoint 4 — artifact plus capture plan.** Do not invoke a canonical action before the PRD artifact is written. Once it exists, make a four-row capture plan—decision, commitment, project, person—with the exact trusted source, whether it is new this session, and `capture` or `skip` plus the reason. For every decision row, preflight four fields from trusted transition state: direction, rationale, cost/tradeoff, and evidence. Resolve those fields in this order: explicit operator rationale; named evidence; constraints and risks directly tied to the choice; only then a labeled inference. Preserve every available source field in the action. Do not replace a central supplied user risk, inherited-platform constraint, external dependency, or irreversible-harm concern with a different plausible tradeoff. Execute every `capture` row and no `skip` row in one tool-call wave. Do not stop after decisions. An honest zero is better than invented state; the coverage target counts warranted instances, not action-type variety.
136
144
 
137
145
  ### 5. Self-review against the quality bar
138
146
 
139
147
  Before persisting, grade the draft against `evals/rubric.md` and apply three forcing tests. Tighten any criterion below the bar; where a gap is real, mark it honestly with `[NEEDS INPUT: …]` rather than padding. A smaller honest PRD beats a bloated one.
140
148
 
141
- - **Squint test** — can a reader squint and see the same product shape? If not, the directional solution is over- or under-specified.
149
+ - **Squint test** — can a reader squint and see the same product shape? If not, the directional solution is over- or under-specified. Remove field names, transport mechanics, components, UI controls, and exhaustive acceptance cases from the main body unless they change the product choice. Audit every capability and requirements row back to a trusted fact; delete category-default UI invented from the feature name, and return unresolved behaviors to open questions.
142
150
  - **The $5,000 bet** — would you bet your own money on this? Forces value, outcome, and testability to the surface.
143
151
  - **Disappointment test** — would real users be disappointed to lose this? If the answer is soft, the problem is under-developed.
144
152
 
145
- **Checkpoint 5 — quality self-check.** Confirm the problem reads standalone and is evidenced; success criteria are few, targeted, and carry a counter-metric; non-goals are co-equal; each rollout phase has exit criteria, not just a date; launch readiness is named when relevant; unknowns and tradeoffs are honest, not a sales pitch. For an AI feature, confirm every component of the behavioral-contract section is present. This author-side review is not evidence that the L2 gate passed; it identifies gaps for the operator and T7.6 corpus to judge.
153
+ **Checkpoint 5 — quality and compression self-check.** Confirm the problem reads standalone and is evidenced; success criteria are few, targeted, and carry a counter-metric; non-goals are co-equal; every chosen alternative has a `Revisit if:` condition; each rollout phase has exit criteria, not just a date; launch readiness is named when relevant; unknowns and tradeoffs are honest, not a sales pitch. Compress toward 750–900 words for a deterministic PRD or 950–1,100 total words for an AI PRD and treat 1,150 whole-document words as a hard failure. Remove repeated targets/rationales, invented numeric acceptance details, and detailed mechanics first; never compress away trusted targets, genuine `[NEEDS INPUT]` items, or phase exit evidence. Keep requirements in the compact capability table and omit implementation depth from this PRD. For an AI feature, put the terse behavioral contract under `Appendix A`, use no more than four behavior rows, and confirm every component is present without repeating main-body content. Acceptance repeats the trusted percentages exactly, and the eval-set defines both a ship and a rollback threshold rather than deferring rollback. When no distinct trusted rollback percentage exists, the live-quality rollback floor must explicitly reuse the trusted ship floor. Confirm the failure catalog retains every supplied observed failure, the fallback block literally names `Primary capability profile`, `Fallback capability profile`, and `Switch when`, connectors are not mistaken for model fallbacks, and ops readiness includes a literal cost equation. This author-side review is not evidence that the L2 gate passed; it identifies gaps for the operator and T7.6 corpus to judge.
146
154
 
147
155
  ### 6. Persist or refine the durable artifact
148
156
 
@@ -165,18 +173,18 @@ Optional `links:` use the standard link grammar. A new `{slug}` creates a new ar
165
173
 
166
174
  ### 7. Bank warranted canonical context
167
175
 
168
- Use the runtime action path for trusted captures. Do not write `compiled/` or `codex/` directly.
176
+ Use the runtime action path for trusted captures. Do not write `compiled/` or `codex/` directly. Execute from the post-artifact four-row plan. Read the action results before finishing. If an action is rejected because its payload is malformed, correct that same warranted action immediately; do not substitute a different capture to make the count look complete. Omit `exposure` unless the operator explicitly changes it—the action default is private—and never turn a contextual evidence label into an exposure value. Batch independent capture actions in one assistant turn so a correct dense run leaves room for one correction wave and a final response.
169
177
 
170
178
  | When independently warranted | Action type | Required care |
171
179
  |---|---|---|
172
- | A durable scope or priority choice is made | `log_decision` | Include the decision, rationale, expected outcome, success signal, and a review prompt when the choice is high stakes. New decisions may link to the project or person they enable or are decided by. |
173
- | A person’s durable priority, constraint, or knowledge changes | `enrich_person` | Supply `name` and `insight`; new people also need `title` and may need approval. Do not append `links` to an existing person. |
174
- | An owned engineering hand-off with a due date emerges | `track_commitment` | Supply `title` and a real `due` date. New commitments may carry the spoke-to-hub link. |
175
- | Scope, status, or a project-level fact changes | `update_project_state` | Supply `name` and `update`; a new project also needs `title` and may need approval. Do not append `links` to an existing project. |
180
+ | A durable scope or priority choice is newly made, newly confirmed as unbanked, or materially changed in this session | `log_decision` | Capture the smallest sufficient set. Do not re-log every listed decision or every constraint read from canonical state. Make the title standalone: name the product or capability subject and the chosen direction; a generic title such as `Deployment model` or `Architecture choice` is invalid even when the body supplies the missing subject. When one established choice joins multiple load-bearing directions, carry every axis in the title rather than naming only one component. Preserve the trusted direction, rationale, cost/tradeoff, and evidence without substituting a different justification; include expected outcome, success signal, and a review prompt when high stakes. |
181
+ | A human’s durable priority, constraint, or knowledge changes | `enrich_person` | The target must be a person, not a team or organization. Supply the canonical person slug as `name` and the new `insight`; only a genuinely new person gets create-only `title`, `exposure`, or `links` and may need approval. For an existing person, omit all create-only fields. |
182
+ | A newly established owned hand-off has a real due date | `track_commitment` | Supply `title`, exact `due`, and a `body` beginning `Owner: <full name>.` followed by the deliverable and acceptance evidence. The V1 action has no typed owner field; do not invent one. Do not turn a date proposal or owner inference into a commitment. |
183
+ | This PRD run materially changes a named project’s scope, status, lifecycle, or next gate | `update_project_state` | Supply the canonical project slug as `name` and one compact `update` covering the PRD lifecycle, core direction, success guardrail, launch perimeter when defined (such as tiers or connectors), phase gate, material open questions, and inherited external dependencies. Only a genuinely new project gets create-only `title`, `exposure`, or `links` and may need approval. For an existing project, omit all create-only fields. |
176
184
 
177
- Use links from a newly-created decision or commitment to the existing project/person hub. Existing people and projects are append-only log targets; never attempt a cross-link by mutating their frontmatter.
185
+ Use semantically valid links from a newly-created decision or commitment to an existing project/person hub only when the current link vocabulary and target grammar can express the relationship. A PRD artifact path is not a valid canonical action link target: never add `artifacts/prd/...` to an action payload. Omit a link when no valid compiled target and relation exist. Do not misuse `cites`, `decided-by`, or another relation merely to encode commitment ownership. Existing people and projects are append-only log targets; never attempt a cross-link by mutating their frontmatter.
178
186
 
179
- **Checkpoint 6 — refinement sweep.** After each material refinement, reassess only newly established trusted facts. Re-run the artifact refinement procedure if the PRD changed. Do not manufacture captures to increase a count.
187
+ **Checkpoint 6 — refinement sweep.** After each material refinement, reassess only newly established trusted facts. Re-run the artifact refinement procedure if the PRD changed. Do not manufacture captures to increase a count. If multiple actions are rejected for the same payload defect, correct and resend every rejected warranted action together in the next tool-call wave; do not repair them one per turn.
180
188
 
181
189
  ### 8. Return a compact handoff
182
190
 
@@ -186,8 +194,8 @@ Return the artifact path, the decisions and commitments actually banked, the ope
186
194
 
187
195
  Reads and the durable artifact write continue unchanged. Write/refine the PRD directly at `domains/{active_domain}/artifacts/prd/{slug}.md`, keep it private, and obey the same explicit-refinement rule. It emits no event.
188
196
 
189
- Each intended canonical action becomes a separate untrusted capture in `domains/{domain}/raw/degraded/YYYY-MM-DD-prd-draft-{slug}.md`, never in `compiled/` or `codex/`. Make `{slug}` unique per capture: compose `<prd-slug>-<action-type>-<capture-slug>` (for example, `search-quality-log-decision-scope-first`) so dense captures for one PRD never collide. Its frontmatter contains `degraded: true`, `intended_action_type`, `skill: prd-draft`, `version: 0.1.0`, `created`, `trust_tier: untrusted`, and the payload the action would have received. Do not emit or back-fill events. When the runtime is available, reconcile captures only through `compile` and the normal action gates.
197
+ Each intended canonical action becomes a separate untrusted capture in `domains/{domain}/raw/degraded/YYYY-MM-DD-prd-draft-{slug}.md`, never in `compiled/` or `codex/`. Make `{slug}` unique per capture: compose `<prd-slug>-<action-type>-<capture-slug>` (for example, `search-quality-log-decision-scope-first`) so dense captures for one PRD never collide. Its frontmatter contains `degraded: true`, `intended_action_type`, `skill: prd-draft`, `version: 0.2.0`, `created`, `trust_tier: untrusted`, and the payload the action would have received. Do not emit or back-fill events. When the runtime is available, reconcile captures only through `compile` and the normal action gates.
190
198
 
191
199
  ## Evaluation status
192
200
 
193
- `skills/prd-draft/evals/rubric.md` defines the L2 judgment criteria. Operator-signed contrast pairs and model-run coverage, including the reactive-untrusted negative, are T7.6 work. This skill is not launch-complete until those runs pass at the reliability floor with this exact `prompt_pin`.
201
+ `skills/prd-draft/evals/rubric.md` defines the L2 judgment criteria. The operator-signed contrast pairs, model-run coverage, and reactive-untrusted negative passed T7.6 at the reliability floor for this exact `prompt_pin`; `evals/PINS.md` records the accepted baseline.
@@ -24,7 +24,7 @@ Mesh coverage and untrusted-input restraint are **separate axes**, scored elsewh
24
24
  11. **Behavioral constraints** — specific never-do rules a new hire could adjudicate. Failure: "should not hallucinate."
25
25
  12. **Failure-mode catalog** — drawn from real prototype outputs; before those exist, an explicit trace-review owner and due date are a pre-launch blocker. Failure: imagined as evidence; absent without an evidence-collection plan.
26
26
  13. **Fallback + model strategy** — refusal/uncertainty/timeout behavior plus a capability profile, a primary/fallback, and switch logic. Failure: happy-path only; a pinned model version.
27
- 14. **Ops readiness** — a golden-dataset owner + due date; cost-at-scale (tokens × volume × price). Failure: both skipped.
27
+ 14. **Ops readiness** — a golden-dataset owner + due date; cost-at-scale (tokens × volume × price). When an external client pays inference, an explicit `$0` product-inference equation plus the product's server-side cost drivers and honestly marked missing sizing inputs satisfies the cost requirement; do not require invented volume. Failure: both skipped.
28
28
 
29
29
  ---
30
30
 
@@ -40,7 +40,7 @@
40
40
  // one garbled entry disables the whole action layer with an error naming it. A config
41
41
  // error is loud and total, never partial.
42
42
 
43
- import { SLUG_SOURCE } from './contract.js';
43
+ import { SLUG_SOURCE, RE_SLUG } from './contract.js';
44
44
 
45
45
  export const PERMISSION_ORDER = ['auto-run', 'ask-once-if-new', 'requires-approval'];
46
46
 
@@ -368,8 +368,11 @@ export function resolveActionTypesAgainst({ registry, codeTable }) {
368
368
  // resolution depends on it, so a malformed list fails closed HERE — the one
369
369
  // validator both `act` and `registry status` consume (a status that calls the
370
370
  // layer disabled while act still writes is the two-matchers drift, rules.md).
371
- if (!Array.isArray(registry.domains) || registry.domains.some((d) => typeof d !== 'string')) {
372
- errors.push('domains: must be a list of domain-slug strings (every action resolves its domain against it)');
371
+ // Enforce the SLUG the message already claims (Codex R3 F3): a non-slug domain (e.g. "../../outside")
372
+ // would path-traverse when any surface joins it into domains/{domain}; the registry is the trust
373
+ // root every domain resolution reads, so fail closed here, not only in each caller's containment guard.
374
+ if (!Array.isArray(registry.domains) || registry.domains.some((d) => typeof d !== 'string' || !RE_SLUG.test(d))) {
375
+ errors.push('domains: must be a list of domain-slug strings (kebab-case; every action resolves its domain against it)');
373
376
  }
374
377
 
375
378
  const section = registry.action_types;
@@ -56,7 +56,6 @@ import { checkObjectContent } from './validate-object.js';
56
56
  import { appendEvent, readEvents, localDateStamp } from './event-log.js';
57
57
  import { digestFile } from './content-digest.js';
58
58
  import { resolveInstall } from './install-root.js';
59
- import { DEFAULT_DOMAIN } from './layout.js';
60
59
  import {
61
60
  slugify,
62
61
  serializeFrontmatter,
@@ -67,6 +66,26 @@ import {
67
66
  stageValidateRenameAll,
68
67
  } from './action-writers.js';
69
68
 
69
+ // A POST-WRITE failure (Codex R4 F2): the artifact IS on disk, but a later audit-append / hook step
70
+ // failed. The error carries `postWrite: true` so a surface adapter treats the mutation as LANDED (and
71
+ // records it in its honest-cancellation ledger), not as a refusal — event-log.md:92 "artifact written
72
+ // but audit failed" spine case. A pre-write refusal is a plain Error (no flag).
73
+ function postWriteError(message) {
74
+ const e = new Error(message);
75
+ e.postWrite = true;
76
+ return e;
77
+ }
78
+
79
+ // A PRE-write cancellation (Codex R6 F1): an OPTIONAL `ctx.signal` aborted BEFORE the durable write —
80
+ // so no artifact landed. `aborted: true` lets a surface report "cancelled", NOT "refused", and NOT as
81
+ // a post-write landed mutation. Backward-compatible: with no `ctx.signal` there is no check and no
82
+ // behavior change for any existing caller.
83
+ function abortedError(type) {
84
+ const e = new Error(`${type}: the turn was cancelled before the durable write — nothing was written.`);
85
+ e.aborted = true;
86
+ return e;
87
+ }
88
+
70
89
  // --- Payload schemas (D2 — hand-rolled, zero-dep) ------------------------------------
71
90
  //
72
91
  // A JS field-spec object per action satisfies ontology's "JSON schema" for 6 fixed
@@ -1193,6 +1212,61 @@ export function resolveActionTypes(registry) {
1193
1212
  return resolveActionTypesAgainst({ registry, codeTable: ACTION_REGISTRY });
1194
1213
  }
1195
1214
 
1215
+ // `describeActionSchema` — read-only resolution of ONE action type's EFFECTIVE payload
1216
+ // schema for `phronesis act <type> --schema` (change 0045). It reads the SAME
1217
+ // `.phronesis/registry.json` action-type table the action core validates against (so it
1218
+ // reflects any operator schema tightening, not just the code defaults) and returns plain,
1219
+ // deep-copied data the CLI formats. No write, no event, no approval — a caller reads the
1220
+ // payload contract instead of learning it by submitting empty payloads. All install/
1221
+ // registry access lives here so the `act` adapter stays a pure parse-and-delegate surface
1222
+ // (validate-actions D7b). Returns a discriminated result:
1223
+ // { status: 'error', message } install not found / registry unreadable
1224
+ // { status: 'invalid-registry', errors } the action layer is disabled (all-or-nothing)
1225
+ // { status: 'unknown-type', type, knownTypes, isCodeType }
1226
+ // { status: 'ok', type, source, fields } fields: [{ name, kind, required, nonEmpty, values? }]
1227
+ export async function describeActionSchema({ dir, cwd, type } = {}) {
1228
+ const installed = resolveInstall({ dir, cwd });
1229
+ if (installed.error) return { status: 'error', message: installed.error };
1230
+ const installRoot = installed.root;
1231
+
1232
+ let registry;
1233
+ try {
1234
+ registry = await fse.readJson(join(installRoot, '.phronesis', 'registry.json'));
1235
+ } catch (err) {
1236
+ return {
1237
+ status: 'error',
1238
+ message: `could not read the install registry (.phronesis/registry.json): ${err.message}`,
1239
+ };
1240
+ }
1241
+
1242
+ const { errors, source, effective } = resolveActionTypes(registry);
1243
+ if (errors.length) return { status: 'invalid-registry', errors };
1244
+
1245
+ // Own-property check, never a truthy prototype-chain lookup (the repo convention for
1246
+ // name-keyed JSON maps): `act constructor --schema` / `toString` / `__proto__` must be
1247
+ // unknown types, not Object.prototype members wearing a trench coat that reach the OK
1248
+ // path with an empty field list.
1249
+ if (!Object.hasOwn(effective, type)) {
1250
+ return {
1251
+ status: 'unknown-type',
1252
+ type,
1253
+ knownTypes: Object.keys(effective).sort(),
1254
+ isCodeType: Object.hasOwn(ACTION_REGISTRY, type),
1255
+ };
1256
+ }
1257
+ const entry = effective[type];
1258
+
1259
+ // Plain, deep-copied field-spec list — no live reference to the code table escapes.
1260
+ const fields = Object.entries(entry.schema || {}).map(([name, spec]) => ({
1261
+ name,
1262
+ kind: spec.kind,
1263
+ required: spec.required === true,
1264
+ nonEmpty: spec.nonEmpty === true,
1265
+ ...(spec.values !== undefined ? { values: [...spec.values] } : {}),
1266
+ }));
1267
+ return { status: 'ok', type, source, fields };
1268
+ }
1269
+
1196
1270
  // --- Permission gate (D5 — detective; specs/trust-scopes.md Scope 1) ------------------
1197
1271
 
1198
1272
  export function permissionGate(permission, { exists = false, approvalSource } = {}) {
@@ -1217,9 +1291,12 @@ export function permissionGate(permission, { exists = false, approvalSource } =
1217
1291
  // --- Execution-context resolution (D10) -----------------------------------------------
1218
1292
 
1219
1293
  // Resolve the active domain: explicit ctx.domain, else the domains/{slug}/ the cwd sits
1220
- // in, else DEFAULT_DOMAIN and the resolved slug MUST be in the install registry. An
1221
- // unregistered domain refuses; an action must never conjure a phantom domains/{slug}/
1222
- // tree as a side effect (the event-log --dir fail-closed lesson, applied to domains).
1294
+ // in, else the sole registered domain (core-0061 D4 the old DEFAULT_DOMAIN "pm" fallback
1295
+ // was wrong for every single-domain install not named pm) and the resolved slug MUST be
1296
+ // in the install registry. With no signal and several domains registered, refuse and list
1297
+ // them; with none registered, refuse. An unregistered domain refuses; an action must never
1298
+ // conjure a phantom domains/{slug}/ tree as a side effect (the event-log --dir fail-closed
1299
+ // lesson, applied to domains).
1223
1300
  // The registry is read ONCE per runAction (it also carries action_types since T2.4)
1224
1301
  // and passed in parsed. EXPORTED since core-0014: the retrieval surface (search.js)
1225
1302
  // resolves its default scope through this exact function — one resolver, two consumers
@@ -1231,9 +1308,21 @@ export function resolveDomain({ installRoot, registry, domain, cwd }) {
1231
1308
  const parts = relative(installRoot, resolve(cwd)).split(sep);
1232
1309
  if (parts[0] === 'domains' && parts[1]) slug = parts[1];
1233
1310
  }
1234
- if (!slug) slug = DEFAULT_DOMAIN;
1235
1311
 
1236
1312
  const registered = Array.isArray(registry.domains) ? registry.domains : [];
1313
+ // No explicit --domain and cwd not inside a domains/<slug>/ tree: the sole registered
1314
+ // domain wins (core-0061 D4). The install's one domain is the unambiguous default; the old
1315
+ // "pm" literal defaulted a business install onto a phantom domain and refused. Several
1316
+ // registered → cannot guess; refuse and list them so the caller picks with --domain. One
1317
+ // resolver, every domain-scoped verb inherits (lint, search, query, ingest, act, …).
1318
+ if (!slug) {
1319
+ if (registered.length === 1) return registered[0];
1320
+ throw new Error(
1321
+ registered.length === 0
1322
+ ? 'no domain specified and none are registered in this installation. Refusing — actions never create a domain as a side effect.'
1323
+ : `no domain specified and this installation has ${registered.length} registered (${registered.join(', ')}). Pass --domain <slug> to choose one.`,
1324
+ );
1325
+ }
1237
1326
  if (!registered.includes(slug)) {
1238
1327
  throw new Error(
1239
1328
  `domain "${slug}" is not registered in this installation (registered: ${registered.join(', ') || 'none'}). Refusing — actions never create a domain as a side effect.`,
@@ -1248,7 +1337,7 @@ export function resolveDomain({ installRoot, registry, domain, cwd }) {
1248
1337
  // dir explicit install dir (marker-checked; optional)
1249
1338
  // cwd where the caller runs (install/domain discovery; default process.cwd())
1250
1339
  // session REQUIRED — session_id on both events
1251
- // domain explicit domain slug (else cwd-derived, else DEFAULT_DOMAIN; D10)
1340
+ // domain explicit domain slug (else cwd-derived, else the sole registered domain; D10 / core-0061 D4)
1252
1341
  // actor / surface event identity (defaults: agent / cli, like `event emit`)
1253
1342
  // invocation_id attribution provenance supplied by a skill adapter or controlled
1254
1343
  // harness; caller-controlled metadata, never authority
@@ -1277,6 +1366,10 @@ export async function runAction({ type, payload = {}, ctx = {} } = {}) {
1277
1366
  ? ctx.invocation_id
1278
1367
  : undefined;
1279
1368
  const now = ctx.now || new Date();
1369
+ // OPTIONAL cancel signal (Codex R6 F1): a surface may pass ctx.signal to close the TOCTOU window —
1370
+ // re-checked immediately before each durable write. Absent ⇒ no check (backward-compatible).
1371
+ // Named cancelSignal to avoid colliding with the approval-signal boolean below.
1372
+ const cancelSignal = ctx.signal;
1280
1373
 
1281
1374
  // invocation_id is execution-context provenance, not action semantics. A model can
1282
1375
  // place arbitrary JSON in payload, so discard a payload-supplied value before schema
@@ -1452,6 +1545,7 @@ export async function runAction({ type, payload = {}, ctx = {} } = {}) {
1452
1545
  // The draft overwrites any prior candidate at this path (idempotent convergence: an
1453
1546
  // identical re-draft re-fires the same candidate, no new canonical state).
1454
1547
  if (built.draft) {
1548
+ if (cancelSignal?.aborted) throw abortedError(type); // R6 F1: no draft write lands after a cancel
1455
1549
  await stageValidateRename({
1456
1550
  installRoot,
1457
1551
  relPath: built.draft.writeRelPath,
@@ -1488,13 +1582,14 @@ export async function runAction({ type, payload = {}, ctx = {} } = {}) {
1488
1582
  // Same "WAS written, but the audit failed" guarantee as the committed path: the
1489
1583
  // draft already landed; say so plainly so a re-run is an idempotent re-draft, not a
1490
1584
  // blind retry into a confusing state.
1491
- throw new Error(
1585
+ throw postWriteError(
1492
1586
  `${type}: the draft WAS written to ${built.draft.writeRelPath}, but the compile.candidate audit event failed to append: ${err.message}`,
1493
1587
  );
1494
1588
  }
1495
1589
  return { ok: true, path: built.draft.writeRelPath, draft: true, changes_summary: built.changesSummary };
1496
1590
  }
1497
1591
 
1592
+ if (cancelSignal?.aborted) throw abortedError(type); // R6 F1: no canonical write lands after a cancel
1498
1593
  if (built.writes) {
1499
1594
  if (built.writes.length) await stageValidateRenameAll({ installRoot, writes: built.writes });
1500
1595
  } else {
@@ -1539,10 +1634,13 @@ export async function runAction({ type, payload = {}, ctx = {} } = {}) {
1539
1634
  now,
1540
1635
  });
1541
1636
  } catch (err) {
1637
+ if (wrote.length) {
1638
+ throw postWriteError(
1639
+ `${type}: the artifact WAS written to ${wrote.join(' and ')}, but the action_type.committed audit event failed to append: ${err.message}`,
1640
+ );
1641
+ }
1542
1642
  throw new Error(
1543
- wrote.length
1544
- ? `${type}: the artifact WAS written to ${wrote.join(' and ')}, but the action_type.committed audit event failed to append: ${err.message}`
1545
- : `${type}: the run was a no-op (nothing written), but the action_type.committed audit event failed to append: ${err.message}`,
1643
+ `${type}: the run was a no-op (nothing written), but the action_type.committed audit event failed to append: ${err.message}`,
1546
1644
  );
1547
1645
  }
1548
1646
 
@@ -1559,19 +1657,25 @@ export async function runAction({ type, payload = {}, ctx = {} } = {}) {
1559
1657
  event: committedEvent,
1560
1658
  });
1561
1659
  } catch (err) {
1660
+ if (wrote.length) {
1661
+ throw postWriteError(
1662
+ `${type}: the artifact WAS written to ${wrote.join(' and ')}, but hook execution after action_type.committed failed: ${err.message}`,
1663
+ );
1664
+ }
1562
1665
  throw new Error(
1563
- wrote.length
1564
- ? `${type}: the artifact WAS written to ${wrote.join(' and ')}, but hook execution after action_type.committed failed: ${err.message}`
1565
- : `${type}: the run was a no-op (nothing written), but hook execution after action_type.committed failed: ${err.message}`,
1666
+ `${type}: the run was a no-op (nothing written), but hook execution after action_type.committed failed: ${err.message}`,
1566
1667
  );
1567
1668
  }
1568
1669
  if (committedHooks.gateFailures.length) {
1569
1670
  const f = committedHooks.gateFailures[0];
1570
1671
  const detail = `${f.error}${f.stderrTail ? ` — ${f.stderrTail.trim()}` : ''}`;
1672
+ if (wrote.length) {
1673
+ throw postWriteError(
1674
+ `${type}: the artifact WAS written to ${wrote.join(' and ')}, but gate hook "${f.id}" failed at action_type.committed: ${detail} (audited in .phronesis/audit.log; the write is not rolled back)`,
1675
+ );
1676
+ }
1571
1677
  throw new Error(
1572
- wrote.length
1573
- ? `${type}: the artifact WAS written to ${wrote.join(' and ')}, but gate hook "${f.id}" failed at action_type.committed: ${detail} (audited in .phronesis/audit.log; the write is not rolled back)`
1574
- : `${type}: the run was a no-op (nothing written), but gate hook "${f.id}" failed at action_type.committed: ${detail} (audited in .phronesis/audit.log)`,
1678
+ `${type}: the run was a no-op (nothing written), but gate hook "${f.id}" failed at action_type.committed: ${detail} (audited in .phronesis/audit.log)`,
1575
1679
  );
1576
1680
  }
1577
1681
 
@@ -55,9 +55,12 @@ export {
55
55
  CONVERGENT_COMMITTED_HOOKS,
56
56
  defaultActionTypesSection,
57
57
  resolveActionTypes,
58
+ describeActionSchema,
58
59
  resolveDomain,
59
60
  } from './actions.js';
60
61
 
62
+ export { matchFrontmatterBlock } from './workspace-scan.js';
63
+
61
64
  export {
62
65
  buildActiveContextProjection,
63
66
  refreshActiveContext,