@felixdotgo/aidlc-kit 0.1.1 → 0.3.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.
- package/DEVELOPMENT.md +14 -13
- package/README.md +37 -43
- package/dist/assets/.agents/aidlc/docker/Dockerfile.claude +7 -0
- package/dist/assets/.agents/aidlc/docker/Dockerfile.codex +9 -0
- package/dist/assets/.agents/aidlc/docker/Dockerfile.opencode +8 -0
- package/dist/assets/.agents/aidlc/orchestrator.md +2 -2
- package/dist/assets/.agents/aidlc/phase-clarify.md +1 -1
- package/dist/assets/.agents/aidlc/phase-plan.md +2 -2
- package/dist/assets/.agents/aidlc/schemas/config.schema.json +2 -1
- package/dist/assets/.agents/aidlc/schemas/state.schema.json +2 -0
- package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +94 -48
- package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -4
- package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +261 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +206 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +100 -14
- package/dist/assets/.agents/aidlc/scripts/lib/loop-env.mjs +279 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +105 -32
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +17 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +24 -7
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +42 -16
- package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +20 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-holder.mjs +376 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +33 -7
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +67 -61
- package/dist/assets/.agents/aidlc/scripts/lib/loop-leases.mjs +268 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +116 -28
- package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +114 -12
- package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +117 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +16 -56
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +149 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +44 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-log.mjs +158 -0
- package/dist/assets/.agents/aidlc/scripts/lib/repo-head.mjs +22 -0
- package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +68 -2
- package/dist/assets/.agents/aidlc/scripts/lib/task-runtime-migrate.mjs +75 -0
- package/dist/assets/.agents/aidlc/scripts/lib/task-runtime.mjs +97 -0
- package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +168 -73
- package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
- package/dist/assets/.agents/aidlc/scripts/loop.cmd +101 -0
- package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
- package/dist/assets/.agents/aidlc/scripts/loop.sh +71 -0
- package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +10 -4
- package/dist/assets/.agents/aidlc/scripts/state.mjs +16 -8
- package/dist/assets/.agents/aidlc/templates/design.md +9 -0
- package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
- package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
- package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
- package/dist/assets/ui/bulma.min.css +3 -0
- package/dist/assets/ui/changes.js +83 -0
- package/dist/assets/ui/common.js +664 -0
- package/dist/assets/ui/dashboard.css +636 -0
- package/dist/assets/ui/dashboard.js +77 -0
- package/dist/assets/ui/detail.js +159 -0
- package/dist/assets/ui/live-log.js +332 -0
- package/dist/services/aidlc-coordination/Dockerfile +11 -3
- package/dist/services/aidlc-coordination/compose.yaml +12 -0
- package/dist/services/aidlc-coordination/package-lock.json +3 -1
- package/dist/services/aidlc-coordination/package.json +9 -4
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +68 -2
- package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
- package/dist/services/aidlc-coordination/src/server.mjs +9 -3
- package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
- package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
- package/dist/src/adapters.d.ts +0 -3
- package/dist/src/adapters.js +3 -5
- package/dist/src/cli/argv.d.ts +12 -0
- package/dist/src/cli/argv.js +68 -0
- package/dist/src/cli/commands.d.ts +8 -0
- package/dist/src/cli/commands.js +159 -0
- package/dist/src/cli/loop-launcher.d.ts +47 -0
- package/dist/src/cli/loop-launcher.js +322 -0
- package/dist/src/cli/options.d.ts +25 -0
- package/dist/src/cli/options.js +32 -0
- package/dist/src/cli/prompt.d.ts +9 -0
- package/dist/src/cli/prompt.js +66 -0
- package/dist/src/cli.js +28 -191
- package/dist/src/gate.js +3 -2
- package/dist/src/installer.d.ts +14 -7
- package/dist/src/installer.js +42 -143
- package/dist/src/legacy.js +1 -0
- package/dist/src/managed-content.d.ts +7 -0
- package/dist/src/managed-content.js +15 -4
- package/dist/src/managed-planning.d.ts +65 -0
- package/dist/src/managed-planning.js +219 -0
- package/dist/src/mcp-setup.js +3 -1
- package/dist/src/model/config-contracts.d.ts +65 -0
- package/dist/src/model/config-contracts.js +1 -0
- package/dist/src/model/install-contracts.d.ts +29 -0
- package/dist/src/model/install-contracts.js +1 -0
- package/dist/src/model/lifecycle-enums.d.ts +13 -0
- package/dist/src/model/lifecycle-enums.js +1 -0
- package/dist/src/model/loop.d.ts +20 -0
- package/dist/src/model/loop.js +1 -0
- package/dist/src/model/manifest.d.ts +16 -0
- package/dist/src/model/manifest.js +1 -0
- package/dist/src/model/records.d.ts +142 -0
- package/dist/src/model/records.js +1 -0
- package/dist/src/model.d.ts +11 -264
- package/dist/src/model.js +1 -1
- package/dist/src/profiles.d.ts +2 -0
- package/dist/src/profiles.js +53 -9
- package/dist/src/state/archive-record.d.ts +9 -0
- package/dist/src/state/archive-record.js +26 -0
- package/dist/src/state/commands.d.ts +18 -0
- package/dist/src/state/commands.js +27 -0
- package/dist/src/state/domain.d.ts +9 -0
- package/dist/src/state/domain.js +9 -0
- package/dist/src/state/guards.d.ts +7 -0
- package/dist/src/state/guards.js +17 -0
- package/dist/src/state/io.d.ts +10 -0
- package/dist/src/state/io.js +35 -0
- package/dist/src/state/items.d.ts +3 -4
- package/dist/src/state/items.js +58 -57
- package/dist/src/state/lessons.js +14 -7
- package/dist/src/state/lifecycle.js +2 -2
- package/dist/src/state/lock.js +7 -6
- package/dist/src/state/memory.js +24 -12
- package/dist/src/state/{archive.js → migrate.js} +7 -7
- package/dist/src/state/partition.d.ts +15 -0
- package/dist/src/state/partition.js +90 -0
- package/dist/src/state/paths.d.ts +11 -0
- package/dist/src/state/paths.js +12 -0
- package/dist/src/state/render.js +9 -6
- package/dist/src/state/store.js +9 -15
- package/dist/src/state/transitions.js +9 -7
- package/dist/src/state/validate.js +55 -7
- package/dist/src/state.d.ts +16 -11
- package/dist/src/state.js +16 -11
- package/dist/src/task-ui.d.ts +33 -0
- package/dist/src/task-ui.js +33 -0
- package/dist/src/ui/assets.d.ts +14 -0
- package/dist/src/ui/assets.js +45 -0
- package/dist/src/ui/changes.d.ts +83 -0
- package/dist/src/ui/changes.js +498 -0
- package/dist/src/ui/containers.d.ts +71 -0
- package/dist/src/ui/containers.js +214 -0
- package/dist/src/ui/daemon.d.ts +75 -0
- package/dist/src/ui/daemon.js +232 -0
- package/dist/src/ui/data.d.ts +42 -0
- package/dist/src/ui/data.js +89 -0
- package/dist/src/ui/docs.d.ts +13 -0
- package/dist/src/ui/docs.js +39 -0
- package/dist/src/ui/files.d.ts +3 -0
- package/dist/src/ui/files.js +20 -0
- package/dist/src/ui/http.d.ts +14 -0
- package/dist/src/ui/http.js +80 -0
- package/dist/src/ui/live.d.ts +5 -0
- package/dist/src/ui/live.js +43 -0
- package/dist/src/ui/markdown.d.ts +2 -0
- package/dist/src/ui/markdown.js +127 -0
- package/dist/src/ui/options.d.ts +48 -0
- package/dist/src/ui/options.js +81 -0
- package/dist/src/ui/security.d.ts +34 -0
- package/dist/src/ui/security.js +38 -0
- package/dist/src/ui/server.d.ts +39 -0
- package/dist/src/ui/server.js +347 -0
- package/dist/src/ui/sse.d.ts +14 -0
- package/dist/src/ui/sse.js +281 -0
- package/dist/src/ui/views/board.d.ts +7 -0
- package/dist/src/ui/views/board.js +131 -0
- package/dist/src/ui/views/changes.d.ts +19 -0
- package/dist/src/ui/views/changes.js +154 -0
- package/dist/src/ui/views/components.d.ts +56 -0
- package/dist/src/ui/views/components.js +113 -0
- package/dist/src/ui/views/docs.d.ts +15 -0
- package/dist/src/ui/views/docs.js +64 -0
- package/dist/src/ui/views/layout.d.ts +44 -0
- package/dist/src/ui/views/layout.js +233 -0
- package/dist/src/ui/views/task.d.ts +25 -0
- package/dist/src/ui/views/task.js +320 -0
- package/dist/src/upgrade-board.js +10 -10
- package/dist/src/upgrade.d.ts +0 -1
- package/dist/src/upgrade.js +24 -85
- package/dist/src/workflow.d.ts +4 -0
- package/dist/src/workflow.js +36 -4
- package/docs/README.md +41 -22
- package/docs/architecture.md +101 -37
- package/docs/command-reference.md +289 -52
- package/docs/configuration.md +70 -30
- package/docs/coordination-service.md +91 -30
- package/docs/design-decisions.md +87 -0
- package/docs/development.md +124 -49
- package/docs/loop-local-run.md +228 -0
- package/docs/loop-runbook.md +214 -0
- package/docs/operating-workflow.md +55 -42
- package/docs/testing-and-release.md +72 -49
- package/docs/troubleshooting.md +221 -0
- package/package.json +4 -1
- package/dist/src/ledger.d.ts +0 -22
- package/dist/src/ledger.js +0 -119
- package/dist/src/state/shared.d.ts +0 -29
- package/dist/src/state/shared.js +0 -71
- package/dist/src/tracker.d.ts +0 -36
- package/dist/src/tracker.js +0 -44
- /package/dist/src/state/{archive.d.ts → migrate.d.ts} +0 -0
package/dist/src/profiles.js
CHANGED
|
@@ -33,10 +33,15 @@ export const validateLoopConfig = (value) => {
|
|
|
33
33
|
if (value === undefined)
|
|
34
34
|
return { mode: "off" };
|
|
35
35
|
const item = object(value, "config.loop");
|
|
36
|
-
if (Object.keys(item).some((key) => !["mode", "agents"].includes(key)) || typeof item.mode !== "string" || !["off", "local", "fleet"].includes(item.mode))
|
|
36
|
+
if (Object.keys(item).some((key) => !["mode", "agents", "workerImage", "pollMs", "limits"].includes(key)) || typeof item.mode !== "string" || !["off", "local", "fleet"].includes(item.mode))
|
|
37
37
|
throw new Error("config.loop requires a valid mode and only supported fields");
|
|
38
|
+
const workerImage = validateWorkerImageMap(item.workerImage);
|
|
39
|
+
if (item.pollMs !== undefined && (!Number.isSafeInteger(item.pollMs) || Number(item.pollMs) < 1_000 || Number(item.pollMs) > 300_000))
|
|
40
|
+
throw new Error("config.loop.pollMs must be an integer from 1000 to 300000");
|
|
41
|
+
const limits = validateLoopLimits(item.limits);
|
|
42
|
+
const optional = { ...(workerImage ? { workerImage } : {}), ...(item.pollMs !== undefined ? { pollMs: Number(item.pollMs) } : {}), ...(limits ? { limits } : {}) };
|
|
38
43
|
if (item.agents === undefined)
|
|
39
|
-
return { mode: item.mode };
|
|
44
|
+
return { mode: item.mode, ...optional };
|
|
40
45
|
const agents = object(item.agents, "config.loop.agents");
|
|
41
46
|
if (Object.keys(agents).some((key) => !["lead", "workers"].includes(key)) || !isLoopProvider(agents.lead))
|
|
42
47
|
throw new Error(`config.loop.agents.lead must be ${loopProviderNames}`);
|
|
@@ -44,7 +49,34 @@ export const validateLoopConfig = (value) => {
|
|
|
44
49
|
for (const [id, provider] of Object.entries(workers))
|
|
45
50
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id) || !isLoopProvider(provider))
|
|
46
51
|
throw new Error(`config.loop.agents.workers must map worker IDs to ${loopProviderNames}`);
|
|
47
|
-
return { mode: item.mode, agents: { lead: agents.lead, workers: workers } };
|
|
52
|
+
return { mode: item.mode, agents: { lead: agents.lead, workers: workers }, ...optional };
|
|
53
|
+
};
|
|
54
|
+
/** Bounds mirror `validateLimits` in assets/aidlc/scripts/lib/loop-runtime.mjs. */
|
|
55
|
+
const LOOP_LIMIT_BOUNDS = { maxTurns: 1_000, turnTimeoutMs: 3_600_000, maxStallTurns: 20 };
|
|
56
|
+
const validateLoopLimits = (value) => {
|
|
57
|
+
if (value === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
const map = object(value, "config.loop.limits");
|
|
60
|
+
const result = {};
|
|
61
|
+
for (const [key, entry] of Object.entries(map)) {
|
|
62
|
+
if (!Object.hasOwn(LOOP_LIMIT_BOUNDS, key))
|
|
63
|
+
throw new Error(`config.loop.limits supports only ${Object.keys(LOOP_LIMIT_BOUNDS).join(", ")}`);
|
|
64
|
+
const max = LOOP_LIMIT_BOUNDS[key];
|
|
65
|
+
if (!Number.isSafeInteger(entry) || Number(entry) < 1 || Number(entry) > max)
|
|
66
|
+
throw new Error(`config.loop.limits.${key} must be an integer from 1 to ${max}`);
|
|
67
|
+
result[key] = Number(entry);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
71
|
+
const validateWorkerImageMap = (value) => {
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
const map = object(value, "config.loop.workerImage");
|
|
75
|
+
for (const [provider, tag] of Object.entries(map)) {
|
|
76
|
+
if (!isLoopProvider(provider) || typeof tag !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._/:@-]{0,255}$/.test(tag))
|
|
77
|
+
throw new Error(`config.loop.workerImage must map ${loopProviderNames} to image tags`);
|
|
78
|
+
}
|
|
79
|
+
return map;
|
|
48
80
|
};
|
|
49
81
|
const strings = (value, label) => {
|
|
50
82
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
|
|
@@ -127,6 +159,7 @@ export const validateProfile = (value, label = "profile") => {
|
|
|
127
159
|
};
|
|
128
160
|
};
|
|
129
161
|
export const defaultConfig = () => ({
|
|
162
|
+
$schema: "./aidlc/schemas/config.schema.json",
|
|
130
163
|
schemaVersion: 2,
|
|
131
164
|
extends: ["topology/single"],
|
|
132
165
|
specs: { roots: [] },
|
|
@@ -138,12 +171,9 @@ export const defaultConfig = () => ({
|
|
|
138
171
|
mcp: { enabled: false },
|
|
139
172
|
loop: { mode: "off" }
|
|
140
173
|
});
|
|
141
|
-
/**
|
|
142
|
-
export const
|
|
143
|
-
const
|
|
144
|
-
if (!existsSync(path))
|
|
145
|
-
return defaultConfig();
|
|
146
|
-
const item = object(JSON.parse(readFileSync(path, "utf8")), ".agents/config.json");
|
|
174
|
+
/** Validates and normalizes a parsed `.agents/config.json` value. */
|
|
175
|
+
export const validateProjectConfig = (value) => {
|
|
176
|
+
const item = object(value, ".agents/config.json");
|
|
147
177
|
const risk = object(item.risk ?? {}, "config.risk");
|
|
148
178
|
const context = object(item.context ?? {}, "config.context");
|
|
149
179
|
const levels = ["low", "normal", "high", "regulated"];
|
|
@@ -159,7 +189,14 @@ export const loadProjectConfig = (root) => {
|
|
|
159
189
|
const maxChars = Number(context.maxChars ?? 16_000);
|
|
160
190
|
if (!Number.isInteger(maxChars) || maxChars < 4_000 || maxChars > 128_000)
|
|
161
191
|
throw new Error("context.maxChars must be an integer from 4000 to 128000");
|
|
192
|
+
let schema;
|
|
193
|
+
if (item.$schema !== undefined) {
|
|
194
|
+
if (typeof item.$schema !== "string" || item.$schema.length > 512)
|
|
195
|
+
throw new Error("config.$schema must be a string of at most 512 characters");
|
|
196
|
+
schema = item.$schema;
|
|
197
|
+
}
|
|
162
198
|
return {
|
|
199
|
+
$schema: schema,
|
|
163
200
|
schemaVersion: Number(item.schemaVersion),
|
|
164
201
|
extends: strings(item.extends ?? ["topology/single"], "config.extends"),
|
|
165
202
|
specs: { roots: strings(object(item.specs ?? {}, "config.specs").roots ?? [], "config.specs.roots").map((entry) => safeRelative(entry, "config.specs.roots")) },
|
|
@@ -173,6 +210,13 @@ export const loadProjectConfig = (root) => {
|
|
|
173
210
|
loop: validateLoopConfig(item.loop)
|
|
174
211
|
};
|
|
175
212
|
};
|
|
213
|
+
/** Loads and validates `.agents/config.json`, falling back to the default config when absent. */
|
|
214
|
+
export const loadProjectConfig = (root) => {
|
|
215
|
+
const path = join(resolve(root), ".agents/config.json");
|
|
216
|
+
if (!existsSync(path))
|
|
217
|
+
return defaultConfig();
|
|
218
|
+
return validateProjectConfig(JSON.parse(readFileSync(path, "utf8")));
|
|
219
|
+
};
|
|
176
220
|
const readLocalProfile = (root, id) => {
|
|
177
221
|
const path = join(resolve(root), ".agents/project/profiles", safeRelative(id, "profile id"), "profile.json");
|
|
178
222
|
if (!existsSync(path))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ArchivedTaskSummary, TaskState } from "../model.js";
|
|
2
|
+
export declare const taskRecordContent: (task: TaskState) => string;
|
|
3
|
+
/** Builds the immutable archive-catalog summary for a terminal task from its record content. */
|
|
4
|
+
export declare const archivedSummary: (task: TaskState, content?: string) => ArchivedTaskSummary;
|
|
5
|
+
/** Single terminal-to-archive policy shared by auto-archive on save and schema migration. */
|
|
6
|
+
export declare const buildArchiveRecord: (task: TaskState) => {
|
|
7
|
+
content: string;
|
|
8
|
+
summary: ArchivedTaskSummary;
|
|
9
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { digest } from "./domain.js";
|
|
2
|
+
import { archiveRecordPath } from "./paths.js";
|
|
3
|
+
export const taskRecordContent = (task) => `${JSON.stringify(task, null, 2)}\n`;
|
|
4
|
+
/** Builds the immutable archive-catalog summary for a terminal task from its record content. */
|
|
5
|
+
export const archivedSummary = (task, content = taskRecordContent(task)) => ({
|
|
6
|
+
id: task.id,
|
|
7
|
+
title: task.title,
|
|
8
|
+
type: task.type,
|
|
9
|
+
phase: task.phase,
|
|
10
|
+
gate: task.gate,
|
|
11
|
+
status: task.status,
|
|
12
|
+
risk: task.risk,
|
|
13
|
+
areas: task.areas,
|
|
14
|
+
record: archiveRecordPath(task.id),
|
|
15
|
+
digest: digest(content),
|
|
16
|
+
lessonCount: task.lessons?.length ?? 0,
|
|
17
|
+
predecessorTaskId: task.predecessorTaskId,
|
|
18
|
+
successorTaskId: task.successorTaskId,
|
|
19
|
+
createdAt: task.createdAt,
|
|
20
|
+
updatedAt: task.updatedAt
|
|
21
|
+
});
|
|
22
|
+
/** Single terminal-to-archive policy shared by auto-archive on save and schema migration. */
|
|
23
|
+
export const buildArchiveRecord = (task) => {
|
|
24
|
+
const content = taskRecordContent(task);
|
|
25
|
+
return { content, summary: archivedSummary(task, content) };
|
|
26
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TaskState } from "../model.js";
|
|
2
|
+
export type NextActionActions = Array<{
|
|
3
|
+
id: "reopen_g1" | "create_successor" | "supersede" | "close" | "record_handoff" | "resume";
|
|
4
|
+
command: string;
|
|
5
|
+
}>;
|
|
6
|
+
export interface CommandKit {
|
|
7
|
+
continueItem: (taskId: string, itemId: string) => string;
|
|
8
|
+
continueVerification: (taskId: string) => string;
|
|
9
|
+
continuePhase: (taskId: string, phase: string) => string;
|
|
10
|
+
recoverG1Wait: (taskId: string) => string;
|
|
11
|
+
recoverGatelessWrap: (taskId: string) => string;
|
|
12
|
+
approveGate: (taskId: string, gate: string) => string;
|
|
13
|
+
handoffActions: (task: TaskState) => NextActionActions;
|
|
14
|
+
pausedActions: (taskId: string) => NextActionActions;
|
|
15
|
+
repairHandoffAction: (taskId: string, kind: "review_exhausted" | "repair_exhausted") => NextActionActions;
|
|
16
|
+
}
|
|
17
|
+
/** Command-string synthesis for `nextAction`: script paths, `--root` suffixes, and CLI placeholders exist only here. */
|
|
18
|
+
export declare const commandKit: (root?: string) => CommandKit;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
/** Command-string synthesis for `nextAction`: script paths, `--root` suffixes, and CLI placeholders exist only here. */
|
|
3
|
+
export const commandKit = (root) => {
|
|
4
|
+
const suffix = root === undefined ? "" : ` --root ${JSON.stringify(resolve(root))}`;
|
|
5
|
+
const script = (name) => root === undefined ? `node .agents/aidlc/scripts/${name}` : `node ${JSON.stringify(join(resolve(root), ".agents/aidlc/scripts", name))}`;
|
|
6
|
+
const stateScript = script("state.mjs");
|
|
7
|
+
const contextScript = script("context.mjs");
|
|
8
|
+
return {
|
|
9
|
+
continueItem: (taskId, itemId) => `${contextScript} ${taskId} --phase build --item ${itemId}${suffix}`,
|
|
10
|
+
continueVerification: (taskId) => `${contextScript} ${taskId} --phase build${suffix}`,
|
|
11
|
+
continuePhase: (taskId, phase) => `${contextScript} ${taskId} --phase ${phase}${suffix}`,
|
|
12
|
+
recoverG1Wait: (taskId) => `${contextScript} ${taskId} --phase plan${suffix}`,
|
|
13
|
+
recoverGatelessWrap: (taskId) => `${stateScript} task status ${taskId} --status active${suffix}`,
|
|
14
|
+
approveGate: (taskId, gate) => `${stateScript} gate approve ${taskId} --gate ${gate} --source <explicit-user-approval>${suffix}`,
|
|
15
|
+
handoffActions: (task) => [
|
|
16
|
+
...(task.phase === "build" ? [{ id: "reopen_g1", command: `${stateScript} task reopen ${task.id} --to plan --reason <reason> --source <explicit-user-request>${suffix}` }] : []),
|
|
17
|
+
{ id: "create_successor", command: `${stateScript} task create <new-task-id> --title <title>${suffix}` },
|
|
18
|
+
{ id: "supersede", command: `${stateScript} task supersede ${task.id} --successor <new-task-id> --reason <reason> --source <explicit-user-request>${suffix}` },
|
|
19
|
+
{ id: "close", command: `${stateScript} task close ${task.id} --reason <reason> --source <explicit-user-request>${suffix}` }
|
|
20
|
+
],
|
|
21
|
+
pausedActions: (taskId) => [
|
|
22
|
+
{ id: "resume", command: `${stateScript} task status ${taskId} --status active${suffix}` },
|
|
23
|
+
{ id: "record_handoff", command: `${stateScript} task handoff ${taskId} --kind <kind> --reason <reason> --source <source>${suffix}` }
|
|
24
|
+
],
|
|
25
|
+
repairHandoffAction: (taskId, kind) => [{ id: "record_handoff", command: `${stateScript} task handoff ${taskId} --kind ${kind} --reason <summary-of-exhausted-repairs> --source <agent>${suffix}` }]
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Gate, HandoffKind, Phase, TaskState, WorkflowState } from "../model.js";
|
|
2
|
+
export declare const emptyState: () => WorkflowState;
|
|
3
|
+
export declare const legacyG2WaitSource = "schema-v4-migration";
|
|
4
|
+
export declare const phases: Phase[];
|
|
5
|
+
export declare const gates: Gate[];
|
|
6
|
+
export declare const handoffKinds: HandoffKind[];
|
|
7
|
+
export declare const terminal: (task: TaskState) => boolean;
|
|
8
|
+
export declare const phaseGate: (phase: Phase) => Gate;
|
|
9
|
+
export declare const digest: (content: string) => string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const emptyState = () => ({ schemaVersion: 4, tasks: {}, archive: {} });
|
|
3
|
+
export const legacyG2WaitSource = "schema-v4-migration";
|
|
4
|
+
export const phases = ["clarify", "plan", "build", "wrap", "done"];
|
|
5
|
+
export const gates = ["none", "G0_confirm", "G1_review", "G2_codereview"];
|
|
6
|
+
export const handoffKinds = ["repair_exhausted", "review_exhausted", "g2_failed", "release_failed", "structural_change", "other"];
|
|
7
|
+
export const terminal = (task) => task.status === "done" || task.status === "closed" || task.status === "superseded";
|
|
8
|
+
export const phaseGate = (phase) => phase === "clarify" ? "G0_confirm" : phase === "plan" ? "G1_review" : phase === "build" ? "G2_codereview" : "none";
|
|
9
|
+
export const digest = (content) => createHash("sha256").update(content).digest("hex");
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const iso: (value: unknown) => value is string;
|
|
2
|
+
export declare const text: (value: unknown) => value is string;
|
|
3
|
+
export declare const idPattern: RegExp;
|
|
4
|
+
export declare const audited: (reason: string, source: string) => void;
|
|
5
|
+
export declare const tokens: (value: string) => string[];
|
|
6
|
+
/** Rejects artifact paths that escape the project-owned tasks directory. */
|
|
7
|
+
export declare const assertTaskArtifactPath: (id: string, path: string) => void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isAbsolute, normalize } from "node:path";
|
|
2
|
+
export const iso = (value) => typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(Date.parse(value)).toISOString() === value;
|
|
3
|
+
export const text = (value) => typeof value === "string" && Boolean(value.trim());
|
|
4
|
+
export const idPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
5
|
+
export const audited = (reason, source) => {
|
|
6
|
+
if (!text(reason) || !text(source))
|
|
7
|
+
throw new Error("Lifecycle mutation requires non-empty --reason and --source");
|
|
8
|
+
};
|
|
9
|
+
export const tokens = (value) => [...new Set(value.toLocaleLowerCase().split(/[^\p{L}\p{N}]+/u).filter((item) => item.length > 1))];
|
|
10
|
+
/** Rejects artifact paths that escape the project-owned tasks directory. */
|
|
11
|
+
export const assertTaskArtifactPath = (id, path) => {
|
|
12
|
+
const separator = process.platform === "win32" ? "\\" : "/";
|
|
13
|
+
const cleaned = normalize(path);
|
|
14
|
+
if (isAbsolute(cleaned) || cleaned === ".." || cleaned.startsWith(`..${separator}`) || !cleaned.startsWith(`.agents${separator}data${separator}tasks${separator}`)) {
|
|
15
|
+
throw new Error(`Invalid artifact path for ${id}: ${path}`);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const assertNoSymlinkPath: (root: string, relative: string) => void;
|
|
2
|
+
export declare const writeAtomic: (path: string, content: string) => void;
|
|
3
|
+
export declare const parseJsonText: (content: string, label: string, recovery?: string) => unknown;
|
|
4
|
+
export declare const parseJsonFile: (path: string, label: string, recovery?: string) => unknown;
|
|
5
|
+
/** Resolves a project-relative state path to its absolute form, rejecting symlink-crossing variants first. */
|
|
6
|
+
export declare const safeResolvedPath: (root: string, relative: string) => string;
|
|
7
|
+
/** Loads and validates a canonical JSON state file, or produces the fallback when the file does not exist. */
|
|
8
|
+
export declare const loadJsonOr: <T>(root: string, relative: string, validate: (value: unknown) => T, fallback: () => T, label: string, recovery?: string) => T;
|
|
9
|
+
/** Persists a JSON state file atomically in the canonical 2-space, trailing-newline formatting. */
|
|
10
|
+
export declare const saveJson: (root: string, relative: string, value: unknown) => void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { resolveProjectPathWithoutSymlinks } from "../project-path.js";
|
|
4
|
+
export const assertNoSymlinkPath = (root, relative) => {
|
|
5
|
+
resolveProjectPathWithoutSymlinks(root, relative, "Unsafe state path", "State path crosses a symlink");
|
|
6
|
+
};
|
|
7
|
+
export const writeAtomic = (path, content) => {
|
|
8
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
9
|
+
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
10
|
+
writeFileSync(temporary, content, "utf8");
|
|
11
|
+
renameSync(temporary, path);
|
|
12
|
+
};
|
|
13
|
+
export const parseJsonText = (content, label, recovery = "") => {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(content);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw new Error(`${label} is corrupted or unreadable: ${error instanceof Error ? error.message : String(error)}${recovery ? `; ${recovery}` : ""}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export const parseJsonFile = (path, label, recovery = "") => parseJsonText(readFileSync(path, "utf8"), label, recovery);
|
|
22
|
+
/** Resolves a project-relative state path to its absolute form, rejecting symlink-crossing variants first. */
|
|
23
|
+
export const safeResolvedPath = (root, relative) => {
|
|
24
|
+
assertNoSymlinkPath(root, relative);
|
|
25
|
+
return join(resolve(root), relative);
|
|
26
|
+
};
|
|
27
|
+
/** Loads and validates a canonical JSON state file, or produces the fallback when the file does not exist. */
|
|
28
|
+
export const loadJsonOr = (root, relative, validate, fallback, label, recovery = "") => {
|
|
29
|
+
const path = safeResolvedPath(root, relative);
|
|
30
|
+
return existsSync(path) ? validate(parseJsonFile(path, label, recovery)) : fallback();
|
|
31
|
+
};
|
|
32
|
+
/** Persists a JSON state file atomically in the canonical 2-space, trailing-newline formatting. */
|
|
33
|
+
export const saveJson = (root, relative, value) => {
|
|
34
|
+
writeAtomic(safeResolvedPath(root, relative), `${JSON.stringify(value, null, 2)}\n`);
|
|
35
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Gate, Phase, TaskState } from "../model.js";
|
|
2
|
+
import { type NextActionActions } from "./commands.js";
|
|
2
3
|
export interface NextAction {
|
|
3
4
|
classification: "await_user" | "run_phase" | "blocked" | "terminal" | "complete";
|
|
4
5
|
phase: Phase;
|
|
@@ -8,10 +9,7 @@ export interface NextAction {
|
|
|
8
9
|
successorTaskId?: string;
|
|
9
10
|
itemId?: string;
|
|
10
11
|
remainingItems?: number;
|
|
11
|
-
actions?:
|
|
12
|
-
id: "reopen_g1" | "create_successor" | "supersede" | "close" | "record_handoff" | "resume";
|
|
13
|
-
command: string;
|
|
14
|
-
}>;
|
|
12
|
+
actions?: NextActionActions;
|
|
15
13
|
reason: string;
|
|
16
14
|
}
|
|
17
15
|
/** Validates execution dependencies and reports which pending items are ready to dispatch. */
|
|
@@ -22,4 +20,5 @@ export declare const readyTaskItems: (task: TaskState) => {
|
|
|
22
20
|
waitingOn: string[];
|
|
23
21
|
}>;
|
|
24
22
|
};
|
|
23
|
+
/** Classifies the next lifecycle action for a task and attaches root-qualified command strings. */
|
|
25
24
|
export declare const nextAction: (task: TaskState, root?: string) => NextAction;
|
package/dist/src/state/items.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { isAbsolute
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { commandKit } from "./commands.js";
|
|
2
3
|
import { hasReview, hasVerification, isLegacyG2Wait, repairBounds } from "./transitions.js";
|
|
3
|
-
const rootSuffix = (root) => root === undefined ? "" : ` --root ${JSON.stringify(resolve(root))}`;
|
|
4
|
-
const script = (root, name) => root === undefined ? `node .agents/aidlc/scripts/${name}` : `node ${JSON.stringify(join(resolve(root), ".agents/aidlc/scripts", name))}`;
|
|
5
4
|
const isPending = (item) => item.status === "in_progress" || item.status === "todo";
|
|
6
5
|
const pendingItems = (task) => task.tasks.filter(isPending);
|
|
7
6
|
/** Validates execution dependencies and reports which pending items are ready to dispatch. */
|
|
@@ -28,7 +27,15 @@ export const readyTaskItems = (task) => {
|
|
|
28
27
|
for (const item of task.tasks) {
|
|
29
28
|
if (item.dependsOn !== undefined && (!Array.isArray(item.dependsOn) || new Set(item.dependsOn).size !== item.dependsOn.length || item.dependsOn.some((dep) => typeof dep !== "string" || !dep)))
|
|
30
29
|
throw new Error(`Invalid execution dependencies: ${task.id}/${item.id}`);
|
|
31
|
-
if (item.ownedPaths !== undefined && (!Array.isArray(item.ownedPaths) ||
|
|
30
|
+
if (item.ownedPaths !== undefined && (!Array.isArray(item.ownedPaths) ||
|
|
31
|
+
new Set(item.ownedPaths).size !== item.ownedPaths.length ||
|
|
32
|
+
item.ownedPaths.some((path) => typeof path !== "string" ||
|
|
33
|
+
!path ||
|
|
34
|
+
isAbsolute(path) ||
|
|
35
|
+
path === ".." ||
|
|
36
|
+
path.startsWith("../") ||
|
|
37
|
+
path.includes("\\") ||
|
|
38
|
+
path.split("/").includes(".."))))
|
|
32
39
|
throw new Error(`Invalid owned paths: ${task.id}/${item.id}`);
|
|
33
40
|
visit(item.id);
|
|
34
41
|
}
|
|
@@ -52,50 +59,27 @@ const dispatchableItem = (open, task) => {
|
|
|
52
59
|
return open.find((entry) => entry.status === "in_progress" && ready.has(entry.id)) ?? open.find((entry) => ready.has(entry.id));
|
|
53
60
|
};
|
|
54
61
|
const noReadyItemReason = (task) => `No dependency-ready build item: ${readyTaskItems(task).blocked.map((entry) => `${entry.id} waits for ${entry.waitingOn.join(",")}`).join("; ")}`;
|
|
55
|
-
const
|
|
56
|
-
...(task.phase === "build" ? [{ id: "reopen_g1", command: `${stateCommand} task reopen ${task.id} --to plan --reason <reason> --source <explicit-user-request>${suffix}` }] : []),
|
|
57
|
-
{ id: "create_successor", command: `${stateCommand} task create <new-task-id> --title <title>${suffix}` },
|
|
58
|
-
{ id: "supersede", command: `${stateCommand} task supersede ${task.id} --successor <new-task-id> --reason <reason> --source <explicit-user-request>${suffix}` },
|
|
59
|
-
{ id: "close", command: `${stateCommand} task close ${task.id} --reason <reason> --source <explicit-user-request>${suffix}` }
|
|
60
|
-
];
|
|
61
|
-
const pausedActions = (task, suffix, stateCommand) => [
|
|
62
|
-
{ id: "resume", command: `${stateCommand} task status ${task.id} --status active${suffix}` },
|
|
63
|
-
{ id: "record_handoff", command: `${stateCommand} task handoff ${task.id} --kind <kind> --reason <reason> --source <source>${suffix}` }
|
|
64
|
-
];
|
|
65
|
-
const repairExhausted = (task, suffix, stateCommand) => {
|
|
62
|
+
const repairExhausted = (task) => {
|
|
66
63
|
if (task.phase !== "build" || isLegacyG2Wait(task))
|
|
67
64
|
return undefined;
|
|
68
65
|
const bounds = repairBounds(task);
|
|
69
66
|
if (!bounds.verifyExhausted && !bounds.reviewExhausted)
|
|
70
67
|
return undefined;
|
|
71
|
-
const
|
|
68
|
+
const handoffKind = bounds.reviewExhausted ? "review_exhausted" : "repair_exhausted";
|
|
72
69
|
const detail = bounds.verifyExhausted ? `3+ failed verify cycles in: ${bounds.exhaustedAreas.join(", ")}` : `${bounds.reviewFailures} failed review passes`;
|
|
73
|
-
return {
|
|
74
|
-
classification: "blocked",
|
|
75
|
-
phase: task.phase,
|
|
76
|
-
gate: task.gate,
|
|
77
|
-
reason: `Repair bound exhausted (${detail}); record a durable handoff`,
|
|
78
|
-
actions: [{ id: "record_handoff", command: `${stateCommand} task handoff ${task.id} --kind ${kind} --reason <summary-of-exhausted-repairs> --source <agent>${suffix}` }]
|
|
79
|
-
};
|
|
70
|
+
return { kind: "repair_exhausted", handoffKind, reason: `Repair bound exhausted (${detail}); record a durable handoff` };
|
|
80
71
|
};
|
|
81
|
-
const continueItem = (item, openCount,
|
|
82
|
-
|
|
83
|
-
phase: task.phase,
|
|
84
|
-
gate: task.gate,
|
|
72
|
+
const continueItem = (item, openCount, premature) => ({
|
|
73
|
+
kind: "continue_item",
|
|
85
74
|
itemId: item.id,
|
|
86
75
|
remainingItems: openCount,
|
|
87
|
-
command: `${contextCommand} ${task.id} --phase build --item ${item.id}${suffix}`,
|
|
88
76
|
reason: premature ? `Invalid premature G2 wait; continue build item ${item.id}` : `Continue build item ${item.id}; ${openCount} actionable item(s) remain before verification, review, and G2`
|
|
89
77
|
});
|
|
90
|
-
const continueVerification = (
|
|
91
|
-
|
|
92
|
-
phase: task.phase,
|
|
93
|
-
gate: task.gate,
|
|
94
|
-
remainingItems: 0,
|
|
95
|
-
command: `${contextCommand} ${task.id} --phase build${suffix}`,
|
|
78
|
+
const continueVerification = (premature) => ({
|
|
79
|
+
kind: "continue_verification",
|
|
96
80
|
reason: premature ? "Invalid premature G2 wait; continue verification and adversarial review before presenting G2" : "All build items are terminal; continue verification, adversarial review, and G2 preparation"
|
|
97
81
|
});
|
|
98
|
-
const prematureG2Wait = (task
|
|
82
|
+
const prematureG2Wait = (task) => {
|
|
99
83
|
if (task.status !== "blocked_on_user" || task.gate !== "G2_codereview")
|
|
100
84
|
return undefined;
|
|
101
85
|
if (!(task.tasks.some((item) => !["done", "deferred"].includes(item.status)) || !hasVerification(task) || !hasReview(task)))
|
|
@@ -103,44 +87,61 @@ const prematureG2Wait = (task, suffix, contextCommand) => {
|
|
|
103
87
|
const open = pendingItems(task);
|
|
104
88
|
const item = dispatchableItem(open, task);
|
|
105
89
|
if (item)
|
|
106
|
-
return continueItem(item, open.length,
|
|
90
|
+
return continueItem(item, open.length, true);
|
|
107
91
|
if (open.length)
|
|
108
|
-
return {
|
|
109
|
-
return continueVerification(
|
|
92
|
+
return { kind: "no_ready_item", reason: noReadyItemReason(task) };
|
|
93
|
+
return continueVerification(true);
|
|
110
94
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const stateCommand = script(root, "state.mjs");
|
|
114
|
-
const contextCommand = script(root, "context.mjs");
|
|
95
|
+
/** Pure classification: maps a task onto its next-action decision without touching any command string. */
|
|
96
|
+
const classify = (task) => {
|
|
115
97
|
if (task.phase === "done")
|
|
116
|
-
return {
|
|
98
|
+
return { kind: "complete", reason: "Task is complete" };
|
|
117
99
|
if (task.status === "closed" || task.status === "superseded")
|
|
118
|
-
return {
|
|
100
|
+
return { kind: "terminal", outcome: task.status, successorTaskId: task.successorTaskId, reason: task.closure?.reason ?? "Task ended without successful completion" };
|
|
119
101
|
if (task.handoff)
|
|
120
|
-
return {
|
|
102
|
+
return { kind: "handoff", reason: task.handoff.reason };
|
|
121
103
|
if (task.status === "paused")
|
|
122
|
-
return {
|
|
123
|
-
const repair = repairExhausted(task
|
|
104
|
+
return { kind: "paused", reason: "Task is paused" };
|
|
105
|
+
const repair = repairExhausted(task);
|
|
124
106
|
if (repair)
|
|
125
107
|
return repair;
|
|
126
|
-
const prematureG2 = prematureG2Wait(task
|
|
108
|
+
const prematureG2 = prematureG2Wait(task);
|
|
127
109
|
if (prematureG2)
|
|
128
110
|
return prematureG2;
|
|
129
|
-
if (task.status === "blocked_on_user" && task.gate === "G1_review" && task.decisions.some((decision) => decision.status === "unresolved"))
|
|
130
|
-
return {
|
|
131
|
-
}
|
|
111
|
+
if (task.status === "blocked_on_user" && task.gate === "G1_review" && task.decisions.some((decision) => decision.status === "unresolved"))
|
|
112
|
+
return { kind: "recover_g1_wait", reason: "Invalid premature G1 wait; resolve every decision before presenting G1" };
|
|
132
113
|
if (task.status === "blocked_on_user" && task.gate === "none")
|
|
133
|
-
return {
|
|
114
|
+
return { kind: "recover_gateless_wrap", reason: "Recover invalid wait at a gateless wrap phase before continuing wrap" };
|
|
134
115
|
if (task.status === "blocked_on_user")
|
|
135
|
-
return {
|
|
116
|
+
return { kind: "await_user", reason: `Explicit human approval is required at ${task.gate}` };
|
|
136
117
|
if (task.phase === "build") {
|
|
137
118
|
const open = pendingItems(task);
|
|
138
119
|
const item = dispatchableItem(open, task);
|
|
139
120
|
if (item)
|
|
140
|
-
return continueItem(item, open.length,
|
|
121
|
+
return continueItem(item, open.length, false);
|
|
141
122
|
if (open.length)
|
|
142
|
-
return {
|
|
143
|
-
return continueVerification(
|
|
123
|
+
return { kind: "no_ready_item", reason: noReadyItemReason(task) };
|
|
124
|
+
return continueVerification(false);
|
|
125
|
+
}
|
|
126
|
+
return { kind: "continue_phase", reason: `Continue ${task.phase} until the next human gate, real blocker, or completion` };
|
|
127
|
+
};
|
|
128
|
+
/** Attaches the command strings a decision implies; the single call site of the command kit. */
|
|
129
|
+
const realize = (task, decision, commands) => {
|
|
130
|
+
const base = { phase: task.phase, gate: task.gate };
|
|
131
|
+
switch (decision.kind) {
|
|
132
|
+
case "complete": return { classification: "complete", ...base, reason: decision.reason };
|
|
133
|
+
case "terminal": return { classification: "terminal", ...base, outcome: decision.outcome, successorTaskId: decision.successorTaskId, reason: decision.reason };
|
|
134
|
+
case "handoff": return { classification: "blocked", ...base, reason: decision.reason, actions: commands.handoffActions(task) };
|
|
135
|
+
case "paused": return { classification: "blocked", ...base, reason: decision.reason, actions: commands.pausedActions(task.id) };
|
|
136
|
+
case "repair_exhausted": return { classification: "blocked", ...base, reason: decision.reason, actions: commands.repairHandoffAction(task.id, decision.handoffKind) };
|
|
137
|
+
case "continue_item": return { classification: "run_phase", ...base, itemId: decision.itemId, remainingItems: decision.remainingItems, command: commands.continueItem(task.id, decision.itemId), reason: decision.reason };
|
|
138
|
+
case "continue_verification": return { classification: "run_phase", ...base, remainingItems: 0, command: commands.continueVerification(task.id), reason: decision.reason };
|
|
139
|
+
case "no_ready_item": return { classification: "blocked", ...base, reason: decision.reason };
|
|
140
|
+
case "recover_g1_wait": return { classification: "run_phase", ...base, command: commands.recoverG1Wait(task.id), reason: decision.reason };
|
|
141
|
+
case "recover_gateless_wrap": return { classification: "run_phase", ...base, command: commands.recoverGatelessWrap(task.id), reason: decision.reason };
|
|
142
|
+
case "await_user": return { classification: "await_user", ...base, command: commands.approveGate(task.id, task.gate), reason: decision.reason };
|
|
143
|
+
case "continue_phase": return { classification: "run_phase", ...base, command: commands.continuePhase(task.id, task.phase), reason: decision.reason };
|
|
144
144
|
}
|
|
145
|
-
return { classification: "run_phase", phase: task.phase, gate: task.gate, command: `${contextCommand} ${task.id} --phase ${task.phase}${suffix}`, reason: `Continue ${task.phase} until the next human gate, real blocker, or completion` };
|
|
146
145
|
};
|
|
146
|
+
/** Classifies the next lifecycle action for a task and attaches root-qualified command strings. */
|
|
147
|
+
export const nextAction = (task, root) => realize(task, classify(task), commandKit(root));
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { digest } from "./domain.js";
|
|
3
|
+
import { audited, tokens } from "./guards.js";
|
|
4
|
+
import { assertNoSymlinkPath, parseJsonFile, saveJson } from "./io.js";
|
|
5
|
+
import { lessonIndexPath, lessonIndexPathRelative } from "./paths.js";
|
|
3
6
|
import { loadState, loadTask } from "./store.js";
|
|
4
|
-
import {
|
|
7
|
+
import { validateLesson } from "./validate.js";
|
|
5
8
|
const lessonEntry = (lesson) => ({ id: lesson.id, taskId: lesson.taskId, areas: lesson.areas, summary: lesson.summary, prevention: lesson.prevention, source: lesson.source, recordedAt: lesson.recordedAt });
|
|
6
9
|
export const recordLesson = (task, lesson, recordedAt = new Date().toISOString()) => {
|
|
7
10
|
if (task.phase !== "wrap" || task.status !== "active")
|
|
@@ -40,13 +43,12 @@ const allLessons = (root, state) => [
|
|
|
40
43
|
];
|
|
41
44
|
export const rebuildLessonIndex = (root, state = loadState(root)) => {
|
|
42
45
|
const index = createLessonIndex(allLessons(root, state), lessonStateDigest(state));
|
|
43
|
-
|
|
44
|
-
writeAtomic(lessonIndexPath(root), `${JSON.stringify(index, null, 2)}\n`);
|
|
46
|
+
saveJson(root, lessonIndexPathRelative, index);
|
|
45
47
|
return index;
|
|
46
48
|
};
|
|
47
49
|
export const searchLessons = (root, state, query, areas = [], limit = 5) => {
|
|
48
50
|
const path = lessonIndexPath(root);
|
|
49
|
-
assertNoSymlinkPath(root,
|
|
51
|
+
assertNoSymlinkPath(root, lessonIndexPathRelative);
|
|
50
52
|
if (!existsSync(path)) {
|
|
51
53
|
const hasLessons = Object.values(state.tasks).some((task) => Boolean(task.lessons?.length)) || Object.values(state.archive ?? {}).some((task) => task.lessonCount > 0);
|
|
52
54
|
if (!hasLessons)
|
|
@@ -58,10 +60,15 @@ export const searchLessons = (root, state, query, areas = [], limit = 5) => {
|
|
|
58
60
|
throw new Error("Lesson index is stale; run state.mjs lesson rebuild");
|
|
59
61
|
const queryTokens = tokens(query);
|
|
60
62
|
const areaSet = new Set(areas.map((area) => area.toLocaleLowerCase()));
|
|
61
|
-
return index.lessons
|
|
63
|
+
return index.lessons
|
|
64
|
+
.map((item) => {
|
|
62
65
|
const haystack = new Set(tokens(`${item.summary} ${item.prevention} ${item.taskId} ${item.areas.join(" ")}`));
|
|
63
66
|
const areaScore = item.areas.some((area) => areaSet.has(area.toLocaleLowerCase())) ? 100 : 0;
|
|
64
67
|
const tokenScore = queryTokens.filter((token) => haystack.has(token)).length;
|
|
65
68
|
return { item, score: areaScore + tokenScore };
|
|
66
|
-
})
|
|
69
|
+
})
|
|
70
|
+
.filter(({ score }) => score > 0 || (!queryTokens.length && !areaSet.size))
|
|
71
|
+
.sort((a, b) => b.score - a.score || b.item.recordedAt.localeCompare(a.item.recordedAt))
|
|
72
|
+
.slice(0, Math.max(1, Math.min(limit, 20)))
|
|
73
|
+
.map(({ item }) => item);
|
|
67
74
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { handoffKinds } from "./
|
|
1
|
+
import { handoffKinds, terminal } from "./domain.js";
|
|
2
|
+
import { audited } from "./guards.js";
|
|
2
3
|
import { hasApproval } from "./transitions.js";
|
|
3
4
|
import { validateState } from "./validate.js";
|
|
4
|
-
import { audited, terminal } from "./shared.js";
|
|
5
5
|
export const handoffTask = (state, id, kind, reason, source, recordedAt = new Date().toISOString()) => {
|
|
6
6
|
const task = state.tasks[id];
|
|
7
7
|
if (!task || terminal(task) || !handoffKinds.includes(kind))
|
package/dist/src/state/lock.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname
|
|
4
|
-
import { assertNoSymlinkPath } from "./
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { assertNoSymlinkPath } from "./io.js";
|
|
5
|
+
import { stateLockGuardPath, stateLockGuardPathRelative, stateLockPath, stateLockPathRelative } from "./paths.js";
|
|
5
6
|
const sleep = (milliseconds) => {
|
|
6
7
|
if (milliseconds > 0)
|
|
7
8
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
@@ -72,10 +73,10 @@ export const acquireStateLock = (root, options = {}) => {
|
|
|
72
73
|
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
73
74
|
const retryMs = options.retryMs ?? 25;
|
|
74
75
|
const staleMs = options.staleMs ?? 30_000;
|
|
75
|
-
const lockPath =
|
|
76
|
-
const guardPath =
|
|
77
|
-
assertNoSymlinkPath(root,
|
|
78
|
-
assertNoSymlinkPath(root,
|
|
76
|
+
const lockPath = stateLockPath(root);
|
|
77
|
+
const guardPath = stateLockGuardPath(root);
|
|
78
|
+
assertNoSymlinkPath(root, stateLockPathRelative);
|
|
79
|
+
assertNoSymlinkPath(root, stateLockGuardPathRelative);
|
|
79
80
|
const token = randomUUID();
|
|
80
81
|
const deadline = Date.now() + timeoutMs;
|
|
81
82
|
const acquireGuard = () => {
|