forgium 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +300 -0
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.js +404 -0
  4. package/dist/core/domain/types.d.ts +190 -0
  5. package/dist/core/domain/types.js +1 -0
  6. package/dist/core/errors/forgium-errors.d.ts +53 -0
  7. package/dist/core/errors/forgium-errors.js +58 -0
  8. package/dist/core/execution/execution-adapter.d.ts +35 -0
  9. package/dist/core/execution/execution-adapter.js +25 -0
  10. package/dist/core/execution/pi-rpc-execution-adapter.d.ts +13 -0
  11. package/dist/core/execution/pi-rpc-execution-adapter.js +119 -0
  12. package/dist/core/execution/spec-flow-execution-adapter.d.ts +14 -0
  13. package/dist/core/execution/spec-flow-execution-adapter.js +233 -0
  14. package/dist/core/index.d.ts +11 -0
  15. package/dist/core/index.js +11 -0
  16. package/dist/core/repository/filesystem-forgium-repository.d.ts +68 -0
  17. package/dist/core/repository/filesystem-forgium-repository.js +656 -0
  18. package/dist/core/repository/paths.d.ts +10 -0
  19. package/dist/core/repository/paths.js +14 -0
  20. package/dist/core/repository/repo-discovery.d.ts +1 -0
  21. package/dist/core/repository/repo-discovery.js +17 -0
  22. package/dist/core/schemas/draft.schema.d.ts +44 -0
  23. package/dist/core/schemas/draft.schema.js +11 -0
  24. package/dist/core/schemas/inbox.schema.d.ts +23 -0
  25. package/dist/core/schemas/inbox.schema.js +9 -0
  26. package/dist/core/schemas/manifest.schema.d.ts +116 -0
  27. package/dist/core/schemas/manifest.schema.js +20 -0
  28. package/dist/core/schemas/receipt.schema.d.ts +355 -0
  29. package/dist/core/schemas/receipt.schema.js +54 -0
  30. package/dist/core/services/id.d.ts +4 -0
  31. package/dist/core/services/id.js +9 -0
  32. package/dist/core/services/spec-flow-commands.d.ts +6 -0
  33. package/dist/core/services/spec-flow-commands.js +7 -0
  34. package/dist/core/transitions/transition-rules.d.ts +3 -0
  35. package/dist/core/transitions/transition-rules.js +10 -0
  36. package/package.json +34 -0
