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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ guess) on any of:
| `_assemble` | the single terminal unit (`{ _file }`) that combines the fragment outputs into the phase's artifact(s) |
| `_produces` | the artifact file(s) the phase writes. Each entry is either a bare filename (unconditional) or an inline conditional map `{ file: <path>, _when: <prose> }` — an artifact produced ONLY when the design predicate holds (e.g. `terraform/eks.tf` only when EKS is in the design). Same `{ file, _when }` shape as `_knowledge`; `_when` is opaque prose the interpreter reads at runtime and CI does NOT evaluate. A trailing-slash `file` (e.g. `kubernetes/`) names a produced DIRECTORY when the unit emits a set of dynamically-named files. |
| `_advances_to` | (backbone phases only) the phase that runs next on success — or a terminal (`complete`). A sidebar has NO `_advances_to`. |
| `_gates` | (sidebar phases only, optional) the backbone phase that must NOT start while this sidebar is UNRESOLVED (its `phases.<sidebar>` status is not `"completed"`). Declares in the contract what previously lived in prose (e.g. the what-if workshop holds Generate until it resolves). A declined sidebar is `"completed"` (resolved), so the gate lifts — this key never makes a sidebar mandatory. The target must be an existing backbone phase, never a terminal. |
| `_exec` | (optional) the phase's EXECUTION MODE. When present, the phase's WORK (fragments + assembler) is dispatched to a fresh isolated sub-agent window with file-only I/O, at the capability tier named by `_exec._agent`; the interpreter keeps the gates, `_init` setup, and the state transition in the MAIN window (see § `_exec`). Requires `_interactive: false`. Absent = the phase runs inline in the main window. |
| `_re_entry_guard` | (backbone phases with a downstream only) the stale-downstream guard — STOP re-running this phase if its downstream phase already completed, unless the user confirms (see below). Terminal phases and sidebars have none. |
| `_preconditions` | the entry gate — an ordered list of checks that MUST pass before the phase does any work (predecessor completed, single active phase, inputs present/valid). See § Gate protocol. |
Expand Down Expand Up @@ -483,6 +484,13 @@ opts in), and it returns control to the flow rather than advancing `current_phas
— so it has no `_advances_to`, and it never appears as a `current_phase` value.
WHERE a sidebar is offered is orchestration prose (see SKILL.md), not part of
the phase contract.
A sidebar MAY declare `_gates: <backbone-phase>`: while the sidebar is
UNRESOLVED (its `phases.<sidebar>` status is not `"completed"`), the interpreter
must not start the named phase. Resolution — including a decline — lifts the
gate (see the status semantics below: declined is still `"completed"`), so
`_gates` holds ordering, never participation. A sidebar without `_gates` blocks
nothing. This generalizes: a future "re-probe Discover" sidebar would use the
same construct anchored at a different point.

**Sidebar status semantics (important):** marking a sidebar's
`phases.<sidebar>` as `"completed"` means the sidebar was RESOLVED (offered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
- Warm-start rule: `current_phase == "estimate"` AND
`phases.estimate == "completed"` AND `phases.workshop == "pending"` →
present the workshop offer; NEVER recompute Estimate.
- The hold-Generate rule above is DECLARED, not just prose: the workshop
phase's frontmatter carries `_gates: generate` (INTERPRETER.md § `_gates`),
so Generate must not start while `phases.workshop` is unresolved. Declining
resolves the sidebar (`"completed"`) and lifts the gate.
- If Generate (or later) is already `completed`, apply the Estimate re-entry
guard (confirm → reset downstream to pending) before any refresh.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ _phase: workshop
_title: "What-If Workshop (Optional)"
_kind: sidebar
_requires_phase: estimate
_gates: generate
_trigger:
{
_when: "user opts in post-Estimate (estimate-assemble offer [A], or says what if / reprice / workshop mode / compare scenarios)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ guess) on any of:
| `_assemble` | the single terminal unit (`{ _file }`) that combines the fragment outputs into the phase's artifact(s) |
| `_produces` | the artifact file(s) the phase writes. Each entry is either a bare filename (unconditional) or an inline conditional map `{ file: <path>, _when: <prose> }` — an artifact produced ONLY when the design predicate holds (e.g. `terraform/eks.tf` only when EKS is in the design). Same `{ file, _when }` shape as `_knowledge`; `_when` is opaque prose the interpreter reads at runtime and CI does NOT evaluate. A trailing-slash `file` (e.g. `kubernetes/`) names a produced DIRECTORY when the unit emits a set of dynamically-named files. |
| `_advances_to` | (backbone phases only) the phase that runs next on success — or a terminal (`complete`). A sidebar has NO `_advances_to`. |
| `_gates` | (sidebar phases only, optional) the backbone phase that must NOT start while this sidebar is UNRESOLVED (its `phases.<sidebar>` status is not `"completed"`). Declares in the contract what previously lived in prose (e.g. the what-if workshop holds Generate until it resolves). A declined sidebar is `"completed"` (resolved), so the gate lifts — this key never makes a sidebar mandatory. The target must be an existing backbone phase, never a terminal. |
| `_exec` | (optional) the phase's EXECUTION MODE. When present, the phase's WORK (fragments + assembler) is dispatched to a fresh isolated sub-agent window with file-only I/O, at the capability tier named by `_exec._agent`; the interpreter keeps the gates, `_init` setup, and the state transition in the MAIN window (see § `_exec`). Requires `_interactive: false`. Absent = the phase runs inline in the main window. |
| `_re_entry_guard` | (backbone phases with a downstream only) the stale-downstream guard — STOP re-running this phase if its downstream phase already completed, unless the user confirms (see below). Terminal phases and sidebars have none. |
| `_preconditions` | the entry gate — an ordered list of checks that MUST pass before the phase does any work (predecessor completed, single active phase, inputs present/valid). See § Gate protocol. |
Expand Down Expand Up @@ -483,6 +484,13 @@ opts in), and it returns control to the flow rather than advancing `current_phas
— so it has no `_advances_to`, and it never appears as a `current_phase` value.
WHERE a sidebar is offered is orchestration prose (see SKILL.md), not part of
the phase contract.
A sidebar MAY declare `_gates: <backbone-phase>`: while the sidebar is
UNRESOLVED (its `phases.<sidebar>` status is not `"completed"`), the interpreter
must not start the named phase. Resolution — including a decline — lifts the
gate (see the status semantics below: declined is still `"completed"`), so
`_gates` holds ordering, never participation. A sidebar without `_gates` blocks
nothing. This generalizes: a future "re-probe Discover" sidebar would use the
same construct anchored at a different point.

