@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.
Files changed (93) hide show
  1. package/DEVELOPMENT.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +59 -0
  4. package/dist/assets/.agents/aidlc/conventions.md +68 -0
  5. package/dist/assets/.agents/aidlc/orchestrator.md +73 -0
  6. package/dist/assets/.agents/aidlc/phase-build.md +23 -0
  7. package/dist/assets/.agents/aidlc/phase-clarify.md +22 -0
  8. package/dist/assets/.agents/aidlc/phase-index.md +14 -0
  9. package/dist/assets/.agents/aidlc/phase-plan.md +21 -0
  10. package/dist/assets/.agents/aidlc/phase-wrap.md +16 -0
  11. package/dist/assets/.agents/aidlc/profiles/generic/profile.json +9 -0
  12. package/dist/assets/.agents/aidlc/profiles/git-submodules/profile.json +9 -0
  13. package/dist/assets/.agents/aidlc/profiles/single/profile.json +9 -0
  14. package/dist/assets/.agents/aidlc/profiles/workspace/profile.json +12 -0
  15. package/dist/assets/.agents/aidlc/rules/gate-check.md +16 -0
  16. package/dist/assets/.agents/aidlc/rules/stack-runtime.md +3 -0
  17. package/dist/assets/.agents/aidlc/schemas/adapter.schema.json +13 -0
  18. package/dist/assets/.agents/aidlc/schemas/config.schema.json +26 -0
  19. package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +12 -0
  20. package/dist/assets/.agents/aidlc/schemas/profile.schema.json +12 -0
  21. package/dist/assets/.agents/aidlc/schemas/state.schema.json +108 -0
  22. package/dist/assets/.agents/aidlc/scripts/context.mjs +15 -0
  23. package/dist/assets/.agents/aidlc/scripts/gate-check.mjs +16 -0
  24. package/dist/assets/.agents/aidlc/scripts/gate-view.mjs +16 -0
  25. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +238 -0
  26. package/dist/assets/.agents/aidlc/scripts/lib/runtime.mjs +2 -0
  27. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +341 -0
  28. package/dist/assets/.agents/aidlc/scripts/render.mjs +24 -0
  29. package/dist/assets/.agents/aidlc/scripts/state.mjs +222 -0
  30. package/dist/assets/.agents/aidlc/scripts/task-next.mjs +27 -0
  31. package/dist/assets/.agents/aidlc/templates/design.md +15 -0
  32. package/dist/assets/.agents/aidlc/templates/intent.md +17 -0
  33. package/dist/assets/.agents/aidlc/templates/lesson.md +28 -0
  34. package/dist/assets/.agents/aidlc/templates/model-contract.md +41 -0
  35. package/dist/assets/.agents/aidlc/templates/workplan.md +9 -0
  36. package/dist/assets/.agents/skills/aidlc-build/SKILL.md +19 -0
  37. package/dist/assets/.agents/skills/aidlc-clarify/SKILL.md +17 -0
  38. package/dist/assets/.agents/skills/aidlc-index/SKILL.md +17 -0
  39. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +17 -0
  40. package/dist/assets/.agents/skills/aidlc-wrap/SKILL.md +17 -0
  41. package/dist/services/aidlc-coordination/Dockerfile +10 -0
  42. package/dist/services/aidlc-coordination/compose.yaml +25 -0
  43. package/dist/services/aidlc-coordination/package-lock.json +1154 -0
  44. package/dist/services/aidlc-coordination/package.json +20 -0
  45. package/dist/services/aidlc-coordination/src/coordination.mjs +68 -0
  46. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +341 -0
  47. package/dist/services/aidlc-coordination/src/providers.mjs +23 -0
  48. package/dist/services/aidlc-coordination/src/repository.mjs +203 -0
  49. package/dist/services/aidlc-coordination/src/server.mjs +49 -0
  50. package/dist/services/aidlc-coordination/test/repository.test.mjs +211 -0
  51. package/dist/src/adapters.d.ts +7 -0
  52. package/dist/src/adapters.js +86 -0
  53. package/dist/src/cli.d.ts +2 -0
  54. package/dist/src/cli.js +192 -0
  55. package/dist/src/context.d.ts +14 -0
  56. package/dist/src/context.js +90 -0
  57. package/dist/src/gate.d.ts +40 -0
  58. package/dist/src/gate.js +176 -0
  59. package/dist/src/installer.d.ts +11 -0
  60. package/dist/src/installer.js +173 -0
  61. package/dist/src/ledger.d.ts +22 -0
  62. package/dist/src/ledger.js +119 -0
  63. package/dist/src/legacy.d.ts +2 -0
  64. package/dist/src/legacy.js +37 -0
  65. package/dist/src/managed-content.d.ts +3 -0
  66. package/dist/src/managed-content.js +16 -0
  67. package/dist/src/mcp-setup.d.ts +20 -0
  68. package/dist/src/mcp-setup.js +31 -0
  69. package/dist/src/model.d.ts +249 -0
  70. package/dist/src/model.js +1 -0
  71. package/dist/src/profiles.d.ts +28 -0
  72. package/dist/src/profiles.js +216 -0
  73. package/dist/src/project-migration.d.ts +2 -0
  74. package/dist/src/project-migration.js +144 -0
  75. package/dist/src/project-path.d.ts +2 -0
  76. package/dist/src/project-path.js +20 -0
  77. package/dist/src/state.d.ts +110 -0
  78. package/dist/src/state.js +764 -0
  79. package/dist/src/tracker.d.ts +29 -0
  80. package/dist/src/tracker.js +41 -0
  81. package/dist/src/upgrade.d.ts +11 -0
  82. package/dist/src/upgrade.js +270 -0
  83. package/dist/src/workflow.d.ts +7 -0
  84. package/dist/src/workflow.js +46 -0
  85. package/docs/README.md +31 -0
  86. package/docs/architecture.md +72 -0
  87. package/docs/command-reference.md +110 -0
  88. package/docs/configuration.md +72 -0
  89. package/docs/coordination-service.md +42 -0
  90. package/docs/development.md +106 -0
  91. package/docs/operating-workflow.md +87 -0
  92. package/docs/testing-and-release.md +113 -0
  93. package/package.json +53 -0
