@noir-ai/workflow 1.0.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/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/index.d.ts +240 -0
- package/dist/index.js +363 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agaaaptr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @noir-ai/workflow
|
|
2
|
+
|
|
3
|
+
The spec-driven development lifecycle engine: a hand-rolled finite-state machine (Intake → Clarify → Spec → Plan → Execute → Verify → Document) with observable, escapable gates. Every decision is recorded; phases can be force-skipped with a reason or jumped to directly. Supports Full, Quick, and Resume modes with cross-session resume.
|
|
4
|
+
|
|
5
|
+
Part of the **[Noir](https://github.com/agaaaptr/noir#readme)** toolkit — the discipline, context, and memory layer for any agentic CLI.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @noir-ai/workflow
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> Most users install the CLI instead, which pulls in the packages it needs:
|
|
14
|
+
>
|
|
15
|
+
> ```bash
|
|
16
|
+
> npm install -g @noir-ai/cli
|
|
17
|
+
> ```
|
|
18
|
+
|
|
19
|
+
## License
|
|
20
|
+
|
|
21
|
+
MIT © agaaaptr
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { ProjectId } from '@noir-ai/core';
|
|
2
|
+
import { Store } from '@noir-ai/store';
|
|
3
|
+
|
|
4
|
+
declare const PHASES: readonly ["intake", "clarify", "spec", "plan", "execute", "verify", "document"];
|
|
5
|
+
type Phase = (typeof PHASES)[number];
|
|
6
|
+
declare const STATES: readonly ["draft", "clarifying", "specified", "planned", "executing", "verifying", "done", "blocked", "abandoned"];
|
|
7
|
+
type WorkflowState = (typeof STATES)[number];
|
|
8
|
+
type Mode = 'full' | 'quick';
|
|
9
|
+
interface GateResult {
|
|
10
|
+
phase: Phase;
|
|
11
|
+
decision: 'approved' | 'forced' | 'skipped';
|
|
12
|
+
reason?: string;
|
|
13
|
+
at: number;
|
|
14
|
+
}
|
|
15
|
+
interface TaskState {
|
|
16
|
+
taskId: string;
|
|
17
|
+
slug: string;
|
|
18
|
+
projectId: ProjectId;
|
|
19
|
+
state: WorkflowState;
|
|
20
|
+
phase: Phase;
|
|
21
|
+
mode: Mode;
|
|
22
|
+
history: GateResult[];
|
|
23
|
+
jumpEntry?: Phase;
|
|
24
|
+
/** Reason captured by `setBlocked` (admin escape; set directly, not via FSM). */
|
|
25
|
+
blockReason?: string;
|
|
26
|
+
updatedAt: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Write intake artifact to .noir/intake/<taskId>.md
|
|
31
|
+
*/
|
|
32
|
+
declare function writeIntake(root: string, taskId: string, content: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Write spec artifact to .noir/specs/<taskId>-<slug>.md
|
|
35
|
+
* Creates markdown with frontmatter and body
|
|
36
|
+
*/
|
|
37
|
+
declare function writeSpec(root: string, taskId: string, slug: string, body: string): void;
|
|
38
|
+
/**
|
|
39
|
+
* Write plan artifact to .noir/plans/<taskId>-<slug>.md
|
|
40
|
+
*/
|
|
41
|
+
declare function writePlan(root: string, taskId: string, slug: string, body: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Write task artifact to .noir/tasks/<taskId>-<taskName>.md
|
|
44
|
+
*/
|
|
45
|
+
declare function writeTask(root: string, taskId: string, taskName: string, body: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Write decision stub to .noir/decisions/<n>.md
|
|
48
|
+
*/
|
|
49
|
+
declare function writeDecisionStub(root: string, n: number, title: string): void;
|
|
50
|
+
/**
|
|
51
|
+
* Write changelog stub entry to .noir/CHANGELOG.md
|
|
52
|
+
*/
|
|
53
|
+
declare function writeChangelogStub(root: string, entry: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Write audit export to .noir/audit/<taskId>.json
|
|
56
|
+
* Exports GateResult array as JSON
|
|
57
|
+
*/
|
|
58
|
+
declare function writeAuditExport(root: string, taskId: string, results: GateResult[]): void;
|
|
59
|
+
|
|
60
|
+
/** Options for {@link WorkflowEngine.advance}. */
|
|
61
|
+
interface AdvanceOpts {
|
|
62
|
+
/**
|
|
63
|
+
* Pass a gate without satisfying its criteria. Requires a non-empty `reason`
|
|
64
|
+
* (validated here, in the engine — `recordGate` is policy-free). The landing
|
|
65
|
+
* gate, if any, is recorded with `decision: 'forced'`. Mutually exclusive
|
|
66
|
+
* with {@link skip}.
|
|
67
|
+
*/
|
|
68
|
+
force?: {
|
|
69
|
+
reason?: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Jump directly to a phase, bypassing the FSM (the escape hatch for blocked /
|
|
73
|
+
* out-of-order resumption). The landing is recorded as `jumpEntry` on the
|
|
74
|
+
* TaskState; a landing gate-state (specified/planned/done) still records its
|
|
75
|
+
* gate so the observable-checkpoint invariant holds.
|
|
76
|
+
*/
|
|
77
|
+
to?: Phase;
|
|
78
|
+
/**
|
|
79
|
+
* Quick-mode: record the landing gate (if any) as `decision: 'skipped'`
|
|
80
|
+
* instead of `approved`. The gate is still RECORDED — never silently dropped
|
|
81
|
+
* (Noir §9.1 observable-checkpoint invariant) — only the decision changes.
|
|
82
|
+
* Mutually exclusive with {@link force}.
|
|
83
|
+
*/
|
|
84
|
+
skip?: true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* WorkflowEngine — drives an SDD task through its lifecycle.
|
|
88
|
+
*
|
|
89
|
+
* The engine is a thin orchestrator over three T1–T3 primitives:
|
|
90
|
+
* • the hand-rolled FSM ({@link applyTransition}) for legal forward moves,
|
|
91
|
+
* • {@link recordGate} for observable checkpoint audit, and
|
|
92
|
+
* • the store KV for persisted {@link TaskState}.
|
|
93
|
+
*
|
|
94
|
+
* Policy that did not belong in T1/T2 lives here:
|
|
95
|
+
* • `--force` requires a non-empty reason (validated before any gate write),
|
|
96
|
+
* • `blocked` / `abandoned` have no incoming FSM edges and are set directly,
|
|
97
|
+
* • `opts.to` jumps past FSM edges and is recorded via `jumpEntry`.
|
|
98
|
+
*
|
|
99
|
+
* Modes (Full/Quick) and cross-session resume are T5; MCP tools are T6 — the
|
|
100
|
+
* engine stays mode-agnostic here and only stores `mode` on the TaskState.
|
|
101
|
+
*/
|
|
102
|
+
declare class WorkflowEngine {
|
|
103
|
+
private readonly store;
|
|
104
|
+
/**
|
|
105
|
+
* Project root — the engine is constructed with it so modes (quickPath
|
|
106
|
+
* writes a spec stub via {@link writeSpec}) and future artifact flushes
|
|
107
|
+
* (checkpoint/audit export) are self-contained. Public so the modes module
|
|
108
|
+
* can pass it to {@link ArtifactWriter}.
|
|
109
|
+
*/
|
|
110
|
+
readonly root: string;
|
|
111
|
+
private readonly projectId;
|
|
112
|
+
constructor(store: Store,
|
|
113
|
+
/**
|
|
114
|
+
* Project root — the engine is constructed with it so modes (quickPath
|
|
115
|
+
* writes a spec stub via {@link writeSpec}) and future artifact flushes
|
|
116
|
+
* (checkpoint/audit export) are self-contained. Public so the modes module
|
|
117
|
+
* can pass it to {@link ArtifactWriter}.
|
|
118
|
+
*/
|
|
119
|
+
root: string, projectId: ProjectId);
|
|
120
|
+
/**
|
|
121
|
+
* Create a new task at draft/intake, persist it, and point `workflow:active`
|
|
122
|
+
* at it. Re-starting an existing taskId overwrites it (intentional — the KV is
|
|
123
|
+
* the source of truth, not a journal).
|
|
124
|
+
*/
|
|
125
|
+
startTask(taskId: string, slug: string, mode: Mode): Promise<TaskState>;
|
|
126
|
+
/**
|
|
127
|
+
* Advance `taskId` to its next phase, or jump with `opts.to`.
|
|
128
|
+
*
|
|
129
|
+
* At a gate-landing state (entering `specified` / `planned` / `done`) a
|
|
130
|
+
* {@link GateResult} is recorded — `approved` by default, `forced` (with the
|
|
131
|
+
* reason) when `opts.force` is supplied, or `skipped` when `opts.skip` is
|
|
132
|
+
* supplied (quick mode). `force` and `skip` are mutually exclusive. Jumps
|
|
133
|
+
* bypass the FSM and additionally stamp `jumpEntry`.
|
|
134
|
+
*/
|
|
135
|
+
advance(taskId: string, opts?: AdvanceOpts): Promise<TaskState>;
|
|
136
|
+
/** Read the persisted TaskState, or null if the task is unknown. */
|
|
137
|
+
status(taskId: string): TaskState | null;
|
|
138
|
+
/**
|
|
139
|
+
* The taskId of the most-recently-started task (`workflow:active` in the
|
|
140
|
+
* store KV), or `null` when no task has been started yet. Lets the MCP
|
|
141
|
+
* `workflow_status` / `checkpoint` tools omit `taskId` and operate on the
|
|
142
|
+
* active task.
|
|
143
|
+
*/
|
|
144
|
+
activeTaskId(): string | null;
|
|
145
|
+
/**
|
|
146
|
+
* Re-flush the current state to KV. In T4 every advance already persists, so
|
|
147
|
+
* this is the explicit "mark a checkpoint" hook (bumps `updatedAt`); T5
|
|
148
|
+
* deepens it to flush artifacts + audit export for cross-session resume.
|
|
149
|
+
*/
|
|
150
|
+
checkpoint(taskId: string): Promise<void>;
|
|
151
|
+
/**
|
|
152
|
+
* Set state directly to `blocked` (no FSM edge — the admin escape). The reason
|
|
153
|
+
* is captured on the TaskState for surfacing in `noir.workflow_status`.
|
|
154
|
+
*/
|
|
155
|
+
setBlocked(taskId: string, reason?: string): Promise<TaskState>;
|
|
156
|
+
/** Set state directly to `abandoned` (terminal; no FSM edge). */
|
|
157
|
+
abandon(taskId: string): Promise<TaskState>;
|
|
158
|
+
private nextPhaseOf;
|
|
159
|
+
private requireTask;
|
|
160
|
+
private persist;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The workflow state a phase's gate guards entry into (Noir §9.1 observable
|
|
165
|
+
* checkpoint). A gate fires when its phase completes:
|
|
166
|
+
* spec-gate → entering `specified`
|
|
167
|
+
* plan-gate → entering `planned`
|
|
168
|
+
* verify-gate → entering `done`
|
|
169
|
+
*
|
|
170
|
+
* Phases without a gate (intake / clarify / execute / document) return `null`.
|
|
171
|
+
* This is distinct from {@link stateForPhase}: `stateForPhase('verify')` is the
|
|
172
|
+
* in-progress `verifying`, while `gateFor('verify')` is the state the verify
|
|
173
|
+
* gate admits you into (`done`).
|
|
174
|
+
*/
|
|
175
|
+
declare function gateFor(phase: Phase): WorkflowState | null;
|
|
176
|
+
/**
|
|
177
|
+
* Append a gate decision to the task's audit log in the store KV.
|
|
178
|
+
*
|
|
179
|
+
* The audit lives at `audit:<taskId>` as a `GateResult[]` and is the source of
|
|
180
|
+
* truth for every gate outcome (the `.noir/audit/` export is a later helper).
|
|
181
|
+
* This is the "quiet observable checkpoint": an `approved`, `forced`, or
|
|
182
|
+
* `skipped` decision is always recorded — never silently dropped — and `forced`
|
|
183
|
+
* carries a `reason`.
|
|
184
|
+
*
|
|
185
|
+
* `at` is stamped here (`Date.now()`) rather than trusted from the caller, so
|
|
186
|
+
* the audit reflects when the gate actually fired. The write is append-only:
|
|
187
|
+
* prior entries are read and preserved (never overwritten).
|
|
188
|
+
*/
|
|
189
|
+
declare function recordGate(store: Store, taskId: string, result: GateResult): void;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Default spec body written by {@link runQuick}. Quick mode is "discipline
|
|
193
|
+
* lite": the spec is stubbed (the spec + plan gates are skipped), but a real
|
|
194
|
+
* spec file still lands on disk so later phases (and humans) have something to
|
|
195
|
+
* read. Exported so callers/tests can assert on the canonical stub text.
|
|
196
|
+
*/
|
|
197
|
+
declare const QUICK_SPEC_STUB = "<quick-mode stub spec>";
|
|
198
|
+
/** Options for {@link runQuick}. */
|
|
199
|
+
interface QuickOpts {
|
|
200
|
+
/**
|
|
201
|
+
* Override the stub spec body (defaults to {@link QUICK_SPEC_STUB}). Useful
|
|
202
|
+
* when a quick task already has a one-line description worth persisting.
|
|
203
|
+
*/
|
|
204
|
+
specBody?: string;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Quick mode — the fast path for small / spike tasks.
|
|
208
|
+
*
|
|
209
|
+
* Writes a stub spec to `.noir/specs/<taskId>-<slug>.md` (via
|
|
210
|
+
* {@link writeSpec}) and fast-forwards the task from `draft` to `executing`,
|
|
211
|
+
* recording the spec and plan gates as `decision: 'skipped'`. The verify gate
|
|
212
|
+
* is intentionally LEFT alone — it still fires as `approved` when the task
|
|
213
|
+
* later reaches `done`, providing the one real checkpoint (discipline lite).
|
|
214
|
+
*
|
|
215
|
+
* The task must already be started (`engine.startTask(..., 'quick')`); this
|
|
216
|
+
* function reads the slug from the persisted TaskState. Skipped gates are
|
|
217
|
+
* RECORDED (Noir §9.1 observable-checkpoint invariant), never silently dropped
|
|
218
|
+
* — the audit KV and `history` both carry the `skipped` entries.
|
|
219
|
+
*/
|
|
220
|
+
declare function runQuick(engine: WorkflowEngine, taskId: string, opts?: QuickOpts): Promise<TaskState>;
|
|
221
|
+
/**
|
|
222
|
+
* Reconstruct the in-flight TaskState across a session break.
|
|
223
|
+
*
|
|
224
|
+
* Reads `workflow:active` from the store KV → the most-recently-started
|
|
225
|
+
* taskId (T4 semantics; v1 is one-task-per-project) → the persisted
|
|
226
|
+
* `workflow:<taskId>` TaskState. Returns `null` when there is no active task,
|
|
227
|
+
* the task record is missing, or the active task is terminal (`done` /
|
|
228
|
+
* `abandoned` — nothing to resume). A `blocked` task IS resumable.
|
|
229
|
+
*
|
|
230
|
+
* Uses only the public {@link Store} API, so a freshly-`openStore`'d handle
|
|
231
|
+
* against the same on-disk DB is sufficient (no live engine required).
|
|
232
|
+
*/
|
|
233
|
+
declare function resumeTask(store: Store): Promise<TaskState | null>;
|
|
234
|
+
|
|
235
|
+
declare function canTransition(from: WorkflowState, to: WorkflowState): boolean;
|
|
236
|
+
declare function applyTransition(from: WorkflowState, to: WorkflowState): WorkflowState;
|
|
237
|
+
declare function stateForPhase(p: Phase): WorkflowState;
|
|
238
|
+
declare function nextPhase(state: WorkflowState): Phase | null;
|
|
239
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// src/artifacts.ts
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { paths } from "@noir-ai/core";
|
|
5
|
+
function writeIntake(root, taskId, content) {
|
|
6
|
+
const dir = join(root, ".noir", "intake");
|
|
7
|
+
const file = join(dir, `${taskId}.md`);
|
|
8
|
+
mkdirSync(dir, { recursive: true });
|
|
9
|
+
writeFileSync(file, content, "utf-8");
|
|
10
|
+
}
|
|
11
|
+
function writeSpec(root, taskId, slug, body) {
|
|
12
|
+
const dir = paths.specsDir(root);
|
|
13
|
+
const file = paths.specFile(root, taskId, slug);
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
const content = `---
|
|
16
|
+
taskId: ${taskId}
|
|
17
|
+
slug: ${slug}
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
${body}`;
|
|
21
|
+
writeFileSync(file, content, "utf-8");
|
|
22
|
+
}
|
|
23
|
+
function writePlan(root, taskId, slug, body) {
|
|
24
|
+
const dir = paths.plansDir(root);
|
|
25
|
+
const file = paths.planFile(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 writeTask(root, taskId, taskName, body) {
|
|
36
|
+
const dir = paths.tasksDir(root);
|
|
37
|
+
const file = paths.taskFile(root, taskId, taskName);
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
const content = `---
|
|
40
|
+
taskId: ${taskId}
|
|
41
|
+
task: ${taskName}
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
${body}`;
|
|
45
|
+
writeFileSync(file, content, "utf-8");
|
|
46
|
+
}
|
|
47
|
+
function writeDecisionStub(root, n, title) {
|
|
48
|
+
const dir = paths.decisionsDir(root);
|
|
49
|
+
const file = paths.decisionFile(root, n);
|
|
50
|
+
mkdirSync(dir, { recursive: true });
|
|
51
|
+
const content = `# ${title}
|
|
52
|
+
|
|
53
|
+
*Decision record ${n}*
|
|
54
|
+
|
|
55
|
+
<!-- Status: pending -->
|
|
56
|
+
`;
|
|
57
|
+
writeFileSync(file, content, "utf-8");
|
|
58
|
+
}
|
|
59
|
+
function writeChangelogStub(root, entry) {
|
|
60
|
+
const file = join(root, ".noir", "CHANGELOG.md");
|
|
61
|
+
mkdirSync(join(root, ".noir"), { recursive: true });
|
|
62
|
+
let content;
|
|
63
|
+
if (existsSync(file)) {
|
|
64
|
+
const existing = readFileSync(file, "utf-8");
|
|
65
|
+
const prefix = existing.endsWith("\n") ? existing : `${existing}
|
|
66
|
+
`;
|
|
67
|
+
content = `${prefix}${entry}
|
|
68
|
+
`;
|
|
69
|
+
} else {
|
|
70
|
+
content = `# Changelog
|
|
71
|
+
|
|
72
|
+
${entry}
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
writeFileSync(file, content, "utf-8");
|
|
76
|
+
}
|
|
77
|
+
function writeAuditExport(root, taskId, results) {
|
|
78
|
+
const dir = paths.auditDir(root);
|
|
79
|
+
const file = paths.auditFile(root, taskId);
|
|
80
|
+
mkdirSync(dir, { recursive: true });
|
|
81
|
+
writeFileSync(file, JSON.stringify(results, null, 2), "utf-8");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/gates.ts
|
|
85
|
+
function gateFor(phase) {
|
|
86
|
+
switch (phase) {
|
|
87
|
+
case "spec":
|
|
88
|
+
return "specified";
|
|
89
|
+
case "plan":
|
|
90
|
+
return "planned";
|
|
91
|
+
case "verify":
|
|
92
|
+
return "done";
|
|
93
|
+
default:
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function recordGate(store, taskId, result) {
|
|
98
|
+
const key = `audit:${taskId}`;
|
|
99
|
+
const prior = store.getState(key) ?? [];
|
|
100
|
+
store.setState(key, [...prior, { ...result, at: Date.now() }]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/types.ts
|
|
104
|
+
var PHASES = [
|
|
105
|
+
"intake",
|
|
106
|
+
"clarify",
|
|
107
|
+
"spec",
|
|
108
|
+
"plan",
|
|
109
|
+
"execute",
|
|
110
|
+
"verify",
|
|
111
|
+
"document"
|
|
112
|
+
];
|
|
113
|
+
var STATES = [
|
|
114
|
+
"draft",
|
|
115
|
+
"clarifying",
|
|
116
|
+
"specified",
|
|
117
|
+
"planned",
|
|
118
|
+
"executing",
|
|
119
|
+
"verifying",
|
|
120
|
+
"done",
|
|
121
|
+
"blocked",
|
|
122
|
+
"abandoned"
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
// src/state-machine.ts
|
|
126
|
+
var TRANSITIONS = {
|
|
127
|
+
draft: ["clarifying"],
|
|
128
|
+
clarifying: ["specified"],
|
|
129
|
+
specified: ["planned"],
|
|
130
|
+
planned: ["executing"],
|
|
131
|
+
executing: ["verifying"],
|
|
132
|
+
verifying: ["done"],
|
|
133
|
+
done: [],
|
|
134
|
+
blocked: ["draft", "clarifying", "specified", "planned", "executing", "verifying"],
|
|
135
|
+
abandoned: []
|
|
136
|
+
};
|
|
137
|
+
function canTransition(from, to) {
|
|
138
|
+
return (TRANSITIONS[from] ?? []).includes(to);
|
|
139
|
+
}
|
|
140
|
+
function applyTransition(from, to) {
|
|
141
|
+
if (!canTransition(from, to)) {
|
|
142
|
+
const hint = to === "planned" ? " (the spec gate must be passed first)" : to === "executing" ? " (the plan gate must be passed first)" : to === "done" ? " (the verify gate must be passed first)" : "";
|
|
143
|
+
throw new Error(`Illegal transition ${from} \u2192 ${to}${hint}`);
|
|
144
|
+
}
|
|
145
|
+
return to;
|
|
146
|
+
}
|
|
147
|
+
function stateForPhase(p) {
|
|
148
|
+
switch (p) {
|
|
149
|
+
case "intake":
|
|
150
|
+
return "draft";
|
|
151
|
+
case "clarify":
|
|
152
|
+
return "clarifying";
|
|
153
|
+
case "spec":
|
|
154
|
+
return "specified";
|
|
155
|
+
case "plan":
|
|
156
|
+
return "planned";
|
|
157
|
+
case "execute":
|
|
158
|
+
return "executing";
|
|
159
|
+
case "verify":
|
|
160
|
+
return "verifying";
|
|
161
|
+
case "document":
|
|
162
|
+
return "done";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function nextPhase(state) {
|
|
166
|
+
const map = {
|
|
167
|
+
draft: "clarify",
|
|
168
|
+
clarifying: "spec",
|
|
169
|
+
specified: "plan",
|
|
170
|
+
planned: "execute",
|
|
171
|
+
executing: "verify",
|
|
172
|
+
verifying: "document"
|
|
173
|
+
};
|
|
174
|
+
return map[state] ?? null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/engine.ts
|
|
178
|
+
var ACTIVE_KEY = "workflow:active";
|
|
179
|
+
var GATE_PHASES = ["spec", "plan", "verify"];
|
|
180
|
+
function gatePhaseForState(state) {
|
|
181
|
+
for (const p of GATE_PHASES) {
|
|
182
|
+
if (gateFor(p) === state) return p;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
var WorkflowEngine = class {
|
|
187
|
+
constructor(store, root, projectId) {
|
|
188
|
+
this.store = store;
|
|
189
|
+
this.root = root;
|
|
190
|
+
this.projectId = projectId;
|
|
191
|
+
}
|
|
192
|
+
store;
|
|
193
|
+
root;
|
|
194
|
+
projectId;
|
|
195
|
+
/**
|
|
196
|
+
* Create a new task at draft/intake, persist it, and point `workflow:active`
|
|
197
|
+
* at it. Re-starting an existing taskId overwrites it (intentional — the KV is
|
|
198
|
+
* the source of truth, not a journal).
|
|
199
|
+
*/
|
|
200
|
+
async startTask(taskId, slug, mode) {
|
|
201
|
+
const task = {
|
|
202
|
+
taskId,
|
|
203
|
+
slug,
|
|
204
|
+
projectId: this.projectId,
|
|
205
|
+
state: "draft",
|
|
206
|
+
phase: "intake",
|
|
207
|
+
mode,
|
|
208
|
+
history: [],
|
|
209
|
+
updatedAt: Date.now()
|
|
210
|
+
};
|
|
211
|
+
this.persist(task);
|
|
212
|
+
this.store.setState(ACTIVE_KEY, taskId);
|
|
213
|
+
return task;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Advance `taskId` to its next phase, or jump with `opts.to`.
|
|
217
|
+
*
|
|
218
|
+
* At a gate-landing state (entering `specified` / `planned` / `done`) a
|
|
219
|
+
* {@link GateResult} is recorded — `approved` by default, `forced` (with the
|
|
220
|
+
* reason) when `opts.force` is supplied, or `skipped` when `opts.skip` is
|
|
221
|
+
* supplied (quick mode). `force` and `skip` are mutually exclusive. Jumps
|
|
222
|
+
* bypass the FSM and additionally stamp `jumpEntry`.
|
|
223
|
+
*/
|
|
224
|
+
async advance(taskId, opts) {
|
|
225
|
+
const task = this.requireTask(taskId);
|
|
226
|
+
if (opts?.force && opts?.skip) {
|
|
227
|
+
throw new Error("cannot combine --force and skip");
|
|
228
|
+
}
|
|
229
|
+
if (opts?.force && !opts.force.reason?.trim()) {
|
|
230
|
+
throw new Error("--force requires a reason");
|
|
231
|
+
}
|
|
232
|
+
const jump = opts?.to !== void 0;
|
|
233
|
+
const targetPhase = jump ? opts?.to : this.nextPhaseOf(task);
|
|
234
|
+
const targetState = stateForPhase(targetPhase);
|
|
235
|
+
if (!jump) {
|
|
236
|
+
applyTransition(task.state, targetState);
|
|
237
|
+
}
|
|
238
|
+
const gatePhase = gatePhaseForState(targetState);
|
|
239
|
+
if (gatePhase !== null) {
|
|
240
|
+
const decision = opts?.force ? "forced" : opts?.skip ? "skipped" : "approved";
|
|
241
|
+
const gate = {
|
|
242
|
+
phase: gatePhase,
|
|
243
|
+
decision,
|
|
244
|
+
// exactOptionalPropertyTypes is false; spread reason only when forced.
|
|
245
|
+
...opts?.force ? { reason: opts.force.reason } : {},
|
|
246
|
+
at: Date.now()
|
|
247
|
+
};
|
|
248
|
+
recordGate(this.store, taskId, gate);
|
|
249
|
+
task.history.push(gate);
|
|
250
|
+
}
|
|
251
|
+
task.state = targetState;
|
|
252
|
+
task.phase = targetPhase;
|
|
253
|
+
if (jump) task.jumpEntry = targetPhase;
|
|
254
|
+
task.updatedAt = Date.now();
|
|
255
|
+
this.persist(task);
|
|
256
|
+
return task;
|
|
257
|
+
}
|
|
258
|
+
/** Read the persisted TaskState, or null if the task is unknown. */
|
|
259
|
+
status(taskId) {
|
|
260
|
+
return this.store.getState(workflowKey(taskId));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The taskId of the most-recently-started task (`workflow:active` in the
|
|
264
|
+
* store KV), or `null` when no task has been started yet. Lets the MCP
|
|
265
|
+
* `workflow_status` / `checkpoint` tools omit `taskId` and operate on the
|
|
266
|
+
* active task.
|
|
267
|
+
*/
|
|
268
|
+
activeTaskId() {
|
|
269
|
+
return this.store.getState(ACTIVE_KEY);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Re-flush the current state to KV. In T4 every advance already persists, so
|
|
273
|
+
* this is the explicit "mark a checkpoint" hook (bumps `updatedAt`); T5
|
|
274
|
+
* deepens it to flush artifacts + audit export for cross-session resume.
|
|
275
|
+
*/
|
|
276
|
+
async checkpoint(taskId) {
|
|
277
|
+
const task = this.store.getState(workflowKey(taskId));
|
|
278
|
+
if (!task) throw new Error(`Unknown task: ${taskId}`);
|
|
279
|
+
task.updatedAt = Date.now();
|
|
280
|
+
this.persist(task);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Set state directly to `blocked` (no FSM edge — the admin escape). The reason
|
|
284
|
+
* is captured on the TaskState for surfacing in `noir.workflow_status`.
|
|
285
|
+
*/
|
|
286
|
+
async setBlocked(taskId, reason) {
|
|
287
|
+
const task = this.requireTask(taskId);
|
|
288
|
+
task.state = "blocked";
|
|
289
|
+
if (reason) task.blockReason = reason;
|
|
290
|
+
task.updatedAt = Date.now();
|
|
291
|
+
this.persist(task);
|
|
292
|
+
return task;
|
|
293
|
+
}
|
|
294
|
+
/** Set state directly to `abandoned` (terminal; no FSM edge). */
|
|
295
|
+
async abandon(taskId) {
|
|
296
|
+
const task = this.requireTask(taskId);
|
|
297
|
+
task.state = "abandoned";
|
|
298
|
+
task.updatedAt = Date.now();
|
|
299
|
+
this.persist(task);
|
|
300
|
+
return task;
|
|
301
|
+
}
|
|
302
|
+
nextPhaseOf(task) {
|
|
303
|
+
const next = nextPhase(task.state);
|
|
304
|
+
if (next === null) {
|
|
305
|
+
throw new Error(`No next phase from state ${task.state}`);
|
|
306
|
+
}
|
|
307
|
+
return next;
|
|
308
|
+
}
|
|
309
|
+
requireTask(taskId) {
|
|
310
|
+
const task = this.store.getState(workflowKey(taskId));
|
|
311
|
+
if (!task) throw new Error(`Unknown task: ${taskId}`);
|
|
312
|
+
return task;
|
|
313
|
+
}
|
|
314
|
+
persist(task) {
|
|
315
|
+
this.store.setState(workflowKey(task.taskId), task);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
function workflowKey(taskId) {
|
|
319
|
+
return `workflow:${taskId}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/modes.ts
|
|
323
|
+
var QUICK_SPEC_STUB = "<quick-mode stub spec>";
|
|
324
|
+
async function runQuick(engine, taskId, opts) {
|
|
325
|
+
const task = engine.status(taskId);
|
|
326
|
+
if (!task) throw new Error(`Unknown task: ${taskId}`);
|
|
327
|
+
writeSpec(engine.root, taskId, task.slug, opts?.specBody ?? QUICK_SPEC_STUB);
|
|
328
|
+
await engine.advance(taskId);
|
|
329
|
+
await engine.advance(taskId, { skip: true });
|
|
330
|
+
await engine.advance(taskId, { skip: true });
|
|
331
|
+
return engine.advance(taskId);
|
|
332
|
+
}
|
|
333
|
+
var TERMINAL_STATES = /* @__PURE__ */ new Set(["done", "abandoned"]);
|
|
334
|
+
async function resumeTask(store) {
|
|
335
|
+
const activeId = store.getState("workflow:active");
|
|
336
|
+
if (!activeId) return null;
|
|
337
|
+
const task = store.getState(`workflow:${activeId}`);
|
|
338
|
+
if (!task) return null;
|
|
339
|
+
if (TERMINAL_STATES.has(task.state)) return null;
|
|
340
|
+
return task;
|
|
341
|
+
}
|
|
342
|
+
export {
|
|
343
|
+
PHASES,
|
|
344
|
+
QUICK_SPEC_STUB,
|
|
345
|
+
STATES,
|
|
346
|
+
WorkflowEngine,
|
|
347
|
+
applyTransition,
|
|
348
|
+
canTransition,
|
|
349
|
+
gateFor,
|
|
350
|
+
nextPhase,
|
|
351
|
+
recordGate,
|
|
352
|
+
resumeTask,
|
|
353
|
+
runQuick,
|
|
354
|
+
stateForPhase,
|
|
355
|
+
writeAuditExport,
|
|
356
|
+
writeChangelogStub,
|
|
357
|
+
writeDecisionStub,
|
|
358
|
+
writeIntake,
|
|
359
|
+
writePlan,
|
|
360
|
+
writeSpec,
|
|
361
|
+
writeTask
|
|
362
|
+
};
|
|
363
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noir-ai/workflow",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "Noir workflow — the spec-driven lifecycle engine (intake → document) with observable, escapable gates.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "agaaaptr",
|
|
7
|
+
"homepage": "https://github.com/agaaaptr/noir#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/agaaaptr/noir.git",
|
|
11
|
+
"directory": "packages/workflow"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/agaaaptr/noir/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"noir",
|
|
18
|
+
"workflow",
|
|
19
|
+
"sdd",
|
|
20
|
+
"spec-driven",
|
|
21
|
+
"lifecycle",
|
|
22
|
+
"fsm",
|
|
23
|
+
"agent"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"provenance": true
|
|
31
|
+
},
|
|
32
|
+
"type": "module",
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"import": "./dist/index.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@noir-ai/core": "1.0.0-beta.1",
|
|
47
|
+
"@noir-ai/store": "1.0.0-beta.1"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^26.1.1"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"typecheck": "tsc --noEmit"
|
|
55
|
+
}
|
|
56
|
+
}
|