@felixdotgo/aidlc-kit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/DEVELOPMENT.md +14 -0
  2. package/README.md +14 -6
  3. package/dist/assets/.agents/aidlc/docker/Dockerfile.claude +7 -0
  4. package/dist/assets/.agents/aidlc/docker/Dockerfile.codex +9 -0
  5. package/dist/assets/.agents/aidlc/docker/Dockerfile.opencode +8 -0
  6. package/dist/assets/.agents/aidlc/orchestrator.md +1 -1
  7. package/dist/assets/.agents/aidlc/phase-clarify.md +1 -1
  8. package/dist/assets/.agents/aidlc/phase-plan.md +2 -2
  9. package/dist/assets/.agents/aidlc/schemas/config.schema.json +2 -1
  10. package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +1 -1
  11. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -0
  12. package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +245 -0
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +232 -0
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +21 -4
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +86 -28
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +7 -0
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +22 -6
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +46 -15
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +22 -9
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +23 -5
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +43 -15
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +82 -6
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +60 -0
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +142 -5
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
  27. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +31 -5
  28. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
  29. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +3 -2
  30. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +59 -4
  31. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +85 -73
  32. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
  33. package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
  34. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +2 -0
  35. package/dist/assets/.agents/aidlc/scripts/state.mjs +4 -0
  36. package/dist/assets/.agents/aidlc/templates/design.md +9 -0
  37. package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
  38. package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
  39. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
  40. package/dist/assets/ui/bulma.min.css +3 -0
  41. package/dist/assets/ui/dashboard.css +414 -0
  42. package/dist/services/aidlc-coordination/Dockerfile +11 -3
  43. package/dist/services/aidlc-coordination/compose.yaml +12 -0
  44. package/dist/services/aidlc-coordination/package-lock.json +3 -1
  45. package/dist/services/aidlc-coordination/package.json +9 -4
  46. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +59 -4
  47. package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
  48. package/dist/services/aidlc-coordination/src/server.mjs +9 -3
  49. package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
  50. package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
  51. package/dist/src/adapters.d.ts +0 -3
  52. package/dist/src/adapters.js +11 -6
  53. package/dist/src/cli/argv.d.ts +12 -0
  54. package/dist/src/cli/argv.js +62 -0
  55. package/dist/src/cli/commands.d.ts +8 -0
  56. package/dist/src/cli/commands.js +129 -0
  57. package/dist/src/cli/options.d.ts +25 -0
  58. package/dist/src/cli/options.js +32 -0
  59. package/dist/src/cli/prompt.d.ts +9 -0
  60. package/dist/src/cli/prompt.js +66 -0
  61. package/dist/src/cli.js +67 -217
  62. package/dist/src/context.js +2 -0
  63. package/dist/src/gate.d.ts +1 -0
  64. package/dist/src/gate.js +51 -37
  65. package/dist/src/installer.d.ts +19 -7
  66. package/dist/src/installer.js +111 -178
  67. package/dist/src/legacy.js +1 -0
  68. package/dist/src/managed-content.d.ts +7 -0
  69. package/dist/src/managed-content.js +15 -4
  70. package/dist/src/managed-planning.d.ts +65 -0
  71. package/dist/src/managed-planning.js +219 -0
  72. package/dist/src/mcp-setup.js +3 -1
  73. package/dist/src/model/config-contracts.d.ts +65 -0
  74. package/dist/src/model/config-contracts.js +1 -0
  75. package/dist/src/model/install-contracts.d.ts +29 -0
  76. package/dist/src/model/install-contracts.js +1 -0
  77. package/dist/src/model/lifecycle-enums.d.ts +13 -0
  78. package/dist/src/model/lifecycle-enums.js +1 -0
  79. package/dist/src/model/loop.d.ts +20 -0
  80. package/dist/src/model/loop.js +1 -0
  81. package/dist/src/model/manifest.d.ts +16 -0
  82. package/dist/src/model/manifest.js +1 -0
  83. package/dist/src/model/records.d.ts +138 -0
  84. package/dist/src/model/records.js +1 -0
  85. package/dist/src/model.d.ts +11 -264
  86. package/dist/src/model.js +1 -1
  87. package/dist/src/profiles.d.ts +8 -0
  88. package/dist/src/profiles.js +63 -8
  89. package/dist/src/project-migration.d.ts +1 -0
  90. package/dist/src/project-migration.js +7 -0
  91. package/dist/src/state/archive-record.d.ts +9 -0
  92. package/dist/src/state/archive-record.js +26 -0
  93. package/dist/src/state/commands.d.ts +18 -0
  94. package/dist/src/state/commands.js +27 -0
  95. package/dist/src/state/domain.d.ts +9 -0
  96. package/dist/src/state/domain.js +9 -0
  97. package/dist/src/state/guards.d.ts +7 -0
  98. package/dist/src/state/guards.js +17 -0
  99. package/dist/src/state/io.d.ts +10 -0
  100. package/dist/src/state/io.js +35 -0
  101. package/dist/src/state/items.d.ts +24 -0
  102. package/dist/src/state/items.js +147 -0
  103. package/dist/src/state/lessons.d.ts +7 -0
  104. package/dist/src/state/lessons.js +74 -0
  105. package/dist/src/state/lifecycle.d.ts +5 -0
  106. package/dist/src/state/lifecycle.js +78 -0
  107. package/dist/src/state/lock.d.ts +7 -0
  108. package/dist/src/state/lock.js +125 -0
  109. package/dist/src/state/memory.d.ts +8 -0
  110. package/dist/src/state/memory.js +100 -0
  111. package/dist/src/state/migrate.d.ts +17 -0
  112. package/dist/src/state/migrate.js +60 -0
  113. package/dist/src/state/partition.d.ts +15 -0
  114. package/dist/src/state/partition.js +90 -0
  115. package/dist/src/state/paths.d.ts +11 -0
  116. package/dist/src/state/paths.js +12 -0
  117. package/dist/src/state/render.d.ts +26 -0
  118. package/dist/src/state/render.js +32 -0
  119. package/dist/src/state/store.d.ts +4 -0
  120. package/dist/src/state/store.js +75 -0
  121. package/dist/src/state/transitions.d.ts +21 -0
  122. package/dist/src/state/transitions.js +95 -0
  123. package/dist/src/state/validate.d.ts +4 -0
  124. package/dist/src/state/validate.js +143 -0
  125. package/dist/src/state.d.ts +20 -117
  126. package/dist/src/state.js +20 -815
  127. package/dist/src/task-ui.d.ts +54 -0
  128. package/dist/src/task-ui.js +1570 -0
  129. package/dist/src/upgrade-board.d.ts +5 -0
  130. package/dist/src/upgrade-board.js +89 -0
  131. package/dist/src/upgrade.d.ts +3 -5
  132. package/dist/src/upgrade.js +174 -255
  133. package/dist/src/workflow.js +4 -4
  134. package/docs/README.md +6 -2
  135. package/docs/architecture.md +12 -8
  136. package/docs/command-reference.md +83 -19
  137. package/docs/configuration.md +30 -7
  138. package/docs/coordination-service.md +20 -5
  139. package/docs/design-decisions.md +83 -0
  140. package/docs/development.md +41 -2
  141. package/docs/loop-local-run.md +495 -0
  142. package/docs/loop-runbook.md +235 -0
  143. package/docs/operating-workflow.md +5 -2
  144. package/docs/testing-and-release.md +11 -7
  145. package/docs/troubleshooting.md +125 -0
  146. package/package.json +5 -2
  147. package/dist/src/ledger.d.ts +0 -22
  148. package/dist/src/ledger.js +0 -119
  149. package/dist/src/tracker.d.ts +0 -29
  150. package/dist/src/tracker.js +0 -41