@@ -0,0 +1,249 @@
1
+ export type AgentId = string;
2
+ export type StateMutationMode = "native" | "scripted";
3
+ export type Phase = "clarify" | "plan" | "build" | "wrap" | "done";
4
+ export type Gate = "none" | "G0_confirm" | "G1_review" | "G2_codereview";
5
+ export type TaskStatus = "active" | "blocked_on_user" | "paused" | "done" | "closed" | "superseded";
6
+ export type RiskLevel = "low" | "normal" | "high" | "regulated";
7
+ export type DecisionStatus = "unresolved" | "approved" | "changed" | "dropped";
8
+ export type HandoffKind = "repair_exhausted" | "review_exhausted" | "g2_failed" | "release_failed" | "structural_change" | "other";
9
+ export interface FileSpec {
10
+ path: string;
11
+ content: string;
12
+ owner: string;
13
+ strategy?: "replace" | "managed-block";
14
+ ownershipClass?: "managed" | "project" | "state";
15
+ }
16
+ export interface Adapter {
17
+ id: AgentId;
18
+ displayName: string;
19
+ detect(root: string): boolean;
20
+ files(): FileSpec[];
21
+ }
22
+ export interface PlannedWrite extends FileSpec {
23
+ action: "create" | "update" | "delete" | "skip" | "conflict" | "migrate" | "preserve";
24
+ reason: string;
25
+ contentEncoding?: "base64";
26
+ }
27
+ export interface InitOptions {
28
+ root: string;
29
+ agents: AgentId[];
30
+ all: boolean;
31
+ dryRun: boolean;
32
+ yes: boolean;
33
+ force: boolean;
34
+ }
35
+ export interface CommandSpec {
36
+ command: string;
37
+ args: string[];
38
+ }
39
+ export interface Profile {
40
+ schemaVersion: 1;
41
+ id: string;
42
+ extends?: string[];
43
+ topology: string;
44
+ discovery?: {
45
+ roots?: string[];
46
+ workspaceMarkers?: string[];
47
+ };
48
+ specs?: {
49
+ roots?: string[];
50
+ };
51
+ commands?: Record<string, CommandSpec>;
52
+ rules?: {
53
+ include?: string[];
54
+ };
55
+ }
56
+ export interface ProjectConfig {
57
+ schemaVersion: 1 | 2 | 3;
58
+ extends: string[];
59
+ specs: {
60
+ roots: string[];
61
+ };
62
+ commands: Record<string, CommandSpec>;
63
+ rules: {
64
+ include: string[];
65
+ };
66
+ risk: {
67
+ default: RiskLevel;
68
+ };
69
+ context: {
70
+ maxChars: number;
71
+ };
72
+ agentState: Partial<Record<AgentId, StateMutationMode>>;
73
+ tracker?: TrackerConfig;
74
+ mcp: McpConfig;
75
+ }
76
+ export interface McpConfig {
77
+ enabled: boolean;
78
+ endpoint?: string;
79
+ workspace?: string;
80
+ tokenEnv?: string;
81
+ pollMs?: number;
82
+ providers?: Array<"jira" | "trello" | "github-issues">;
83
+ }
84
+ export interface TrackerConfig {
85
+ enabled: boolean;
86
+ provider: "backlog";
87
+ spaceUrl: string;
88
+ project: string;
89
+ tokenEnv: string;
90
+ workflow: "waterfall" | "scrum";
91
+ mapping: {
92
+ statuses?: Partial<Record<"clarify" | "plan" | "build" | "wrap" | "done", number>>;
93
+ gateFieldId?: number;
94
+ };
95
+ }
96
+ export interface TaskDecision {
97
+ id: string;
98
+ label: string;
99
+ status: DecisionStatus;
100
+ resolution?: string;
101
+ }
102
+ export interface Evidence {
103
+ kind: "approval" | "spec" | "test" | "lint" | "review" | "diagnostic";
104
+ gate?: Gate;
105
+ area?: string;
106
+ source: string;
107
+ result: "pass" | "fail" | "skip";
108
+ detail?: string;
109
+ recordedAt: string;
110
+ }
111
+ export interface LessonRecord {
112
+ id: string;
113
+ taskId: string;
114
+ areas: string[];
115
+ summary: string;
116
+ prevention: string;
117
+ example: string;
118
+ promotion: string;
119
+ source: string;
120
+ recordedAt: string;
121
+ }
122
+ export interface LessonDisposition {
123
+ status: "captured" | "none";
124
+ reason?: string;
125
+ source: string;
126
+ recordedAt: string;
127
+ }
128
+ export interface TaskState {
129
+ id: string;
130
+ title: string;
131
+ type: "feature" | "bug" | "refactor" | "infra";
132
+ phase: Phase;
133
+ gate: Gate;
134
+ status: TaskStatus;
135
+ language: "vi" | "en";
136
+ risk: RiskLevel;
137
+ areas: string[];
138
+ branch: string;
139
+ artifacts: {
140
+ intent?: string;
141
+ design?: string;
142
+ workplan?: string;
143
+ };
144
+ decisions: TaskDecision[];
145
+ tasks: Array<{
146
+ id: string;
147
+ label: string;
148
+ status: "todo" | "in_progress" | "done" | "deferred";
149
+ }>;
150
+ evidence: Evidence[];
151
+ lessons?: LessonRecord[];
152
+ lessonDisposition?: LessonDisposition;
153
+ handoff?: {
154
+ kind: HandoffKind;
155
+ reason: string;
156
+ source: string;
157
+ recordedAt: string;
158
+ };
159
+ closure?: {
160
+ reason: string;
161
+ source: string;
162
+ recordedAt: string;
163
+ };
164
+ legacyG2Wait?: {
165
+ migratedAt: string;
166
+ source: string;
167
+ };
168
+ predecessorTaskId?: string;
169
+ successorTaskId?: string;
170
+ createdAt: string;
171
+ updatedAt: string;
172
+ }
173
+ export interface WorkflowState {
174
+ schemaVersion: 1 | 2 | 3 | 4;
175
+ tasks: Record<string, TaskState>;
176
+ archive?: Record<string, ArchivedTaskSummary>;
177
+ }
178
+ export interface ArchivedTaskSummary {
179
+ id: string;
180
+ title: string;
181
+ type: TaskState["type"];
182
+ phase: Phase;
183
+ gate: Gate;
184
+ status: "done" | "closed" | "superseded";
185
+ risk: RiskLevel;
186
+ areas: string[];
187
+ record: string;
188
+ digest: string;
189
+ lessonCount: number;
190
+ predecessorTaskId?: string;
191
+ successorTaskId?: string;
192
+ createdAt: string;
193
+ updatedAt: string;
194
+ }
195
+ export interface LessonIndexEntry {
196
+ id: string;
197
+ taskId: string;
198
+ areas: string[];
199
+ summary: string;
200
+ prevention: string;
201
+ source: string;
202
+ recordedAt: string;
203
+ }
204
+ export interface LessonIndex {
205
+ schemaVersion: 1;
206
+ sourceDigest: string;
207
+ lessons: LessonIndexEntry[];
208
+ }
209
+ export interface AgenticMemoryEntry {
210
+ id: string;
211
+ summary: string;
212
+ guidance: string;
213
+ areas: string[];
214
+ phases: Array<Exclude<Phase, "done"> | "*">;
215
+ priority: number;
216
+ sourceTaskId: string;
217
+ sourceLessonId: string;
218
+ approvedBy: string;
219
+ approvedAt: string;
220
+ }
221
+ export interface AgenticMemoryRegistry {
222
+ schemaVersion: 1;
223
+ entries: AgenticMemoryEntry[];
224
+ retired: Array<AgenticMemoryEntry & {
225
+ retiredBy: string;
226
+ retiredAt: string;
227
+ reason: string;
228
+ }>;
229
+ }
230
+ export interface ManifestInventoryItem {
231
+ hash: string;
232
+ owner: string;
233
+ strategy: "replace" | "managed-block";
234
+ }
235
+ export interface WorkflowManifest {
236
+ schemaVersion: 2;
237
+ packageVersion: string;
238
+ workflow: "AI-DLC";
239
+ source: "local-package-assets";
240
+ remoteUpdates: false;
241
+ managedBy: "aidlc-workflow";
242
+ adapters: AgentId[];
243
+ files: Record<string, ManifestInventoryItem>;
244
+ }
245
+ export interface Diagnostic {
246
+ level: "ERROR" | "WARN" | "INFO";
247
+ code: string;
248
+ message: string;
249
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import type { McpConfig, Profile, ProjectConfig, RiskLevel } from "./model.js";
2
+ export declare const builtInProfiles: Readonly<Record<string, Profile>>;
3
+ export declare const validateProfile: (value: unknown, label?: string) => Profile;
4
+ export declare const defaultConfig: () => ProjectConfig;
5
+ export declare const loadProjectConfig: (root: string) => ProjectConfig;
6
+ export declare const resolveProfiles: (root: string, ids: string[]) => Profile[];
7
+ export declare const resolveEffectiveConfig: (root: string, config: ProjectConfig) => {
8
+ profiles: Profile[];
9
+ discovery: {
10
+ roots: string[];
11
+ workspaceMarkers: string[];
12
+ };
13
+ specs: {
14
+ roots: string[];
15
+ };
16
+ commands: any;
17
+ rules: {
18
+ include: string[];
19
+ };
20
+ risk: {
21
+ default: RiskLevel;
22
+ };
23
+ context: {
24
+ maxChars: number;
25
+ };
26
+ mcp: McpConfig;
27
+ };
28
+ export declare const includedRuleFiles: (root: string, patterns: string[]) => string[];
@@ -0,0 +1,216 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
3
+ const profile = (id, markers = []) => ({
4
+ schemaVersion: 1,
5
+ id: `topology/${id}`,
6
+ topology: id,
7
+ discovery: { roots: ["."], workspaceMarkers: markers },
8
+ specs: { roots: [] },
9
+ commands: {},
10
+ rules: { include: [] }
11
+ });
12
+ export const builtInProfiles = Object.freeze({
13
+ "topology/generic": profile("generic"),
14
+ "topology/single": profile("single"),
15
+ "topology/workspace": profile("workspace", ["package.json", "pnpm-workspace.yaml", "lerna.json"]),
16
+ "topology/git-submodules": profile("git-submodules", [".gitmodules"])
17
+ });
18
+ const object = (value, label) => {
19
+ if (!value || typeof value !== "object" || Array.isArray(value))
20
+ throw new Error(`${label} must be an object`);
21
+ return value;
22
+ };
23
+ const strings = (value, label) => {
24
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
25
+ throw new Error(`${label} must be a string array`);
26
+ return value;
27
+ };
28
+ const command = (value, label) => {
29
+ const item = object(value, label);
30
+ if (typeof item.command !== "string" || !item.command || isAbsolute(item.command))
31
+ throw new Error(`${label}.command must be a non-absolute executable name`);
32
+ return { command: item.command, args: strings(item.args ?? [], `${label}.args`) };
33
+ };
34
+ const commands = (value, label) => {
35
+ if (value === undefined)
36
+ return {};
37
+ return Object.fromEntries(Object.entries(object(value, label)).map(([key, item]) => [key, command(item, `${label}.${key}`)]));
38
+ };
39
+ const safeRelative = (path, label) => {
40
+ const cleaned = normalize(path);
41
+ if (isAbsolute(cleaned) || cleaned === ".." || cleaned.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`))
42
+ throw new Error(`${label} must stay inside the project`);
43
+ return cleaned;
44
+ };
45
+ const tracker = (value) => {
46
+ if (value === undefined)
47
+ return undefined;
48
+ const item = object(value, "config.tracker");
49
+ if (item.enabled === false)
50
+ return undefined;
51
+ if (item.enabled !== true || item.provider !== "backlog" || typeof item.spaceUrl !== "string" || !/^https:\/\/[^/]+\/?$/.test(item.spaceUrl) || typeof item.project !== "string" || !item.project.trim() || typeof item.tokenEnv !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(item.tokenEnv) || !["waterfall", "scrum"].includes(String(item.workflow)))
52
+ throw new Error("config.tracker must be a complete enabled Backlog configuration");
53
+ const mapping = object(item.mapping ?? {}, "config.tracker.mapping");
54
+ const statuses = mapping.statuses === undefined ? undefined : object(mapping.statuses, "config.tracker.mapping.statuses");
55
+ const statusValues = statuses ? Object.fromEntries(Object.entries(statuses).map(([phase, id]) => {
56
+ if (!["clarify", "plan", "build", "wrap", "done"].includes(phase) || !Number.isInteger(id) || Number(id) < 1)
57
+ throw new Error("config.tracker.mapping.statuses is invalid");
58
+ return [phase, Number(id)];
59
+ })) : undefined;
60
+ if (mapping.gateFieldId !== undefined && (!Number.isInteger(mapping.gateFieldId) || Number(mapping.gateFieldId) < 1))
61
+ throw new Error("config.tracker.mapping.gateFieldId is invalid");
62
+ return { enabled: true, provider: "backlog", spaceUrl: item.spaceUrl.replace(/\/$/, ""), project: item.project, tokenEnv: item.tokenEnv, workflow: item.workflow, mapping: { ...(statusValues ? { statuses: statusValues } : {}), ...(mapping.gateFieldId === undefined ? {} : { gateFieldId: mapping.gateFieldId }) } };
63
+ };
64
+ const mcp = (value) => {
65
+ if (value === undefined)
66
+ return { enabled: false };
67
+ const item = object(value, "config.mcp");
68
+ if (item.enabled === false)
69
+ return { enabled: false };
70
+ if (item.enabled !== true || typeof item.endpoint !== "string" || !/^https?:\/\//.test(item.endpoint) || typeof item.workspace !== "string" || !item.workspace.trim())
71
+ throw new Error("config.mcp.enabled requires endpoint and workspace");
72
+ if (item.tokenEnv !== undefined && (typeof item.tokenEnv !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(item.tokenEnv)))
73
+ throw new Error("config.mcp.tokenEnv is invalid");
74
+ if (item.pollMs !== undefined && (!Number.isInteger(item.pollMs) || Number(item.pollMs) < 1_000 || Number(item.pollMs) > 3_600_000))
75
+ throw new Error("config.mcp.pollMs must be an integer from 1000 to 3600000");
76
+ const providers = item.providers === undefined ? undefined : strings(item.providers, "config.mcp.providers");
77
+ if (providers?.some((provider) => !["jira", "trello", "github-issues"].includes(provider)))
78
+ throw new Error("config.mcp.providers is invalid");
79
+ return { enabled: true, endpoint: item.endpoint, workspace: item.workspace, ...(item.tokenEnv === undefined ? {} : { tokenEnv: item.tokenEnv }), ...(item.pollMs === undefined ? {} : { pollMs: Number(item.pollMs) }), ...(providers === undefined ? {} : { providers: providers }) };
80
+ };
81
+ export const validateProfile = (value, label = "profile") => {
82
+ const item = object(value, label);
83
+ if (item.schemaVersion !== 1 || typeof item.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/.test(item.id) || typeof item.topology !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/.test(item.topology))
84
+ throw new Error(`${label} has an unsupported schema, id, or topology`);
85
+ const discovery = item.discovery === undefined ? undefined : object(item.discovery, `${label}.discovery`);
86
+ const specs = item.specs === undefined ? undefined : object(item.specs, `${label}.specs`);
87
+ const rules = item.rules === undefined ? undefined : object(item.rules, `${label}.rules`);
88
+ return {
89
+ schemaVersion: 1,
90
+ id: item.id,
91
+ extends: item.extends === undefined ? [] : strings(item.extends, `${label}.extends`),
92
+ topology: item.topology,
93
+ discovery: discovery ? {
94
+ roots: strings(discovery.roots ?? [], `${label}.discovery.roots`).map((path) => safeRelative(path, `${label}.discovery.roots`)),
95
+ workspaceMarkers: strings(discovery.workspaceMarkers ?? [], `${label}.discovery.workspaceMarkers`).map((path) => safeRelative(path, `${label}.discovery.workspaceMarkers`))
96
+ } : undefined,
97
+ specs: specs ? { roots: strings(specs.roots ?? [], `${label}.specs.roots`).map((path) => safeRelative(path, `${label}.specs.roots`)) } : undefined,
98
+ commands: commands(item.commands, `${label}.commands`),
99
+ rules: rules ? { include: strings(rules.include ?? [], `${label}.rules.include`).map((path) => safeRelative(path, `${label}.rules.include`)) } : undefined
100
+ };
101
+ };
102
+ export const defaultConfig = () => ({
103
+ schemaVersion: 2,
104
+ extends: ["topology/single"],
105
+ specs: { roots: [] },
106
+ commands: {},
107
+ rules: { include: [] },
108
+ risk: { default: "normal" },
109
+ context: { maxChars: 16_000 },
110
+ agentState: {},
111
+ mcp: { enabled: false }
112
+ });
113
+ export const loadProjectConfig = (root) => {
114
+ const path = join(resolve(root), ".agents/config.json");
115
+ if (!existsSync(path))
116
+ return defaultConfig();
117
+ const item = object(JSON.parse(readFileSync(path, "utf8")), ".agents/config.json");
118
+ const risk = object(item.risk ?? {}, "config.risk");
119
+ const context = object(item.context ?? {}, "config.context");
120
+ const levels = ["low", "normal", "high", "regulated"];
121
+ const riskDefault = String(risk.default ?? "normal");
122
+ if (![1, 2, 3].includes(Number(item.schemaVersion)) || !levels.includes(riskDefault))
123
+ throw new Error("Unsupported project config schema or risk level");
124
+ const agentStateRaw = object(item.agentState ?? {}, "config.agentState");
125
+ const agentState = Object.fromEntries(Object.entries(agentStateRaw).map(([id, mode]) => {
126
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id) || (mode !== "native" && mode !== "scripted"))
127
+ throw new Error("config.agentState must map adapter ids to native or scripted");
128
+ return [id, mode];
129
+ }));
130
+ const maxChars = Number(context.maxChars ?? 16_000);
131
+ if (!Number.isInteger(maxChars) || maxChars < 4_000 || maxChars > 128_000)
132
+ throw new Error("context.maxChars must be an integer from 4000 to 128000");
133
+ return {
134
+ schemaVersion: Number(item.schemaVersion),
135
+ extends: strings(item.extends ?? ["topology/single"], "config.extends"),
136
+ specs: { roots: strings(object(item.specs ?? {}, "config.specs").roots ?? [], "config.specs.roots").map((entry) => safeRelative(entry, "config.specs.roots")) },
137
+ commands: commands(item.commands, "config.commands"),
138
+ rules: { include: strings(object(item.rules ?? {}, "config.rules").include ?? [], "config.rules.include").map((entry) => safeRelative(entry, "config.rules.include")) },
139
+ risk: { default: riskDefault },
140
+ context: { maxChars },
141
+ agentState,
142
+ tracker: tracker(item.tracker),
143
+ mcp: mcp(item.mcp)
144
+ };
145
+ };
146
+ const readLocalProfile = (root, id) => {
147
+ const path = join(resolve(root), ".agents/project/profiles", safeRelative(id, "profile id"), "profile.json");
148
+ if (!existsSync(path))
149
+ return undefined;
150
+ if (lstatSync(path).isSymbolicLink() || !realpathSync(path).startsWith(`${realpathSync(resolve(root))}${process.platform === "win32" ? "\\" : "/"}`))
151
+ throw new Error(`Profile escapes the project: ${id}`);
152
+ return validateProfile(JSON.parse(readFileSync(path, "utf8")), id);
153
+ };
154
+ export const resolveProfiles = (root, ids) => {
155
+ const resolved = [];
156
+ const visiting = new Set();
157
+ const visit = (id) => {
158
+ const resolvedId = builtInProfiles[id] ? id : builtInProfiles[`topology/${id}`] ? `topology/${id}` : id;
159
+ if (resolved.some((entry) => entry.id === resolvedId))
160
+ return;
161
+ if (visiting.has(resolvedId))
162
+ throw new Error(`Profile cycle detected at ${resolvedId}`);
163
+ visiting.add(resolvedId);
164
+ const current = builtInProfiles[resolvedId] ?? readLocalProfile(root, resolvedId);
165
+ if (!current)
166
+ throw new Error(`Unknown profile: ${id}`);
167
+ for (const parent of current.extends ?? [])
168
+ visit(parent);
169
+ visiting.delete(resolvedId);
170
+ resolved.push(current);
171
+ };
172
+ ids.forEach(visit);
173
+ return resolved;
174
+ };
175
+ const stable = (values) => [...new Set(values)];
176
+ export const resolveEffectiveConfig = (root, config) => {
177
+ const profiles = resolveProfiles(root, config.extends);
178
+ return {
179
+ profiles,
180
+ discovery: { roots: stable(profiles.flatMap((item) => item.discovery?.roots ?? ["."])), workspaceMarkers: stable(profiles.flatMap((item) => item.discovery?.workspaceMarkers ?? [])) },
181
+ specs: { roots: stable([...profiles.flatMap((item) => item.specs?.roots ?? []), ...config.specs.roots]) },
182
+ commands: Object.assign({}, ...profiles.map((item) => item.commands ?? {}), config.commands),
183
+ rules: { include: stable([...profiles.flatMap((item) => item.rules?.include ?? []), ...config.rules.include]) },
184
+ risk: config.risk,
185
+ context: config.context,
186
+ mcp: config.mcp
187
+ };
188
+ };
189
+ export const includedRuleFiles = (root, patterns) => {
190
+ const project = resolve(root);
191
+ const realProject = realpathSync(project);
192
+ const inside = (path) => {
193
+ if (lstatSync(path).isSymbolicLink())
194
+ return false;
195
+ return realpathSync(path).startsWith(`${realProject}${process.platform === "win32" ? "\\" : "/"}`);
196
+ };
197
+ const files = new Set();
198
+ for (const pattern of patterns) {
199
+ const safe = safeRelative(pattern, "rule include");
200
+ if (safe.endsWith("/*.md")) {
201
+ const directory = join(project, safe.slice(0, -5));
202
+ if (existsSync(directory) && inside(directory))
203
+ for (const entry of readdirSync(directory)) {
204
+ const path = join(directory, entry);
205
+ if (entry.endsWith(".md") && existsSync(path) && inside(path))
206
+ files.add(path);
207
+ }
208
+ }
209
+ else {
210
+ const target = join(project, safe);
211
+ if (existsSync(target) && inside(target))
212
+ files.add(target);
213
+ }
214
+ }
215
+ return [...files].filter((path) => dirname(path).startsWith(project) || path.startsWith(project)).sort();
216
+ };
@@ -0,0 +1,2 @@
1
+ import type { PlannedWrite } from "./model.js";
2
+ export declare const planProjectLayoutMigration: (project: string, plan: PlannedWrite[]) => void;
@@ -0,0 +1,144 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
2
+ import { resolveProjectPath } from "./project-path.js";
3
+ const safePath = (root, relative) => resolveProjectPath(root, relative, "Unsafe migration path");
4
+ const legacyReference = (value) => {
5
+ if (value === ".aidlc/config.json")
6
+ return ".agents/config.json";
7
+ if (value === ".aidlc/index")
8
+ return ".agents/index";
9
+ if (value.startsWith(".aidlc/index/"))
10
+ return `.agents/data/index/${value.slice(".aidlc/index/".length)}`;
11
+ if (value === ".aidlc")
12
+ return ".agents/project";
13
+ if (value.startsWith(".aidlc/"))
14
+ return `.agents/project/${value.slice(".aidlc/".length)}`;
15
+ return value;
16
+ };
17
+ const rewriteStrings = (value) => Array.isArray(value) ? value.map((entry) => typeof entry === "string" ? legacyReference(entry) : entry) : value;
18
+ const rewriteKnownJson = (relative, content) => {
19
+ const isConfig = relative === "config.json";
20
+ const isProfile = /^profiles\/.+\/profile\.json$/.test(relative);
21
+ const isAdapter = /^adapters\/.+\/adapter\.json$/.test(relative);
22
+ if (!isConfig && !isProfile && !isAdapter)
23
+ return undefined;
24
+ const source = content.toString("utf8");
25
+ const value = JSON.parse(source);
26
+ const before = JSON.stringify(value);
27
+ if (isConfig || isProfile) {
28
+ const discovery = value.discovery;
29
+ const specs = value.specs;
30
+ const rules = value.rules;
31
+ if (discovery) {
32
+ discovery.roots = rewriteStrings(discovery.roots);
33
+ discovery.workspaceMarkers = rewriteStrings(discovery.workspaceMarkers);
34
+ }
35
+ if (specs)
36
+ specs.roots = rewriteStrings(specs.roots);
37
+ if (rules)
38
+ rules.include = rewriteStrings(rules.include);
39
+ }
40
+ if (isAdapter) {
41
+ value.detect = rewriteStrings(value.detect);
42
+ if (Array.isArray(value.files))
43
+ for (const file of value.files) {
44
+ if (file && typeof file === "object" && typeof file.path === "string") {
45
+ file.path = legacyReference(file.path);
46
+ }
47
+ }
48
+ }
49
+ const changed = before !== JSON.stringify(value);
50
+ return { content: changed ? `${JSON.stringify(value, null, 2)}\n` : source, changed };
51
+ };
52
+ const migrationPayload = (relative, source) => {
53
+ const rewritten = rewriteKnownJson(relative, source);
54
+ return rewritten ? { content: rewritten.content } : { content: source.toString("base64"), contentEncoding: "base64" };
55
+ };
56
+ const payloadBuffer = (item) => item.contentEncoding === "base64" ? Buffer.from(item.content, "base64") : Buffer.from(item.content, "utf8");
57
+ const legacyDestination = (relative) => {
58
+ if (relative === "config.json")
59
+ return ".agents/config.json";
60
+ if (relative === "index")
61
+ return ".agents/index";
62
+ if (relative.startsWith("index/"))
63
+ return `.agents/data/index/${relative.slice("index/".length)}`;
64
+ return `.agents/project/${relative}`;
65
+ };
66
+ const planMigrationFile = (project, plan, sourcePath, destinationPath, legacyRelative) => {
67
+ const source = safePath(project, sourcePath);
68
+ const payload = migrationPayload(legacyRelative, readFileSync(source));
69
+ const destination = safePath(project, destinationPath);
70
+ const existingPlan = plan.find((item) => item.path === destinationPath);
71
+ const destinationContent = existingPlan && ["update", "migrate"].includes(existingPlan.action)
72
+ ? payloadBuffer(existingPlan)
73
+ : existsSync(destination) && lstatSync(destination).isFile() ? readFileSync(destination) : undefined;
74
+ if (existsSync(destination) && !lstatSync(destination).isFile()) {
75
+ plan.push({ path: destinationPath, owner: "aidlc-project", ownershipClass: "project", content: "", action: "conflict", reason: `legacy migration destination is not a regular file: ${sourcePath}` });
76
+ return;
77
+ }
78
+ if (destinationContent) {
79
+ if (!destinationContent.equals(payloadBuffer(payload))) {
80
+ plan.push({ path: destinationPath, owner: "aidlc-project", ownershipClass: "project", ...payload, action: "conflict", reason: `legacy source differs from existing destination: ${sourcePath}` });
81
+ return;
82
+ }
83
+ }
84
+ else if (existingPlan?.action === "create") {
85
+ if (existingPlan.ownershipClass === "managed" && !payloadBuffer(existingPlan).equals(payloadBuffer(payload))) {
86
+ Object.assign(existingPlan, { action: "conflict", reason: `legacy managed file differs from the new managed content: ${sourcePath}` });
87
+ return;
88
+ }
89
+ Object.assign(existingPlan, payload, { action: "migrate", reason: `migrate legacy project file from ${sourcePath}` });
90
+ }
91
+ else {
92
+ plan.push({ path: destinationPath, owner: "aidlc-project", ownershipClass: "project", ...payload, action: "migrate", reason: `migrate legacy project file from ${sourcePath}` });
93
+ }
94
+ if (!plan.some((item) => item.path === sourcePath && item.action === "delete")) {
95
+ plan.push({ path: sourcePath, owner: "aidlc-project", ownershipClass: "project", content: "", action: "delete", reason: `remove migrated legacy project file after creating ${destinationPath}` });
96
+ }
97
+ };
98
+ const planLegacyTreeMigration = (project, plan) => {
99
+ const legacyRoot = safePath(project, ".aidlc");
100
+ if (!existsSync(legacyRoot))
101
+ return;
102
+ if (lstatSync(legacyRoot).isSymbolicLink() || !lstatSync(legacyRoot).isDirectory()) {
103
+ plan.push({ path: ".aidlc", owner: "aidlc-project", ownershipClass: "project", content: "", action: "conflict", reason: "legacy migration root must be a real directory" });
104
+ return;
105
+ }
106
+ const visit = (directory, prefix = "") => {
107
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
108
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
109
+ const sourcePath = `.aidlc/${relative}`;
110
+ if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile())) {
111
+ plan.push({ path: sourcePath, owner: "aidlc-project", ownershipClass: "project", content: "", action: "conflict", reason: "legacy migration refuses symlink or special filesystem entry" });
112
+ }
113
+ else if (entry.isDirectory())
114
+ visit(safePath(project, sourcePath), relative);
115
+ else
116
+ planMigrationFile(project, plan, sourcePath, legacyDestination(relative), relative);
117
+ }
118
+ };
119
+ visit(legacyRoot);
120
+ };
121
+ const planExistingConfigRewrite = (project, plan) => {
122
+ const path = ".agents/config.json";
123
+ const target = safePath(project, path);
124
+ if (!existsSync(target))
125
+ return;
126
+ const rewritten = rewriteKnownJson("config.json", readFileSync(target));
127
+ if (!rewritten?.changed)
128
+ return;
129
+ const item = plan.find((candidate) => candidate.path === path);
130
+ if (item)
131
+ Object.assign(item, { content: rewritten.content, action: "migrate", reason: "rewrite legacy .aidlc references in canonical project config" });
132
+ };
133
+ const planLegacyIndexMigration = (project, plan) => {
134
+ for (const name of ["repo-map.md", "specs-index.md"]) {
135
+ const source = `.agents/data/state/${name}`;
136
+ if (existsSync(safePath(project, source)))
137
+ planMigrationFile(project, plan, source, `.agents/data/index/${name}`, `index/${name}`);
138
+ }
139
+ };
140
+ export const planProjectLayoutMigration = (project, plan) => {
141
+ planExistingConfigRewrite(project, plan);
142
+ planLegacyTreeMigration(project, plan);
143
+ planLegacyIndexMigration(project, plan);
144
+ };
@@ -0,0 +1,2 @@
1
+ export declare const resolveProjectPath: (root: string, path: string, escapeMessage: string) => string;
2
+ export declare const resolveProjectPathWithoutSymlinks: (root: string, path: string, escapeMessage: string, symlinkMessage: string) => string;
@@ -0,0 +1,20 @@
1
+ import { existsSync, lstatSync } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ export const resolveProjectPath = (root, path, escapeMessage) => {
4
+ const project = resolve(root);
5
+ const target = resolve(project, path);
6
+ const fromRoot = relative(project, target);
7
+ if (!path || isAbsolute(path) || fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot))
8
+ throw new Error(`${escapeMessage}: ${path}`);
9
+ return target;
10
+ };
11
+ export const resolveProjectPathWithoutSymlinks = (root, path, escapeMessage, symlinkMessage) => {
12
+ const target = resolveProjectPath(root, path, escapeMessage);
13
+ let cursor = resolve(root);
14
+ for (const part of relative(cursor, target).split(sep).filter(Boolean)) {
15
+ cursor = join(cursor, part);
16
+ if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink())
17
+ throw new Error(`${symlinkMessage}: ${path}`);
18
+ }
19
+ return target;
20
+ };