# v5 manifest schema

Source of truth for v5 manifest validation rules. The v5 reader and verifier
in `src/work-manifest.ts` implement against this document. v4 manifests continue
to parse (single-slice synthesis, see §6).

---

## 1. Top-level fields

Required:

| Field | Type | Notes |
|---|---|---|
| `schema_version` | string | `"5"` for v5 manifests |
| `name` | string | kebab-case identifier; matches `.forge/work/{type}/{name}/` |
| `type` | enum | `feature` \| `bugfix` \| `hotfix` \| `refactor` \| `greenfield` |
| `description` | string | one-line human description |
| `status` | enum | `in-progress` \| `paused` \| `completed` \| `escalated` |
| `created` | ISO-8601 date | |
| `command` | string | which slash command produced the manifest |
| `phases` | object | pre-build (discover, plan) and post-build (quality, deliver, support) gates |

Optional:

| Field | Type | Notes |
|---|---|---|
| `complexity` | enum | `trivial` \| `standard` \| `major` |
| `escalated_from` | string \| null | upstream manifest path on escalation |
| `successor_path` | string \| null | downstream manifest path on escalation |
| `slice_graph` | object | v5 NEW (§3); absent for v4-shaped manifests |
| `artifacts` | object | free-form artifact registry (e.g. brainstorm-approved.md) |

`phases.build` is **intentionally absent** in v5 manifests. v4 manifests
that contain `phases.build.tasks: []` still parse (see §6).

---

## 2. Phase gates (unchanged from v4)

Each gate inside `phases.{discover,plan,quality}.{gate-name}` has shape:

```yaml
{ status: <enum>, gate-passed: <bool> }
```

`status` ∈ `pending` \| `in-progress` \| `complete` \| `skipped` \| `not-applicable`.

`phases.deliver` and `phases.support` use plain booleans (not gate objects)
for v4 compatibility — these are not enforced by `gate-enforcer.sh`.

Gate names are flat strings. `gate-enforcer.sh` looks up requirements by
exact gate name in `hooks/config/gate-requirements.json`.

---

## 3. Slice graph (v5 NEW)

```yaml
slice_graph:
  current_slice: <slice-id>     # which slice is active
  slices:
    <slice-id>:
      type: skeleton | feature-slice | refactor-slice
      variant: <string>          # only for type: skeleton; identifies stack
      depends_on: [<slice-id>...]
      status: pending | in-progress | gated | complete
      gates:
        <gate-name>: { status: <enum>, gate-passed: <bool> }
```

### 3.1 Slice IDs

- Kebab-case strings, unique within the manifest.
- Must match `^[a-z][a-z0-9-]*$`.
- `current_slice` MUST refer to an existing slice ID.

### 3.2 DAG invariants (verifier MUST reject)

1. **Unresolved dependency** — every entry in `depends_on` resolves to an existing slice ID in the same manifest.
2. **Self-dependency** — slice ID may not appear in its own `depends_on`.
3. **Cycle** — the directed graph induced by `depends_on` must be acyclic. Verifier runs Kahn's algorithm; any node remaining after the pass = cycle.
4. **Skeleton uniqueness** — at most one slice has `type: skeleton`.
5. **Skeleton root** — if a skeleton slice exists, its `depends_on` MUST be empty.
6. **Skeleton ancestry** — if a skeleton slice exists, every non-skeleton slice MUST transitively depend on it. Verifier walks reverse-BFS from the skeleton; any unreached non-skeleton slice = error.
7. **Variant required for skeleton** — if `type: skeleton`, `variant` MUST be a non-empty string.
8. **Variant forbidden for non-skeleton** — if `type` ≠ `skeleton`, `variant` MUST be absent or null.

### 3.3 Sibling ordering

When two slices have the same depth in the DAG (siblings), execution order
is **insertion order in the `slices:` map**. This is deterministic but
explicit — agents adding new slices MUST insert them at the position they
should execute. This avoids hidden dependencies on hash-map iteration.

**Implementation contract.** The verifier uses `js-yaml` with default
schema; js-yaml preserves YAML map insertion order on `load()` and `dump()`.
If the implementer discovers round-trip order loss in any code path
(library upgrade, alternate serializer, etc.), they MUST add an
explicit `order: <int>` field per slice as a follow-up before Phase 2.
Detect by writing a unit test that dumps then re-parses a 5-slice manifest
and asserts identical key order.

### 3.4 Gate name collisions (verifier MUST reject)