@@ -1,264 +1,11 @@
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
- loop?: LoopConfig;
35
- }
36
- export type LoopMode = "off" | "local" | "fleet";
37
- export declare const LOOP_PROVIDERS: readonly ["codex", "claude", "opencode"];
38
- export type LoopAgent = (typeof LOOP_PROVIDERS)[number];
39
- export interface LoopConfig {
40
- mode: LoopMode;
41
- agents?: {
42
- lead: LoopAgent;
43
- workers: Record<string, LoopAgent>;
44
- };
45
- }
46
- export interface CommandSpec {
47
- command: string;
48
- args: string[];
49
- }
50
- export interface Profile {
51
- schemaVersion: 1;
52
- id: string;
53
- extends?: string[];
54
- topology: string;
55
- discovery?: {
56
- roots?: string[];
57
- workspaceMarkers?: string[];
58
- };
59
- specs?: {
60
- roots?: string[];
61
- };
62
- commands?: Record<string, CommandSpec>;
63
- rules?: {
64
- include?: string[];
65
- };
66
- }
67
- export interface ProjectConfig {
68
- schemaVersion: 1 | 2 | 3;
69
- extends: string[];
70
- specs: {
71
- roots: string[];
72
- };
73
- commands: Record<string, CommandSpec>;
74
- rules: {
75
- include: string[];
76
- };
77
- risk: {
78
- default: RiskLevel;
79
- };
80
- context: {
81
- maxChars: number;
82
- };
83
- agentState: Partial<Record<AgentId, StateMutationMode>>;
84
- tracker?: TrackerConfig;
85
- mcp: McpConfig;
86
- loop?: LoopConfig;
87
- }
88
- export interface McpConfig {
89
- enabled: boolean;
90
- endpoint?: string;
91
- workspace?: string;
92
- tokenEnv?: string;
93
- pollMs?: number;
94
- providers?: Array<"jira" | "trello" | "github-issues">;
95
- }
96
- export interface TrackerConfig {
97
- enabled: boolean;
98
- provider: "backlog";
99
- spaceUrl: string;
100
- project: string;
101
- tokenEnv: string;
102
- workflow: "waterfall" | "scrum";
103
- mapping: {
104
- statuses?: Partial<Record<"clarify" | "plan" | "build" | "wrap" | "done", number>>;
105
- gateFieldId?: number;
106
- };
107
- }
108
- export interface TaskDecision {
109
- id: string;
110
- label: string;
111
- status: DecisionStatus;
112
- resolution?: string;
113
- }
114
- export interface Evidence {
115
- kind: "approval" | "spec" | "test" | "lint" | "review" | "diagnostic";
116
- gate?: Gate;
117
- area?: string;
118
- source: string;
119
- result: "pass" | "fail" | "skip";
120
- detail?: string;
121
- recordedAt: string;
122
- }
123
- export interface LessonRecord {
124
- id: string;
125
- taskId: string;
126
- areas: string[];
127
- summary: string;
128
- prevention: string;
129
- example: string;
130
- promotion: string;
131
- source: string;
132
- recordedAt: string;
133
- }
134
- export interface LessonDisposition {
135
- status: "captured" | "none";
136
- reason?: string;
137
- source: string;
138
- recordedAt: string;
139
- }
140
- export interface TaskState {
141
- id: string;
142
- featureId?: string;
143
- title: string;
144
- type: "feature" | "bug" | "refactor" | "infra";
145
- phase: Phase;
146
- gate: Gate;
147
- status: TaskStatus;
148
- language: "vi" | "en";
149
- risk: RiskLevel;
150
- areas: string[];
151
- branch: string;
152
- artifacts: {
153
- intent?: string;
154
- design?: string;
155
- workplan?: string;
156
- };
157
- decisions: TaskDecision[];
158
- tasks: Array<{
159
- id: string;
160
- label: string;
161
- status: "todo" | "in_progress" | "done" | "deferred";
162
- dependsOn?: string[];
163
- ownedPaths?: string[];
164
- }>;
165
- evidence: Evidence[];
166
- lessons?: LessonRecord[];
167
- lessonDisposition?: LessonDisposition;
168
- handoff?: {
169
- kind: HandoffKind;
170
- reason: string;
171
- source: string;
172
- recordedAt: string;
173
- };
174
- closure?: {
175
- reason: string;
176
- source: string;
177
- recordedAt: string;
178
- };
179
- legacyG2Wait?: {
180
- migratedAt: string;
181
- source: string;
182
- };
183
- predecessorTaskId?: string;
184
- successorTaskId?: string;
185
- createdAt: string;
186
- updatedAt: string;
187
- }
188
- export interface WorkflowState {
189
- schemaVersion: 1 | 2 | 3 | 4;
190
- tasks: Record<string, TaskState>;
191
- archive?: Record<string, ArchivedTaskSummary>;
192
- }
193
- export interface ArchivedTaskSummary {
194
- id: string;
195
- title: string;
196
- type: TaskState["type"];
197
- phase: Phase;
198
- gate: Gate;
199
- status: "done" | "closed" | "superseded";
200
- risk: RiskLevel;
201
- areas: string[];
202
- record: string;
203
- digest: string;
204
- lessonCount: number;
205
- predecessorTaskId?: string;
206
- successorTaskId?: string;
207
- createdAt: string;
208
- updatedAt: string;
209
- }
210
- export interface LessonIndexEntry {
211
- id: string;
212
- taskId: string;
213
- areas: string[];
214
- summary: string;
215
- prevention: string;
216
- source: string;
217
- recordedAt: string;
218
- }
219
- export interface LessonIndex {
220
- schemaVersion: 1;
221
- sourceDigest: string;
222
- lessons: LessonIndexEntry[];
223
- }
224
- export interface AgenticMemoryEntry {
225
- id: string;
226
- summary: string;
227
- guidance: string;
228
- areas: string[];
229
- phases: Array<Exclude<Phase, "done"> | "*">;
230
- priority: number;
231
- sourceTaskId: string;
232
- sourceLessonId: string;
233
- approvedBy: string;
234
- approvedAt: string;
235
- }
236
- export interface AgenticMemoryRegistry {
237
- schemaVersion: 1;
238
- entries: AgenticMemoryEntry[];
239
- retired: Array<AgenticMemoryEntry & {
240
- retiredBy: string;
241
- retiredAt: string;
242
- reason: string;
243
- }>;
244
- }
245
- export interface ManifestInventoryItem {
246
- hash: string;
247
- owner: string;
248
- strategy: "replace" | "managed-block";
249
- }
250
- export interface WorkflowManifest {
251
- schemaVersion: 2;
252
- packageVersion: string;
253
- workflow: "AI-DLC";
254
- source: "local-package-assets";
255
- remoteUpdates: false;
256
- managedBy: "aidlc-workflow";
257
- adapters: AgentId[];
258
- files: Record<string, ManifestInventoryItem>;
259
- }
260
- export interface Diagnostic {
261
- level: "ERROR" | "WARN" | "INFO";
262
- code: string;
263
- message: string;
264
- }
1
+ /**
2
+ * Facade for the shared type hub. Behavior-preserving re-export of the split
3
+ * modules under `src/model/`; import paths from outside remain unchanged.
4
+ */
5
+ export type { AgentId, StateMutationMode, Phase, Gate, TaskStatus, RiskLevel, DecisionStatus, HandoffKind, Diagnostic } from "./model/lifecycle-enums.js";
6
+ export type { FileSpec, Adapter, PlannedWrite, InitOptions } from "./model/install-contracts.js";
7
+ export type { LoopMode, LoopAgent, LoopConfig, LoopLimits } from "./model/loop.js";
8
+ export { LOOP_PROVIDERS } from "./model/loop.js";
9
+ export type { CommandSpec, Profile, ProjectConfig, McpConfig, TrackerConfig } from "./model/config-contracts.js";
10
+ export type { TaskDecision, Evidence, LessonRecord, LessonDisposition, TaskState, WorkflowState, ArchivedTaskSummary, LessonIndexEntry, LessonIndex, AgenticMemoryEntry, AgenticMemoryRegistry } from "./model/records.js";
11
+ export type { ManifestInventoryItem, WorkflowManifest } from "./model/manifest.js";
package/dist/src/model.js CHANGED
@@ -1 +1 @@
1
- export const LOOP_PROVIDERS = Object.freeze(["codex", "claude", "opencode"]);
1
+ export { LOOP_PROVIDERS } from "./model/loop.js";
@@ -1,10 +1,17 @@
1
1
  import type { LoopConfig, McpConfig, Profile, ProjectConfig, RiskLevel } from "./model.js";
