@felixdotgo/aidlc-kit 0.1.0 → 0.1.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/README.md +16 -6
- package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +1 -1
- package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +6 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +6 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +8 -3
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +1 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +7 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +3 -2
- package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +2 -2
- package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/state.mjs +4 -0
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +2 -2
- package/dist/src/adapters.js +8 -1
- package/dist/src/cli.js +111 -95
- package/dist/src/context.js +2 -0
- package/dist/src/gate.d.ts +1 -0
- package/dist/src/gate.js +48 -35
- package/dist/src/installer.d.ts +5 -0
- package/dist/src/installer.js +116 -80
- package/dist/src/model.d.ts +1 -1
- package/dist/src/profiles.d.ts +6 -0
- package/dist/src/profiles.js +11 -0
- package/dist/src/project-migration.d.ts +1 -0
- package/dist/src/project-migration.js +7 -0
- package/dist/src/state/archive.d.ts +17 -0
- package/dist/src/state/archive.js +60 -0
- package/dist/src/state/items.d.ts +25 -0
- package/dist/src/state/items.js +146 -0
- package/dist/src/state/lessons.d.ts +7 -0
- package/dist/src/state/lessons.js +67 -0
- package/dist/src/state/lifecycle.d.ts +5 -0
- package/dist/src/state/lifecycle.js +78 -0
- package/dist/src/state/lock.d.ts +7 -0
- package/dist/src/state/lock.js +124 -0
- package/dist/src/state/memory.d.ts +8 -0
- package/dist/src/state/memory.js +88 -0
- package/dist/src/state/render.d.ts +26 -0
- package/dist/src/state/render.js +29 -0
- package/dist/src/state/shared.d.ts +29 -0
- package/dist/src/state/shared.js +71 -0
- package/dist/src/state/store.d.ts +4 -0
- package/dist/src/state/store.js +81 -0
- package/dist/src/state/transitions.d.ts +21 -0
- package/dist/src/state/transitions.js +93 -0
- package/dist/src/state/validate.d.ts +4 -0
- package/dist/src/state/validate.js +100 -0
- package/dist/src/state.d.ts +15 -117
- package/dist/src/state.js +15 -815
- package/dist/src/tracker.d.ts +7 -0
- package/dist/src/tracker.js +3 -0
- package/dist/src/upgrade-board.d.ts +5 -0
- package/dist/src/upgrade-board.js +89 -0
- package/dist/src/upgrade.d.ts +4 -5
- package/dist/src/upgrade.js +197 -214
- package/dist/src/workflow.js +1 -1
- package/docs/README.md +2 -2
- package/docs/architecture.md +4 -1
- package/docs/command-reference.md +15 -12
- package/docs/configuration.md +7 -4
- package/docs/coordination-service.md +5 -2
- package/docs/development.md +15 -1
- package/docs/operating-workflow.md +4 -1
- package/docs/testing-and-release.md +7 -7
- package/package.json +2 -2
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ArchivedTaskSummary, Gate, HandoffKind, Phase, TaskState, WorkflowState } from "../model.js";
|
|
2
|
+
export declare const statePath: (root: string) => string;
|
|
3
|
+
export declare const lessonIndexPath: (root: string) => string;
|
|
4
|
+
export declare const memoryRegistryPath: (root: string) => string;
|
|
5
|
+
export declare const stateLockPath: (root: string) => string;
|
|
6
|
+
export declare const stateLockGuardPath: (root: string) => string;
|
|
7
|
+
export declare const emptyState: () => WorkflowState;
|
|
8
|
+
export declare const legacyG2WaitSource = "schema-v4-migration";
|
|
9
|
+
export declare const phases: Phase[];
|
|
10
|
+
export declare const gates: Gate[];
|
|
11
|
+
export declare const handoffKinds: HandoffKind[];
|
|
12
|
+
export declare const terminal: (task: TaskState) => boolean;
|
|
13
|
+
export declare const phaseGate: (phase: Phase) => Gate;
|
|
14
|
+
export declare const iso: (value: unknown) => value is string;
|
|
15
|
+
export declare const text: (value: unknown) => value is string;
|
|
16
|
+
export declare const idPattern: RegExp;
|
|
17
|
+
export declare const digest: (content: string) => string;
|
|
18
|
+
export declare const archiveRecordPath: (id: string) => string;
|
|
19
|
+
export declare const taskRecordContent: (task: TaskState) => string;
|
|
20
|
+
export declare const assertNoSymlinkPath: (root: string, relative: string) => void;
|
|
21
|
+
export declare const writeAtomic: (path: string, content: string) => void;
|
|
22
|
+
export declare const parseJsonText: (content: string, label: string, recovery?: string) => unknown;
|
|
23
|
+
export declare const parseJsonFile: (path: string, label: string, recovery?: string) => unknown;
|
|
24
|
+
export declare const audited: (reason: string, source: string) => void;
|
|
25
|
+
export declare const tokens: (value: string) => string[];
|
|
26
|
+
/** Rejects artifact paths that escape the project-owned tasks directory. */
|
|
27
|
+
export declare const assertTaskArtifactPath: (id: string, path: string) => void;
|
|
28
|
+
/** Builds the immutable archive-catalog summary for a terminal task from its record content. */
|
|
29
|
+
export declare const archivedSummary: (task: TaskState, content?: string) => ArchivedTaskSummary;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
4
|
+
import { resolveProjectPathWithoutSymlinks } from "../project-path.js";
|
|
5
|
+
export const statePath = (root) => join(resolve(root), ".agents/data/state/aidlc-state.json");
|
|
6
|
+
export const lessonIndexPath = (root) => join(resolve(root), ".agents/data/lessons/index.json");
|
|
7
|
+
export const memoryRegistryPath = (root) => join(resolve(root), ".agents/data/memory/agentic-memory.json");
|
|
8
|
+
export const stateLockPath = (root) => join(resolve(root), ".agents/data/state/.aidlc-state.lock");
|
|
9
|
+
export const stateLockGuardPath = (root) => join(resolve(root), ".agents/data/state/.aidlc-state.lock.guard");
|
|
10
|
+
export const emptyState = () => ({ schemaVersion: 4, tasks: {}, archive: {} });
|
|
11
|
+
export const legacyG2WaitSource = "schema-v4-migration";
|
|
12
|
+
export const phases = ["clarify", "plan", "build", "wrap", "done"];
|
|
13
|
+
export const gates = ["none", "G0_confirm", "G1_review", "G2_codereview"];
|
|
14
|
+
export const handoffKinds = ["repair_exhausted", "review_exhausted", "g2_failed", "release_failed", "structural_change", "other"];
|
|
15
|
+
export const terminal = (task) => task.status === "done" || task.status === "closed" || task.status === "superseded";
|
|
16
|
+
export const phaseGate = (phase) => phase === "clarify" ? "G0_confirm" : phase === "plan" ? "G1_review" : phase === "build" ? "G2_codereview" : "none";
|
|
17
|
+
export const iso = (value) => typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(Date.parse(value)).toISOString() === value;
|
|
18
|
+
export const text = (value) => typeof value === "string" && Boolean(value.trim());
|
|
19
|
+
export const idPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
20
|
+
export const digest = (content) => createHash("sha256").update(content).digest("hex");
|
|
21
|
+
export const archiveRecordPath = (id) => `.agents/data/state/archive/${id}.json`;
|
|
22
|
+
export const taskRecordContent = (task) => `${JSON.stringify(task, null, 2)}\n`;
|
|
23
|
+
export const assertNoSymlinkPath = (root, relative) => {
|
|
24
|
+
resolveProjectPathWithoutSymlinks(root, relative, "Unsafe state path", "State path crosses a symlink");
|
|
25
|
+
};
|
|
26
|
+
export const writeAtomic = (path, content) => {
|
|
27
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
28
|
+
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
29
|
+
writeFileSync(temporary, content, "utf8");
|
|
30
|
+
renameSync(temporary, path);
|
|
31
|
+
};
|
|
32
|
+
export const parseJsonText = (content, label, recovery = "") => {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(content);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new Error(`${label} is corrupted or unreadable: ${error instanceof Error ? error.message : String(error)}${recovery ? `; ${recovery}` : ""}`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export const parseJsonFile = (path, label, recovery = "") => parseJsonText(readFileSync(path, "utf8"), label, recovery);
|
|
41
|
+
export const audited = (reason, source) => {
|
|
42
|
+
if (!text(reason) || !text(source))
|
|
43
|
+
throw new Error("Lifecycle mutation requires non-empty --reason and --source");
|
|
44
|
+
};
|
|
45
|
+
export const tokens = (value) => [...new Set(value.toLocaleLowerCase().split(/[^\p{L}\p{N}]+/u).filter((item) => item.length > 1))];
|
|
46
|
+
/** Rejects artifact paths that escape the project-owned tasks directory. */
|
|
47
|
+
export const assertTaskArtifactPath = (id, path) => {
|
|
48
|
+
const separator = process.platform === "win32" ? "\\" : "/";
|
|
49
|
+
const cleaned = normalize(path);
|
|
50
|
+
if (isAbsolute(cleaned) || cleaned === ".." || cleaned.startsWith(`..${separator}`) || !cleaned.startsWith(`.agents${separator}data${separator}tasks${separator}`)) {
|
|
51
|
+
throw new Error(`Invalid artifact path for ${id}: ${path}`);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/** Builds the immutable archive-catalog summary for a terminal task from its record content. */
|
|
55
|
+
export const archivedSummary = (task, content = taskRecordContent(task)) => ({
|
|
56
|
+
id: task.id,
|
|
57
|
+
title: task.title,
|
|
58
|
+
type: task.type,
|
|
59
|
+
phase: task.phase,
|
|
60
|
+
gate: task.gate,
|
|
61
|
+
status: task.status,
|
|
62
|
+
risk: task.risk,
|
|
63
|
+
areas: task.areas,
|
|
64
|
+
record: archiveRecordPath(task.id),
|
|
65
|
+
digest: digest(content),
|
|
66
|
+
lessonCount: task.lessons?.length ?? 0,
|
|
67
|
+
predecessorTaskId: task.predecessorTaskId,
|
|
68
|
+
successorTaskId: task.successorTaskId,
|
|
69
|
+
createdAt: task.createdAt,
|
|
70
|
+
updatedAt: task.updatedAt
|
|
71
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TaskState, WorkflowState } from "../model.js";
|
|
2
|
+
export declare const loadState: (root: string) => WorkflowState;
|
|
3
|
+
export declare const loadTask: (root: string, id: string, state?: WorkflowState) => TaskState | undefined;
|
|
4
|
+
export declare const saveState: (root: string, state: WorkflowState) => string[];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { validateState, validateTask } from "./validate.js";
|
|
4
|
+
import { assertNoSymlinkPath, digest, emptyState, parseJsonFile, parseJsonText, statePath, taskRecordContent, terminal, writeAtomic, archivedSummary } from "./shared.js";
|
|
5
|
+
export const loadState = (root) => {
|
|
6
|
+
const path = statePath(root);
|
|
7
|
+
assertNoSymlinkPath(root, ".agents/data/state/aidlc-state.json");
|
|
8
|
+
return existsSync(path) ? validateState(parseJsonFile(path, "AI-DLC state file", "restore it from git history or the archive records")) : emptyState();
|
|
9
|
+
};
|
|
10
|
+
export const loadTask = (root, id, state = loadState(root)) => {
|
|
11
|
+
if (state.tasks[id])
|
|
12
|
+
return state.tasks[id];
|
|
13
|
+
const summary = state.archive?.[id];
|
|
14
|
+
if (!summary)
|
|
15
|
+
return undefined;
|
|
16
|
+
const path = join(resolve(root), summary.record);
|
|
17
|
+
assertNoSymlinkPath(root, summary.record);
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
throw new Error(`Archived task record is missing: ${summary.record}`);
|
|
20
|
+
const content = readFileSync(path, "utf8");
|
|
21
|
+
if (digest(content) !== summary.digest)
|
|
22
|
+
throw new Error(`Archived task digest mismatch: ${id}`);
|
|
23
|
+
const task = parseJsonText(content, `Archived task record ${id}`);
|
|
24
|
+
validateTask(id, task);
|
|
25
|
+
if (!terminal(task) || task.status !== summary.status || task.updatedAt !== summary.updatedAt || (task.lessons?.length ?? 0) !== summary.lessonCount)
|
|
26
|
+
throw new Error(`Archived task summary mismatch: ${id}`);
|
|
27
|
+
return task;
|
|
28
|
+
};
|
|
29
|
+
const archiveTerminalTasks = (normalized, persisted, root) => {
|
|
30
|
+
const archived = [];
|
|
31
|
+
normalized.archive ??= {};
|
|
32
|
+
for (const task of Object.values(normalized.tasks)) {
|
|
33
|
+
if (!terminal(task))
|
|
34
|
+
continue;
|
|
35
|
+
const content = taskRecordContent(task);
|
|
36
|
+
const summary = archivedSummary(task, content);
|
|
37
|
+
const path = join(resolve(root), summary.record);
|
|
38
|
+
assertNoSymlinkPath(root, summary.record);
|
|
39
|
+
if (existsSync(path)) {
|
|
40
|
+
const current = readFileSync(path, "utf8");
|
|
41
|
+
if (digest(current) !== summary.digest)
|
|
42
|
+
commitRecoveryRecordOrThrow(task, content, path, current, persisted);
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
writeAtomic(path, content);
|
|
46
|
+
normalized.archive[task.id] = summary;
|
|
47
|
+
delete normalized.tasks[task.id];
|
|
48
|
+
archived.push(task.id);
|
|
49
|
+
}
|
|
50
|
+
return archived;
|
|
51
|
+
};
|
|
52
|
+
/** Replaces an interrupted-write orphan only when the persisted catalog still proves the same active task identity. */
|
|
53
|
+
const commitRecoveryRecordOrThrow = (task, content, path, current, persisted) => {
|
|
54
|
+
let orphan;
|
|
55
|
+
try {
|
|
56
|
+
orphan = JSON.parse(current);
|
|
57
|
+
validateTask(task.id, orphan);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error(`Archived task record conflicts: ${task.id}`);
|
|
61
|
+
}
|
|
62
|
+
const authority = persisted?.tasks[task.id];
|
|
63
|
+
const identityHolds = authority && authority.createdAt === task.createdAt;
|
|
64
|
+
const orphanMatches = terminal(orphan) && orphan.id === task.id && orphan.createdAt === task.createdAt;
|
|
65
|
+
if (persisted?.archive?.[task.id] || !identityHolds || !orphanMatches)
|
|
66
|
+
throw new Error(`Archived task record conflicts: ${task.id}`);
|
|
67
|
+
writeAtomic(path, content);
|
|
68
|
+
};
|
|
69
|
+
export const saveState = (root, state) => {
|
|
70
|
+
const normalized = validateState(structuredClone(state));
|
|
71
|
+
const persisted = existsSync(statePath(root)) ? loadState(root) : undefined;
|
|
72
|
+
const archived = normalized.schemaVersion >= 3 ? archiveTerminalTasks(normalized, persisted, root) : [];
|
|
73
|
+
if (normalized.schemaVersion >= 3)
|
|
74
|
+
validateState(normalized);
|
|
75
|
+
assertNoSymlinkPath(root, ".agents/data/state/aidlc-state.json");
|
|
76
|
+
writeAtomic(statePath(root), `${JSON.stringify(normalized, null, 2)}\n`);
|
|
77
|
+
state.tasks = normalized.tasks;
|
|
78
|
+
state.archive = normalized.archive;
|
|
79
|
+
state.schemaVersion = normalized.schemaVersion;
|
|
80
|
+
return archived;
|
|
81
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Diagnostic, Gate, Phase, TaskState, WorkflowState } from "../model.js";
|
|
2
|
+
export declare const latestApproval: (task: TaskState, gate: Gate) => import("../model.js").Evidence | undefined;
|
|
3
|
+
/** Evidence index where the currently effective Approve plan approval starts; undefined when none exists. */
|
|
4
|
+
export declare const buildBoundary: (task: TaskState) => number | undefined;
|
|
5
|
+
export declare const hasApproval: (task: TaskState, gate: Gate) => boolean;
|
|
6
|
+
export declare const hasCurrentBuildApproval: (task: TaskState, gate: Gate) => boolean;
|
|
7
|
+
export declare const hasAreaVerification: (task: TaskState, area: string) => boolean;
|
|
8
|
+
export declare const hasVerification: (task: TaskState) => boolean;
|
|
9
|
+
export declare const hasReview: (task: TaskState) => boolean;
|
|
10
|
+
export interface RepairBounds {
|
|
11
|
+
verifyFailures: Record<string, number>;
|
|
12
|
+
reviewFailures: number;
|
|
13
|
+
exhaustedAreas: string[];
|
|
14
|
+
verifyExhausted: boolean;
|
|
15
|
+
reviewExhausted: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare const repairBounds: (task: TaskState) => RepairBounds;
|
|
18
|
+
/** True only for a migration-stamped legacy G2 wait that is still fully ready; the stamp is written exclusively by schema migration. */
|
|
19
|
+
export declare const isLegacyG2Wait: (task: TaskState) => boolean;
|
|
20
|
+
export declare const transitionDiagnostics: (task: TaskState, target: Phase) => Diagnostic[];
|
|
21
|
+
export declare const transitionTask: (state: WorkflowState, id: string, target: Phase) => TaskState;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { phases, phaseGate, terminal } from "./shared.js";
|
|
2
|
+
export const latestApproval = (task, gate) => {
|
|
3
|
+
for (let index = task.evidence.length - 1; index >= 0; index -= 1) {
|
|
4
|
+
const item = task.evidence[index];
|
|
5
|
+
if (item.kind === "approval" && item.gate === gate)
|
|
6
|
+
return item;
|
|
7
|
+
}
|
|
8
|
+
return undefined;
|
|
9
|
+
};
|
|
10
|
+
/** Evidence index where the currently effective Approve plan approval starts; undefined when none exists. */
|
|
11
|
+
export const buildBoundary = (task) => {
|
|
12
|
+
let boundary;
|
|
13
|
+
for (let index = 0; index < task.evidence.length; index += 1) {
|
|
14
|
+
const item = task.evidence[index];
|
|
15
|
+
if (item.kind === "approval" && item.gate === "G1_review" && item.result === "pass")
|
|
16
|
+
boundary = index;
|
|
17
|
+
}
|
|
18
|
+
return boundary;
|
|
19
|
+
};
|
|
20
|
+
export const hasApproval = (task, gate) => latestApproval(task, gate)?.result === "pass";
|
|
21
|
+
const currentBuildEvidence = (task) => {
|
|
22
|
+
const boundary = buildBoundary(task);
|
|
23
|
+
return boundary === undefined ? [] : task.evidence.slice(boundary + 1);
|
|
24
|
+
};
|
|
25
|
+
const latestInOrder = (items, predicate) => {
|
|
26
|
+
for (let index = items.length - 1; index >= 0; index -= 1)
|
|
27
|
+
if (predicate(items[index]))
|
|
28
|
+
return items[index];
|
|
29
|
+
return undefined;
|
|
30
|
+
};
|
|
31
|
+
export const hasCurrentBuildApproval = (task, gate) => latestInOrder(currentBuildEvidence(task), (item) => item.kind === "approval" && item.gate === gate)?.result === "pass";
|
|
32
|
+
export const hasAreaVerification = (task, area) => {
|
|
33
|
+
const evidence = currentBuildEvidence(task).filter((item) => (item.kind === "test" || item.kind === "lint") && (item.area === area || (!item.area && task.areas.length === 1)));
|
|
34
|
+
const kinds = [...new Set(evidence.map((item) => item.kind))];
|
|
35
|
+
return kinds.length > 0 && kinds.every((kind) => latestInOrder(evidence, (item) => item.kind === kind)?.result === "pass");
|
|
36
|
+
};
|
|
37
|
+
export const hasVerification = (task) => task.areas.every((area) => hasAreaVerification(task, area));
|
|
38
|
+
export const hasReview = (task) => latestInOrder(currentBuildEvidence(task), (item) => item.kind === "review")?.result === "pass";
|
|
39
|
+
export const repairBounds = (task) => {
|
|
40
|
+
const evidence = currentBuildEvidence(task);
|
|
41
|
+
const verifyFailures = {};
|
|
42
|
+
for (const item of evidence)
|
|
43
|
+
if (["test", "lint"].includes(item.kind) && item.result === "fail") {
|
|
44
|
+
const area = item.area ?? (task.areas.length === 1 ? task.areas[0] : "unscoped");
|
|
45
|
+
verifyFailures[area] = (verifyFailures[area] ?? 0) + 1;
|
|
46
|
+
}
|
|
47
|
+
const reviewFailures = evidence.filter((item) => item.kind === "review" && item.result === "fail").length;
|
|
48
|
+
const exhaustedAreas = Object.keys(verifyFailures).filter((area) => verifyFailures[area] >= 3).sort();
|
|
49
|
+
return { verifyFailures, reviewFailures, exhaustedAreas, verifyExhausted: exhaustedAreas.length > 0, reviewExhausted: reviewFailures >= 2 };
|
|
50
|
+
};
|
|
51
|
+
/** True only for a migration-stamped legacy G2 wait that is still fully ready; the stamp is written exclusively by schema migration. */
|
|
52
|
+
export const isLegacyG2Wait = (task) => task.legacyG2Wait !== undefined && task.phase === "build" && task.gate === "G2_codereview" && task.status === "blocked_on_user" && task.tasks.every((item) => item.status === "done" || item.status === "deferred") && hasVerification(task) && hasReview(task);
|
|
53
|
+
export const transitionDiagnostics = (task, target) => {
|
|
54
|
+
const diagnostics = [];
|
|
55
|
+
if (target === task.phase || phases.indexOf(target) !== phases.indexOf(task.phase) + 1)
|
|
56
|
+
diagnostics.push({ level: "ERROR", code: "STATE_TRANSITION", message: `Cannot transition ${task.phase} → ${target}` });
|
|
57
|
+
if (target === "plan" && !hasApproval(task, "G0_confirm"))
|
|
58
|
+
diagnostics.push({ level: "ERROR", code: "G0_APPROVAL", message: "G0 approval evidence is required" });
|
|
59
|
+
if (target === "build") {
|
|
60
|
+
if (!hasApproval(task, "G1_review"))
|
|
61
|
+
diagnostics.push({ level: "ERROR", code: "G1_APPROVAL", message: "G1 approval evidence is required" });
|
|
62
|
+
for (const decision of task.decisions)
|
|
63
|
+
if (decision.status === "unresolved")
|
|
64
|
+
diagnostics.push({ level: "ERROR", code: "UNRESOLVED_DECISION", message: `Decision ${decision.id} is unresolved` });
|
|
65
|
+
}
|
|
66
|
+
if (target === "wrap") {
|
|
67
|
+
if (!hasVerification(task))
|
|
68
|
+
diagnostics.push({ level: "ERROR", code: "VERIFY_EVIDENCE", message: "Latest current-build verification evidence must pass for every affected area" });
|
|
69
|
+
if (!hasReview(task))
|
|
70
|
+
diagnostics.push({ level: "ERROR", code: "REVIEW_EVIDENCE", message: "Latest current-build code-review evidence must pass" });
|
|
71
|
+
if (!hasCurrentBuildApproval(task, "G2_codereview"))
|
|
72
|
+
diagnostics.push({ level: "ERROR", code: "G2_APPROVAL", message: "Current-build G2 approval evidence is required" });
|
|
73
|
+
}
|
|
74
|
+
if (target === "done" && !task.lessonDisposition)
|
|
75
|
+
diagnostics.push({ level: "ERROR", code: "LESSON_DISPOSITION", message: "Wrap must record lessons or an explicit no-lesson disposition" });
|
|
76
|
+
return diagnostics;
|
|
77
|
+
};
|
|
78
|
+
export const transitionTask = (state, id, target) => {
|
|
79
|
+
const task = state.tasks[id];
|
|
80
|
+
if (!task)
|
|
81
|
+
throw new Error(`Unknown active task: ${id}`);
|
|
82
|
+
if (terminal(task))
|
|
83
|
+
throw new Error(`Terminal task cannot transition: ${id}`);
|
|
84
|
+
const errors = transitionDiagnostics(task, target).filter((item) => item.level === "ERROR");
|
|
85
|
+
if (errors.length)
|
|
86
|
+
throw new Error(errors.map((item) => item.message).join("; "));
|
|
87
|
+
task.phase = target;
|
|
88
|
+
task.gate = phaseGate(target);
|
|
89
|
+
task.status = target === "done" ? "done" : "active";
|
|
90
|
+
delete task.legacyG2Wait;
|
|
91
|
+
task.updatedAt = new Date().toISOString();
|
|
92
|
+
return task;
|
|
93
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { LessonRecord, TaskState, WorkflowState } from "../model.js";
|
|
2
|
+
export declare const validateLesson: (taskId: string, lesson: LessonRecord) => void;
|
|
3
|
+
export declare const validateTask: (id: string, task: TaskState) => void;
|
|
4
|
+
export declare const validateState: (value: unknown) => WorkflowState;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readyTaskItems } from "./items.js";
|
|
2
|
+
import { assertTaskArtifactPath, gates, handoffKinds, idPattern, iso, phases, phaseGate, text } from "./shared.js";
|
|
3
|
+
const taskStatuses = ["active", "blocked_on_user", "paused", "done", "closed", "superseded"];
|
|
4
|
+
export const validateLesson = (taskId, lesson) => {
|
|
5
|
+
if (!lesson || !idPattern.test(lesson.id) || lesson.taskId !== taskId || !Array.isArray(lesson.areas) || !lesson.areas.length || lesson.areas.some((area) => !text(area)) || !text(lesson.summary) || !text(lesson.prevention) || !text(lesson.example) || !text(lesson.promotion) || !text(lesson.source) || !iso(lesson.recordedAt))
|
|
6
|
+
throw new Error(`Invalid lesson: ${taskId}`);
|
|
7
|
+
};
|
|
8
|
+
export const validateTask = (id, task) => {
|
|
9
|
+
if (!idPattern.test(id))
|
|
10
|
+
throw new Error(`Invalid task id: ${id}`);
|
|
11
|
+
if (!task || task.id !== id || !phases.includes(task.phase) || !gates.includes(task.gate) || task.gate !== phaseGate(task.phase))
|
|
12
|
+
throw new Error(`Invalid task state: ${id}`);
|
|
13
|
+
if (!Array.isArray(task.decisions) || !Array.isArray(task.tasks) || !Array.isArray(task.evidence) || (task.lessons !== undefined && !Array.isArray(task.lessons)))
|
|
14
|
+
throw new Error(`Invalid task collections: ${id}`);
|
|
15
|
+
if (!task.title || !["feature", "bug", "refactor", "infra"].includes(task.type) || !taskStatuses.includes(task.status) || (task.phase === "done") !== (task.status === "done"))
|
|
16
|
+
throw new Error(`Invalid task fields: ${id}`);
|
|
17
|
+
if (!["vi", "en"].includes(task.language) || !["low", "normal", "high", "regulated"].includes(task.risk) || !Array.isArray(task.areas) || !task.areas.length || task.areas.some((area) => !text(area)))
|
|
18
|
+
throw new Error(`Invalid task language, risk, or areas: ${id}`);
|
|
19
|
+
if (task.decisions.some((item) => !item.id || !item.label || !["unresolved", "approved", "changed", "dropped"].includes(item.status) || (item.status === "changed" && !item.resolution)))
|
|
20
|
+
throw new Error(`Invalid decisions: ${id}`);
|
|
21
|
+
if (task.featureId !== undefined && !idPattern.test(task.featureId))
|
|
22
|
+
throw new Error(`Invalid feature id: ${id}`);
|
|
23
|
+
if (task.tasks.some((item) => !item.id || !item.label || !["todo", "in_progress", "done", "deferred"].includes(item.status)))
|
|
24
|
+
throw new Error(`Invalid execution tasks: ${id}`);
|
|
25
|
+
readyTaskItems(task);
|
|
26
|
+
if (task.evidence.some((item) => !["approval", "spec", "test", "lint", "review", "diagnostic"].includes(item.kind) || !["pass", "fail", "skip"].includes(item.result) || !item.source || !iso(item.recordedAt) || (item.area !== undefined && !text(item.area)) || (item.gate !== undefined && !gates.includes(item.gate)) || (item.kind === "approval" && (!item.gate || item.gate === "none"))))
|
|
27
|
+
throw new Error(`Invalid evidence: ${id}`);
|
|
28
|
+
const lessonIds = new Set();
|
|
29
|
+
for (const lesson of task.lessons ?? []) {
|
|
30
|
+
validateLesson(id, lesson);
|
|
31
|
+
if (lessonIds.has(lesson.id))
|
|
32
|
+
throw new Error(`Duplicate lesson id: ${id}/${lesson.id}`);
|
|
33
|
+
lessonIds.add(lesson.id);
|
|
34
|
+
}
|
|
35
|
+
if (task.lessonDisposition && (!["captured", "none"].includes(task.lessonDisposition.status) || !text(task.lessonDisposition.source) || !iso(task.lessonDisposition.recordedAt) || (task.lessonDisposition.status === "captured" && !(task.lessons?.length)) || (task.lessonDisposition.status === "none" && (Boolean(task.lessons?.length) || !text(task.lessonDisposition.reason)))))
|
|
36
|
+
throw new Error(`Invalid lesson disposition: ${id}`);
|
|
37
|
+
if (task.handoff && (!handoffKinds.includes(task.handoff.kind) || !text(task.handoff.reason) || !text(task.handoff.source) || !iso(task.handoff.recordedAt) || task.status !== "paused"))
|
|
38
|
+
throw new Error(`Invalid task handoff: ${id}`);
|
|
39
|
+
if (task.legacyG2Wait !== undefined && (typeof task.legacyG2Wait !== "object" || Array.isArray(task.legacyG2Wait) || !iso(task.legacyG2Wait.migratedAt) || !text(task.legacyG2Wait.source) || task.phase !== "build" || task.gate !== "G2_codereview" || task.status !== "blocked_on_user"))
|
|
40
|
+
throw new Error(`Invalid legacy G2 wait marker: ${id}`);
|
|
41
|
+
if ((task.status === "closed" || task.status === "superseded") !== Boolean(task.closure))
|
|
42
|
+
throw new Error(`Invalid task closure: ${id}`);
|
|
43
|
+
if (task.closure && (!text(task.closure.reason) || !text(task.closure.source) || !iso(task.closure.recordedAt)))
|
|
44
|
+
throw new Error(`Invalid task closure: ${id}`);
|
|
45
|
+
if ((task.status === "superseded") !== Boolean(task.successorTaskId) || (task.status === "closed" && task.successorTaskId))
|
|
46
|
+
throw new Error(`Invalid task successor: ${id}`);
|
|
47
|
+
if (task.predecessorTaskId !== undefined && !text(task.predecessorTaskId))
|
|
48
|
+
throw new Error(`Invalid task predecessor: ${id}`);
|
|
49
|
+
for (const path of Object.values(task.artifacts))
|
|
50
|
+
if (path)
|
|
51
|
+
assertTaskArtifactPath(id, path);
|
|
52
|
+
};
|
|
53
|
+
const validateArchiveSummary = (id, item) => {
|
|
54
|
+
if (!idPattern.test(id) || !item || item.id !== id || !text(item.title) || !["feature", "bug", "refactor", "infra"].includes(item.type) || !phases.includes(item.phase) || !gates.includes(item.gate) || item.gate !== phaseGate(item.phase) || !["done", "closed", "superseded"].includes(item.status) || !["low", "normal", "high", "regulated"].includes(item.risk) || !Array.isArray(item.areas) || !item.areas.length || item.areas.some((area) => !text(area)) || item.record !== `.agents/data/state/archive/${id}.json` || !/^[a-f0-9]{64}$/.test(item.digest) || !Number.isInteger(item.lessonCount) || item.lessonCount < 0 || !iso(item.createdAt) || !iso(item.updatedAt))
|
|
55
|
+
throw new Error(`Invalid archived task summary: ${id}`);
|
|
56
|
+
if ((item.status === "superseded") !== Boolean(item.successorTaskId) || (item.status === "closed" && item.successorTaskId) || (item.predecessorTaskId !== undefined && !text(item.predecessorTaskId)))
|
|
57
|
+
throw new Error(`Invalid archived task links: ${id}`);
|
|
58
|
+
};
|
|
59
|
+
export const validateState = (value) => {
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
61
|
+
throw new Error("Workflow state must be an object");
|
|
62
|
+
const state = value;
|
|
63
|
+
if (![1, 2, 3, 4].includes(Number(state.schemaVersion)) || !state.tasks || typeof state.tasks !== "object" || Array.isArray(state.tasks))
|
|
64
|
+
throw new Error("Unsupported workflow state schema");
|
|
65
|
+
if (Number(state.schemaVersion) >= 3 && (!state.archive || typeof state.archive !== "object" || Array.isArray(state.archive)))
|
|
66
|
+
throw new Error("Schema v3+ requires an archive catalog");
|
|
67
|
+
if (Number(state.schemaVersion) < 3 && state.archive !== undefined)
|
|
68
|
+
throw new Error("Legacy workflow state cannot contain an archive catalog");
|
|
69
|
+
if (Number(state.schemaVersion) < 4 && Object.values(state.tasks).some((task) => task?.legacyG2Wait !== undefined))
|
|
70
|
+
throw new Error("Legacy G2 wait markers require schema v4");
|
|
71
|
+
for (const [id, task] of Object.entries(state.tasks))
|
|
72
|
+
validateTask(id, task);
|
|
73
|
+
for (const [id, item] of Object.entries(state.archive ?? {})) {
|
|
74
|
+
if (state.tasks[id])
|
|
75
|
+
throw new Error(`Task exists in active and archive state: ${id}`);
|
|
76
|
+
validateArchiveSummary(id, item);
|
|
77
|
+
}
|
|
78
|
+
const references = { ...(state.archive ?? {}), ...state.tasks };
|
|
79
|
+
for (const [id, item] of Object.entries(references)) {
|
|
80
|
+
if (item.successorTaskId) {
|
|
81
|
+
const successor = references[item.successorTaskId];
|
|
82
|
+
if (!successor || successor.id === id || successor.predecessorTaskId !== id)
|
|
83
|
+
throw new Error(`Invalid successor link: ${id}`);
|
|
84
|
+
}
|
|
85
|
+
if (item.predecessorTaskId) {
|
|
86
|
+
const predecessor = references[item.predecessorTaskId];
|
|
87
|
+
if (!predecessor || predecessor.id === id || predecessor.status !== "superseded" || predecessor.successorTaskId !== id)
|
|
88
|
+
throw new Error(`Invalid predecessor link: ${id}`);
|
|
89
|
+
}
|
|
90
|
+
const seen = new Set([id]);
|
|
91
|
+
let cursor = item;
|
|
92
|
+
while (cursor.successorTaskId) {
|
|
93
|
+
if (seen.has(cursor.successorTaskId))
|
|
94
|
+
throw new Error(`Cyclic successor link: ${id}`);
|
|
95
|
+
seen.add(cursor.successorTaskId);
|
|
96
|
+
cursor = references[cursor.successorTaskId];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return state;
|
|
100
|
+
};
|
package/dist/src/state.d.ts
CHANGED
|
@@ -1,117 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export
|
|
16
|
-
export declare const validateMemoryRegistry: (value: unknown) => AgenticMemoryRegistry;
|
|
17
|
-
export declare const loadMemoryRegistry: (root: string) => AgenticMemoryRegistry;
|
|
18
|
-
export declare const saveMemoryRegistry: (root: string, registry: AgenticMemoryRegistry) => void;
|
|
19
|
-
export declare const selectAgenticMemory: (registry: AgenticMemoryRegistry, areas: string[], phase: Exclude<Phase, "done">, maxChars: number) => AgenticMemoryEntry[];
|
|
20
|
-
export declare const promoteAgenticMemory: (root: string, entry: AgenticMemoryEntry) => AgenticMemoryEntry;
|
|
21
|
-
export declare const retireAgenticMemory: (root: string, id: string, reason: string, retiredBy: string, retiredAt?: string) => AgenticMemoryEntry;
|
|
22
|
-
export declare const validateState: (value: unknown) => WorkflowState;
|
|
23
|
-
/** Marks pre-existing ready G2 waits during migration; the marker is the only exemption from repair-bound enforcement. */
|
|
24
|
-
export declare const stampLegacyG2Waits: (state: WorkflowState) => string[];
|
|
25
|
-
export declare const prepareV4Migration: (value: WorkflowState) => {
|
|
26
|
-
state: WorkflowState;
|
|
27
|
-
records: Array<{
|
|
28
|
-
path: string;
|
|
29
|
-
content: string;
|
|
30
|
-
}>;
|
|
31
|
-
lessonIndex: LessonIndex;
|
|
32
|
-
stamped: string[];
|
|
33
|
-
};
|
|
34
|
-
export declare const migrateState: (root: string, state?: WorkflowState) => {
|
|
35
|
-
migrated: boolean;
|
|
36
|
-
archived: string[];
|
|
37
|
-
stamped: string[];
|
|
38
|
-
};
|
|
39
|
-
export declare const loadState: (root: string) => WorkflowState;
|
|
40
|
-
export declare const loadTask: (root: string, id: string, state?: WorkflowState) => TaskState | undefined;
|
|
41
|
-
export declare const saveState: (root: string, state: WorkflowState) => string[];
|
|
42
|
-
export declare const latestApproval: (task: TaskState, gate: Gate) => import("./model.js").Evidence | undefined;
|
|
43
|
-
export declare const buildBoundary: (task: TaskState) => number | undefined;
|
|
44
|
-
export declare const hasApproval: (task: TaskState, gate: Gate) => boolean;
|
|
45
|
-
export declare const hasCurrentBuildApproval: (task: TaskState, gate: Gate) => boolean;
|
|
46
|
-
export declare const hasAreaVerification: (task: TaskState, area: string) => boolean;
|
|
47
|
-
export declare const hasVerification: (task: TaskState) => boolean;
|
|
48
|
-
export declare const hasReview: (task: TaskState) => boolean;
|
|
49
|
-
export interface RepairBounds {
|
|
50
|
-
verifyFailures: Record<string, number>;
|
|
51
|
-
reviewFailures: number;
|
|
52
|
-
exhaustedAreas: string[];
|
|
53
|
-
verifyExhausted: boolean;
|
|
54
|
-
reviewExhausted: boolean;
|
|
55
|
-
}
|
|
56
|
-
export declare const repairBounds: (task: TaskState) => RepairBounds;
|
|
57
|
-
/** True only for a migration-stamped legacy G2 wait that is still fully ready; the stamp is written exclusively by schema migration. */
|
|
58
|
-
export declare const isLegacyG2Wait: (task: TaskState) => boolean;
|
|
59
|
-
export declare const transitionDiagnostics: (task: TaskState, target: Phase) => Diagnostic[];
|
|
60
|
-
export declare const transitionTask: (state: WorkflowState, id: string, target: Phase) => TaskState;
|
|
61
|
-
export interface NextAction {
|
|
62
|
-
classification: "await_user" | "run_phase" | "blocked" | "terminal" | "complete";
|
|
63
|
-
phase: Phase;
|
|
64
|
-
gate: Gate;
|
|
65
|
-
command?: string;
|
|
66
|
-
outcome?: "closed" | "superseded";
|
|
67
|
-
successorTaskId?: string;
|
|
68
|
-
itemId?: string;
|
|
69
|
-
remainingItems?: number;
|
|
70
|
-
actions?: Array<{
|
|
71
|
-
id: "reopen_g1" | "create_successor" | "supersede" | "close" | "record_handoff" | "resume";
|
|
72
|
-
command: string;
|
|
73
|
-
}>;
|
|
74
|
-
reason: string;
|
|
75
|
-
}
|
|
76
|
-
export declare const readyTaskItems: (task: TaskState) => {
|
|
77
|
-
ready: string[];
|
|
78
|
-
blocked: Array<{
|
|
79
|
-
id: string;
|
|
80
|
-
waitingOn: string[];
|
|
81
|
-
}>;
|
|
82
|
-
};
|
|
83
|
-
export declare const nextAction: (task: TaskState, root?: string) => NextAction;
|
|
84
|
-
export declare const handoffTask: (state: WorkflowState, id: string, kind: HandoffKind, reason: string, source: string, recordedAt?: string) => TaskState;
|
|
85
|
-
export declare const closeTask: (state: WorkflowState, id: string, reason: string, source: string, recordedAt?: string) => TaskState;
|
|
86
|
-
export declare const supersedeTask: (state: WorkflowState, id: string, successorId: string, reason: string, source: string, recordedAt?: string) => TaskState;
|
|
87
|
-
export declare const reopenTask: (state: WorkflowState, id: string, target: "plan", reason: string, source: string, recordedAt?: string) => TaskState;
|
|
88
|
-
export declare const recordLesson: (task: TaskState, lesson: Omit<LessonRecord, "taskId" | "recordedAt">, recordedAt?: string) => LessonRecord;
|
|
89
|
-
export declare const recordNoLessons: (task: TaskState, reason: string, source: string, recordedAt?: string) => void;
|
|
90
|
-
export declare const lessonStateDigest: (state: WorkflowState) => string;
|
|
91
|
-
export declare const createLessonIndex: (lessons: LessonRecord[], sourceDigest?: string) => LessonIndex;
|
|
92
|
-
export declare const rebuildLessonIndex: (root: string, state?: WorkflowState) => LessonIndex;
|
|
93
|
-
export declare const searchLessons: (root: string, state: WorkflowState, query: string, areas?: string[], limit?: number) => LessonIndexEntry[];
|
|
94
|
-
export interface TaskSummary {
|
|
95
|
-
id: string;
|
|
96
|
-
title: string;
|
|
97
|
-
phase: Phase;
|
|
98
|
-
gate: Gate;
|
|
99
|
-
status: TaskState["status"];
|
|
100
|
-
risk: TaskState["risk"];
|
|
101
|
-
areas: string[];
|
|
102
|
-
updatedAt: string;
|
|
103
|
-
source: "active" | "archive";
|
|
104
|
-
}
|
|
105
|
-
export declare const listTaskSummaries: (state: WorkflowState, options?: {
|
|
106
|
-
includeArchive?: boolean;
|
|
107
|
-
statuses?: string[];
|
|
108
|
-
query?: string;
|
|
109
|
-
limit?: number;
|
|
110
|
-
cursor?: number;
|
|
111
|
-
}) => {
|
|
112
|
-
items: TaskSummary[];
|
|
113
|
-
nextCursor: string | null;
|
|
114
|
-
total: number;
|
|
115
|
-
};
|
|
116
|
-
export declare const renderWorkplan: (task: TaskState) => string;
|
|
117
|
-
export declare const renderViews: (root: string, state?: WorkflowState, selected?: TaskState[]) => void;
|
|
1
|
+
/**
|
|
2
|
+
* Facade for the lifecycle-state domain. Behavior-preserving re-export of the
|
|
3
|
+
* split modules under `src/state/`; import paths from outside remain unchanged.
|
|
4
|
+
*/
|
|
5
|
+
export * from "./state/shared.js";
|
|
6
|
+
export * from "./state/lock.js";
|
|
7
|
+
export * from "./state/items.js";
|
|
8
|
+
export * from "./state/transitions.js";
|
|
9
|
+
export * from "./state/validate.js";
|
|
10
|
+
export * from "./state/store.js";
|
|
11
|
+
export * from "./state/memory.js";
|
|
12
|
+
export * from "./state/lessons.js";
|
|
13
|
+
export * from "./state/archive.js";
|
|
14
|
+
export * from "./state/lifecycle.js";
|
|
15
|
+
export * from "./state/render.js";
|