A slice gate name MUST NOT collide with any manifest-level gate name in
`phases.{discover,plan,quality}`. Reason: `gate-enforcer.sh` matches by
unscoped name. Reusing a name would silently apply the wrong requirement.

Reserved manifest-level gate names (v4 + v5): `codebase-analysis`,
`brainstorm`, `design-system`, `architecture`, `task-decompose`,
`code-review-final`, `test-plan`, `test-execution`, `uiux-review`.

Allowed slice gate names: `skeleton-runs`, `build-tdd`, `wiki-lint`,
`runtime-reach`, `code-review`. These differ from the manifest-level set
by design (per Codex round 1, §3).

### 3.5 Slice status lifecycle (convention, not enforced)

> **By convention, this section describes a *lifecycle convention* that
> skills follow voluntarily. The verifier does NOT block transitions.
> Observed usage has not shown agents bypassing this discipline silently;
> if it ever does, restore `verifyTransition` from git history
> (commit `be5ab93`'s parent).**

Lifecycle path (recommended; skills are written to follow this):

```
pending → in-progress → gated → complete
gated → in-progress       (gate failed; rework)
```

`complete` is the conventional terminal state. If a downstream change
invalidates a completed slice's work, the convention is to add a NEW
slice that supersedes the old (preserving the audit trail) rather than
mutate the completed slice's status. Skills enforce this in their
prompts; the verifier does not.

A slice should enter `in-progress` only when every slice in its
`depends_on` is in `complete` status. Skills check this before
advancing.

### 3.6 Fan-out / fan-in completion (planning convention)

The slice graph is **terminally settled** when every slice is in
`complete` status. Skills running the post-build
`phases.quality.code-review-final` gate check this convention before
proceeding.

Fan-out: a slice with multiple non-dependent successors creates parallel
branches. Each branch advances independently when its own gates pass.

Fan-in: a slice with multiple `depends_on` entries waits for ALL of them
to reach `complete` before entering `in-progress`.

`current_slice` is a single global pointer in v5.0; multi-active-slice
(per-worker pointer for parallel terminal tabs) is deferred to v6.0
alongside skills-first interruptibility. v5.0 does NOT enforce
manifest-write serialization at the file level — single-session
discipline + git review catches this in practice. Restore `flock(2)`
write serialization if real usage shows concurrent-write corruption.

---

## 4. Slice gate semantics

| Gate | Skill | Agent | When |
|---|---|---|---|
| `build-tdd` | `build-tdd` | `builder` | Per slice |
| `wiki-lint` | `support-wiki-lint` | — | Per slice (validates the slice's wiki contributions) |
| `runtime-reach` | `support-runtime-reachability` | — | Per slice |
| `code-review` | `quality-code-review` | `craft-reviewer` | Per slice (lighter than `code-review-final`) |

Per-slice `code-review` is **distinct** from manifest-level `code-review-final`.
The slice version checks pattern conformance + stub detection for that
slice's diff. The final version reviews the cross-slice integration.

**Phase-close wiki lint is a hook, not a gate.** The phase-close gate
hook ALSO invokes `support-wiki-lint`, but against the CUMULATIVE
`aiwiki/` state at phase close — not against a slice. It does not
correspond to any manifest gate slot (the per-slice `wiki-lint` already
gates each slice's contributions). The phase-close invocation is a
defense-in-depth check that catches drift across the whole wiki between
when a slice's lint passed and when the phase closes (e.g. another slice
edited the same file). The hook fails phase-close if lint fails; no
manifest field tracks it (the failure is its own evidence).

---

## 5. Verifier interface

`src/work-manifest.ts` exports two layers — structural (stateless) and
transitional (stateful):

```ts
type ManifestParseResult =
  | { ok: true; manifest: Manifest; schema: 'v4' | 'v5' }
  | { ok: false; errors: ManifestError[] };

// Structural: validates a single manifest snapshot. Stateless.
// Implements §1, §3.1-3.4, §3.6, §6.
function parseManifest(yaml: string): ManifestParseResult;
function readManifest(path: string): ManifestParseResult;

// NOTE: verifyTransition() and the transitional ErrorCode entries
// (E_BAD_STATUS_TRANSITION, E_DEPENDENCY_NOT_COMPLETE,
// E_TERMINAL_SLICE_MUTATED) were removed; slice status transitions
// remain a documented convention, not enforced by the verifier.

type ManifestError = {
  code: ErrorCode;     // see enum below
  message: string;     // human-readable, includes the offending value
  path: string;        // YAML path, e.g. "slice_graph.slices.auth-login.depends_on[1]"
};

type ErrorCode =
  | 'E_UNRESOLVED_DEP' | 'E_SELF_DEP' | 'E_CYCLE'
  | 'E_MULTIPLE_SKELETONS' | 'E_SKELETON_NOT_ROOT' | 'E_ORPHAN_SLICE'
  | 'E_VARIANT_REQUIRED' | 'E_VARIANT_FORBIDDEN'
  | 'E_GATE_COLLISION' | 'E_BAD_SLICE_ID' | 'E_BAD_CURRENT_SLICE'
  | 'E_MISSING_REQUIRED_FIELD' | 'E_BAD_ENUM_VALUE'
  | 'E_PHASE_GATE_PREMATURE'   // §3.6: phases.quality.code-review-final.gate-passed = true
                                //       while any slice is not in terminal state (complete)
  | 'E_YAML_PARSE' | 'E_FILE_NOT_FOUND';
```

`parseManifest` returns ALL errors (not first-fail) so users fix the
manifest in one pass.

### 5.1 Error message format

```
<E_CODE>  <YAML path>
  <human message; one sentence; quotes the offending value>
  Fix: <one-sentence remediation>
```

Example:

```
E_CYCLE  slice_graph.slices
  Cycle detected: auth-login → profile-page → auth-login.
  Fix: remove one edge from depends_on, or restructure into a single fan-in slice.
```

### 5.2 CLI surface

The verifier ships as a dedicated sub-command:

```
$ npx @jamie-tam/forge verify-manifest [path]
  --json                   Machine-readable output

Exit codes:
  0  No errors
  1  Verification errors (errors written to stdout in human or JSON format)
  2  Internal error (file not found, bad YAML syntax, etc.)
```

---

## 6. v4 → v5 migration mapping

A v4 manifest (no `slice_graph`, has `phases.build.tasks: []`) is
synthesized into a v5-shaped in-memory structure:

```yaml
slice_graph:
  current_slice: legacy-build
  slices:
    legacy-build:
      type: feature-slice
      depends_on: []
      status: <derived from phases.build state>
      gates:
        build-tdd: { status: <derived>, gate-passed: <derived> }
```

Derivation rules (evaluated in order; first match wins):

1. v4 `tasks: []` is empty (no build work yet planned) → `status: pending`, `gate-passed: false`. Treats vacuous "all complete / all pending" as not-yet-started.
2. all tasks `complete` (with at least one task) → `status: complete`, `gate-passed: true`.
3. any task `in-progress` and none failed → `status: in-progress`, `gate-passed: false`.
4. all tasks `pending` (with at least one task) → `status: pending`, `gate-passed: false`.
5. otherwise (mixed states without rule 3) → `status: in-progress`, `gate-passed: false`.

The v4 `phases.quality.*` gates are kept AS-IS at manifest level (NOT
duplicated into the synthesized slice). They run at v4-style "post-build"
time — after `legacy-build` slice completes.

The migration is a **read-time synthesis**, not an on-disk rewrite. v4
manifests on disk stay v4-shaped until `/forge-evolve` runs, which writes a
true v5 `slice_graph` block.

### 6.1 Migration semantic limitation (acknowledged)

The "all v4 tasks complete = `build-tdd` passed" inference is
**pragmatic, not semantic**. v4 task names were arbitrary (e.g.
"core-rule", "discover-codebase-analysis", "readme-update") and many
weren't TDD work. The synthesis treats the migrated slice as a
black-box "build phase that finished", not as evidence that TDD was
followed.

Consequence: a v4-migrated manifest read by v5 tooling will show
`build-tdd: passed` even if no TDD occurred. This is acceptable because
(a) the v4 work has already shipped — re-litigating the gate would
block all in-flight v4 features, and (b) `/forge-evolve` prompts the user to
review the synthesis and rewrite the slice graph if they want true
v5 gate semantics for ongoing work.

`/forge-evolve` shows the synthesized slice graph alongside the v4 task list
and asks: "Accept this synthesis (treat as legacy build), or rewrite as
v5 slice graph (re-run gates per slice)?". The default is "accept".

---

## 7. Reserved evolutions

Out of scope for v5.0; slots reserved in the schema so we don't have
to break it again:

- **Conditional edges** (`depends_on: [{slice: X, when: <cond>}]`) — deferred to v5.2.
- **Slice retry policy** (`retry: { max, backoff }`) — deferred; rework via `gated → in-progress` is the v5.0 mechanism.
- **Per-worker `current_slice`** for parallel terminal tabs — deferred to v6.0 with skills-first interruptibility.