2
2
  export declare const builtInProfiles: Readonly<Record<string, Profile>>;
3
+ /** Validates and normalizes a loop-execution configuration block. */
3
4
  export declare const validateLoopConfig: (value: unknown) => LoopConfig;
5
+ /** Validates a single profile file (built-in or local). */
4
6
  export declare const validateProfile: (value: unknown, label?: string) => Profile;
5
7
  export declare const defaultConfig: () => ProjectConfig;
8
+ /** Validates and normalizes a parsed `.agents/config.json` value. */
9
+ export declare const validateProjectConfig: (value: unknown) => ProjectConfig;
10
+ /** Loads and validates `.agents/config.json`, falling back to the default config when absent. */
6
11
  export declare const loadProjectConfig: (root: string) => ProjectConfig;
12
+ /** Resolves a list of profile ids into an ordered, cycle-free list of profiles. */
7
13
  export declare const resolveProfiles: (root: string, ids: string[]) => Profile[];
14
+ /** Merges built-in and local profiles with the project config into the effective configuration. */
8
15
  export declare const resolveEffectiveConfig: (root: string, config: ProjectConfig) => {
9
16
  profiles: Profile[];
10
17
  discovery: {
@@ -27,4 +34,5 @@ export declare const resolveEffectiveConfig: (root: string, config: ProjectConfi
27
34
  mcp: McpConfig;
28
35
  loop: LoopConfig;
29
36
  };
37
+ /** Expands rule include patterns into concrete markdown file paths inside the project. */
30
38
  export declare const includedRuleFiles: (root: string, patterns: string[]) => string[];
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Project profiles and configuration. Loads `.agents/config.json`, validates
3
+ * every nested section (loop, tracker, MCP, risk, context), and resolves the
4
+ * effective configuration by merging built-in and local profiles.
5
+ */
1
6
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
2
7
  import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
3
8
  import { LOOP_PROVIDERS } from "./model.js";
@@ -23,14 +28,20 @@ const object = (value, label) => {
23
28
  throw new Error(`${label} must be an object`);
24
29
  return value;
25
30
  };
31
+ /** Validates and normalizes a loop-execution configuration block. */
26
32
  export const validateLoopConfig = (value) => {
27
33
  if (value === undefined)
28
34
  return { mode: "off" };
29
35
  const item = object(value, "config.loop");
30
- 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))
31
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 } : {}) };
32
43
  if (item.agents === undefined)