**Sidebar status semantics (important):** marking a sidebar's
`phases.<sidebar>` as `"completed"` means the sidebar was RESOLVED (offered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
- Warm-start rule: `current_phase == "estimate"` AND
`phases.estimate == "completed"` AND `phases.workshop == "pending"` →
present the workshop offer; NEVER recompute Estimate.
- The hold-Generate rule above is DECLARED, not just prose: the workshop
phase's frontmatter carries `_gates: generate` (INTERPRETER.md § `_gates`),
so Generate must not start while `phases.workshop` is unresolved. Declining
resolves the sidebar (`"completed"`) and lifts the gate.
- If Generate (or later) is already `completed`, apply the Estimate re-entry
guard (confirm → reset downstream to pending) before any refresh.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ guess) on any of:
| `_assemble` | the single terminal unit (`{ _file }`) that combines the fragment outputs into the phase's artifact(s) |
| `_produces` | the artifact file(s) the phase writes. Each entry is either a bare filename (unconditional) or an inline conditional map `{ file: <path>, _when: <prose> }` — an artifact produced ONLY when the design predicate holds (e.g. `terraform/eks.tf` only when EKS is in the design). Same `{ file, _when }` shape as `_knowledge`; `_when` is opaque prose the interpreter reads at runtime and CI does NOT evaluate. A trailing-slash `file` (e.g. `kubernetes/`) names a produced DIRECTORY when the unit emits a set of dynamically-named files. |
| `_advances_to` | (backbone phases only) the phase that runs next on success — or a terminal (`complete`). A sidebar has NO `_advances_to`. |
| `_gates` | (sidebar phases only, optional) the backbone phase that must NOT start while this sidebar is UNRESOLVED (its `phases.<sidebar>` status is not `"completed"`). Declares in the contract what previously lived in prose (e.g. the what-if workshop holds Generate until it resolves). A declined sidebar is `"completed"` (resolved), so the gate lifts — this key never makes a sidebar mandatory. The target must be an existing backbone phase, never a terminal. |
| `_exec` | (optional) the phase's EXECUTION MODE. When present, the phase's WORK (fragments + assembler) is dispatched to a fresh isolated sub-agent window with file-only I/O, at the capability tier named by `_exec._agent`; the interpreter keeps the gates, `_init` setup, and the state transition in the MAIN window (see § `_exec`). Requires `_interactive: false`. Absent = the phase runs inline in the main window. |
| `_re_entry_guard` | (backbone phases with a downstream only) the stale-downstream guard — STOP re-running this phase if its downstream phase already completed, unless the user confirms (see below). Terminal phases and sidebars have none. |
| `_preconditions` | the entry gate — an ordered list of checks that MUST pass before the phase does any work (predecessor completed, single active phase, inputs present/valid). See § Gate protocol. |
Expand Down Expand Up @@ -483,6 +484,13 @@ opts in), and it returns control to the flow rather than advancing `current_phas
— so it has no `_advances_to`, and it never appears as a `current_phase` value.
WHERE a sidebar is offered is orchestration prose (see SKILL.md), not part of
the phase contract.
A sidebar MAY declare `_gates: <backbone-phase>`: while the sidebar is
UNRESOLVED (its `phases.<sidebar>` status is not `"completed"`), the interpreter
must not start the named phase. Resolution — including a decline — lifts the
gate (see the status semantics below: declined is still `"completed"`), so
`_gates` holds ordering, never participation. A sidebar without `_gates` blocks
nothing. This generalizes: a future "re-probe Discover" sidebar would use the
same construct anchored at a different point.

**Sidebar status semantics (important):** marking a sidebar's
`phases.<sidebar>` as `"completed"` means the sidebar was RESOLVED (offered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
- Warm-start rule: `current_phase == "estimate"` AND
`phases.estimate == "completed"` AND `phases.workshop == "pending"` →
present the workshop offer; NEVER recompute Estimate.
- The hold-Generate rule above is DECLARED, not just prose: the workshop
phase's frontmatter carries `_gates: generate` (INTERPRETER.md § `_gates`),
so Generate must not start while `phases.workshop` is unresolved. Declining
resolves the sidebar (`"completed"`) and lifts the gate.
- If Generate (or later) is already `completed`, apply the Estimate re-entry
guard (confirm → reset downstream to pending) before any refresh.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,98 @@ _produces:
assert.match(findings.map((f) => f.message).join('\n'), /sidebar phase 'feedback' must declare a phase-level _trigger/);
});

// ---- _gates (sidebar holds a backbone phase) ----
it('accepts a sidebar with _gates naming an existing backbone phase', () => {
const files = chainSkill();
files['references/phases/feedback/feedback.md'] = files[
'references/phases/feedback/feedback.md'
].replace('_kind: sidebar', '_kind: sidebar\n_gates: clarify');
const findings = validateFixture(files);
assert.equal(findings.length, 0, `expected clean, got: ${JSON.stringify(findings)}`);
});

it('rejects _gates naming a phase that does not exist on disk (dangling gate)', () => {
const files = chainSkill();
files['references/phases/feedback/feedback.md'] = files[
'references/phases/feedback/feedback.md'
].replace('_kind: sidebar', '_kind: sidebar\n_gates: generate');
const findings = validateFixture(files);
assert.match(findings.map((f) => f.message).join('\n'), /_gates 'generate' names no existing phase/);
});

it('rejects _gates naming a terminal', () => {
const files = chainSkill();
files['references/phases/feedback/feedback.md'] = files[
'references/phases/feedback/feedback.md'
].replace('_kind: sidebar', '_kind: sidebar\n_gates: complete');
const findings = validateFixture(files);
assert.match(findings.map((f) => f.message).join('\n'), /_gates 'complete' names a terminal/);
});

it('rejects _gates on a backbone phase', () => {
const files = chainSkill();
files['references/phases/discover/discover.md'] = files[
'references/phases/discover/discover.md'
].replace('_init: true', '_init: true\n_gates: clarify');
const findings = validateFixture(files);
assert.match(findings.map((f) => f.message).join('\n'), /backbone phase 'discover' must NOT declare _gates/);
});

it('rejects _gates naming a sidebar when that target declares frontmatter', () => {
const files = {
...chainSkill(),
...(() => {
const name = 'workshop';
return {
[`references/phases/${name}/${name}.md`]:
`---
_phase: ${name}
_title: "${name}"
_kind: sidebar
_gates: feedback
_requires_phase: discover
_trigger: { _when: "user opts in" }
_fragments:
- _id: ${name}
_trigger: { _always: true }
_file: phases/${name}/${name}-frag.md
_assemble:
_file: phases/${name}/${name}-asm.md
_produces:
- ${name}.json
---
# ${name}
`,
[`references/phases/${name}/${name}-frag.md`]:
`---
_fragment: ${name}
_of_phase: ${name}
_contributes:
- ${name}.json
---
# frag
`,
[`references/phases/${name}/${name}-asm.md`]:
`---
_assemble: asm-${name}
_of_phase: ${name}
_reads:
- ${name}
_produces:
- ${name}.json
---
# asm
`,
};
})(),
};
const findings = validateFixture(files);
assert.match(
findings.map((f) => f.message).join('\n'),
/_gates 'feedback' names a sidebar phase/,
);
});

it('rejects a backbone phase that declares a phase-level _trigger', () => {
const files = chainSkill();
files['references/phases/clarify/clarify.md'] = files[
Expand Down
Loading