@felixdotgo/aidlc-kit 0.0.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/DEVELOPMENT.md +31 -0
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/assets/.agents/aidlc/conventions.md +68 -0
- package/dist/assets/.agents/aidlc/orchestrator.md +73 -0
- package/dist/assets/.agents/aidlc/phase-build.md +23 -0
- package/dist/assets/.agents/aidlc/phase-clarify.md +22 -0
- package/dist/assets/.agents/aidlc/phase-index.md +14 -0
- package/dist/assets/.agents/aidlc/phase-plan.md +21 -0
- package/dist/assets/.agents/aidlc/phase-wrap.md +16 -0
- package/dist/assets/.agents/aidlc/profiles/generic/profile.json +9 -0
- package/dist/assets/.agents/aidlc/profiles/git-submodules/profile.json +9 -0
- package/dist/assets/.agents/aidlc/profiles/single/profile.json +9 -0
- package/dist/assets/.agents/aidlc/profiles/workspace/profile.json +12 -0
- package/dist/assets/.agents/aidlc/rules/gate-check.md +16 -0
- package/dist/assets/.agents/aidlc/rules/stack-runtime.md +3 -0
- package/dist/assets/.agents/aidlc/schemas/adapter.schema.json +13 -0
- package/dist/assets/.agents/aidlc/schemas/config.schema.json +26 -0
- package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +12 -0
- package/dist/assets/.agents/aidlc/schemas/profile.schema.json +12 -0
- package/dist/assets/.agents/aidlc/schemas/state.schema.json +108 -0
- package/dist/assets/.agents/aidlc/scripts/context.mjs +15 -0
- package/dist/assets/.agents/aidlc/scripts/gate-check.mjs +16 -0
- package/dist/assets/.agents/aidlc/scripts/gate-view.mjs +16 -0
- package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +238 -0
- package/dist/assets/.agents/aidlc/scripts/lib/runtime.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +341 -0
- package/dist/assets/.agents/aidlc/scripts/render.mjs +24 -0
- package/dist/assets/.agents/aidlc/scripts/state.mjs +222 -0
- package/dist/assets/.agents/aidlc/scripts/task-next.mjs +27 -0
- package/dist/assets/.agents/aidlc/templates/design.md +15 -0
- package/dist/assets/.agents/aidlc/templates/intent.md +17 -0
- package/dist/assets/.agents/aidlc/templates/lesson.md +28 -0
- package/dist/assets/.agents/aidlc/templates/model-contract.md +41 -0
- package/dist/assets/.agents/aidlc/templates/workplan.md +9 -0
- package/dist/assets/.agents/skills/aidlc-build/SKILL.md +19 -0
- package/dist/assets/.agents/skills/aidlc-clarify/SKILL.md +17 -0
- package/dist/assets/.agents/skills/aidlc-index/SKILL.md +17 -0
- package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +17 -0
- package/dist/assets/.agents/skills/aidlc-wrap/SKILL.md +17 -0
- package/dist/services/aidlc-coordination/Dockerfile +10 -0
- package/dist/services/aidlc-coordination/compose.yaml +25 -0
- package/dist/services/aidlc-coordination/package-lock.json +1154 -0
- package/dist/services/aidlc-coordination/package.json +20 -0
- package/dist/services/aidlc-coordination/src/coordination.mjs +68 -0
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +341 -0
- package/dist/services/aidlc-coordination/src/providers.mjs +23 -0
- package/dist/services/aidlc-coordination/src/repository.mjs +203 -0
- package/dist/services/aidlc-coordination/src/server.mjs +49 -0
- package/dist/services/aidlc-coordination/test/repository.test.mjs +211 -0
- package/dist/src/adapters.d.ts +7 -0
- package/dist/src/adapters.js +86 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +192 -0
- package/dist/src/context.d.ts +14 -0
- package/dist/src/context.js +90 -0
- package/dist/src/gate.d.ts +40 -0
- package/dist/src/gate.js +176 -0
- package/dist/src/installer.d.ts +11 -0
- package/dist/src/installer.js +173 -0
- package/dist/src/ledger.d.ts +22 -0
- package/dist/src/ledger.js +119 -0
- package/dist/src/legacy.d.ts +2 -0
- package/dist/src/legacy.js +37 -0
- package/dist/src/managed-content.d.ts +3 -0
- package/dist/src/managed-content.js +16 -0
- package/dist/src/mcp-setup.d.ts +20 -0
- package/dist/src/mcp-setup.js +31 -0
- package/dist/src/model.d.ts +249 -0
- package/dist/src/model.js +1 -0
- package/dist/src/profiles.d.ts +28 -0
- package/dist/src/profiles.js +216 -0
- package/dist/src/project-migration.d.ts +2 -0
- package/dist/src/project-migration.js +144 -0
- package/dist/src/project-path.d.ts +2 -0
- package/dist/src/project-path.js +20 -0
- package/dist/src/state.d.ts +110 -0
- package/dist/src/state.js +764 -0
- package/dist/src/tracker.d.ts +29 -0
- package/dist/src/tracker.js +41 -0
- package/dist/src/upgrade.d.ts +11 -0
- package/dist/src/upgrade.js +270 -0
- package/dist/src/workflow.d.ts +7 -0
- package/dist/src/workflow.js +46 -0
- package/docs/README.md +31 -0
- package/docs/architecture.md +72 -0
- package/docs/command-reference.md +110 -0
- package/docs/configuration.md +72 -0
- package/docs/coordination-service.md +42 -0
- package/docs/development.md +106 -0
- package/docs/operating-workflow.md +87 -0
- package/docs/testing-and-release.md +113 -0
- package/package.json +53 -0
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { installableAdapters } from "./adapters.js";
|
|
6
|
+
import { applyPlan, detectedAgents, doctor, formatPlan, planInit, planUninstall, selectAgents, status } from "./installer.js";
|
|
7
|
+
import { loadProjectConfig, resolveProfiles } from "./profiles.js";
|
|
8
|
+
import { applyUpgrade, planUpgrade } from "./upgrade.js";
|
|
9
|
+
import { packageVersion } from "./workflow.js";
|
|
10
|
+
import { applyMcpSetup, planMcpSetup } from "./mcp-setup.js";
|
|
11
|
+
const usage = `Usage:
|
|
12
|
+
npx @felixdotgo/aidlc-kit init [path] [--agent <name[,name]> | --all] [--yes] [--dry-run] [--force]
|
|
13
|
+
npx @felixdotgo/aidlc-kit upgrade [path] [--dry-run]
|
|
14
|
+
npx @felixdotgo/aidlc-kit uninstall [path] [--yes] [--dry-run]
|
|
15
|
+
npx @felixdotgo/aidlc-kit status [path]
|
|
16
|
+
npx @felixdotgo/aidlc-kit doctor [path] [--strict]
|
|
17
|
+
npx @felixdotgo/aidlc-kit profile validate [path]
|
|
18
|
+
npx @felixdotgo/aidlc-kit mcp setup [path] [--dry-run] [--yes] [--storage sqlite|postgres] [--deployment docker|remote] [--bind localhost|network] [--workspace <id>] [--poll-ms <n>] [--enable]`;
|
|
19
|
+
const interactive = () => Boolean(stdin.isTTY && stdout.isTTY);
|
|
20
|
+
const prompts = async () => import("@inquirer/prompts");
|
|
21
|
+
const value = (args, name) => {
|
|
22
|
+
const index = args.indexOf(name);
|
|
23
|
+
return index < 0 ? undefined : args[index + 1];
|
|
24
|
+
};
|
|
25
|
+
const flags = (args) => new Set(args.filter((item) => item.startsWith("--")));
|
|
26
|
+
const valueFlags = new Set(["--agent", "--storage", "--deployment", "--bind", "--workspace", "--poll-ms", "--token-env", "--providers"]);
|
|
27
|
+
const positional = (args) => {
|
|
28
|
+
const result = [];
|
|
29
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
30
|
+
if (valueFlags.has(args[index])) {
|
|
31
|
+
index += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!args[index].startsWith("--"))
|
|
35
|
+
result.push(args[index]);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
const rootArg = (args, fallback = ".") => positional(args).find((item) => !["init", "upgrade", "uninstall", "status", "doctor", "validate", "run", "list"].includes(item)) ?? fallback;
|
|
40
|
+
const initOptions = (args) => ({
|
|
41
|
+
root: rootArg(args),
|
|
42
|
+
agents: (value(args, "--agent") ?? "").split(",").filter(Boolean),
|
|
43
|
+
all: flags(args).has("--all"),
|
|
44
|
+
dryRun: flags(args).has("--dry-run"),
|
|
45
|
+
yes: flags(args).has("--yes"),
|
|
46
|
+
force: flags(args).has("--force")
|
|
47
|
+
});
|
|
48
|
+
const chooseAgents = async (options) => {
|
|
49
|
+
const selected = selectAgents(options);
|
|
50
|
+
if (selected.length)
|
|
51
|
+
return { ...options, agents: selected };
|
|
52
|
+
const detected = detectedAgents(options.root);
|
|
53
|
+
if (detected.length === 1)
|
|
54
|
+
return { ...options, agents: detected };
|
|
55
|
+
if (!interactive())
|
|
56
|
+
throw new Error(`Unable to select an agent automatically. Use --agent <name> or --all. Detected: ${detected.join(", ") || "none"}`);
|
|
57
|
+
const { checkbox } = await prompts();
|
|
58
|
+
const choices = installableAdapters();
|
|
59
|
+
const agents = await checkbox({
|
|
60
|
+
message: "Select one or more agents",
|
|
61
|
+
choices: choices.map((adapter) => ({
|
|
62
|
+
name: `${adapter.displayName}${detected.includes(adapter.id) ? " (detected)" : ""}`,
|
|
63
|
+
value: adapter.id
|
|
64
|
+
})),
|
|
65
|
+
required: true
|
|
66
|
+
});
|
|
67
|
+
return { ...options, agents };
|
|
68
|
+
};
|
|
69
|
+
const preview = (operation, plan) => {
|
|
70
|
+
console.log(`◆ ${operation} preview — changes have NOT been applied`);
|
|
71
|
+
console.log(formatPlan(plan));
|
|
72
|
+
};
|
|
73
|
+
const confirmGeneral = async (options, operation) => {
|
|
74
|
+
if (options.dryRun)
|
|
75
|
+
return false;
|
|
76
|
+
if (options.yes)
|
|
77
|
+
return true;
|
|
78
|
+
if (!interactive())
|
|
79
|
+
return false;
|
|
80
|
+
const { confirm } = await prompts();
|
|
81
|
+
return confirm({ message: `${operation} this plan?`, default: false });
|
|
82
|
+
};
|
|
83
|
+
const mcpSetupOptions = async (args) => {
|
|
84
|
+
const selected = {
|
|
85
|
+
deployment: value(args, "--deployment"),
|
|
86
|
+
storage: value(args, "--storage"),
|
|
87
|
+
bind: value(args, "--bind"),
|
|
88
|
+
workspace: value(args, "--workspace"),
|
|
89
|
+
pollMs: Number(value(args, "--poll-ms") ?? 30_000),
|
|
90
|
+
tokenEnv: value(args, "--token-env") ?? "AIDLC_MCP_TOKEN",
|
|
91
|
+
providers: (value(args, "--providers") ?? "").split(",").filter(Boolean)
|
|
92
|
+
};
|
|
93
|
+
if ((!selected.deployment || !selected.storage || !selected.bind || !selected.workspace) && !interactive())
|
|
94
|
+
throw new Error("mcp setup outside a TTY requires --deployment, --storage, --bind and --workspace");
|
|
95
|
+
if (interactive()) {
|
|
96
|
+
const { input, select, checkbox } = await prompts();
|
|
97
|
+
selected.deployment ??= await select({ message: "State service deployment", choices: [{ name: "Docker on this machine", value: "docker" }, { name: "Remote service (configure endpoint later)", value: "remote" }] });
|
|
98
|
+
selected.storage ??= await select({ message: "Database", choices: [{ name: "SQLite (local Docker volume)", value: "sqlite" }, { name: "PostgreSQL (multi-host production)", value: "postgres" }] });
|
|
99
|
+
selected.bind ??= await select({ message: "Network binding", choices: [{ name: "localhost only", value: "localhost" }, { name: "network (requires token/TLS)", value: "network" }] });
|
|
100
|
+
selected.workspace ??= await input({ message: "Workspace ID", default: "default", required: true });
|
|
101
|
+
if (!selected.providers.length)
|
|
102
|
+
selected.providers = await checkbox({ message: "Project-management connectors to configure", choices: ["jira", "trello", "github-issues"].map((value) => ({ name: value, value })) });
|
|
103
|
+
}
|
|
104
|
+
if (!Number.isInteger(selected.pollMs) || selected.pollMs < 1_000 || selected.pollMs > 3_600_000)
|
|
105
|
+
throw new Error("--poll-ms must be an integer from 1000 to 3600000");
|
|
106
|
+
return { root: rootArg(args), template: join(fileURLToPath(new URL("../services/aidlc-coordination", import.meta.url))), dryRun: flags(args).has("--dry-run"), deployment: selected.deployment, storage: selected.storage, bind: selected.bind, workspace: selected.workspace, pollMs: selected.pollMs, tokenEnv: selected.tokenEnv, providers: selected.providers, enable: flags(args).has("--enable") };
|
|
107
|
+
};
|
|
108
|
+
const main = async () => {
|
|
109
|
+
const [command = "help", ...args] = process.argv.slice(2);
|
|
110
|
+
if (command === "help" || command === "--help")
|
|
111
|
+
return void console.log(usage);
|
|
112
|
+
if (command === "status")
|
|
113
|
+
return void console.log(status(rootArg(args)));
|
|
114
|
+
if (command === "doctor") {
|
|
115
|
+
const result = doctor(rootArg(args), flags(args).has("--strict"));
|
|
116
|
+
console.log(result);
|
|
117
|
+
if (result.startsWith("ERROR:"))
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (command === "init") {
|
|
122
|
+
const options = await chooseAgents(initOptions(args));
|
|
123
|
+
const plan = planInit(options);
|
|
124
|
+
preview("INIT", plan);
|
|
125
|
+
if (plan.some((item) => item.action === "conflict"))
|
|
126
|
+
throw new Error("No files were written because the plan contains conflicts.");
|
|
127
|
+
if (!await confirmGeneral(options, "Apply"))
|
|
128
|
+
return;
|
|
129
|
+
const migratesLegacyData = plan.some((item) => item.action === "migrate" || item.action === "delete");
|
|
130
|
+
if (migratesLegacyData) {
|
|
131
|
+
const result = applyUpgrade(options.root, plan);
|
|
132
|
+
console.log(`Installed local AI-DLC workflow assets and migrated legacy data. Backup: ${result.backup}; changed: ${result.changed}`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
applyPlan(options.root, plan);
|
|
136
|
+
console.log("Installed local AI-DLC workflow assets.");
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (command === "uninstall") {
|
|
141
|
+
const options = initOptions(args);
|
|
142
|
+
const plan = planUninstall(options.root);
|
|
143
|
+
preview("UNINSTALL", plan);
|
|
144
|
+
if (!await confirmGeneral(options, "Remove"))
|
|
145
|
+
return;
|
|
146
|
+
applyPlan(options.root, plan);
|
|
147
|
+
console.log("Removed eligible managed assets; project config and state were preserved.");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (command === "upgrade") {
|
|
151
|
+
if (flags(args).has("--yes") || flags(args).has("--force"))
|
|
152
|
+
throw new Error("upgrade does not support --yes or --force; apply requires a human interactive TTY confirmation");
|
|
153
|
+
const root = rootArg(args);
|
|
154
|
+
const plan = planUpgrade(root);
|
|
155
|
+
preview("UPGRADE", plan);
|
|
156
|
+
if (plan.some((item) => item.action === "conflict"))
|
|
157
|
+
throw new Error("Upgrade contains conflicts; no files were written.");
|
|
158
|
+
if (flags(args).has("--dry-run"))
|
|
159
|
+
return;
|
|
160
|
+
if (!interactive())
|
|
161
|
+
throw new Error("Upgrade apply requires an interactive TTY. AI and non-interactive processes must not apply upgrades.");
|
|
162
|
+
const expected = packageVersion();
|
|
163
|
+
const { input } = await prompts();
|
|
164
|
+
if ((await input({ message: `Type ${expected} to apply this user-initiated upgrade` })).trim() !== expected)
|
|
165
|
+
throw new Error("Upgrade confirmation did not match the target version");
|
|
166
|
+
const result = applyUpgrade(root, plan);
|
|
167
|
+
console.log(`Upgrade complete. Backup: ${result.backup}; changed: ${result.changed}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (command === "mcp" && args[0] === "setup") {
|
|
171
|
+
const options = await mcpSetupOptions(args.slice(1));
|
|
172
|
+
const plan = planMcpSetup(options);
|
|
173
|
+
console.log(`◆ MCP SETUP preview — ${options.dryRun ? "no files will be written" : "review before applying"}`);
|
|
174
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
175
|
+
if (options.dryRun)
|
|
176
|
+
return;
|
|
177
|
+
if (!await confirmGeneral({ ...initOptions(args), root: options.root }, "Apply MCP setup"))
|
|
178
|
+
return;
|
|
179
|
+
applyMcpSetup(options);
|
|
180
|
+
console.log(`MCP setup written to ${plan.target}. MCP remains ${options.enable ? "enabled" : "disabled"}; start Docker explicitly with docker compose --env-file .env -f ${join(plan.target, "compose.yaml")} up -d.`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (command === "profile" && args[0] === "validate") {
|
|
184
|
+
const root = rootArg(args.slice(1));
|
|
185
|
+
const config = loadProjectConfig(root);
|
|
186
|
+
const profiles = resolveProfiles(root, config.extends);
|
|
187
|
+
console.log(`OK: ${profiles.map((item) => item.id).join(" → ")}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`Unknown command: ${command}`);
|
|
191
|
+
};
|
|
192
|
+
main().catch((error) => { console.error(`aidlc-workflow: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Phase, ProjectConfig, TaskState } from "./model.js";
|
|
2
|
+
export interface ContextPacket {
|
|
3
|
+
content: string;
|
|
4
|
+
chars: number;
|
|
5
|
+
estimatedTokens: number;
|
|
6
|
+
omittedRules: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface ContextOptions {
|
|
9
|
+
mode?: "standard" | "economy";
|
|
10
|
+
itemId?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const compileContext: (root: string, config: ProjectConfig, task: TaskState, phase: Phase, options?: ContextOptions) => ContextPacket;
|
|
13
|
+
export declare const writeableContextFormat: (packet: ContextPacket, format: "markdown" | "json") => string;
|
|
14
|
+
export declare const contextAssetsAvailable: () => boolean;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { includedRuleFiles, resolveEffectiveConfig } from "./profiles.js";
|
|
5
|
+
import { loadMemoryRegistry, nextAction, selectAgenticMemory } from "./state.js";
|
|
6
|
+
const assetRoot = fileURLToPath(new URL("../assets/.agents/aidlc", import.meta.url));
|
|
7
|
+
const phaseFile = (phase) => phase === "done" ? "phase-wrap.md" : `phase-${phase}.md`;
|
|
8
|
+
const readAsset = (path) => readFileSync(join(assetRoot, path), "utf8");
|
|
9
|
+
const requiredEvidence = (task) => task.evidence.filter((item, index, entries) => {
|
|
10
|
+
const key = `${item.kind}:${item.area ?? ""}:${item.gate ?? ""}`;
|
|
11
|
+
return !entries.slice(index + 1).some((candidate) => `${candidate.kind}:${candidate.area ?? ""}:${candidate.gate ?? ""}` === key);
|
|
12
|
+
}).map(({ detail: _detail, ...item }) => item);
|
|
13
|
+
const compactTask = (task, itemId) => JSON.stringify({
|
|
14
|
+
id: task.id,
|
|
15
|
+
title: task.title,
|
|
16
|
+
type: task.type,
|
|
17
|
+
phase: task.phase,
|
|
18
|
+
gate: task.gate,
|
|
19
|
+
status: task.status,
|
|
20
|
+
risk: task.risk,
|
|
21
|
+
areas: task.areas,
|
|
22
|
+
handoff: task.handoff,
|
|
23
|
+
closure: task.closure,
|
|
24
|
+
predecessorTaskId: task.predecessorTaskId,
|
|
25
|
+
successorTaskId: task.successorTaskId,
|
|
26
|
+
decisions: task.decisions.map(({ id, label, status }) => ({ id, label, status })),
|
|
27
|
+
tasks: itemId ? task.tasks.filter((item) => item.id === itemId) : task.tasks,
|
|
28
|
+
evidence: requiredEvidence(task),
|
|
29
|
+
lessonDisposition: task.lessonDisposition,
|
|
30
|
+
artifacts: task.artifacts
|
|
31
|
+
}, null, 2);
|
|
32
|
+
const profileSummary = (profiles) => profiles.map((item) => JSON.stringify({ id: item.id, topology: item.topology, discovery: item.discovery, specs: item.specs, commands: item.commands })).join("\n");
|
|
33
|
+
export const compileContext = (root, config, task, phase, options = {}) => {
|
|
34
|
+
if (!["clarify", "plan", "build", "wrap", "done"].includes(phase))
|
|
35
|
+
throw new Error(`Unsupported phase: ${phase}`);
|
|
36
|
+
const effective = resolveEffectiveConfig(root, config);
|
|
37
|
+
const profiles = effective.profiles;
|
|
38
|
+
const ruleFiles = includedRuleFiles(root, effective.rules.include);
|
|
39
|
+
if (options.itemId && !task.tasks.some((item) => item.id === options.itemId))
|
|
40
|
+
throw new Error(`Unknown task item: ${options.itemId}`);
|
|
41
|
+
const taskState = compactTask(task, options.itemId);
|
|
42
|
+
const actionableItems = options.itemId ? task.tasks.filter((item) => item.status === "in_progress" || item.status === "todo").map((item) => item.id) : [];
|
|
43
|
+
const invariants = [
|
|
44
|
+
options.itemId ? `- Focused item context does not narrow the workplan: actionable items are ${actionableItems.join(", ") || "none"}; after this item, execute nextAction immediately.` : "",
|
|
45
|
+
"- Continue after a non-terminal transition or item/evidence mutation; yield only at a human gate, real blocker, or completion.",
|
|
46
|
+
"- Item completion is progress for commentary, never a final response while nextAction is run_phase.",
|
|
47
|
+
"- Never omit approved decisions, spec anchors, safety constraints, or applicable verification evidence.",
|
|
48
|
+
"- Agents may run configured build/test/lint commands, but workflow installation and upgrades remain human-only npm/npx operations."
|
|
49
|
+
].filter(Boolean).join("\n");
|
|
50
|
+
const action = JSON.stringify(nextAction(task, root), null, 2);
|
|
51
|
+
const required = [
|
|
52
|
+
`# AI-DLC phase packet — ${phase}`,
|
|
53
|
+
`mode: ${options.mode ?? "standard"}${options.itemId ? ` · item: ${options.itemId}` : ""}`,
|
|
54
|
+
"## Next action / stop contract",
|
|
55
|
+
action,
|
|
56
|
+
"## Phase contract",
|
|
57
|
+
readAsset(phaseFile(phase)),
|
|
58
|
+
"## Canonical task state",
|
|
59
|
+
taskState,
|
|
60
|
+
"## Resolved profiles",
|
|
61
|
+
profileSummary(profiles),
|
|
62
|
+
"## Project configuration",
|
|
63
|
+
JSON.stringify({ discovery: effective.discovery, specs: effective.specs, commands: effective.commands, risk: effective.risk, context: effective.context }, null, 2),
|
|
64
|
+
"## Invariants",
|
|
65
|
+
invariants
|
|
66
|
+
].join("\n\n");
|
|
67
|
+
if (required.length > config.context.maxChars)
|
|
68
|
+
throw new Error("Context budget is too small for the full phase contract, next action, canonical task state, and mandatory invariants");
|
|
69
|
+
let content = required;
|
|
70
|
+
const omittedRules = [];
|
|
71
|
+
for (const path of ruleFiles) {
|
|
72
|
+
const relative = path.slice(resolve(root).length + 1);
|
|
73
|
+
const block = `\n\n## Project rule — ${relative}\n${readFileSync(path, "utf8")}`;
|
|
74
|
+
if (content.length + block.length <= config.context.maxChars)
|
|
75
|
+
content += block;
|
|
76
|
+
else
|
|
77
|
+
omittedRules.push(relative);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const memory = selectAgenticMemory(loadMemoryRegistry(root), task.areas, phase === "done" ? "wrap" : phase, config.context.maxChars - content.length - "\n\n## Advisory project memory\n".length);
|
|
81
|
+
if (memory.length)
|
|
82
|
+
content += `\n\n## Advisory project memory\n${memory.map((entry) => `- ${entry.id} — ${entry.summary} — guidance: ${entry.guidance} — source: ${entry.sourceTaskId}/${entry.sourceLessonId}`).join("\n")}`;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
omittedRules.push(`memory:unavailable:${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
return { content, chars: content.length, estimatedTokens: Math.ceil(content.length / 4), omittedRules };
|
|
88
|
+
};
|
|
89
|
+
export const writeableContextFormat = (packet, format) => format === "json" ? `${JSON.stringify(packet, null, 2)}\n` : `${packet.content}\n\n---\nchars: ${packet.chars} · estimated tokens: ${packet.estimatedTokens} · omitted rules: ${packet.omittedRules.length}\n`;
|
|
90
|
+
export const contextAssetsAvailable = () => existsSync(assetRoot);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Diagnostic, Gate, TaskState, WorkflowState } from "./model.js";
|
|
2
|
+
export declare const checkGate: (root: string, state: WorkflowState, taskId: string, gate: Gate) => Diagnostic[];
|
|
3
|
+
export declare const approveAndAdvance: (root: string, state: WorkflowState, taskId: string, gate: Gate, source: string, recordedAt?: string) => {
|
|
4
|
+
task: TaskState;
|
|
5
|
+
nextAction: import("./state.js").NextAction;
|
|
6
|
+
idempotent: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare const formatDiagnostics: (diagnostics: Diagnostic[]) => string;
|
|
9
|
+
export type GateViewFormat = "markdown" | "plain" | "json";
|
|
10
|
+
export declare const gateView: (task: TaskState, diagnostics: Diagnostic[]) => {
|
|
11
|
+
schemaVersion: number;
|
|
12
|
+
gate: "G0_confirm" | "G1_review" | "G2_codereview";
|
|
13
|
+
icon: string;
|
|
14
|
+
title: string;
|
|
15
|
+
action: string;
|
|
16
|
+
task: {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
status: "blocked_on_user";
|
|
20
|
+
risk: import("./model.js").RiskLevel;
|
|
21
|
+
areas: string[];
|
|
22
|
+
};
|
|
23
|
+
artifacts: {
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
}[];
|
|
27
|
+
decisions: Record<string, number>;
|
|
28
|
+
execution: Record<string, number>;
|
|
29
|
+
evidence: {
|
|
30
|
+
kind: "approval" | "spec" | "test" | "lint" | "review" | "diagnostic";
|
|
31
|
+
result: "skip" | "pass" | "fail";
|
|
32
|
+
source: string;
|
|
33
|
+
}[];
|
|
34
|
+
diagnostics: {
|
|
35
|
+
level: "ERROR" | "WARN" | "INFO";
|
|
36
|
+
code: string;
|
|
37
|
+
message: string;
|
|
38
|
+
}[];
|
|
39
|
+
};
|
|
40
|
+
export declare const formatGateView: (task: TaskState, diagnostics: Diagnostic[], format?: GateViewFormat) => string;
|
package/dist/src/gate.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolveProjectPathWithoutSymlinks } from "./project-path.js";
|
|
3
|
+
import { hasAreaVerification, hasReview, isLegacyG2Wait, nextAction, repairBounds, transitionDiagnostics, transitionTask } from "./state.js";
|
|
4
|
+
const safeArtifactTarget = (root, path) => resolveProjectPathWithoutSymlinks(root, path, "Unsafe artifact path", "Artifact path crosses a symlink");
|
|
5
|
+
const artifactContent = (root, path) => {
|
|
6
|
+
if (!path)
|
|
7
|
+
return undefined;
|
|
8
|
+
try {
|
|
9
|
+
const target = safeArtifactTarget(root, path);
|
|
10
|
+
return existsSync(target) ? readFileSync(target, "utf8") : undefined;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const artifactDiagnostics = (root, task, names) => names.flatMap((name) => {
|
|
17
|
+
const path = task.artifacts[name];
|
|
18
|
+
if (!path)
|
|
19
|
+
return [{ level: "ERROR", code: "ARTIFACT_REFERENCE", message: `${String(name)} artifact is not referenced` }];
|
|
20
|
+
try {
|
|
21
|
+
return existsSync(safeArtifactTarget(root, path)) ? [] : [{ level: "ERROR", code: "ARTIFACT_MISSING", message: `${path} does not exist` }];
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return [{ level: "ERROR", code: "ARTIFACT_UNSAFE", message: error instanceof Error ? error.message : String(error) }];
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
export const checkGate = (root, state, taskId, gate) => {
|
|
28
|
+
const task = state.tasks[taskId];
|
|
29
|
+
if (!task)
|
|
30
|
+
return [{ level: "ERROR", code: "TASK_UNKNOWN", message: `Unknown task: ${taskId}` }];
|
|
31
|
+
const diagnostics = [];
|
|
32
|
+
if (task.gate !== gate)
|
|
33
|
+
diagnostics.push({ level: "ERROR", code: "GATE_STATE", message: `Task is at ${task.gate}, not ${gate}` });
|
|
34
|
+
if (task.status === "closed" || task.status === "superseded")
|
|
35
|
+
diagnostics.push({ level: "ERROR", code: "TASK_TERMINAL", message: `Terminal task cannot approve ${gate}` });
|
|
36
|
+
if (task.handoff)
|
|
37
|
+
diagnostics.push({ level: "ERROR", code: "TASK_HANDOFF", message: `Task has unresolved handoff: ${task.handoff.reason}` });
|
|
38
|
+
if (task.id !== taskId)
|
|
39
|
+
diagnostics.push({ level: "ERROR", code: "TASK_ID", message: "Task key and id differ" });
|
|
40
|
+
if (!task.title.trim() || !task.areas.length)
|
|
41
|
+
diagnostics.push({ level: "ERROR", code: "TASK_FIELDS", message: "Task title and affected areas are required" });
|
|
42
|
+
if (gate === "G0_confirm") {
|
|
43
|
+
diagnostics.push(...artifactDiagnostics(root, task, ["intent"]));
|
|
44
|
+
const content = artifactContent(root, task.artifacts.intent);
|
|
45
|
+
if (content !== undefined)
|
|
46
|
+
for (const heading of ["## 📋 Problem", "## 🗺️ Affected areas", "## 💭 Assumptions", "## ❓ Open questions", "## 🎯 Scope"])
|
|
47
|
+
if (!content.includes(heading))
|
|
48
|
+
diagnostics.push({ level: "ERROR", code: "INTENT_HEADING", message: `Intent is missing ${heading}` });
|
|
49
|
+
}
|
|
50
|
+
if (gate === "G1_review") {
|
|
51
|
+
diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"]));
|
|
52
|
+
diagnostics.push(...transitionDiagnostics(task, "build").filter((item) => item.code === "UNRESOLVED_DECISION"));
|
|
53
|
+
const content = artifactContent(root, task.artifacts.design);
|
|
54
|
+
if (content !== undefined)
|
|
55
|
+
for (const heading of ["## 🧩 Solution per affected area", "## 📌 Spec traceability", "## 🔗 Cross-service contracts", "## ⚠️ Risks / edge cases"])
|
|
56
|
+
if (!content.includes(heading))
|
|
57
|
+
diagnostics.push({ level: "ERROR", code: "DESIGN_HEADING", message: `Design is missing ${heading}` });
|
|
58
|
+
}
|
|
59
|
+
if (gate === "G2_codereview") {
|
|
60
|
+
diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"]));
|
|
61
|
+
if (task.tasks.some((item) => item.status !== "done" && item.status !== "deferred"))
|
|
62
|
+
diagnostics.push({ level: "ERROR", code: "TASKS_OPEN", message: "Build tasks remain open" });
|
|
63
|
+
for (const area of task.areas)
|
|
64
|
+
if (!hasAreaVerification(task, area))
|
|
65
|
+
diagnostics.push({ level: "ERROR", code: "VERIFY_EVIDENCE", message: `Latest post-G1 verification evidence must pass for affected area: ${area}` });
|
|
66
|
+
if (!hasReview(task))
|
|
67
|
+
diagnostics.push({ level: "ERROR", code: "REVIEW_EVIDENCE", message: "Latest post-G1 review evidence must pass" });
|
|
68
|
+
const bounds = repairBounds(task);
|
|
69
|
+
if (!isLegacyG2Wait(task) && (bounds.verifyExhausted || bounds.reviewExhausted))
|
|
70
|
+
diagnostics.push({ level: "ERROR", code: "REPAIR_BOUND", message: "Repair bound exhausted; record a durable handoff instead of presenting G2" });
|
|
71
|
+
}
|
|
72
|
+
if (!diagnostics.length)
|
|
73
|
+
diagnostics.push({ level: "INFO", code: "GATE_OK", message: `${gate} checks passed for ${taskId}` });
|
|
74
|
+
return diagnostics;
|
|
75
|
+
};
|
|
76
|
+
const gateTarget = { G0_confirm: "plan", G1_review: "build", G2_codereview: "wrap" };
|
|
77
|
+
export const approveAndAdvance = (root, state, taskId, gate, source, recordedAt = new Date().toISOString()) => {
|
|
78
|
+
const task = state.tasks[taskId];
|
|
79
|
+
if (!task)
|
|
80
|
+
throw new Error(`Unknown task: ${taskId}`);
|
|
81
|
+
if (task.status === "closed" || task.status === "superseded" || task.handoff)
|
|
82
|
+
throw new Error(`Terminal or handed-off task cannot approve ${gate}`);
|
|
83
|
+
const target = gateTarget[gate];
|
|
84
|
+
if (!target)
|
|
85
|
+
throw new Error(`Gate ${gate} cannot be approved`);
|
|
86
|
+
if (task.phase === target)
|
|
87
|
+
return { task, nextAction: nextAction(task, root), idempotent: true };
|
|
88
|
+
if (task.gate !== gate || task.status !== "blocked_on_user")
|
|
89
|
+
throw new Error(`Task must be blocked_on_user at ${gate}`);
|
|
90
|
+
const errors = checkGate(root, state, taskId, gate).filter((item) => item.level === "ERROR");
|
|
91
|
+
if (errors.length)
|
|
92
|
+
throw new Error(errors.map((item) => item.message).join("; "));
|
|
93
|
+
task.evidence.push({ kind: "approval", gate, result: "pass", source, recordedAt });
|
|
94
|
+
transitionTask(state, taskId, target);
|
|
95
|
+
return { task, nextAction: nextAction(task, root), idempotent: false };
|
|
96
|
+
};
|
|
97
|
+
export const formatDiagnostics = (diagnostics) => diagnostics.map((item) => `${item.level} ${item.code}: ${item.message}`).join("\n");
|
|
98
|
+
const oneLine = (value) => value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").replace(/ACTION\s+REQUIRED/gi, "ACTION-REQUIRED").trim();
|
|
99
|
+
const markdownText = (value) => oneLine(value).replace(/([\\`*_{}\[\]()<>#+.!|>~])/g, "\\$1");
|
|
100
|
+
const gateMeta = {
|
|
101
|
+
G0_confirm: { icon: "🟢", title: "GATE G0 · CONFIRM INTENT", action: "Reply `ok` to approve intent, or state changes." },
|
|
102
|
+
G1_review: { icon: "🔵", title: "GATE G1 · REVIEW PLAN", action: "Reply `approve` to start build, or state decision changes." },
|
|
103
|
+
G2_codereview: { icon: "🟣", title: "GATE G2 · REVIEW CODE", action: "Approve to wrap, or point out what to fix." }
|
|
104
|
+
};
|
|
105
|
+
const artifactRows = (task) => Object.entries(task.artifacts).flatMap(([name, path]) => path ? [{ name: oneLine(name), path: oneLine(path) }] : []);
|
|
106
|
+
const markdownLinkTarget = (value) => encodeURI(value).replace(/[()#?]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
107
|
+
export const gateView = (task, diagnostics) => {
|
|
108
|
+
if (task.gate === "none")
|
|
109
|
+
throw new Error(`Task has no human gate: ${task.id}`);
|
|
110
|
+
if (task.status !== "blocked_on_user")
|
|
111
|
+
throw new Error(`Task must be blocked_on_user before presenting ${task.gate}`);
|
|
112
|
+
const errors = diagnostics.filter((item) => item.level === "ERROR");
|
|
113
|
+
if (errors.length)
|
|
114
|
+
throw new Error(errors.map((item) => item.message).join("; "));
|
|
115
|
+
const meta = gateMeta[task.gate];
|
|
116
|
+
const decisions = task.decisions.reduce((counts, item) => ({ ...counts, [item.status]: (counts[item.status] ?? 0) + 1 }), {});
|
|
117
|
+
const latestEvidence = task.evidence.filter((item) => item.kind === "test" || item.kind === "lint" || item.kind === "review").slice(-5).map((item) => ({ kind: item.kind, result: item.result, source: oneLine(item.source) }));
|
|
118
|
+
return {
|
|
119
|
+
schemaVersion: 1,
|
|
120
|
+
gate: task.gate,
|
|
121
|
+
icon: meta.icon,
|
|
122
|
+
title: meta.title,
|
|
123
|
+
action: meta.action,
|
|
124
|
+
task: { id: task.id, title: oneLine(task.title), status: task.status, risk: task.risk, areas: task.areas },
|
|
125
|
+
artifacts: artifactRows(task),
|
|
126
|
+
decisions,
|
|
127
|
+
execution: task.tasks.reduce((counts, item) => ({ ...counts, [item.status]: (counts[item.status] ?? 0) + 1 }), {}),
|
|
128
|
+
evidence: latestEvidence,
|
|
129
|
+
diagnostics: diagnostics.filter((item) => item.level !== "INFO").map((item) => ({ level: item.level, code: item.code, message: oneLine(item.message) }))
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
export const formatGateView = (task, diagnostics, format = "markdown") => {
|
|
133
|
+
const view = gateView(task, diagnostics);
|
|
134
|
+
if (format === "json")
|
|
135
|
+
return `${JSON.stringify(view, null, 2)}\n`;
|
|
136
|
+
const artifactText = view.artifacts.length ? view.artifacts.map((item) => format === "markdown" ? `- **${markdownText(item.name)}:** [${markdownText(item.path)}](${markdownLinkTarget(item.path)})` : `- ${item.name}: ${item.path}`).join("\n") : "- none";
|
|
137
|
+
const decisionText = Object.entries(view.decisions).map(([status, count]) => `${count} ${status}`).join(" · ") || "none";
|
|
138
|
+
const executionText = Object.entries(view.execution).map(([status, count]) => `${count} ${status}`).join(" · ") || "none";
|
|
139
|
+
const evidenceText = view.evidence.length ? view.evidence.map((item) => `- ${item.kind}: ${item.result} — ${format === "markdown" ? markdownText(item.source) : item.source}`).join("\n") : "- none yet";
|
|
140
|
+
const warningText = view.diagnostics.length ? `\n\n${view.diagnostics.map((item) => `- ${item.level} ${item.code}: ${format === "markdown" ? markdownText(item.message) : item.message}`).join("\n")}` : "";
|
|
141
|
+
if (format === "plain")
|
|
142
|
+
return [
|
|
143
|
+
`[IMPORTANT] ${view.icon} ${view.title}`,
|
|
144
|
+
`Task: ${view.task.id} — ${view.task.title}`,
|
|
145
|
+
`Status: ${view.task.status} · Risk: ${view.task.risk} · Areas: ${view.task.areas.join(", ")}`,
|
|
146
|
+
"",
|
|
147
|
+
"Review artifacts",
|
|
148
|
+
artifactText,
|
|
149
|
+
"",
|
|
150
|
+
`Decisions: ${decisionText}`,
|
|
151
|
+
`Execution: ${executionText}`,
|
|
152
|
+
"Evidence",
|
|
153
|
+
evidenceText,
|
|
154
|
+
warningText,
|
|
155
|
+
"",
|
|
156
|
+
`ACTION REQUIRED -> ${oneLine(view.action)}`
|
|
157
|
+
].filter((line) => line !== undefined).join("\n").replace(/\n{3,}/g, "\n\n") + "\n";
|
|
158
|
+
return [
|
|
159
|
+
"> [!IMPORTANT]",
|
|
160
|
+
`> ${view.icon} **${view.title}**`,
|
|
161
|
+
`> **Task:** \`${view.task.id}\` — ${markdownText(view.task.title)}`,
|
|
162
|
+
`> **Status:** \`${view.task.status}\` · **Risk:** \`${view.task.risk}\` · **Areas:** ${view.task.areas.map((area) => `\`${markdownText(area)}\``).join(", ")}`,
|
|
163
|
+
"",
|
|
164
|
+
"### Review artifacts",
|
|
165
|
+
artifactText,
|
|
166
|
+
"",
|
|
167
|
+
`**Decisions:** ${decisionText} `,
|
|
168
|
+
`**Execution:** ${executionText}`,
|
|
169
|
+
"",
|
|
170
|
+
"### Verification evidence",
|
|
171
|
+
evidenceText,
|
|
172
|
+
warningText,
|
|
173
|
+
"",
|
|
174
|
+
`> **ACTION REQUIRED →** ${view.action}`
|
|
175
|
+
].join("\n").replace(/\n{3,}/g, "\n\n") + "\n";
|
|
176
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentId, FileSpec, InitOptions, PlannedWrite, WorkflowManifest } from "./model.js";
|
|
2
|
+
export declare const managedContentHash: (spec: Pick<FileSpec, "owner" | "strategy">, content: string) => string | undefined;
|
|
3
|
+
export declare const readManifest: (root: string) => WorkflowManifest | undefined;
|
|
4
|
+
export declare const detectedAgents: (root: string) => AgentId[];
|
|
5
|
+
export declare const selectAgents: (options: Pick<InitOptions, "root" | "agents" | "all">) => AgentId[];
|
|
6
|
+
export declare const planInit: (options: InitOptions) => PlannedWrite[];
|
|
7
|
+
export declare const applyPlan: (root: string, plan: PlannedWrite[]) => void;
|
|
8
|
+
export declare const formatPlan: (plan: PlannedWrite[]) => string;
|
|
9
|
+
export declare const planUninstall: (root: string) => PlannedWrite[];
|
|
10
|
+
export declare const status: (root: string) => string;
|
|
11
|
+
export declare const doctor: (root: string, strict?: boolean) => string;
|