33
- return { mode: item.mode };
44
+ return { mode: item.mode, ...optional };
34
45
  const agents = object(item.agents, "config.loop.agents");
35
46
  if (Object.keys(agents).some((key) => !["lead", "workers"].includes(key)) || !isLoopProvider(agents.lead))
36
47
  throw new Error(`config.loop.agents.lead must be ${loopProviderNames}`);
@@ -38,7 +49,34 @@ export const validateLoopConfig = (value) => {
38
49
  for (const [id, provider] of Object.entries(workers))
39
50
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id) || !isLoopProvider(provider))
40
51
  throw new Error(`config.loop.agents.workers must map worker IDs to ${loopProviderNames}`);
41
- 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;
42
80
  };
43
81
  const strings = (value, label) => {
44
82
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
@@ -98,6 +136,7 @@ const mcp = (value) => {
98
136
  throw new Error("config.mcp.providers is invalid");
99
137
  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 }) };
100
138
  };
139
+ /** Validates a single profile file (built-in or local). */
101
140
  export const validateProfile = (value, label = "profile") => {
102
141
  const item = object(value, label);
103
142
  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))
@@ -120,6 +159,7 @@ export const validateProfile = (value, label = "profile") => {
120
159
  };
121
160
  };
122
161
  export const defaultConfig = () => ({
162
+ $schema: "./aidlc/schemas/config.schema.json",
123
163
  schemaVersion: 2,
124
164
  extends: ["topology/single"],
125
165
  specs: { roots: [] },
@@ -131,11 +171,9 @@ export const defaultConfig = () => ({
131
171
  mcp: { enabled: false },
132
172
  loop: { mode: "off" }
133
173
  });
134
- export const loadProjectConfig = (root) => {
135
- const path = join(resolve(root), ".agents/config.json");
136
- if (!existsSync(path))
137
- return defaultConfig();
138
- 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");
139
177
  const risk = object(item.risk ?? {}, "config.risk");
140
178
  const context = object(item.context ?? {}, "config.context");
141
179
  const levels = ["low", "normal", "high", "regulated"];
@@ -151,7 +189,14 @@ export const loadProjectConfig = (root) => {
151
189
  const maxChars = Number(context.maxChars ?? 16_000);
152
190
  if (!Number.isInteger(maxChars) || maxChars < 4_000 || maxChars > 128_000)
153
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
+ }
154
198
  return {
199
+ $schema: schema,
155
200
  schemaVersion: Number(item.schemaVersion),
156
201
  extends: strings(item.extends ?? ["topology/single"], "config.extends"),
157
202
  specs: { roots: strings(object(item.specs ?? {}, "config.specs").roots ?? [], "config.specs.roots").map((entry) => safeRelative(entry, "config.specs.roots")) },
@@ -165,6 +210,13 @@ export const loadProjectConfig = (root) => {
165
210
  loop: validateLoopConfig(item.loop)
166
211
  };
167
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
+ };
168
220
  const readLocalProfile = (root, id) => {
169
221
  const path = join(resolve(root), ".agents/project/profiles", safeRelative(id, "profile id"), "profile.json");
170
222
  if (!existsSync(path))
@@ -173,6 +225,7 @@ const readLocalProfile = (root, id) => {
173
225
  throw new Error(`Profile escapes the project: ${id}`);
174
226
  return validateProfile(JSON.parse(readFileSync(path, "utf8")), id);
175
227
  };
228
+ /** Resolves a list of profile ids into an ordered, cycle-free list of profiles. */
176
229
  export const resolveProfiles = (root, ids) => {
177
230
  const resolved = [];
178
231
  const visiting = new Set();
@@ -195,6 +248,7 @@ export const resolveProfiles = (root, ids) => {
195
248
  return resolved;
196
249
  };
197
250
  const stable = (values) => [...new Set(values)];
251
+ /** Merges built-in and local profiles with the project config into the effective configuration. */
198
252
  export const resolveEffectiveConfig = (root, config) => {
199
253
  const profiles = resolveProfiles(root, config.extends);
200
254
  return {
@@ -209,6 +263,7 @@ export const resolveEffectiveConfig = (root, config) => {
209
263
  loop: config.loop ?? { mode: "off" }
210
264
  };
211
265
  };
266
+ /** Expands rule include patterns into concrete markdown file paths inside the project. */
212
267
  export const includedRuleFiles = (root, patterns) => {
213
268
  const project = resolve(root);
214
269
  const realProject = realpathSync(project);
@@ -1,2 +1,3 @@
1
1
  import type { PlannedWrite } from "./model.js";
2
+ /** Plans the full legacy-layout migration: config rewrite, `.aidlc/` tree, and legacy index files. */
2
3
  export declare const planProjectLayoutMigration: (project: string, plan: PlannedWrite[]) => void;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { resolveProjectPath } from "./project-path.js";
3
3
  const safePath = (root, relative) => resolveProjectPath(root, relative, "Unsafe migration path");
4
+ /* Legacy `.aidlc/...` path translation into the canonical `.agents/` layout. */
4
5
  const legacyReference = (value) => {
5
6
  if (value === ".aidlc/config.json")
6
7
  return ".agents/config.json";
@@ -15,6 +16,7 @@ const legacyReference = (value) => {
15
16
  return value;
16
17
  };
17
18
  const rewriteStrings = (value) => Array.isArray(value) ? value.map((entry) => typeof entry === "string" ? legacyReference(entry) : entry) : value;
19
+ /** Rewrites legacy `.aidlc/` path references inside the JSON file formats that are known to hold them. */
18
20
  const rewriteKnownJson = (relative, content) => {
19
21
  const isConfig = relative === "config.json";
20
22
  const isProfile = /^profiles\/.+\/profile\.json$/.test(relative);
@@ -63,6 +65,7 @@ const legacyDestination = (relative) => {
63
65
  return `.agents/data/index/${relative.slice("index/".length)}`;
64
66
  return `.agents/project/${relative}`;
65
67
  };
68
+ /** Plans the copy of one legacy file into its destination, including a tracked delete of the source. */
66
69
  const planMigrationFile = (project, plan, sourcePath, destinationPath, legacyRelative) => {
67
70
  const source = safePath(project, sourcePath);
68
71
  const payload = migrationPayload(legacyRelative, readFileSync(source));
@@ -95,6 +98,7 @@ const planMigrationFile = (project, plan, sourcePath, destinationPath, legacyRel
95
98
  plan.push({ path: sourcePath, owner: "aidlc-project", ownershipClass: "project", content: "", action: "delete", reason: `remove migrated legacy project file after creating ${destinationPath}` });
96
99
  }
97
100
  };
101
+ /** Walks the legacy `.aidlc/` tree, refusing symlinks and special entries with a conflicted plan entry. */
98
102
  const planLegacyTreeMigration = (project, plan) => {
99
103
  const legacyRoot = safePath(project, ".aidlc");
100
104
  if (!existsSync(legacyRoot))
@@ -118,6 +122,7 @@ const planLegacyTreeMigration = (project, plan) => {
118
122
  };
119
123
  visit(legacyRoot);
120
124
  };
125
+ /** Rewrites legacy `.aidlc/` references inside an already-installed canonical project config. */
121
126
  const planExistingConfigRewrite = (project, plan) => {
122
127
  const path = ".agents/config.json";
123
128
  const target = safePath(project, path);
@@ -130,6 +135,7 @@ const planExistingConfigRewrite = (project, plan) => {
130
135
  if (item)
131
136
  Object.assign(item, { content: rewritten.content, action: "migrate", reason: "rewrite legacy .aidlc references in canonical project config" });
132
137
  };
138
+ /** Moves the legacy `.agents/data/state/{repo-map,specs-index}` files into the data/index directory. */
133
139
  const planLegacyIndexMigration = (project, plan) => {
134
140
  for (const name of ["repo-map.md", "specs-index.md"]) {
135
141
  const source = `.agents/data/state/${name}`;
@@ -137,6 +143,7 @@ const planLegacyIndexMigration = (project, plan) => {
137
143
  planMigrationFile(project, plan, source, `.agents/data/index/${name}`, `index/${name}`);
138
144
  }
139
145
  };
146
+ /** Plans the full legacy-layout migration: config rewrite, `.aidlc/` tree, and legacy index files. */
140
147
  export const planProjectLayoutMigration = (project, plan) => {
141
148
  planExistingConfigRewrite(project, plan);
142
149
  planLegacyTreeMigration(project, plan);
@@ -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;