@@ -0,0 +1,58 @@
1
+ export class ForgiumError extends Error {
2
+ code;
3
+ exitCode;
4
+ constructor(message, code, exitCode = 1) {
5
+ super(message);
6
+ this.code = code;
7
+ this.exitCode = exitCode;
8
+ this.name = new.target.name;
9
+ }
10
+ }
11
+ export class ForgiumRepositoryNotFoundError extends ForgiumError {
12
+ constructor() { super("No Git repository found. Run inside a Git repository or pass an explicit root.", "FORGIUM_REPOSITORY_NOT_FOUND", 3); }
13
+ }
14
+ export class ForgiumNotInitializedError extends ForgiumError {
15
+ constructor() { super("Forgium is not initialized. Run `forgium init` first.", "FORGIUM_NOT_INITIALIZED", 3); }
16
+ }
17
+ export class InvalidInboxItemError extends ForgiumError {
18
+ constructor(message) { super(message, "INVALID_INBOX_ITEM", 2); }
19
+ }
20
+ export class InvalidDraftError extends ForgiumError {
21
+ constructor(message) { super(message, "INVALID_DRAFT", 2); }
22
+ }
23
+ export class InvalidManifestError extends ForgiumError {
24
+ constructor(message) { super(message, "INVALID_MANIFEST", 2); }
25
+ }
26
+ export class DraftNotFoundError extends ForgiumError {
27
+ constructor(id) { super(`Draft not found: ${id}`, "DRAFT_NOT_FOUND", 3); }
28
+ }
29
+ export class DraftAlreadyExistsError extends ForgiumError {
30
+ constructor(id) { super(`Draft already exists: ${id}`, "DRAFT_ALREADY_EXISTS", 3); }
31
+ }
32
+ export class EditorUnavailableError extends ForgiumError {
33
+ constructor() { super("No editor configured. Set $VISUAL or $EDITOR.", "EDITOR_UNAVAILABLE", 3); }
34
+ }
35
+ export class ReceiptAlreadyExistsError extends ForgiumError {
36
+ constructor(id) { super(`Receipt already exists: ${id}`, "RECEIPT_ALREADY_EXISTS", 3); }
37
+ }
38
+ export class InvalidReceiptError extends ForgiumError {
39
+ constructor(message) { super(message, "INVALID_RECEIPT", 2); }
40
+ }
41
+ export class ReviewReceiptRequiredError extends ForgiumError {
42
+ constructor(id) { super(`Approved review receipt required before completing Feature: ${id}`, "REVIEW_RECEIPT_REQUIRED", 2); }
43
+ }
44
+ export class RunOptionsInvalidError extends ForgiumError {
45
+ constructor(message) { super(message, "RUN_OPTIONS_INVALID", 2); }
46
+ }
47
+ export class FeatureNotFoundError extends ForgiumError {
48
+ constructor(id) { super(`Feature not found: ${id}`, "FEATURE_NOT_FOUND", 3); }
49
+ }
50
+ export class FeatureAlreadyExistsError extends ForgiumError {
51
+ constructor(id) { super(`Feature already exists: ${id}`, "FEATURE_ALREADY_EXISTS", 3); }
52
+ }
53
+ export class FeatureStateConflictError extends ForgiumError {
54
+ constructor(message) { super(message, "FEATURE_STATE_CONFLICT", 3); }
55
+ }
56
+ export class InvalidStateTransitionError extends ForgiumError {
57
+ constructor(from, to) { super(`Invalid Feature transition: ${from} → ${to}`, "INVALID_STATE_TRANSITION", 3); }
58
+ }
@@ -0,0 +1,35 @@
1
+ import type { ExecutionProfile, Feature } from "../domain/types.js";
2
+ export type ExecutionOutcome = "completed" | "verification_failed" | "blocked" | "needs_human" | "cancelled";
3
+ export interface ExecutionRequest {
4
+ root: string;
5
+ feature: Feature;
6
+ profile: ExecutionProfile;
7
+ runId: string;
8
+ permissions: "repository";
9
+ signal?: AbortSignal;
10
+ }
11
+ export interface ExecutionResult {
12
+ outcome: ExecutionOutcome;
13
+ summary: string;
14
+ artifacts?: string[];
15
+ reason?: string;
16
+ }
17
+ export interface ExecutionAdapter {
18
+ readonly id: string;
19
+ supports(profile: ExecutionProfile): boolean;
20
+ isAvailable(request: ExecutionRequest): Promise<boolean>;
21
+ execute(request: ExecutionRequest): Promise<ExecutionResult>;
22
+ }
23
+ export declare class ExecutionAdapterRegistry {
24
+ private readonly adapters;
25
+ constructor(adapters?: ExecutionAdapter[]);
26
+ resolve(request: ExecutionRequest): Promise<ExecutionAdapter | null>;
27
+ }
28
+ export declare class DeterministicExecutionAdapter implements ExecutionAdapter {
29
+ private readonly result;
30
+ readonly id = "deterministic-test";
31
+ constructor(result?: ExecutionResult | ((request: ExecutionRequest) => ExecutionResult | Promise<ExecutionResult>));
32
+ supports(_profile: ExecutionProfile): boolean;
33
+ isAvailable(_request: ExecutionRequest): Promise<boolean>;
34
+ execute(request: ExecutionRequest): Promise<ExecutionResult>;
35
+ }
@@ -0,0 +1,25 @@
1
+ export class ExecutionAdapterRegistry {
2
+ adapters;
3
+ constructor(adapters = []) {
4
+ this.adapters = adapters;
5
+ }
6
+ async resolve(request) {
7
+ for (const adapter of this.adapters) {
8
+ if (adapter.supports(request.profile) && await adapter.isAvailable(request))
9
+ return adapter;
10
+ }
11
+ return null;
12
+ }
13
+ }
14
+ export class DeterministicExecutionAdapter {
15
+ result;
16
+ id = "deterministic-test";
17
+ constructor(result = { outcome: "completed", summary: "Deterministic execution completed." }) {
18
+ this.result = result;
19
+ }
20
+ supports(_profile) { return true; }
21
+ async isAvailable(_request) { return true; }
22
+ async execute(request) {
23
+ return typeof this.result === "function" ? this.result(request) : this.result;
24
+ }
25
+ }
@@ -0,0 +1,13 @@
1
+ import type { ExecutionProfile } from "../domain/types.js";
2
+ import type { ExecutionAdapter, ExecutionRequest, ExecutionResult } from "./execution-adapter.js";
3
+ export declare class PiRpcExecutionAdapter implements ExecutionAdapter {
4
+ private readonly command;
5
+ private readonly timeoutMs;
6
+ readonly id = "pi-rpc";
7
+ constructor(command?: string, timeoutMs?: number);
8
+ supports(profile: ExecutionProfile): boolean;
9
+ isAvailable(_request: ExecutionRequest): Promise<boolean>;
10
+ execute(request: ExecutionRequest): Promise<ExecutionResult>;
11
+ }
12
+ export declare function buildPiPrompt(request: ExecutionRequest): string;
13
+ export declare function parsePiResult(text: string): ExecutionResult;
@@ -0,0 +1,119 @@
1
+ import { spawn } from "node:child_process";
2
+ import { StringDecoder } from "node:string_decoder";
3
+ const RESULT_PATTERN = /FORGIUM_RESULT:\s*(completed|verification_failed|blocked|needs_human|cancelled)/i;
4
+ export class PiRpcExecutionAdapter {
5
+ command;
6
+ timeoutMs;
7
+ id = "pi-rpc";
8
+ constructor(command = process.env.FORGIUM_PI_COMMAND ?? "pi", timeoutMs = 30 * 60 * 1000) {
9
+ this.command = command;
10
+ this.timeoutMs = timeoutMs;
11
+ }
12
+ supports(profile) { return profile.kind === "direct"; }
13
+ async isAvailable(_request) {
14
+ return new Promise((resolve) => {
15
+ const child = spawn(this.command, ["--version"], { stdio: ["ignore", "ignore", "ignore"] });
16
+ child.once("error", () => resolve(false));
17
+ child.once("close", (code) => resolve(code === 0));
18
+ });
19
+ }
20
+ async execute(request) {
21
+ const child = spawn(this.command, ["--mode", "rpc", "--no-session"], { cwd: request.root, stdio: ["pipe", "pipe", "pipe"] });
22
+ const decoder = new StringDecoder("utf8");
23
+ let buffer = "";
24
+ let assistantText = "";
25
+ let settled = false;
26
+ return new Promise((resolve, reject) => {
27
+ const finish = (result) => {
28
+ if (settled)
29
+ return;
30
+ settled = true;
31
+ clearTimeout(timeout);
32
+ request.signal?.removeEventListener("abort", abort);
33
+ child.kill();
34
+ resolve(result);
35
+ };
36
+ const abort = () => {
37
+ child.stdin.write(`${JSON.stringify({ type: "abort" })}\n`);
38
+ finish({ outcome: "cancelled", summary: "Pi execution was cancelled." });
39
+ };
40
+ const timeout = setTimeout(() => finish({ outcome: "cancelled", summary: `Pi execution exceeded ${this.timeoutMs}ms.` }), this.timeoutMs);
41
+ const handleLine = (line) => {
42
+ if (!line.trim())
43
+ return;
44
+ let event;
45
+ try {
46
+ event = JSON.parse(line);
47
+ }
48
+ catch {
49
+ return;
50
+ }
51
+ if (event.type === "message_end" && event.message?.role === "assistant")
52
+ assistantText = assistantTextFrom(event.message.content);
53
+ if (event.type === "agent_end") {
54
+ const messages = event.messages ?? [];
55
+ const lastAssistant = [...messages].reverse().find((message) => message.role === "assistant");
56
+ if (lastAssistant)
57
+ assistantText = assistantTextFrom(lastAssistant.content);
58
+ }
59
+ if (event.type === "agent_settled")
60
+ finish(parsePiResult(assistantText));
61
+ };
62
+ child.stdout.on("data", (chunk) => {
63
+ buffer += decoder.write(chunk);
64
+ let newline = buffer.indexOf("\n");
65
+ while (newline >= 0) {
66
+ const line = buffer.slice(0, newline).replace(/\r$/, "");
67
+ buffer = buffer.slice(newline + 1);
68
+ handleLine(line);
69
+ newline = buffer.indexOf("\n");
70
+ }
71
+ });
72
+ child.once("error", (error) => {
73
+ if (!settled) {
74
+ clearTimeout(timeout);
75
+ reject(error);
76
+ }
77
+ });
78
+ child.once("close", (code) => {
79
+ if (!settled) {
80
+ clearTimeout(timeout);
81
+ resolve(code === 0 ? parsePiResult(assistantText) : { outcome: "needs_human", summary: `Pi exited before producing a Forgium result (code ${code ?? "unknown"}).` });
82
+ }
83
+ });
84
+ request.signal?.addEventListener("abort", abort, { once: true });
85
+ child.stdin.write(`${JSON.stringify({ id: request.runId, type: "prompt", message: buildPiPrompt(request) })}\n`);
86
+ });
87
+ }
88
+ }
89
+ export function buildPiPrompt(request) {
90
+ const profile = request.profile.kind === "direct"
91
+ ? "Implement the Feature directly."
92
+ : `Follow the repository's spec-driven workflow using ${request.profile.specPath}.`;
93
+ return [
94
+ "You are the execution engine for Forgium.",
95
+ `Work only inside repository: ${request.root}`,
96
+ `Feature: ${request.feature.id}`,
97
+ `Title: ${request.feature.manifest.title}`,
98
+ `Goal: ${request.feature.manifest.goal}`,
99
+ `Acceptance criteria:\n${request.feature.manifest.acceptance.map((criterion) => `- ${criterion}`).join("\n")}`,
100
+ profile,
101
+ "Do not edit Forgium state directories, manifests, or receipts.",
102
+ "When finished, print exactly one final line: FORGIUM_RESULT: completed, verification_failed, blocked, needs_human, or cancelled.",
103
+ "If you cannot establish completion safely, use FORGIUM_RESULT: needs_human."
104
+ ].join("\n\n");
105
+ }
106
+ export function parsePiResult(text) {
107
+ const match = text.match(RESULT_PATTERN);
108
+ if (!match)
109
+ return { outcome: "needs_human", summary: "Pi finished without a recognized Forgium result marker." };
110
+ const outcome = match[1].toLowerCase();
111
+ return { outcome, summary: `Pi reported ${outcome}.` };
112
+ }
113
+ function assistantTextFrom(content) {
114
+ if (typeof content === "string")
115
+ return content;
116
+ if (!Array.isArray(content))
117
+ return "";
118
+ return content.filter((part) => typeof part === "object" && part !== null && part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
119
+ }
@@ -0,0 +1,14 @@
1
+ import type { ExecutionProfile } from "../domain/types.js";
2
+ import type { ExecutionAdapter, ExecutionRequest, ExecutionResult } from "./execution-adapter.js";
3
+ export declare class SpecFlowExecutionAdapter implements ExecutionAdapter {
4
+ private readonly command;
5
+ private readonly timeoutMs;
6
+ readonly id = "pi-spec-flow";
7
+ constructor(command?: string, timeoutMs?: number);
8
+ supports(profile: ExecutionProfile): boolean;
9
+ isAvailable(_request: ExecutionRequest): Promise<boolean>;
10
+ execute(request: ExecutionRequest): Promise<ExecutionResult>;
11
+ }
12
+ export declare function buildSpecFlowStatusPrompt(request: ExecutionRequest): string;
13
+ export declare function buildSpecFlowSafetyPrompt(request: ExecutionRequest): string;
14
+ export declare function parseSpecFlowStatusResult(value: unknown): ExecutionResult | null;
@@ -0,0 +1,233 @@
1
+ import { spawn } from "node:child_process";
2
+ import { appendFileSync } from "node:fs";
3
+ import { StringDecoder } from "node:string_decoder";
4
+ const RESULT_PATTERN = /FORGIUM_SPEC_FLOW_RESULT:\s*(completed|blocked|needs_human|cancelled)/i;
5
+ export class SpecFlowExecutionAdapter {
6
+ command;
7
+ timeoutMs;
8
+ id = "pi-spec-flow";
9
+ constructor(command = process.env.FORGIUM_PI_COMMAND ?? "pi", timeoutMs = 30 * 60 * 1000) {
10
+ this.command = command;
11
+ this.timeoutMs = timeoutMs;
12
+ }
13
+ supports(profile) {
14
+ return profile.kind === "spec-flow";
15
+ }
16
+ async isAvailable(_request) {
17
+ return new Promise((resolve) => {
18
+ const child = spawn(this.command, ["--version"], { stdio: ["ignore", "ignore", "ignore"] });
19
+ child.once("error", () => resolve(false));
20
+ child.once("close", (code) => resolve(code === 0));
21
+ });
22
+ }
23
+ async execute(request) {
24
+ const profile = request.profile;
25
+ if (profile.kind !== "spec-flow") {
26
+ return { outcome: "needs_human", summary: "Spec Flow adapter received an unsupported execution profile." };
27
+ }
28
+ const child = spawn(this.command, [
29
+ "--mode",
30
+ "rpc",
31
+ "--no-session",
32
+ "--append-system-prompt",
33
+ buildSpecFlowSafetyPrompt(request),
34
+ ], {
35
+ cwd: request.root,
36
+ stdio: ["pipe", "pipe", "pipe"],
37
+ });
38
+ const decoder = new StringDecoder("utf8");
39
+ let buffer = "";
40
+ let assistantText = "";
41
+ let statusResult = null;
42
+ let statusRequested = false;
43
+ let settled = false;
44
+ return new Promise((resolve, reject) => {
45
+ const finish = (result) => {
46
+ if (settled)
47
+ return;
48
+ settled = true;
49
+ clearTimeout(timeout);
50
+ request.signal?.removeEventListener("abort", abort);
51
+ child.kill();
52
+ resolve(result);
53
+ };
54
+ const send = (message) => {
55
+ if (child.stdin.destroyed)
56
+ return;
57
+ child.stdin.write(`${JSON.stringify(message)}\n`);
58
+ };
59
+ const abort = () => {
60
+ send({ type: "abort" });
61
+ finish({ outcome: "cancelled", summary: "Spec Flow execution was cancelled." });
62
+ };
63
+ const timeout = setTimeout(() => finish({ outcome: "needs_human", summary: `Spec Flow execution exceeded ${this.timeoutMs}ms.` }), this.timeoutMs);
64
+ const requestStatus = () => {
65
+ if (statusRequested)
66
+ return;
67
+ statusRequested = true;
68
+ send({
69
+ id: `${request.runId}-status`,
70
+ type: "prompt",
71
+ message: buildSpecFlowStatusPrompt(request),
72
+ });
73
+ };
74
+ const handleUiRequest = (event) => {
75
+ if (!event.id || !event.method)
76
+ return;
77
+ if (event.method === "select") {
78
+ const options = Array.isArray(event.options)
79
+ ? event.options.filter((option) => typeof option === "string")
80
+ : [];
81
+ const proceed = options.find((option) => /yes,? proceed/i.test(option));
82
+ send(proceed
83
+ ? { type: "extension_ui_response", id: event.id, value: proceed }
84
+ : { type: "extension_ui_response", id: event.id, cancelled: true });
85
+ return;
86
+ }
87
+ if (event.method === "confirm") {
88
+ send({ type: "extension_ui_response", id: event.id, confirmed: false });
89
+ }
90
+ };
91
+ const handleLine = (line) => {
92
+ if (!line.trim())
93
+ return;
94
+ appendRpcDebugLine(line);
95
+ let event;
96
+ try {
97
+ event = JSON.parse(line);
98
+ }
99
+ catch {
100
+ return;
101
+ }
102
+ if (event.type === "extension_ui_request")
103
+ handleUiRequest(event);
104
+ if (event.type === "tool_execution_end" && event.toolName === "spec_flow_status") {
105
+ statusResult = parseSpecFlowStatusResult(event.result?.details);
106
+ }
107
+ if (event.type === "message_end" && event.message?.role === "assistant") {
108
+ assistantText = assistantTextFrom(event.message.content);
109
+ }
110
+ if (event.type === "agent_end") {
111
+ const messages = event.messages ?? [];
112
+ const lastAssistant = [...messages].reverse().find((message) => message.role === "assistant");
113
+ if (lastAssistant)
114
+ assistantText = assistantTextFrom(lastAssistant.content);
115
+ }
116
+ if (event.type === "agent_settled") {
117
+ if (!statusRequested) {
118
+ requestStatus();
119
+ return;
120
+ }
121
+ finish(statusResult
122
+ ?? parseSpecFlowMarker(assistantText)
123
+ ?? { outcome: "needs_human", summary: "Spec Flow finished without a structured status result." });
124
+ }
125
+ };
126
+ child.stdout.on("data", (chunk) => {
127
+ buffer += decoder.write(chunk);
128
+ let newline = buffer.indexOf("\n");
129
+ while (newline >= 0) {
130
+ const line = buffer.slice(0, newline).replace(/\r$/, "");
131
+ buffer = buffer.slice(newline + 1);
132
+ handleLine(line);
133
+ newline = buffer.indexOf("\n");
134
+ }
135
+ });
136
+ child.once("error", (error) => {
137
+ if (!settled) {
138
+ clearTimeout(timeout);
139
+ reject(error);
140
+ }
141
+ });
142
+ child.once("close", (code) => {
143
+ if (!settled) {
144
+ clearTimeout(timeout);
145
+ resolve(code === 0
146
+ ? statusResult ?? { outcome: "needs_human", summary: "Spec Flow exited without a structured status result." }
147
+ : { outcome: "needs_human", summary: `Pi exited before producing a Spec Flow status (code ${code ?? "unknown"}).` });
148
+ }
149
+ });
150
+ request.signal?.addEventListener("abort", abort, { once: true });
151
+ send({ id: request.runId, type: "prompt", message: profile.commands.implement });
152
+ });
153
+ }
154
+ }
155
+ function appendRpcDebugLine(line) {
156
+ const debugPath = process.env.FORGIUM_PI_RPC_LOG;
157
+ if (!debugPath)
158
+ return;
159
+ try {
160
+ appendFileSync(debugPath, `${line}\n`, "utf8");
161
+ }
162
+ catch {
163
+ // Diagnostics must never alter execution behaviour.
164
+ }
165
+ }
166
+ export function buildSpecFlowStatusPrompt(request) {
167
+ if (request.profile.kind !== "spec-flow")
168
+ return "";
169
+ return [
170
+ "The Spec Flow implementation command has settled.",
171
+ "Do not edit files or start another implementation block.",
172
+ `Call the read-only spec_flow_status tool with spec_path: ${JSON.stringify(request.profile.specPath)}.`,
173
+ "Use the tool result as the only source of truth.",
174
+ "If complete is true, report FORGIUM_SPEC_FLOW_RESULT: completed.",
175
+ "Otherwise report FORGIUM_SPEC_FLOW_RESULT: needs_human.",
176
+ ].join("\n");
177
+ }
178
+ export function buildSpecFlowSafetyPrompt(request) {
179
+ return [
180
+ "You are operating as a delegated Spec Flow implementation agent inside Forgium.",
181
+ "Modify only files required by the current Spec Flow ticket.",
182
+ "Never modify, move, delete, or create files under product/, features/, or .forgium/.",
183
+ "Forgium owns Feature manifests, Feature state directories, leases, and receipts.",
184
+ `The repository root is ${request.root}.`,
185
+ ].join("\n");
186
+ }
187
+ export function parseSpecFlowStatusResult(value) {
188
+ if (!value || typeof value !== "object")
189
+ return null;
190
+ const status = value;
191
+ if (typeof status.complete !== "boolean") {
192
+ return { outcome: "needs_human", summary: "Spec Flow returned an invalid status payload." };
193
+ }
194
+ if (typeof status.total !== "number" || status.total < 1) {
195
+ return { outcome: "needs_human", summary: "Spec Flow has no inspectable tickets." };
196
+ }
197
+ if (status.complete) {
198
+ return {
199
+ outcome: "completed",
200
+ summary: `Spec Flow completed ${status.done ?? status.total}/${status.total} tickets.`,
201
+ };
202
+ }
203
+ const pendingReview = status.checkpoints?.pendingReview ?? 0;
204
+ const issueSummary = status.issues?.filter(Boolean).slice(0, 3).join(" ");
205
+ const reason = pendingReview > 0
206
+ ? `${pendingReview} checkpoint review(s) are pending.`
207
+ : issueSummary || `Spec Flow has not completed all ${status.total} tickets.`;
208
+ return {
209
+ outcome: "needs_human",
210
+ summary: `Spec Flow requires human attention: ${reason}`,
211
+ reason,
212
+ };
213
+ }
214
+ function parseSpecFlowMarker(text) {
215
+ const match = text.match(RESULT_PATTERN);
216
+ if (!match)
217
+ return null;
218
+ const outcome = match[1].toLowerCase();
219
+ return { outcome, summary: `Spec Flow reported ${outcome}.` };
220
+ }
221
+ function assistantTextFrom(content) {
222
+ if (typeof content === "string")
223
+ return content;
224
+ if (!Array.isArray(content))
225
+ return "";
226
+ return content
227
+ .filter((part) => typeof part === "object"
228
+ && part !== null
229
+ && part.type === "text"
230
+ && typeof part.text === "string")
231
+ .map((part) => part.text)
232
+ .join("\n");
233
+ }
@@ -0,0 +1,11 @@
1
+ export * from "./domain/types.js";
2
+ export * from "./errors/forgium-errors.js";
3
+ export * from "./repository/filesystem-forgium-repository.js";
4
+ export * from "./repository/repo-discovery.js";
5
+ export * from "./services/id.js";
6
+ export * from "./services/spec-flow-commands.js";
7
+ export * from "./schemas/draft.schema.js";
8
+ export * from "./schemas/receipt.schema.js";
9
+ export * from "./execution/execution-adapter.js";
10
+ export * from "./execution/pi-rpc-execution-adapter.js";
11
+ export * from "./execution/spec-flow-execution-adapter.js";
@@ -0,0 +1,11 @@
1
+ export * from "./domain/types.js";
2
+ export * from "./errors/forgium-errors.js";
3
+ export * from "./repository/filesystem-forgium-repository.js";
4
+ export * from "./repository/repo-discovery.js";
5
+ export * from "./services/id.js";
6
+ export * from "./services/spec-flow-commands.js";
7
+ export * from "./schemas/draft.schema.js";
8
+ export * from "./schemas/receipt.schema.js";
9
+ export * from "./execution/execution-adapter.js";
10
+ export * from "./execution/pi-rpc-execution-adapter.js";
11
+ export * from "./execution/spec-flow-execution-adapter.js";
@@ -0,0 +1,68 @@
1
+ import type { CaptureInput, CreateFeatureInput, Draft, ExecutionProfile, Feature, FeatureState, InboxItem, Receipt, ReceiptEvidence, RepositoryStatus, ReviewDecision, ValidationReport, VerificationReceipt } from "../domain/types.js";
2
+ import type { ExecutionOutcome } from "../execution/execution-adapter.js";
3
+ import { ExecutionAdapterRegistry } from "../execution/execution-adapter.js";
4
+ export declare class FilesystemForgiumRepository {
5
+ readonly root: string;
6
+ private readonly paths;
7
+ constructor(root: string);
8
+ init(): Promise<void>;
9
+ capture(input: CaptureInput): Promise<InboxItem>;
10
+ listInbox(): Promise<InboxItem[]>;
11
+ listDrafts(): Promise<Draft[]>;
12
+ getDraft(id: string): Promise<Draft | null>;
13
+ createDraftFromInbox(id: string): Promise<Draft>;
14
+ deferInbox(id: string): Promise<InboxItem>;
15
+ mergeInbox(id: string, featureId: string): Promise<InboxItem>;
16
+ promoteDraft(id: string): Promise<Feature>;
17
+ createFeature(input: CreateFeatureInput): Promise<Feature>;
18
+ listReceipts(featureId: string): Promise<Receipt[]>;
19
+ verifyFeature(id: string, options?: {
20
+ evidence?: ReceiptEvidence[];
21
+ runId?: string;
22
+ }): Promise<VerificationReceipt>;
23
+ reviewFeature(id: string, decision: ReviewDecision, summary: string, actorName?: string): Promise<Feature>;
24
+ executeFeature(id: string, registry: ExecutionAdapterRegistry): Promise<{
25
+ outcome: ExecutionOutcome | "needs_human";
26
+ feature: Feature;
27
+ summary: string;
28
+ adapterId?: string;
29
+ }>;
30
+ listFeatures(state?: FeatureState): Promise<Feature[]>;
31
+ getFeature(id: string): Promise<Feature | null>;
32
+ getNextReady(): Promise<Feature | null>;
33
+ startFeature(id: string): Promise<Feature>;
34
+ submitForReview(id: string): Promise<Feature>;
35
+ completeFeature(id: string): Promise<Feature>;
36
+ returnToDoing(id: string): Promise<Feature>;
37
+ unblockFeature(id: string): Promise<Feature>;
38
+ blockFeature(id: string, reason: string): Promise<Feature>;
39
+ inspectExecutionMode(id: string): Promise<ExecutionProfile>;
40
+ getStatus(): Promise<RepositoryStatus>;
41
+ validate(): Promise<ValidationReport>;
42
+ private transition;
43
+ private createLease;
44
+ private requireFeature;
45
+ private requireInbox;
46
+ private requireDraft;
47
+ private assertInitialized;
48
+ private createVerificationReceipt;
49
+ private createReviewReceipt;
50
+ private createExecutionReceipt;
51
+ private createHandoffReceipt;
52
+ private receiptBase;
53
+ private persistReceipt;
54
+ private validateReceiptFiles;
55
+ private validateReceiptReferences;
56
+ private hasApprovedReviewReceipt;
57
+ private readFeature;
58
+ private readDraft;
59
+ private readInboxFile;
60
+ private writeInboxItem;
61
+ private renderDraft;
62
+ private parsePromotableDraft;
63
+ private featureIdExists;
64
+ private countDirectories;
65
+ private renderInbox;
66
+ private ensureGitignoreRuntime;
67
+ private writeFileAtomic;
68
+ }