@noir-ai/workflow 1.0.0-beta.1 → 1.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +147 -14
- package/dist/index.js +114 -14
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -6,10 +6,36 @@ type Phase = (typeof PHASES)[number];
|
|
|
6
6
|
declare const STATES: readonly ["draft", "clarifying", "specified", "planned", "executing", "verifying", "done", "blocked", "abandoned"];
|
|
7
7
|
type WorkflowState = (typeof STATES)[number];
|
|
8
8
|
type Mode = 'full' | 'quick';
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Task classification — drives the soft, escapable gate predicates (slice P:
|
|
11
|
+
* the PRD recommendation at the spec gate). Mirrors the user-facing
|
|
12
|
+
* `prd.mandatoryFor` enum in @noir-ai/core's `NoirConfigSchema` (declared
|
|
13
|
+
* LOCALLY here so workflow has no core-cycle concern; the two literals stay in
|
|
14
|
+
* sync by tests). `undefined` (the default for legacy tasks) ⇒ no soft gate
|
|
15
|
+
* fires — additive, fully backward-compatible.
|
|
16
|
+
*/
|
|
17
|
+
declare const TASK_CLASSES: readonly ["feature", "epic", "enhancement", "bugfix", "spike", "quick-task", "refactor"];
|
|
18
|
+
type TaskClass = (typeof TASK_CLASSES)[number];
|
|
19
|
+
/**
|
|
20
|
+
* Input shape for {@link recordGate} — what CALLERS pass. Omits `at` (the
|
|
21
|
+
* recorder stamps it from `Date.now()` so the audit reflects when the gate
|
|
22
|
+
* actually fired, not when the caller constructed the object). Split out of
|
|
23
|
+
* {@link GateResult} (debt-batch A / W3): callers used to pass a throwaway
|
|
24
|
+
* `at: 0` that recordGate overrode — this shape makes the override implicit.
|
|
25
|
+
*/
|
|
26
|
+
interface GateResultInput {
|
|
10
27
|
phase: Phase;
|
|
11
28
|
decision: 'approved' | 'forced' | 'skipped';
|
|
12
29
|
reason?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A recorded gate decision — the AUTHORITATIVE shape stored in the
|
|
33
|
+
* `audit:<taskId>` KV (the SOT per S4 spec §5 / §11 OQ-5) and surfaced via
|
|
34
|
+
* `task.history` (a derived view the engine regenerates from the audit KV).
|
|
35
|
+
* `at` is always present here; callers that want to RECORD a gate pass
|
|
36
|
+
* {@link GateResultInput} (no `at`) to {@link recordGate}.
|
|
37
|
+
*/
|
|
38
|
+
interface GateResult extends GateResultInput {
|
|
13
39
|
at: number;
|
|
14
40
|
}
|
|
15
41
|
interface TaskState {
|
|
@@ -19,12 +45,43 @@ interface TaskState {
|
|
|
19
45
|
state: WorkflowState;
|
|
20
46
|
phase: Phase;
|
|
21
47
|
mode: Mode;
|
|
48
|
+
/**
|
|
49
|
+
* DERIVED view of the gate audit for this task, regenerated from the
|
|
50
|
+
* authoritative `audit:<taskId>` KV (S4 spec §11 OQ-5) by the engine on
|
|
51
|
+
* every write and every status read. Kept on the TaskState so consumers
|
|
52
|
+
* (CLI `task status`, the daemon `workflow_status` tool, MCP clients) can
|
|
53
|
+
* read the gate history from the persisted TaskState without a second KV
|
|
54
|
+
* lookup. Never edited directly outside the engine — derive via
|
|
55
|
+
* {@link readGateHistory}.
|
|
56
|
+
*/
|
|
22
57
|
history: GateResult[];
|
|
58
|
+
/** Task classification (slice P). `undefined` ⇒ no soft PRD gate fires. */
|
|
59
|
+
taskClass?: TaskClass;
|
|
23
60
|
jumpEntry?: Phase;
|
|
24
61
|
/** Reason captured by `setBlocked` (admin escape; set directly, not via FSM). */
|
|
25
62
|
blockReason?: string;
|
|
26
63
|
updatedAt: number;
|
|
27
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* The gate-config slice the engine consumes (debt-batch A / P4). Mirrors the
|
|
67
|
+
* user-facing `prd:` block from @noir-ai/core's `NoirConfigSchema` — declared
|
|
68
|
+
* locally (with a `readonly` array) so workflow has no core-cycle concern; the
|
|
69
|
+
* daemon / CLI bridges NoirConfig → this shape at construction time. Every
|
|
70
|
+
* field has a sane default, so an unspecified `gateConfig` (the legacy
|
|
71
|
+
* constructor call) resolves to "feature/epic trigger the PRD recommendation".
|
|
72
|
+
*/
|
|
73
|
+
interface WorkflowGateConfig {
|
|
74
|
+
/**
|
|
75
|
+
* PRD soft-gate config. `mandatoryFor` lists the task classes for which a
|
|
76
|
+
* missing PRD artifact (at the moment of entering the spec phase, in full
|
|
77
|
+
* mode) is surfaced as an observable, escapable recommendation on the spec
|
|
78
|
+
* gate. The advance still proceeds — `--force <reason>` is the explicit
|
|
79
|
+
* override path; quick mode + unlisted classes skip the check entirely.
|
|
80
|
+
*/
|
|
81
|
+
prd: {
|
|
82
|
+
mandatoryFor: readonly TaskClass[];
|
|
83
|
+
};
|
|
84
|
+
}
|
|
28
85
|
|
|
29
86
|
/**
|
|
30
87
|
* Write intake artifact to .noir/intake/<taskId>.md
|
|
@@ -35,6 +92,13 @@ declare function writeIntake(root: string, taskId: string, content: string): voi
|
|
|
35
92
|
* Creates markdown with frontmatter and body
|
|
36
93
|
*/
|
|
37
94
|
declare function writeSpec(root: string, taskId: string, slug: string, body: string): void;
|
|
95
|
+
/**
|
|
96
|
+
* Write PRD artifact to .noir/prd/<taskId>-<slug>.md
|
|
97
|
+
* Pre-SDD product document; the spec @imports it (prdRef). No FSM change.
|
|
98
|
+
*/
|
|
99
|
+
declare function writePrd(root: string, taskId: string, slug: string, body: string): void;
|
|
100
|
+
/** Read a PRD artifact, or null if absent. */
|
|
101
|
+
declare function readPrd(root: string, taskId: string, slug: string): string | null;
|
|
38
102
|
/**
|
|
39
103
|
* Write plan artifact to .noir/plans/<taskId>-<slug>.md
|
|
40
104
|
*/
|
|
@@ -63,7 +127,10 @@ interface AdvanceOpts {
|
|
|
63
127
|
* Pass a gate without satisfying its criteria. Requires a non-empty `reason`
|
|
64
128
|
* (validated here, in the engine — `recordGate` is policy-free). The landing
|
|
65
129
|
* gate, if any, is recorded with `decision: 'forced'`. Mutually exclusive
|
|
66
|
-
* with {@link skip}.
|
|
130
|
+
* with {@link skip}. This is ALSO the explicit-override path for the soft
|
|
131
|
+
* PRD recommendation (debt-batch A / P4): supplying `--force <reason>` at the
|
|
132
|
+
* spec gate of a mandatoryFor task with no PRD records `forced` with the
|
|
133
|
+
* user's reason instead of the recommendation note.
|
|
67
134
|
*/
|
|
68
135
|
force?: {
|
|
69
136
|
reason?: string;
|
|
@@ -72,7 +139,9 @@ interface AdvanceOpts {
|
|
|
72
139
|
* Jump directly to a phase, bypassing the FSM (the escape hatch for blocked /
|
|
73
140
|
* out-of-order resumption). The landing is recorded as `jumpEntry` on the
|
|
74
141
|
* TaskState; a landing gate-state (specified/planned/done) still records its
|
|
75
|
-
* gate so the observable-checkpoint invariant holds.
|
|
142
|
+
* gate so the observable-checkpoint invariant holds. A no-op jump (target
|
|
143
|
+
* equal to the current phase) returns the task unchanged without re-recording
|
|
144
|
+
* any gate (W3 guard — the prior behavior double-stamped the audit).
|
|
76
145
|
*/
|
|
77
146
|
to?: Phase;
|
|
78
147
|
/**
|
|
@@ -94,7 +163,8 @@ interface AdvanceOpts {
|
|
|
94
163
|
* Policy that did not belong in T1/T2 lives here:
|
|
95
164
|
* • `--force` requires a non-empty reason (validated before any gate write),
|
|
96
165
|
* • `blocked` / `abandoned` have no incoming FSM edges and are set directly,
|
|
97
|
-
* • `opts.to` jumps past FSM edges and is recorded via `jumpEntry
|
|
166
|
+
* • `opts.to` jumps past FSM edges and is recorded via `jumpEntry`,
|
|
167
|
+
* • (P4) the soft PRD recommendation at the spec gate for mandatoryFor tasks.
|
|
98
168
|
*
|
|
99
169
|
* Modes (Full/Quick) and cross-session resume are T5; MCP tools are T6 — the
|
|
100
170
|
* engine stays mode-agnostic here and only stores `mode` on the TaskState.
|
|
@@ -109,6 +179,7 @@ declare class WorkflowEngine {
|
|
|
109
179
|
*/
|
|
110
180
|
readonly root: string;
|
|
111
181
|
private readonly projectId;
|
|
182
|
+
private readonly gateConfig;
|
|
112
183
|
constructor(store: Store,
|
|
113
184
|
/**
|
|
114
185
|
* Project root — the engine is constructed with it so modes (quickPath
|
|
@@ -116,13 +187,26 @@ declare class WorkflowEngine {
|
|
|
116
187
|
* (checkpoint/audit export) are self-contained. Public so the modes module
|
|
117
188
|
* can pass it to {@link ArtifactWriter}.
|
|
118
189
|
*/
|
|
119
|
-
root: string, projectId: ProjectId
|
|
190
|
+
root: string, projectId: ProjectId,
|
|
191
|
+
/**
|
|
192
|
+
* Gate-config slice (debt-batch A / P4). Optional — the legacy 3-arg call
|
|
193
|
+
* shape (every existing consumer) resolves to {@link DEFAULT_GATE_CONFIG}
|
|
194
|
+
* (PRD recommendation fires for feature/epic). The daemon / CLI bridge
|
|
195
|
+
* passes the resolved `prd.mandatoryFor` from NoirConfig so user overrides
|
|
196
|
+
* take effect; tests pass an explicit shape to pin behavior.
|
|
197
|
+
*/
|
|
198
|
+
gateConfig?: WorkflowGateConfig);
|
|
120
199
|
/**
|
|
121
200
|
* Create a new task at draft/intake, persist it, and point `workflow:active`
|
|
122
201
|
* at it. Re-starting an existing taskId overwrites it (intentional — the KV is
|
|
123
202
|
* the source of truth, not a journal).
|
|
203
|
+
*
|
|
204
|
+
* `taskClass` (debt-batch A / P4) is optional and additive — legacy callers
|
|
205
|
+
* (and existing tests) omit it; the soft PRD gate then never fires for the
|
|
206
|
+
* task (consistent with the "additive, no-op when absent" rule). New callers
|
|
207
|
+
* that want the recommendation pass `'feature'` / `'epic'` / etc.
|
|
124
208
|
*/
|
|
125
|
-
startTask(taskId: string, slug: string, mode: Mode): Promise<TaskState>;
|
|
209
|
+
startTask(taskId: string, slug: string, mode: Mode, taskClass?: TaskClass): Promise<TaskState>;
|
|
126
210
|
/**
|
|
127
211
|
* Advance `taskId` to its next phase, or jump with `opts.to`.
|
|
128
212
|
*
|
|
@@ -131,8 +215,25 @@ declare class WorkflowEngine {
|
|
|
131
215
|
* reason) when `opts.force` is supplied, or `skipped` when `opts.skip` is
|
|
132
216
|
* supplied (quick mode). `force` and `skip` are mutually exclusive. Jumps
|
|
133
217
|
* bypass the FSM and additionally stamp `jumpEntry`.
|
|
218
|
+
*
|
|
219
|
+
* Single source of truth (W1): the gate is written ONCE to the audit KV via
|
|
220
|
+
* {@link recordGate}, and `task.history` is RE-DERIVED from that KV. No
|
|
221
|
+
* second write, no second timestamp — the S4 sub-ms drift is gone.
|
|
134
222
|
*/
|
|
135
223
|
advance(taskId: string, opts?: AdvanceOpts): Promise<TaskState>;
|
|
224
|
+
/**
|
|
225
|
+
* Compute the soft PRD-recommendation message (P4), or `null` when the
|
|
226
|
+
* recommendation does NOT apply. The recommendation applies when ALL of:
|
|
227
|
+
* • the gate landing now is the spec gate (entering `specified`), AND
|
|
228
|
+
* • the task is in full mode (quick mode skips — quickPath writes a stub), AND
|
|
229
|
+
* • the task has a `taskClass` listed in `gateConfig.prd.mandatoryFor`, AND
|
|
230
|
+
* • no PRD artifact exists at `.noir/prd/<id>-<slug>.md` (readPrd), AND
|
|
231
|
+
* • the user did NOT supply --force (force is the explicit-override path).
|
|
232
|
+
*
|
|
233
|
+
* Returns the observable note (audited on the spec gate's `reason`) so a
|
|
234
|
+
* downstream consumer (CLI status, workflow_status MCP tool) can surface it.
|
|
235
|
+
*/
|
|
236
|
+
private prdRecommendation;
|
|
136
237
|
/** Read the persisted TaskState, or null if the task is unknown. */
|
|
137
238
|
status(taskId: string): TaskState | null;
|
|
138
239
|
/**
|
|
@@ -143,14 +244,27 @@ declare class WorkflowEngine {
|
|
|
143
244
|
*/
|
|
144
245
|
activeTaskId(): string | null;
|
|
145
246
|
/**
|
|
146
|
-
* Re-flush the current state to KV
|
|
147
|
-
*
|
|
148
|
-
*
|
|
247
|
+
* Re-flush the current state to KV + flush the gate audit export to
|
|
248
|
+
* `.noir/audit/<taskId>.json`. W2 (debt-batch A): the prior implementation
|
|
249
|
+
* only bumped `updatedAt`, which every advance already does — vestigial.
|
|
250
|
+
* Cross-session resume (`resumeTask`) reads `workflow:<id>` straight from the
|
|
251
|
+
* KV and consumes nothing from this method; the S4 ledger noted the write was
|
|
252
|
+
* dead. The fix is to WIRE the checkpoint to a real cross-tool artifact
|
|
253
|
+
* flush: the audit JSON on disk (the S4 spec §11 OQ-5 "export to
|
|
254
|
+
* `.noir/audit/<taskId>.json`" that {@link writeAuditExport} already
|
|
255
|
+
* implemented but nothing called). The MCP `checkpoint { action:'save' }`
|
|
256
|
+
* tool stays the public surface; its save now leaves a human-inspectable
|
|
257
|
+
* audit JSON alongside the KV.
|
|
149
258
|
*/
|
|
150
259
|
checkpoint(taskId: string): Promise<void>;
|
|
151
260
|
/**
|
|
152
261
|
* Set state directly to `blocked` (no FSM edge — the admin escape). The reason
|
|
153
262
|
* is captured on the TaskState for surfacing in `noir.workflow_status`.
|
|
263
|
+
*
|
|
264
|
+
* W3: a SUPPLIED reason must be non-empty after trimming (mirrors the
|
|
265
|
+
* `--force` policy). `setBlocked(id)` with no reason stays valid — it clears
|
|
266
|
+
* no field and just flips state. A whitespace-only reason is rejected as
|
|
267
|
+
* malformed (consistent with `--force`'s whitespace rejection).
|
|
154
268
|
*/
|
|
155
269
|
setBlocked(taskId: string, reason?: string): Promise<TaskState>;
|
|
156
270
|
/** Set state directly to `abandoned` (terminal; no FSM edge). */
|
|
@@ -176,17 +290,36 @@ declare function gateFor(phase: Phase): WorkflowState | null;
|
|
|
176
290
|
/**
|
|
177
291
|
* Append a gate decision to the task's audit log in the store KV.
|
|
178
292
|
*
|
|
179
|
-
* The audit lives at `audit:<taskId>` as a `GateResult[]` and is the
|
|
180
|
-
*
|
|
293
|
+
* The audit lives at `audit:<taskId>` as a `GateResult[]` and is the
|
|
294
|
+
* AUTHORITATIVE record for every gate outcome (S4 spec §5 / §11 OQ-5 — "audit
|
|
295
|
+
* in store KV as source of truth + export to `.noir/audit/<taskId>.json`").
|
|
296
|
+
* The {@link TaskState.history} field is a DERIVED view, regenerated from this
|
|
297
|
+
* KV by the engine — never edited directly. This collapses the S4 dual source
|
|
298
|
+
* of truth (debt-batch A / W1): one write, one timestamp, one read-back.
|
|
299
|
+
*
|
|
181
300
|
* This is the "quiet observable checkpoint": an `approved`, `forced`, or
|
|
182
301
|
* `skipped` decision is always recorded — never silently dropped — and `forced`
|
|
183
302
|
* carries a `reason`.
|
|
184
303
|
*
|
|
185
304
|
* `at` is stamped here (`Date.now()`) rather than trusted from the caller, so
|
|
186
|
-
* the audit reflects when the gate actually fired
|
|
305
|
+
* the audit reflects when the gate actually fired (single timestamp — the W3
|
|
306
|
+
* sub-ms drift between the engine's history stamp and the audit KV stamp is
|
|
307
|
+
* gone, because there is no longer a second stamp). The write is append-only:
|
|
187
308
|
* prior entries are read and preserved (never overwritten).
|
|
188
309
|
*/
|
|
189
|
-
declare function recordGate(store: Store, taskId: string, result:
|
|
310
|
+
declare function recordGate(store: Store, taskId: string, result: GateResultInput): GateResult;
|
|
311
|
+
/**
|
|
312
|
+
* Read the authoritative gate audit for `taskId` from the store KV. Returns an
|
|
313
|
+
* empty array when no gate has fired yet (a fresh task) — never `null`, so
|
|
314
|
+
* callers can spread/index without a separate undefined check.
|
|
315
|
+
*
|
|
316
|
+
* The engine uses this to regenerate {@link TaskState.history} (the derived
|
|
317
|
+
* view) on every advance and every status read; downstream consumers
|
|
318
|
+
* (`buildWorkflowStatus`, the CLI `task status`) read `task.history` and so get
|
|
319
|
+
* the derived-from-KV value indirectly — the audit KV remains the SOT they all
|
|
320
|
+
* ultimately derive from.
|
|
321
|
+
*/
|
|
322
|
+
declare function readGateHistory(store: Store, taskId: string): GateResult[];
|
|
190
323
|
|
|
191
324
|
/**
|
|
192
325
|
* Default spec body written by {@link runQuick}. Quick mode is "discipline
|
|
@@ -237,4 +370,4 @@ declare function applyTransition(from: WorkflowState, to: WorkflowState): Workfl
|
|
|
237
370
|
declare function stateForPhase(p: Phase): WorkflowState;
|
|
238
371
|
declare function nextPhase(state: WorkflowState): Phase | null;
|
|
239
372
|
|
|
240
|
-
export { type AdvanceOpts, type GateResult, type Mode, PHASES, type Phase, QUICK_SPEC_STUB, type QuickOpts, STATES, type TaskState, WorkflowEngine, type WorkflowState, applyTransition, canTransition, gateFor, nextPhase, recordGate, resumeTask, runQuick, stateForPhase, writeAuditExport, writeChangelogStub, writeDecisionStub, writeIntake, writePlan, writeSpec, writeTask };
|
|
373
|
+
export { type AdvanceOpts, type GateResult, type GateResultInput, type Mode, PHASES, type Phase, QUICK_SPEC_STUB, type QuickOpts, STATES, TASK_CLASSES, type TaskClass, type TaskState, WorkflowEngine, type WorkflowGateConfig, type WorkflowState, applyTransition, canTransition, gateFor, nextPhase, readGateHistory, readPrd, recordGate, resumeTask, runQuick, stateForPhase, writeAuditExport, writeChangelogStub, writeDecisionStub, writeIntake, writePlan, writePrd, writeSpec, writeTask };
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,23 @@ slug: ${slug}
|
|
|
20
20
|
${body}`;
|
|
21
21
|
writeFileSync(file, content, "utf-8");
|
|
22
22
|
}
|
|
23
|
+
function writePrd(root, taskId, slug, body) {
|
|
24
|
+
const dir = paths.prdDir(root);
|
|
25
|
+
const file = paths.prdFile(root, taskId, slug);
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
const content = `---
|
|
28
|
+
taskId: ${taskId}
|
|
29
|
+
slug: ${slug}
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
${body}`;
|
|
33
|
+
writeFileSync(file, content, "utf-8");
|
|
34
|
+
}
|
|
35
|
+
function readPrd(root, taskId, slug) {
|
|
36
|
+
const file = paths.prdFile(root, taskId, slug);
|
|
37
|
+
if (!existsSync(file)) return null;
|
|
38
|
+
return readFileSync(file, "utf-8");
|
|
39
|
+
}
|
|
23
40
|
function writePlan(root, taskId, slug, body) {
|
|
24
41
|
const dir = paths.plansDir(root);
|
|
25
42
|
const file = paths.planFile(root, taskId, slug);
|
|
@@ -97,7 +114,12 @@ function gateFor(phase) {
|
|
|
97
114
|
function recordGate(store, taskId, result) {
|
|
98
115
|
const key = `audit:${taskId}`;
|
|
99
116
|
const prior = store.getState(key) ?? [];
|
|
100
|
-
|
|
117
|
+
const recorded = { ...result, at: Date.now() };
|
|
118
|
+
store.setState(key, [...prior, recorded]);
|
|
119
|
+
return recorded;
|
|
120
|
+
}
|
|
121
|
+
function readGateHistory(store, taskId) {
|
|
122
|
+
return store.getState(`audit:${taskId}`) ?? [];
|
|
101
123
|
}
|
|
102
124
|
|
|
103
125
|
// src/types.ts
|
|
@@ -121,6 +143,15 @@ var STATES = [
|
|
|
121
143
|
"blocked",
|
|
122
144
|
"abandoned"
|
|
123
145
|
];
|
|
146
|
+
var TASK_CLASSES = [
|
|
147
|
+
"feature",
|
|
148
|
+
"epic",
|
|
149
|
+
"enhancement",
|
|
150
|
+
"bugfix",
|
|
151
|
+
"spike",
|
|
152
|
+
"quick-task",
|
|
153
|
+
"refactor"
|
|
154
|
+
];
|
|
124
155
|
|
|
125
156
|
// src/state-machine.ts
|
|
126
157
|
var TRANSITIONS = {
|
|
@@ -177,6 +208,9 @@ function nextPhase(state) {
|
|
|
177
208
|
// src/engine.ts
|
|
178
209
|
var ACTIVE_KEY = "workflow:active";
|
|
179
210
|
var GATE_PHASES = ["spec", "plan", "verify"];
|
|
211
|
+
var DEFAULT_GATE_CONFIG = {
|
|
212
|
+
prd: { mandatoryFor: ["feature", "epic"] }
|
|
213
|
+
};
|
|
180
214
|
function gatePhaseForState(state) {
|
|
181
215
|
for (const p of GATE_PHASES) {
|
|
182
216
|
if (gateFor(p) === state) return p;
|
|
@@ -184,20 +218,27 @@ function gatePhaseForState(state) {
|
|
|
184
218
|
return null;
|
|
185
219
|
}
|
|
186
220
|
var WorkflowEngine = class {
|
|
187
|
-
constructor(store, root, projectId) {
|
|
221
|
+
constructor(store, root, projectId, gateConfig) {
|
|
188
222
|
this.store = store;
|
|
189
223
|
this.root = root;
|
|
190
224
|
this.projectId = projectId;
|
|
225
|
+
this.gateConfig = gateConfig ?? DEFAULT_GATE_CONFIG;
|
|
191
226
|
}
|
|
192
227
|
store;
|
|
193
228
|
root;
|
|
194
229
|
projectId;
|
|
230
|
+
gateConfig;
|
|
195
231
|
/**
|
|
196
232
|
* Create a new task at draft/intake, persist it, and point `workflow:active`
|
|
197
233
|
* at it. Re-starting an existing taskId overwrites it (intentional — the KV is
|
|
198
234
|
* the source of truth, not a journal).
|
|
235
|
+
*
|
|
236
|
+
* `taskClass` (debt-batch A / P4) is optional and additive — legacy callers
|
|
237
|
+
* (and existing tests) omit it; the soft PRD gate then never fires for the
|
|
238
|
+
* task (consistent with the "additive, no-op when absent" rule). New callers
|
|
239
|
+
* that want the recommendation pass `'feature'` / `'epic'` / etc.
|
|
199
240
|
*/
|
|
200
|
-
async startTask(taskId, slug, mode) {
|
|
241
|
+
async startTask(taskId, slug, mode, taskClass) {
|
|
201
242
|
const task = {
|
|
202
243
|
taskId,
|
|
203
244
|
slug,
|
|
@@ -206,6 +247,7 @@ var WorkflowEngine = class {
|
|
|
206
247
|
phase: "intake",
|
|
207
248
|
mode,
|
|
208
249
|
history: [],
|
|
250
|
+
...taskClass !== void 0 ? { taskClass } : {},
|
|
209
251
|
updatedAt: Date.now()
|
|
210
252
|
};
|
|
211
253
|
this.persist(task);
|
|
@@ -220,6 +262,10 @@ var WorkflowEngine = class {
|
|
|
220
262
|
* reason) when `opts.force` is supplied, or `skipped` when `opts.skip` is
|
|
221
263
|
* supplied (quick mode). `force` and `skip` are mutually exclusive. Jumps
|
|
222
264
|
* bypass the FSM and additionally stamp `jumpEntry`.
|
|
265
|
+
*
|
|
266
|
+
* Single source of truth (W1): the gate is written ONCE to the audit KV via
|
|
267
|
+
* {@link recordGate}, and `task.history` is RE-DERIVED from that KV. No
|
|
268
|
+
* second write, no second timestamp — the S4 sub-ms drift is gone.
|
|
223
269
|
*/
|
|
224
270
|
async advance(taskId, opts) {
|
|
225
271
|
const task = this.requireTask(taskId);
|
|
@@ -231,6 +277,9 @@ var WorkflowEngine = class {
|
|
|
231
277
|
}
|
|
232
278
|
const jump = opts?.to !== void 0;
|
|
233
279
|
const targetPhase = jump ? opts?.to : this.nextPhaseOf(task);
|
|
280
|
+
if (jump && targetPhase === task.phase) {
|
|
281
|
+
return task;
|
|
282
|
+
}
|
|
234
283
|
const targetState = stateForPhase(targetPhase);
|
|
235
284
|
if (!jump) {
|
|
236
285
|
applyTransition(task.state, targetState);
|
|
@@ -238,15 +287,17 @@ var WorkflowEngine = class {
|
|
|
238
287
|
const gatePhase = gatePhaseForState(targetState);
|
|
239
288
|
if (gatePhase !== null) {
|
|
240
289
|
const decision = opts?.force ? "forced" : opts?.skip ? "skipped" : "approved";
|
|
241
|
-
const
|
|
290
|
+
const prdHint = this.prdRecommendation(task, gatePhase, opts);
|
|
291
|
+
const input = {
|
|
242
292
|
phase: gatePhase,
|
|
243
293
|
decision,
|
|
244
|
-
// exactOptionalPropertyTypes is false; spread reason only when
|
|
245
|
-
|
|
246
|
-
|
|
294
|
+
// exactOptionalPropertyTypes is false; spread reason only when present.
|
|
295
|
+
// Force-path wins over the soft hint (a user who forces is explicitly
|
|
296
|
+
// accepting the recommendation; their reason is the override signal).
|
|
297
|
+
...opts?.force ? { reason: opts.force.reason } : prdHint !== null ? { reason: prdHint } : {}
|
|
247
298
|
};
|
|
248
|
-
recordGate(this.store, taskId,
|
|
249
|
-
task.history.
|
|
299
|
+
recordGate(this.store, taskId, input);
|
|
300
|
+
task.history = readGateHistory(this.store, taskId);
|
|
250
301
|
}
|
|
251
302
|
task.state = targetState;
|
|
252
303
|
task.phase = targetPhase;
|
|
@@ -255,9 +306,34 @@ var WorkflowEngine = class {
|
|
|
255
306
|
this.persist(task);
|
|
256
307
|
return task;
|
|
257
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Compute the soft PRD-recommendation message (P4), or `null` when the
|
|
311
|
+
* recommendation does NOT apply. The recommendation applies when ALL of:
|
|
312
|
+
* • the gate landing now is the spec gate (entering `specified`), AND
|
|
313
|
+
* • the task is in full mode (quick mode skips — quickPath writes a stub), AND
|
|
314
|
+
* • the task has a `taskClass` listed in `gateConfig.prd.mandatoryFor`, AND
|
|
315
|
+
* • no PRD artifact exists at `.noir/prd/<id>-<slug>.md` (readPrd), AND
|
|
316
|
+
* • the user did NOT supply --force (force is the explicit-override path).
|
|
317
|
+
*
|
|
318
|
+
* Returns the observable note (audited on the spec gate's `reason`) so a
|
|
319
|
+
* downstream consumer (CLI status, workflow_status MCP tool) can surface it.
|
|
320
|
+
*/
|
|
321
|
+
prdRecommendation(task, gatePhase, opts) {
|
|
322
|
+
if (gatePhase !== "spec") return null;
|
|
323
|
+
if (task.mode === "quick") return null;
|
|
324
|
+
const taskClass = task.taskClass;
|
|
325
|
+
if (taskClass === void 0) return null;
|
|
326
|
+
if (!this.gateConfig.prd.mandatoryFor.includes(taskClass)) return null;
|
|
327
|
+
if (opts?.force) return null;
|
|
328
|
+
if (readPrd(this.root, task.taskId, task.slug) !== null) return null;
|
|
329
|
+
return `PRD recommended for ${taskClass} \u2014 provide one (noir-prd) or --force <reason> to skip`;
|
|
330
|
+
}
|
|
258
331
|
/** Read the persisted TaskState, or null if the task is unknown. */
|
|
259
332
|
status(taskId) {
|
|
260
|
-
|
|
333
|
+
const task = this.store.getState(workflowKey(taskId));
|
|
334
|
+
if (!task) return null;
|
|
335
|
+
task.history = readGateHistory(this.store, taskId);
|
|
336
|
+
return task;
|
|
261
337
|
}
|
|
262
338
|
/**
|
|
263
339
|
* The taskId of the most-recently-started task (`workflow:active` in the
|
|
@@ -269,24 +345,44 @@ var WorkflowEngine = class {
|
|
|
269
345
|
return this.store.getState(ACTIVE_KEY);
|
|
270
346
|
}
|
|
271
347
|
/**
|
|
272
|
-
* Re-flush the current state to KV
|
|
273
|
-
*
|
|
274
|
-
*
|
|
348
|
+
* Re-flush the current state to KV + flush the gate audit export to
|
|
349
|
+
* `.noir/audit/<taskId>.json`. W2 (debt-batch A): the prior implementation
|
|
350
|
+
* only bumped `updatedAt`, which every advance already does — vestigial.
|
|
351
|
+
* Cross-session resume (`resumeTask`) reads `workflow:<id>` straight from the
|
|
352
|
+
* KV and consumes nothing from this method; the S4 ledger noted the write was
|
|
353
|
+
* dead. The fix is to WIRE the checkpoint to a real cross-tool artifact
|
|
354
|
+
* flush: the audit JSON on disk (the S4 spec §11 OQ-5 "export to
|
|
355
|
+
* `.noir/audit/<taskId>.json`" that {@link writeAuditExport} already
|
|
356
|
+
* implemented but nothing called). The MCP `checkpoint { action:'save' }`
|
|
357
|
+
* tool stays the public surface; its save now leaves a human-inspectable
|
|
358
|
+
* audit JSON alongside the KV.
|
|
275
359
|
*/
|
|
276
360
|
async checkpoint(taskId) {
|
|
277
361
|
const task = this.store.getState(workflowKey(taskId));
|
|
278
362
|
if (!task) throw new Error(`Unknown task: ${taskId}`);
|
|
279
363
|
task.updatedAt = Date.now();
|
|
280
364
|
this.persist(task);
|
|
365
|
+
writeAuditExport(this.root, taskId, readGateHistory(this.store, taskId));
|
|
281
366
|
}
|
|
282
367
|
/**
|
|
283
368
|
* Set state directly to `blocked` (no FSM edge — the admin escape). The reason
|
|
284
369
|
* is captured on the TaskState for surfacing in `noir.workflow_status`.
|
|
370
|
+
*
|
|
371
|
+
* W3: a SUPPLIED reason must be non-empty after trimming (mirrors the
|
|
372
|
+
* `--force` policy). `setBlocked(id)` with no reason stays valid — it clears
|
|
373
|
+
* no field and just flips state. A whitespace-only reason is rejected as
|
|
374
|
+
* malformed (consistent with `--force`'s whitespace rejection).
|
|
285
375
|
*/
|
|
286
376
|
async setBlocked(taskId, reason) {
|
|
287
377
|
const task = this.requireTask(taskId);
|
|
378
|
+
if (reason !== void 0) {
|
|
379
|
+
const trimmed = reason.trim();
|
|
380
|
+
if (trimmed.length === 0) {
|
|
381
|
+
throw new Error("setBlocked reason must be non-empty (or omitted to leave unset)");
|
|
382
|
+
}
|
|
383
|
+
task.blockReason = trimmed;
|
|
384
|
+
}
|
|
288
385
|
task.state = "blocked";
|
|
289
|
-
if (reason) task.blockReason = reason;
|
|
290
386
|
task.updatedAt = Date.now();
|
|
291
387
|
this.persist(task);
|
|
292
388
|
return task;
|
|
@@ -343,11 +439,14 @@ export {
|
|
|
343
439
|
PHASES,
|
|
344
440
|
QUICK_SPEC_STUB,
|
|
345
441
|
STATES,
|
|
442
|
+
TASK_CLASSES,
|
|
346
443
|
WorkflowEngine,
|
|
347
444
|
applyTransition,
|
|
348
445
|
canTransition,
|
|
349
446
|
gateFor,
|
|
350
447
|
nextPhase,
|
|
448
|
+
readGateHistory,
|
|
449
|
+
readPrd,
|
|
351
450
|
recordGate,
|
|
352
451
|
resumeTask,
|
|
353
452
|
runQuick,
|
|
@@ -357,6 +456,7 @@ export {
|
|
|
357
456
|
writeDecisionStub,
|
|
358
457
|
writeIntake,
|
|
359
458
|
writePlan,
|
|
459
|
+
writePrd,
|
|
360
460
|
writeSpec,
|
|
361
461
|
writeTask
|
|
362
462
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/artifacts.ts","../src/gates.ts","../src/types.ts","../src/state-machine.ts","../src/engine.ts","../src/modes.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { paths } from '@noir-ai/core';\nimport type { GateResult } from './types.js';\n\n/**\n * Write intake artifact to .noir/intake/<taskId>.md\n */\nexport function writeIntake(root: string, taskId: string, content: string): void {\n const dir = join(root, '.noir', 'intake');\n const file = join(dir, `${taskId}.md`);\n\n mkdirSync(dir, { recursive: true });\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write spec artifact to .noir/specs/<taskId>-<slug>.md\n * Creates markdown with frontmatter and body\n */\nexport function writeSpec(root: string, taskId: string, slug: string, body: string): void {\n const dir = paths.specsDir(root);\n const file = paths.specFile(root, taskId, slug);\n\n mkdirSync(dir, { recursive: true });\n\n const content = `---\ntaskId: ${taskId}\nslug: ${slug}\n---\n\n${body}`;\n\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write plan artifact to .noir/plans/<taskId>-<slug>.md\n */\nexport function writePlan(root: string, taskId: string, slug: string, body: string): void {\n const dir = paths.plansDir(root);\n const file = paths.planFile(root, taskId, slug);\n\n mkdirSync(dir, { recursive: true });\n\n const content = `---\ntaskId: ${taskId}\nslug: ${slug}\n---\n\n${body}`;\n\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write task artifact to .noir/tasks/<taskId>-<taskName>.md\n */\nexport function writeTask(root: string, taskId: string, taskName: string, body: string): void {\n const dir = paths.tasksDir(root);\n const file = paths.taskFile(root, taskId, taskName);\n\n mkdirSync(dir, { recursive: true });\n\n const content = `---\ntaskId: ${taskId}\ntask: ${taskName}\n---\n\n${body}`;\n\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write decision stub to .noir/decisions/<n>.md\n */\nexport function writeDecisionStub(root: string, n: number, title: string): void {\n const dir = paths.decisionsDir(root);\n const file = paths.decisionFile(root, n);\n\n mkdirSync(dir, { recursive: true });\n\n const content = `# ${title}\n\n*Decision record ${n}*\n\n<!-- Status: pending -->\n`;\n\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write changelog stub entry to .noir/CHANGELOG.md\n */\nexport function writeChangelogStub(root: string, entry: string): void {\n const file = join(root, '.noir', 'CHANGELOG.md');\n\n // Create directory if it doesn't exist\n mkdirSync(join(root, '.noir'), { recursive: true });\n\n let content: string;\n if (existsSync(file)) {\n // Append: read existing content and add the new entry on its own line,\n // preserving the header and all prior entries.\n const existing = readFileSync(file, 'utf-8');\n const prefix = existing.endsWith('\\n') ? existing : `${existing}\\n`;\n content = `${prefix}${entry}\\n`;\n } else {\n content = `# Changelog\n\n${entry}\n`;\n }\n\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write audit export to .noir/audit/<taskId>.json\n * Exports GateResult array as JSON\n */\nexport function writeAuditExport(root: string, taskId: string, results: GateResult[]): void {\n const dir = paths.auditDir(root);\n const file = paths.auditFile(root, taskId);\n\n mkdirSync(dir, { recursive: true });\n writeFileSync(file, JSON.stringify(results, null, 2), 'utf-8');\n}\n","import type { Store } from '@noir-ai/store';\nimport type { GateResult, Phase, WorkflowState } from './types.js';\n\n/**\n * The workflow state a phase's gate guards entry into (Noir §9.1 observable\n * checkpoint). A gate fires when its phase completes:\n * spec-gate → entering `specified`\n * plan-gate → entering `planned`\n * verify-gate → entering `done`\n *\n * Phases without a gate (intake / clarify / execute / document) return `null`.\n * This is distinct from {@link stateForPhase}: `stateForPhase('verify')` is the\n * in-progress `verifying`, while `gateFor('verify')` is the state the verify\n * gate admits you into (`done`).\n */\nexport function gateFor(phase: Phase): WorkflowState | null {\n switch (phase) {\n case 'spec':\n return 'specified';\n case 'plan':\n return 'planned';\n case 'verify':\n return 'done';\n default:\n return null;\n }\n}\n\n/**\n * Append a gate decision to the task's audit log in the store KV.\n *\n * The audit lives at `audit:<taskId>` as a `GateResult[]` and is the source of\n * truth for every gate outcome (the `.noir/audit/` export is a later helper).\n * This is the \"quiet observable checkpoint\": an `approved`, `forced`, or\n * `skipped` decision is always recorded — never silently dropped — and `forced`\n * carries a `reason`.\n *\n * `at` is stamped here (`Date.now()`) rather than trusted from the caller, so\n * the audit reflects when the gate actually fired. The write is append-only:\n * prior entries are read and preserved (never overwritten).\n */\nexport function recordGate(store: Store, taskId: string, result: GateResult): void {\n const key = `audit:${taskId}`;\n const prior = store.getState<GateResult[]>(key) ?? [];\n store.setState<GateResult[]>(key, [...prior, { ...result, at: Date.now() }]);\n}\n","import type { ProjectId } from '@noir-ai/core';\n\nexport const PHASES = [\n 'intake',\n 'clarify',\n 'spec',\n 'plan',\n 'execute',\n 'verify',\n 'document',\n] as const;\nexport type Phase = (typeof PHASES)[number];\n\nexport const STATES = [\n 'draft',\n 'clarifying',\n 'specified',\n 'planned',\n 'executing',\n 'verifying',\n 'done',\n 'blocked',\n 'abandoned',\n] as const;\nexport type WorkflowState = (typeof STATES)[number];\n\nexport type Mode = 'full' | 'quick';\n\nexport interface GateResult {\n phase: Phase;\n decision: 'approved' | 'forced' | 'skipped';\n reason?: string;\n at: number;\n}\n\nexport interface TaskState {\n taskId: string;\n slug: string;\n projectId: ProjectId;\n state: WorkflowState;\n phase: Phase;\n mode: Mode;\n history: GateResult[]; // gate decisions (audit in-process view)\n jumpEntry?: Phase; // recorded if a jump-to-phase happened\n /** Reason captured by `setBlocked` (admin escape; set directly, not via FSM). */\n blockReason?: string;\n updatedAt: number;\n}\n","import type { Phase, WorkflowState } from './types.js';\n\n// Re-export the phase/state vocabularies so consumers can import the entire FSM\n// surface from one module (the test imports PHASES/STATES from here).\nexport { PHASES, STATES } from './types.js';\n\n// Legal state transitions (happy path + terminal). Gates (spec/plan/verify) are\n// modeled as the transition INTO specified/planned/done — the engine records a\n// GateResult at that point (see gates.ts).\nconst TRANSITIONS: Record<WorkflowState, WorkflowState[]> = {\n draft: ['clarifying'],\n clarifying: ['specified'],\n specified: ['planned'],\n planned: ['executing'],\n executing: ['verifying'],\n verifying: ['done'],\n done: [],\n blocked: ['draft', 'clarifying', 'specified', 'planned', 'executing', 'verifying'],\n abandoned: [],\n};\n\nexport function canTransition(from: WorkflowState, to: WorkflowState): boolean {\n return (TRANSITIONS[from] ?? []).includes(to);\n}\n\nexport function applyTransition(from: WorkflowState, to: WorkflowState): WorkflowState {\n if (!canTransition(from, to)) {\n const hint =\n to === 'planned'\n ? ' (the spec gate must be passed first)'\n : to === 'executing'\n ? ' (the plan gate must be passed first)'\n : to === 'done'\n ? ' (the verify gate must be passed first)'\n : '';\n throw new Error(`Illegal transition ${from} → ${to}${hint}`);\n }\n return to;\n}\n\n// Phase <-> state mapping for the engine.\nexport function stateForPhase(p: Phase): WorkflowState {\n switch (p) {\n case 'intake':\n return 'draft';\n case 'clarify':\n return 'clarifying';\n case 'spec':\n return 'specified';\n case 'plan':\n return 'planned';\n case 'execute':\n return 'executing';\n case 'verify':\n return 'verifying';\n case 'document':\n return 'done';\n }\n}\n\nexport function nextPhase(state: WorkflowState): Phase | null {\n const map: Partial<Record<WorkflowState, Phase>> = {\n draft: 'clarify',\n clarifying: 'spec',\n specified: 'plan',\n planned: 'execute',\n executing: 'verify',\n verifying: 'document',\n };\n return map[state] ?? null;\n}\n","import type { ProjectId } from '@noir-ai/core';\nimport type { Store } from '@noir-ai/store';\nimport { gateFor, recordGate } from './gates.js';\nimport { applyTransition, nextPhase, stateForPhase } from './state-machine.js';\nimport type { GateResult, Mode, Phase, TaskState, WorkflowState } from './types.js';\n\n/**\n * Store KV layout (single source of truth for machine state):\n * workflow:active → taskId of the most recently started task\n * workflow:<taskId> → TaskState (JSON)\n * Audit decisions live at `audit:<taskId>` (see {@link recordGate}).\n */\nconst ACTIVE_KEY = 'workflow:active';\nconst GATE_PHASES = ['spec', 'plan', 'verify'] as const satisfies ReadonlyArray<Phase>;\n\n/** Options for {@link WorkflowEngine.advance}. */\nexport interface AdvanceOpts {\n /**\n * Pass a gate without satisfying its criteria. Requires a non-empty `reason`\n * (validated here, in the engine — `recordGate` is policy-free). The landing\n * gate, if any, is recorded with `decision: 'forced'`. Mutually exclusive\n * with {@link skip}.\n */\n force?: { reason?: string };\n /**\n * Jump directly to a phase, bypassing the FSM (the escape hatch for blocked /\n * out-of-order resumption). The landing is recorded as `jumpEntry` on the\n * TaskState; a landing gate-state (specified/planned/done) still records its\n * gate so the observable-checkpoint invariant holds.\n */\n to?: Phase;\n /**\n * Quick-mode: record the landing gate (if any) as `decision: 'skipped'`\n * instead of `approved`. The gate is still RECORDED — never silently dropped\n * (Noir §9.1 observable-checkpoint invariant) — only the decision changes.\n * Mutually exclusive with {@link force}.\n */\n skip?: true;\n}\n\n/**\n * The phase whose gate admits entry into `state` (the inverse of {@link gateFor}).\n *\n * The verify gate fires on entering `done` — but `done` is `stateForPhase('document')`,\n * so the target *phase* of that transition is `document`, not `verify`. The gate\n * must therefore be looked up from the target *state*, not the target phase.\n */\nfunction gatePhaseForState(state: WorkflowState): Phase | null {\n for (const p of GATE_PHASES) {\n if (gateFor(p) === state) return p;\n }\n return null;\n}\n\n/**\n * WorkflowEngine — drives an SDD task through its lifecycle.\n *\n * The engine is a thin orchestrator over three T1–T3 primitives:\n * • the hand-rolled FSM ({@link applyTransition}) for legal forward moves,\n * • {@link recordGate} for observable checkpoint audit, and\n * • the store KV for persisted {@link TaskState}.\n *\n * Policy that did not belong in T1/T2 lives here:\n * • `--force` requires a non-empty reason (validated before any gate write),\n * • `blocked` / `abandoned` have no incoming FSM edges and are set directly,\n * • `opts.to` jumps past FSM edges and is recorded via `jumpEntry`.\n *\n * Modes (Full/Quick) and cross-session resume are T5; MCP tools are T6 — the\n * engine stays mode-agnostic here and only stores `mode` on the TaskState.\n */\nexport class WorkflowEngine {\n constructor(\n private readonly store: Store,\n /**\n * Project root — the engine is constructed with it so modes (quickPath\n * writes a spec stub via {@link writeSpec}) and future artifact flushes\n * (checkpoint/audit export) are self-contained. Public so the modes module\n * can pass it to {@link ArtifactWriter}.\n */\n readonly root: string,\n private readonly projectId: ProjectId,\n ) {}\n\n /**\n * Create a new task at draft/intake, persist it, and point `workflow:active`\n * at it. Re-starting an existing taskId overwrites it (intentional — the KV is\n * the source of truth, not a journal).\n */\n async startTask(taskId: string, slug: string, mode: Mode): Promise<TaskState> {\n const task: TaskState = {\n taskId,\n slug,\n projectId: this.projectId,\n state: 'draft',\n phase: 'intake',\n mode,\n history: [],\n updatedAt: Date.now(),\n };\n this.persist(task);\n this.store.setState<string>(ACTIVE_KEY, taskId);\n return task;\n }\n\n /**\n * Advance `taskId` to its next phase, or jump with `opts.to`.\n *\n * At a gate-landing state (entering `specified` / `planned` / `done`) a\n * {@link GateResult} is recorded — `approved` by default, `forced` (with the\n * reason) when `opts.force` is supplied, or `skipped` when `opts.skip` is\n * supplied (quick mode). `force` and `skip` are mutually exclusive. Jumps\n * bypass the FSM and additionally stamp `jumpEntry`.\n */\n async advance(taskId: string, opts?: AdvanceOpts): Promise<TaskState> {\n const task = this.requireTask(taskId);\n\n // Policy: --force and skip are mutually exclusive gate decisions (a gate\n // can't be both forced AND skipped). Validated BEFORE any gate write so a\n // bad combination never leaves a partial audit trail behind.\n if (opts?.force && opts?.skip) {\n throw new Error('cannot combine --force and skip');\n }\n // Policy: --force requires a non-empty reason.\n if (opts?.force && !opts.force.reason?.trim()) {\n throw new Error('--force requires a reason');\n }\n\n const jump = opts?.to !== undefined;\n const targetPhase: Phase = jump ? (opts?.to as Phase) : this.nextPhaseOf(task);\n\n const targetState = stateForPhase(targetPhase);\n if (!jump) {\n // applyTransition surfaces the FSM's gate hint on illegal moves.\n applyTransition(task.state, targetState);\n }\n\n // Observable checkpoint: entering specified/planned/done always records a\n // gate — looked up from the target STATE (see gatePhaseForState).\n const gatePhase = gatePhaseForState(targetState);\n if (gatePhase !== null) {\n const decision = opts?.force ? 'forced' : opts?.skip ? 'skipped' : 'approved';\n const gate: GateResult = {\n phase: gatePhase,\n decision,\n // exactOptionalPropertyTypes is false; spread reason only when forced.\n ...(opts?.force ? { reason: opts.force.reason } : {}),\n at: Date.now(),\n };\n recordGate(this.store, taskId, gate);\n task.history.push(gate);\n }\n\n task.state = targetState;\n task.phase = targetPhase;\n if (jump) task.jumpEntry = targetPhase;\n task.updatedAt = Date.now();\n\n this.persist(task);\n return task;\n }\n\n /** Read the persisted TaskState, or null if the task is unknown. */\n status(taskId: string): TaskState | null {\n return this.store.getState<TaskState>(workflowKey(taskId));\n }\n\n /**\n * The taskId of the most-recently-started task (`workflow:active` in the\n * store KV), or `null` when no task has been started yet. Lets the MCP\n * `workflow_status` / `checkpoint` tools omit `taskId` and operate on the\n * active task.\n */\n activeTaskId(): string | null {\n return this.store.getState<string>(ACTIVE_KEY);\n }\n\n /**\n * Re-flush the current state to KV. In T4 every advance already persists, so\n * this is the explicit \"mark a checkpoint\" hook (bumps `updatedAt`); T5\n * deepens it to flush artifacts + audit export for cross-session resume.\n */\n async checkpoint(taskId: string): Promise<void> {\n const task = this.store.getState<TaskState>(workflowKey(taskId));\n if (!task) throw new Error(`Unknown task: ${taskId}`);\n task.updatedAt = Date.now();\n this.persist(task);\n }\n\n /**\n * Set state directly to `blocked` (no FSM edge — the admin escape). The reason\n * is captured on the TaskState for surfacing in `noir.workflow_status`.\n */\n async setBlocked(taskId: string, reason?: string): Promise<TaskState> {\n const task = this.requireTask(taskId);\n task.state = 'blocked';\n if (reason) task.blockReason = reason;\n task.updatedAt = Date.now();\n this.persist(task);\n return task;\n }\n\n /** Set state directly to `abandoned` (terminal; no FSM edge). */\n async abandon(taskId: string): Promise<TaskState> {\n const task = this.requireTask(taskId);\n task.state = 'abandoned';\n task.updatedAt = Date.now();\n this.persist(task);\n return task;\n }\n\n private nextPhaseOf(task: TaskState): Phase {\n const next = nextPhase(task.state);\n if (next === null) {\n throw new Error(`No next phase from state ${task.state}`);\n }\n return next;\n }\n\n private requireTask(taskId: string): TaskState {\n const task = this.store.getState<TaskState>(workflowKey(taskId));\n if (!task) throw new Error(`Unknown task: ${taskId}`);\n return task;\n }\n\n private persist(task: TaskState): void {\n this.store.setState<TaskState>(workflowKey(task.taskId), task);\n }\n}\n\nfunction workflowKey(taskId: string): string {\n return `workflow:${taskId}`;\n}\n","import type { Store } from '@noir-ai/store';\nimport { writeSpec } from './artifacts.js';\nimport type { WorkflowEngine } from './engine.js';\nimport type { TaskState, WorkflowState } from './types.js';\n\n/**\n * Default spec body written by {@link runQuick}. Quick mode is \"discipline\n * lite\": the spec is stubbed (the spec + plan gates are skipped), but a real\n * spec file still lands on disk so later phases (and humans) have something to\n * read. Exported so callers/tests can assert on the canonical stub text.\n */\nexport const QUICK_SPEC_STUB = '<quick-mode stub spec>';\n\n/** Options for {@link runQuick}. */\nexport interface QuickOpts {\n /**\n * Override the stub spec body (defaults to {@link QUICK_SPEC_STUB}). Useful\n * when a quick task already has a one-line description worth persisting.\n */\n specBody?: string;\n}\n\n/**\n * Quick mode — the fast path for small / spike tasks.\n *\n * Writes a stub spec to `.noir/specs/<taskId>-<slug>.md` (via\n * {@link writeSpec}) and fast-forwards the task from `draft` to `executing`,\n * recording the spec and plan gates as `decision: 'skipped'`. The verify gate\n * is intentionally LEFT alone — it still fires as `approved` when the task\n * later reaches `done`, providing the one real checkpoint (discipline lite).\n *\n * The task must already be started (`engine.startTask(..., 'quick')`); this\n * function reads the slug from the persisted TaskState. Skipped gates are\n * RECORDED (Noir §9.1 observable-checkpoint invariant), never silently dropped\n * — the audit KV and `history` both carry the `skipped` entries.\n */\nexport async function runQuick(\n engine: WorkflowEngine,\n taskId: string,\n opts?: QuickOpts,\n): Promise<TaskState> {\n const task = engine.status(taskId);\n if (!task) throw new Error(`Unknown task: ${taskId}`);\n\n // Flush the stub spec first so the artifact exists before any gate fires.\n writeSpec(engine.root, taskId, task.slug, opts?.specBody ?? QUICK_SPEC_STUB);\n\n // draft → clarifying → specified (spec gate SKIPPED) → planned (plan gate\n // SKIPPED) → executing. Verify is left for the normal flow (runs as approved).\n await engine.advance(taskId); // draft → clarifying (no gate)\n await engine.advance(taskId, { skip: true }); // → specified (spec gate skipped)\n await engine.advance(taskId, { skip: true }); // → planned (plan gate skipped)\n return engine.advance(taskId); // → executing (no gate) — land here\n}\n\n/** Terminal states with nothing left to resume. `blocked` is NOT terminal. */\nconst TERMINAL_STATES: ReadonlySet<WorkflowState> = new Set<WorkflowState>(['done', 'abandoned']);\n\n/**\n * Reconstruct the in-flight TaskState across a session break.\n *\n * Reads `workflow:active` from the store KV → the most-recently-started\n * taskId (T4 semantics; v1 is one-task-per-project) → the persisted\n * `workflow:<taskId>` TaskState. Returns `null` when there is no active task,\n * the task record is missing, or the active task is terminal (`done` /\n * `abandoned` — nothing to resume). A `blocked` task IS resumable.\n *\n * Uses only the public {@link Store} API, so a freshly-`openStore`'d handle\n * against the same on-disk DB is sufficient (no live engine required).\n */\nexport async function resumeTask(store: Store): Promise<TaskState | null> {\n const activeId = store.getState<string>('workflow:active');\n if (!activeId) return null;\n\n const task = store.getState<TaskState>(`workflow:${activeId}`);\n if (!task) return null;\n if (TERMINAL_STATES.has(task.state)) return null;\n\n return task;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/artifacts.ts","../src/gates.ts","../src/types.ts","../src/state-machine.ts","../src/engine.ts","../src/modes.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { paths } from '@noir-ai/core';\nimport type { GateResult } from './types.js';\n\n/**\n * Write intake artifact to .noir/intake/<taskId>.md\n */\nexport function writeIntake(root: string, taskId: string, content: string): void {\n const dir = join(root, '.noir', 'intake');\n const file = join(dir, `${taskId}.md`);\n\n mkdirSync(dir, { recursive: true });\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write spec artifact to .noir/specs/<taskId>-<slug>.md\n * Creates markdown with frontmatter and body\n */\nexport function writeSpec(root: string, taskId: string, slug: string, body: string): void {\n const dir = paths.specsDir(root);\n const file = paths.specFile(root, taskId, slug);\n\n mkdirSync(dir, { recursive: true });\n\n const content = `---\ntaskId: ${taskId}\nslug: ${slug}\n---\n\n${body}`;\n\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write PRD artifact to .noir/prd/<taskId>-<slug>.md\n * Pre-SDD product document; the spec @imports it (prdRef). No FSM change.\n */\nexport function writePrd(root: string, taskId: string, slug: string, body: string): void {\n const dir = paths.prdDir(root);\n const file = paths.prdFile(root, taskId, slug);\n\n mkdirSync(dir, { recursive: true });\n\n const content = `---\ntaskId: ${taskId}\nslug: ${slug}\n---\n\n${body}`;\n\n writeFileSync(file, content, 'utf-8');\n}\n\n/** Read a PRD artifact, or null if absent. */\nexport function readPrd(root: string, taskId: string, slug: string): string | null {\n const file = paths.prdFile(root, taskId, slug);\n if (!existsSync(file)) return null;\n return readFileSync(file, 'utf-8');\n}\n\n/**\n * Write plan artifact to .noir/plans/<taskId>-<slug>.md\n */\nexport function writePlan(root: string, taskId: string, slug: string, body: string): void {\n const dir = paths.plansDir(root);\n const file = paths.planFile(root, taskId, slug);\n\n mkdirSync(dir, { recursive: true });\n\n const content = `---\ntaskId: ${taskId}\nslug: ${slug}\n---\n\n${body}`;\n\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write task artifact to .noir/tasks/<taskId>-<taskName>.md\n */\nexport function writeTask(root: string, taskId: string, taskName: string, body: string): void {\n const dir = paths.tasksDir(root);\n const file = paths.taskFile(root, taskId, taskName);\n\n mkdirSync(dir, { recursive: true });\n\n const content = `---\ntaskId: ${taskId}\ntask: ${taskName}\n---\n\n${body}`;\n\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write decision stub to .noir/decisions/<n>.md\n */\nexport function writeDecisionStub(root: string, n: number, title: string): void {\n const dir = paths.decisionsDir(root);\n const file = paths.decisionFile(root, n);\n\n mkdirSync(dir, { recursive: true });\n\n const content = `# ${title}\n\n*Decision record ${n}*\n\n<!-- Status: pending -->\n`;\n\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write changelog stub entry to .noir/CHANGELOG.md\n */\nexport function writeChangelogStub(root: string, entry: string): void {\n const file = join(root, '.noir', 'CHANGELOG.md');\n\n // Create directory if it doesn't exist\n mkdirSync(join(root, '.noir'), { recursive: true });\n\n let content: string;\n if (existsSync(file)) {\n // Append: read existing content and add the new entry on its own line,\n // preserving the header and all prior entries.\n const existing = readFileSync(file, 'utf-8');\n const prefix = existing.endsWith('\\n') ? existing : `${existing}\\n`;\n content = `${prefix}${entry}\\n`;\n } else {\n content = `# Changelog\n\n${entry}\n`;\n }\n\n writeFileSync(file, content, 'utf-8');\n}\n\n/**\n * Write audit export to .noir/audit/<taskId>.json\n * Exports GateResult array as JSON\n */\nexport function writeAuditExport(root: string, taskId: string, results: GateResult[]): void {\n const dir = paths.auditDir(root);\n const file = paths.auditFile(root, taskId);\n\n mkdirSync(dir, { recursive: true });\n writeFileSync(file, JSON.stringify(results, null, 2), 'utf-8');\n}\n","import type { Store } from '@noir-ai/store';\nimport type { GateResult, GateResultInput, Phase, WorkflowState } from './types.js';\n\n/**\n * The workflow state a phase's gate guards entry into (Noir §9.1 observable\n * checkpoint). A gate fires when its phase completes:\n * spec-gate → entering `specified`\n * plan-gate → entering `planned`\n * verify-gate → entering `done`\n *\n * Phases without a gate (intake / clarify / execute / document) return `null`.\n * This is distinct from {@link stateForPhase}: `stateForPhase('verify')` is the\n * in-progress `verifying`, while `gateFor('verify')` is the state the verify\n * gate admits you into (`done`).\n */\nexport function gateFor(phase: Phase): WorkflowState | null {\n switch (phase) {\n case 'spec':\n return 'specified';\n case 'plan':\n return 'planned';\n case 'verify':\n return 'done';\n default:\n return null;\n }\n}\n\n/**\n * Append a gate decision to the task's audit log in the store KV.\n *\n * The audit lives at `audit:<taskId>` as a `GateResult[]` and is the\n * AUTHORITATIVE record for every gate outcome (S4 spec §5 / §11 OQ-5 — \"audit\n * in store KV as source of truth + export to `.noir/audit/<taskId>.json`\").\n * The {@link TaskState.history} field is a DERIVED view, regenerated from this\n * KV by the engine — never edited directly. This collapses the S4 dual source\n * of truth (debt-batch A / W1): one write, one timestamp, one read-back.\n *\n * This is the \"quiet observable checkpoint\": an `approved`, `forced`, or\n * `skipped` decision is always recorded — never silently dropped — and `forced`\n * carries a `reason`.\n *\n * `at` is stamped here (`Date.now()`) rather than trusted from the caller, so\n * the audit reflects when the gate actually fired (single timestamp — the W3\n * sub-ms drift between the engine's history stamp and the audit KV stamp is\n * gone, because there is no longer a second stamp). The write is append-only:\n * prior entries are read and preserved (never overwritten).\n */\nexport function recordGate(store: Store, taskId: string, result: GateResultInput): GateResult {\n const key = `audit:${taskId}`;\n const prior = store.getState<GateResult[]>(key) ?? [];\n const recorded: GateResult = { ...result, at: Date.now() };\n store.setState<GateResult[]>(key, [...prior, recorded]);\n return recorded;\n}\n\n/**\n * Read the authoritative gate audit for `taskId` from the store KV. Returns an\n * empty array when no gate has fired yet (a fresh task) — never `null`, so\n * callers can spread/index without a separate undefined check.\n *\n * The engine uses this to regenerate {@link TaskState.history} (the derived\n * view) on every advance and every status read; downstream consumers\n * (`buildWorkflowStatus`, the CLI `task status`) read `task.history` and so get\n * the derived-from-KV value indirectly — the audit KV remains the SOT they all\n * ultimately derive from.\n */\nexport function readGateHistory(store: Store, taskId: string): GateResult[] {\n return store.getState<GateResult[]>(`audit:${taskId}`) ?? [];\n}\n","import type { ProjectId } from '@noir-ai/core';\n\nexport const PHASES = [\n 'intake',\n 'clarify',\n 'spec',\n 'plan',\n 'execute',\n 'verify',\n 'document',\n] as const;\nexport type Phase = (typeof PHASES)[number];\n\nexport const STATES = [\n 'draft',\n 'clarifying',\n 'specified',\n 'planned',\n 'executing',\n 'verifying',\n 'done',\n 'blocked',\n 'abandoned',\n] as const;\nexport type WorkflowState = (typeof STATES)[number];\n\nexport type Mode = 'full' | 'quick';\n\n/**\n * Task classification — drives the soft, escapable gate predicates (slice P:\n * the PRD recommendation at the spec gate). Mirrors the user-facing\n * `prd.mandatoryFor` enum in @noir-ai/core's `NoirConfigSchema` (declared\n * LOCALLY here so workflow has no core-cycle concern; the two literals stay in\n * sync by tests). `undefined` (the default for legacy tasks) ⇒ no soft gate\n * fires — additive, fully backward-compatible.\n */\nexport const TASK_CLASSES = [\n 'feature',\n 'epic',\n 'enhancement',\n 'bugfix',\n 'spike',\n 'quick-task',\n 'refactor',\n] as const;\nexport type TaskClass = (typeof TASK_CLASSES)[number];\n\n/**\n * Input shape for {@link recordGate} — what CALLERS pass. Omits `at` (the\n * recorder stamps it from `Date.now()` so the audit reflects when the gate\n * actually fired, not when the caller constructed the object). Split out of\n * {@link GateResult} (debt-batch A / W3): callers used to pass a throwaway\n * `at: 0` that recordGate overrode — this shape makes the override implicit.\n */\nexport interface GateResultInput {\n phase: Phase;\n decision: 'approved' | 'forced' | 'skipped';\n reason?: string;\n}\n\n/**\n * A recorded gate decision — the AUTHORITATIVE shape stored in the\n * `audit:<taskId>` KV (the SOT per S4 spec §5 / §11 OQ-5) and surfaced via\n * `task.history` (a derived view the engine regenerates from the audit KV).\n * `at` is always present here; callers that want to RECORD a gate pass\n * {@link GateResultInput} (no `at`) to {@link recordGate}.\n */\nexport interface GateResult extends GateResultInput {\n at: number;\n}\n\nexport interface TaskState {\n taskId: string;\n slug: string;\n projectId: ProjectId;\n state: WorkflowState;\n phase: Phase;\n mode: Mode;\n /**\n * DERIVED view of the gate audit for this task, regenerated from the\n * authoritative `audit:<taskId>` KV (S4 spec §11 OQ-5) by the engine on\n * every write and every status read. Kept on the TaskState so consumers\n * (CLI `task status`, the daemon `workflow_status` tool, MCP clients) can\n * read the gate history from the persisted TaskState without a second KV\n * lookup. Never edited directly outside the engine — derive via\n * {@link readGateHistory}.\n */\n history: GateResult[];\n /** Task classification (slice P). `undefined` ⇒ no soft PRD gate fires. */\n taskClass?: TaskClass;\n jumpEntry?: Phase; // recorded if a jump-to-phase happened\n /** Reason captured by `setBlocked` (admin escape; set directly, not via FSM). */\n blockReason?: string;\n updatedAt: number;\n}\n\n/**\n * The gate-config slice the engine consumes (debt-batch A / P4). Mirrors the\n * user-facing `prd:` block from @noir-ai/core's `NoirConfigSchema` — declared\n * locally (with a `readonly` array) so workflow has no core-cycle concern; the\n * daemon / CLI bridges NoirConfig → this shape at construction time. Every\n * field has a sane default, so an unspecified `gateConfig` (the legacy\n * constructor call) resolves to \"feature/epic trigger the PRD recommendation\".\n */\nexport interface WorkflowGateConfig {\n /**\n * PRD soft-gate config. `mandatoryFor` lists the task classes for which a\n * missing PRD artifact (at the moment of entering the spec phase, in full\n * mode) is surfaced as an observable, escapable recommendation on the spec\n * gate. The advance still proceeds — `--force <reason>` is the explicit\n * override path; quick mode + unlisted classes skip the check entirely.\n */\n prd: {\n mandatoryFor: readonly TaskClass[];\n };\n}\n","import type { Phase, WorkflowState } from './types.js';\n\n// Re-export the phase/state vocabularies so consumers can import the entire FSM\n// surface from one module (the test imports PHASES/STATES from here).\nexport { PHASES, STATES } from './types.js';\n\n// Legal state transitions (happy path + terminal). Gates (spec/plan/verify) are\n// modeled as the transition INTO specified/planned/done — the engine records a\n// GateResult at that point (see gates.ts).\nconst TRANSITIONS: Record<WorkflowState, WorkflowState[]> = {\n draft: ['clarifying'],\n clarifying: ['specified'],\n specified: ['planned'],\n planned: ['executing'],\n executing: ['verifying'],\n verifying: ['done'],\n done: [],\n blocked: ['draft', 'clarifying', 'specified', 'planned', 'executing', 'verifying'],\n abandoned: [],\n};\n\nexport function canTransition(from: WorkflowState, to: WorkflowState): boolean {\n return (TRANSITIONS[from] ?? []).includes(to);\n}\n\nexport function applyTransition(from: WorkflowState, to: WorkflowState): WorkflowState {\n if (!canTransition(from, to)) {\n const hint =\n to === 'planned'\n ? ' (the spec gate must be passed first)'\n : to === 'executing'\n ? ' (the plan gate must be passed first)'\n : to === 'done'\n ? ' (the verify gate must be passed first)'\n : '';\n throw new Error(`Illegal transition ${from} → ${to}${hint}`);\n }\n return to;\n}\n\n// Phase <-> state mapping for the engine.\nexport function stateForPhase(p: Phase): WorkflowState {\n switch (p) {\n case 'intake':\n return 'draft';\n case 'clarify':\n return 'clarifying';\n case 'spec':\n return 'specified';\n case 'plan':\n return 'planned';\n case 'execute':\n return 'executing';\n case 'verify':\n return 'verifying';\n case 'document':\n return 'done';\n }\n}\n\nexport function nextPhase(state: WorkflowState): Phase | null {\n const map: Partial<Record<WorkflowState, Phase>> = {\n draft: 'clarify',\n clarifying: 'spec',\n specified: 'plan',\n planned: 'execute',\n executing: 'verify',\n verifying: 'document',\n };\n return map[state] ?? null;\n}\n","import type { ProjectId } from '@noir-ai/core';\nimport type { Store } from '@noir-ai/store';\nimport { readPrd, writeAuditExport } from './artifacts.js';\nimport { gateFor, readGateHistory, recordGate } from './gates.js';\nimport { applyTransition, nextPhase, stateForPhase } from './state-machine.js';\nimport type {\n GateResult,\n GateResultInput,\n Mode,\n Phase,\n TaskClass,\n TaskState,\n WorkflowGateConfig,\n WorkflowState,\n} from './types.js';\n\n/**\n * Store KV layout. The TaskState lives at `workflow:<taskId>`; the gate audit\n * lives at `audit:<taskId>` and is the AUTHORITATIVE record for every gate\n * outcome (S4 spec §11 OQ-5). `task.history` on the TaskState is a DERIVED view\n * the engine regenerates from the audit KV (debt-batch A / W1 collapse) so\n * there is a single write, single timestamp, single read-back — no drift.\n */\nconst ACTIVE_KEY = 'workflow:active';\nconst GATE_PHASES = ['spec', 'plan', 'verify'] as const satisfies ReadonlyArray<Phase>;\n\n/**\n * Default gate config — used when the engine is constructed without an explicit\n * {@link WorkflowGateConfig} (the legacy 3-arg call shape every existing\n * consumer uses). Mirrors @noir-ai/core's `prd.mandatoryFor` default so the\n * soft PRD recommendation fires for feature/epic tasks out of the box.\n */\nconst DEFAULT_GATE_CONFIG: WorkflowGateConfig = {\n prd: { mandatoryFor: ['feature', 'epic'] },\n};\n\n/** Options for {@link WorkflowEngine.advance}. */\nexport interface AdvanceOpts {\n /**\n * Pass a gate without satisfying its criteria. Requires a non-empty `reason`\n * (validated here, in the engine — `recordGate` is policy-free). The landing\n * gate, if any, is recorded with `decision: 'forced'`. Mutually exclusive\n * with {@link skip}. This is ALSO the explicit-override path for the soft\n * PRD recommendation (debt-batch A / P4): supplying `--force <reason>` at the\n * spec gate of a mandatoryFor task with no PRD records `forced` with the\n * user's reason instead of the recommendation note.\n */\n force?: { reason?: string };\n /**\n * Jump directly to a phase, bypassing the FSM (the escape hatch for blocked /\n * out-of-order resumption). The landing is recorded as `jumpEntry` on the\n * TaskState; a landing gate-state (specified/planned/done) still records its\n * gate so the observable-checkpoint invariant holds. A no-op jump (target\n * equal to the current phase) returns the task unchanged without re-recording\n * any gate (W3 guard — the prior behavior double-stamped the audit).\n */\n to?: Phase;\n /**\n * Quick-mode: record the landing gate (if any) as `decision: 'skipped'`\n * instead of `approved`. The gate is still RECORDED — never silently dropped\n * (Noir §9.1 observable-checkpoint invariant) — only the decision changes.\n * Mutually exclusive with {@link force}.\n */\n skip?: true;\n}\n\n/**\n * The phase whose gate admits entry into `state` (the inverse of {@link gateFor}).\n *\n * The verify gate fires on entering `done` — but `done` is `stateForPhase('document')`,\n * so the target *phase* of that transition is `document`, not `verify`. The gate\n * must therefore be looked up from the target *state*, not the target phase.\n */\nfunction gatePhaseForState(state: WorkflowState): Phase | null {\n for (const p of GATE_PHASES) {\n if (gateFor(p) === state) return p;\n }\n return null;\n}\n\n/**\n * WorkflowEngine — drives an SDD task through its lifecycle.\n *\n * The engine is a thin orchestrator over three T1–T3 primitives:\n * • the hand-rolled FSM ({@link applyTransition}) for legal forward moves,\n * • {@link recordGate} for observable checkpoint audit, and\n * • the store KV for persisted {@link TaskState}.\n *\n * Policy that did not belong in T1/T2 lives here:\n * • `--force` requires a non-empty reason (validated before any gate write),\n * • `blocked` / `abandoned` have no incoming FSM edges and are set directly,\n * • `opts.to` jumps past FSM edges and is recorded via `jumpEntry`,\n * • (P4) the soft PRD recommendation at the spec gate for mandatoryFor tasks.\n *\n * Modes (Full/Quick) and cross-session resume are T5; MCP tools are T6 — the\n * engine stays mode-agnostic here and only stores `mode` on the TaskState.\n */\nexport class WorkflowEngine {\n private readonly gateConfig: WorkflowGateConfig;\n\n constructor(\n private readonly store: Store,\n /**\n * Project root — the engine is constructed with it so modes (quickPath\n * writes a spec stub via {@link writeSpec}) and future artifact flushes\n * (checkpoint/audit export) are self-contained. Public so the modes module\n * can pass it to {@link ArtifactWriter}.\n */\n readonly root: string,\n private readonly projectId: ProjectId,\n /**\n * Gate-config slice (debt-batch A / P4). Optional — the legacy 3-arg call\n * shape (every existing consumer) resolves to {@link DEFAULT_GATE_CONFIG}\n * (PRD recommendation fires for feature/epic). The daemon / CLI bridge\n * passes the resolved `prd.mandatoryFor` from NoirConfig so user overrides\n * take effect; tests pass an explicit shape to pin behavior.\n */\n gateConfig?: WorkflowGateConfig,\n ) {\n this.gateConfig = gateConfig ?? DEFAULT_GATE_CONFIG;\n }\n\n /**\n * Create a new task at draft/intake, persist it, and point `workflow:active`\n * at it. Re-starting an existing taskId overwrites it (intentional — the KV is\n * the source of truth, not a journal).\n *\n * `taskClass` (debt-batch A / P4) is optional and additive — legacy callers\n * (and existing tests) omit it; the soft PRD gate then never fires for the\n * task (consistent with the \"additive, no-op when absent\" rule). New callers\n * that want the recommendation pass `'feature'` / `'epic'` / etc.\n */\n async startTask(\n taskId: string,\n slug: string,\n mode: Mode,\n taskClass?: TaskClass,\n ): Promise<TaskState> {\n const task: TaskState = {\n taskId,\n slug,\n projectId: this.projectId,\n state: 'draft',\n phase: 'intake',\n mode,\n history: [],\n ...(taskClass !== undefined ? { taskClass } : {}),\n updatedAt: Date.now(),\n };\n this.persist(task);\n this.store.setState<string>(ACTIVE_KEY, taskId);\n return task;\n }\n\n /**\n * Advance `taskId` to its next phase, or jump with `opts.to`.\n *\n * At a gate-landing state (entering `specified` / `planned` / `done`) a\n * {@link GateResult} is recorded — `approved` by default, `forced` (with the\n * reason) when `opts.force` is supplied, or `skipped` when `opts.skip` is\n * supplied (quick mode). `force` and `skip` are mutually exclusive. Jumps\n * bypass the FSM and additionally stamp `jumpEntry`.\n *\n * Single source of truth (W1): the gate is written ONCE to the audit KV via\n * {@link recordGate}, and `task.history` is RE-DERIVED from that KV. No\n * second write, no second timestamp — the S4 sub-ms drift is gone.\n */\n async advance(taskId: string, opts?: AdvanceOpts): Promise<TaskState> {\n const task = this.requireTask(taskId);\n\n // Policy: --force and skip are mutually exclusive gate decisions (a gate\n // can't be both forced AND skipped). Validated BEFORE any gate write so a\n // bad combination never leaves a partial audit trail behind.\n if (opts?.force && opts?.skip) {\n throw new Error('cannot combine --force and skip');\n }\n // Policy: --force requires a non-empty reason.\n if (opts?.force && !opts.force.reason?.trim()) {\n throw new Error('--force requires a reason');\n }\n\n const jump = opts?.to !== undefined;\n const targetPhase: Phase = jump ? (opts?.to as Phase) : this.nextPhaseOf(task);\n\n // W3 guard: a jump to the CURRENT phase is a no-op. Previously the engine\n // re-stamped the audit (the landing gate fired again), producing a spurious\n // duplicate entry. Return the task unchanged — no gate, no state change.\n if (jump && targetPhase === task.phase) {\n return task;\n }\n\n const targetState = stateForPhase(targetPhase);\n if (!jump) {\n // applyTransition surfaces the FSM's gate hint on illegal moves.\n applyTransition(task.state, targetState);\n }\n\n // Observable checkpoint: entering specified/planned/done always records a\n // gate — looked up from the target STATE (see gatePhaseForState).\n const gatePhase = gatePhaseForState(targetState);\n if (gatePhase !== null) {\n const decision = opts?.force ? 'forced' : opts?.skip ? 'skipped' : 'approved';\n // P4 soft PRD recommendation: when entering `specified` (the spec gate),\n // the task is mandatoryFor-eligible, no PRD artifact exists, and the user\n // did NOT supply --force, fold a recommendation note into the recorded\n // gate's `reason`. The advance STILL PROCEEDS — this is the \"quiet\n // observable nudge\" doctrine (§9.1): never a hard block, never silently\n // dropped. Quick-mode + unlisted taskClasses skip the check entirely;\n // --force records `forced` with the user's reason (the explicit override).\n const prdHint = this.prdRecommendation(task, gatePhase, opts);\n const input: GateResultInput = {\n phase: gatePhase,\n decision,\n // exactOptionalPropertyTypes is false; spread reason only when present.\n // Force-path wins over the soft hint (a user who forces is explicitly\n // accepting the recommendation; their reason is the override signal).\n ...(opts?.force\n ? { reason: opts.force.reason }\n : prdHint !== null\n ? { reason: prdHint }\n : {}),\n };\n // W1: record ONCE to the authoritative audit KV; derive history from it.\n recordGate(this.store, taskId, input);\n task.history = readGateHistory(this.store, taskId);\n }\n\n task.state = targetState;\n task.phase = targetPhase;\n if (jump) task.jumpEntry = targetPhase;\n task.updatedAt = Date.now();\n\n this.persist(task);\n return task;\n }\n\n /**\n * Compute the soft PRD-recommendation message (P4), or `null` when the\n * recommendation does NOT apply. The recommendation applies when ALL of:\n * • the gate landing now is the spec gate (entering `specified`), AND\n * • the task is in full mode (quick mode skips — quickPath writes a stub), AND\n * • the task has a `taskClass` listed in `gateConfig.prd.mandatoryFor`, AND\n * • no PRD artifact exists at `.noir/prd/<id>-<slug>.md` (readPrd), AND\n * • the user did NOT supply --force (force is the explicit-override path).\n *\n * Returns the observable note (audited on the spec gate's `reason`) so a\n * downstream consumer (CLI status, workflow_status MCP tool) can surface it.\n */\n private prdRecommendation(task: TaskState, gatePhase: Phase, opts?: AdvanceOpts): string | null {\n if (gatePhase !== 'spec') return null;\n if (task.mode === 'quick') return null;\n const taskClass = task.taskClass;\n if (taskClass === undefined) return null;\n if (!this.gateConfig.prd.mandatoryFor.includes(taskClass)) return null;\n if (opts?.force) return null; // explicit override — user's reason wins\n if (readPrd(this.root, task.taskId, task.slug) !== null) return null;\n return `PRD recommended for ${taskClass} — provide one (noir-prd) or --force <reason> to skip`;\n }\n\n /** Read the persisted TaskState, or null if the task is unknown. */\n status(taskId: string): TaskState | null {\n const task = this.store.getState<TaskState>(workflowKey(taskId));\n if (!task) return null;\n // W1: re-derive history from the authoritative audit KV on every read, so\n // consumers see any externally-mutated audit (e.g. a manual KV write or a\n // future audit-import path) without waiting for the next advance.\n task.history = readGateHistory(this.store, taskId);\n return task;\n }\n\n /**\n * The taskId of the most-recently-started task (`workflow:active` in the\n * store KV), or `null` when no task has been started yet. Lets the MCP\n * `workflow_status` / `checkpoint` tools omit `taskId` and operate on the\n * active task.\n */\n activeTaskId(): string | null {\n return this.store.getState<string>(ACTIVE_KEY);\n }\n\n /**\n * Re-flush the current state to KV + flush the gate audit export to\n * `.noir/audit/<taskId>.json`. W2 (debt-batch A): the prior implementation\n * only bumped `updatedAt`, which every advance already does — vestigial.\n * Cross-session resume (`resumeTask`) reads `workflow:<id>` straight from the\n * KV and consumes nothing from this method; the S4 ledger noted the write was\n * dead. The fix is to WIRE the checkpoint to a real cross-tool artifact\n * flush: the audit JSON on disk (the S4 spec §11 OQ-5 \"export to\n * `.noir/audit/<taskId>.json`\" that {@link writeAuditExport} already\n * implemented but nothing called). The MCP `checkpoint { action:'save' }`\n * tool stays the public surface; its save now leaves a human-inspectable\n * audit JSON alongside the KV.\n */\n async checkpoint(taskId: string): Promise<void> {\n const task = this.store.getState<TaskState>(workflowKey(taskId));\n if (!task) throw new Error(`Unknown task: ${taskId}`);\n task.updatedAt = Date.now();\n this.persist(task);\n // Flush the authoritative audit KV to disk for cross-tool inspection. The\n // audit is read straight from the KV (the SOT) so the JSON matches what\n // `status()` would report as `task.history`.\n writeAuditExport(this.root, taskId, readGateHistory(this.store, taskId));\n }\n\n /**\n * Set state directly to `blocked` (no FSM edge — the admin escape). The reason\n * is captured on the TaskState for surfacing in `noir.workflow_status`.\n *\n * W3: a SUPPLIED reason must be non-empty after trimming (mirrors the\n * `--force` policy). `setBlocked(id)` with no reason stays valid — it clears\n * no field and just flips state. A whitespace-only reason is rejected as\n * malformed (consistent with `--force`'s whitespace rejection).\n */\n async setBlocked(taskId: string, reason?: string): Promise<TaskState> {\n const task = this.requireTask(taskId);\n if (reason !== undefined) {\n const trimmed = reason.trim();\n if (trimmed.length === 0) {\n throw new Error('setBlocked reason must be non-empty (or omitted to leave unset)');\n }\n task.blockReason = trimmed;\n }\n task.state = 'blocked';\n task.updatedAt = Date.now();\n this.persist(task);\n return task;\n }\n\n /** Set state directly to `abandoned` (terminal; no FSM edge). */\n async abandon(taskId: string): Promise<TaskState> {\n const task = this.requireTask(taskId);\n task.state = 'abandoned';\n task.updatedAt = Date.now();\n this.persist(task);\n return task;\n }\n\n private nextPhaseOf(task: TaskState): Phase {\n const next = nextPhase(task.state);\n if (next === null) {\n throw new Error(`No next phase from state ${task.state}`);\n }\n return next;\n }\n\n private requireTask(taskId: string): TaskState {\n const task = this.store.getState<TaskState>(workflowKey(taskId));\n if (!task) throw new Error(`Unknown task: ${taskId}`);\n return task;\n }\n\n private persist(task: TaskState): void {\n this.store.setState<TaskState>(workflowKey(task.taskId), task);\n }\n}\n\nfunction workflowKey(taskId: string): string {\n return `workflow:${taskId}`;\n}\n","import type { Store } from '@noir-ai/store';\nimport { writeSpec } from './artifacts.js';\nimport type { WorkflowEngine } from './engine.js';\nimport type { TaskState, WorkflowState } from './types.js';\n\n/**\n * Default spec body written by {@link runQuick}. Quick mode is \"discipline\n * lite\": the spec is stubbed (the spec + plan gates are skipped), but a real\n * spec file still lands on disk so later phases (and humans) have something to\n * read. Exported so callers/tests can assert on the canonical stub text.\n */\nexport const QUICK_SPEC_STUB = '<quick-mode stub spec>';\n\n/** Options for {@link runQuick}. */\nexport interface QuickOpts {\n /**\n * Override the stub spec body (defaults to {@link QUICK_SPEC_STUB}). Useful\n * when a quick task already has a one-line description worth persisting.\n */\n specBody?: string;\n}\n\n/**\n * Quick mode — the fast path for small / spike tasks.\n *\n * Writes a stub spec to `.noir/specs/<taskId>-<slug>.md` (via\n * {@link writeSpec}) and fast-forwards the task from `draft` to `executing`,\n * recording the spec and plan gates as `decision: 'skipped'`. The verify gate\n * is intentionally LEFT alone — it still fires as `approved` when the task\n * later reaches `done`, providing the one real checkpoint (discipline lite).\n *\n * The task must already be started (`engine.startTask(..., 'quick')`); this\n * function reads the slug from the persisted TaskState. Skipped gates are\n * RECORDED (Noir §9.1 observable-checkpoint invariant), never silently dropped\n * — the audit KV and `history` both carry the `skipped` entries.\n */\nexport async function runQuick(\n engine: WorkflowEngine,\n taskId: string,\n opts?: QuickOpts,\n): Promise<TaskState> {\n const task = engine.status(taskId);\n if (!task) throw new Error(`Unknown task: ${taskId}`);\n\n // Flush the stub spec first so the artifact exists before any gate fires.\n writeSpec(engine.root, taskId, task.slug, opts?.specBody ?? QUICK_SPEC_STUB);\n\n // draft → clarifying → specified (spec gate SKIPPED) → planned (plan gate\n // SKIPPED) → executing. Verify is left for the normal flow (runs as approved).\n await engine.advance(taskId); // draft → clarifying (no gate)\n await engine.advance(taskId, { skip: true }); // → specified (spec gate skipped)\n await engine.advance(taskId, { skip: true }); // → planned (plan gate skipped)\n return engine.advance(taskId); // → executing (no gate) — land here\n}\n\n/** Terminal states with nothing left to resume. `blocked` is NOT terminal. */\nconst TERMINAL_STATES: ReadonlySet<WorkflowState> = new Set<WorkflowState>(['done', 'abandoned']);\n\n/**\n * Reconstruct the in-flight TaskState across a session break.\n *\n * Reads `workflow:active` from the store KV → the most-recently-started\n * taskId (T4 semantics; v1 is one-task-per-project) → the persisted\n * `workflow:<taskId>` TaskState. Returns `null` when there is no active task,\n * the task record is missing, or the active task is terminal (`done` /\n * `abandoned` — nothing to resume). A `blocked` task IS resumable.\n *\n * Uses only the public {@link Store} API, so a freshly-`openStore`'d handle\n * against the same on-disk DB is sufficient (no live engine required).\n */\nexport async function resumeTask(store: Store): Promise<TaskState | null> {\n const activeId = store.getState<string>('workflow:active');\n if (!activeId) return null;\n\n const task = store.getState<TaskState>(`workflow:${activeId}`);\n if (!task) return null;\n if (TERMINAL_STATES.has(task.state)) return null;\n\n return task;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noir-ai/workflow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-beta.1",
|
|
4
4
|
"description": "Noir workflow — the spec-driven lifecycle engine (intake → document) with observable, escapable gates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "agaaaptr",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"README.md"
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@noir-ai/core": "1.
|
|
47
|
-
"@noir-ai/store": "1.
|
|
46
|
+
"@noir-ai/core": "1.2.0-beta.1",
|
|
47
|
+
"@noir-ai/store": "1.2.0-beta.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^26.1.1"
|