@felixdotgo/aidlc-kit 0.1.1 → 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 (134) hide show
  1. package/DEVELOPMENT.md +14 -0
  2. package/README.md +11 -13
  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/scripts/lib/loop-authority.mjs +245 -0
  11. package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
  12. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +232 -0
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +15 -3
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +86 -28
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +7 -0
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +22 -6
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +40 -15
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +14 -6
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +23 -5
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +42 -15
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +75 -5
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +60 -0
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +142 -5
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +31 -5
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
  27. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +57 -2
  28. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +85 -73
  29. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
  30. package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
  31. package/dist/assets/.agents/aidlc/templates/design.md +9 -0
  32. package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
  33. package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
  34. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
  35. package/dist/assets/ui/bulma.min.css +3 -0
  36. package/dist/assets/ui/dashboard.css +414 -0
  37. package/dist/services/aidlc-coordination/Dockerfile +11 -3
  38. package/dist/services/aidlc-coordination/compose.yaml +12 -0
  39. package/dist/services/aidlc-coordination/package-lock.json +3 -1
  40. package/dist/services/aidlc-coordination/package.json +9 -4
  41. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +57 -2
  42. package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
  43. package/dist/services/aidlc-coordination/src/server.mjs +9 -3
  44. package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
  45. package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
  46. package/dist/src/adapters.d.ts +0 -3
  47. package/dist/src/adapters.js +3 -5
  48. package/dist/src/cli/argv.d.ts +12 -0
  49. package/dist/src/cli/argv.js +62 -0
  50. package/dist/src/cli/commands.d.ts +8 -0
  51. package/dist/src/cli/commands.js +129 -0
  52. package/dist/src/cli/options.d.ts +25 -0
  53. package/dist/src/cli/options.js +32 -0
  54. package/dist/src/cli/prompt.d.ts +9 -0
  55. package/dist/src/cli/prompt.js +66 -0
  56. package/dist/src/cli.js +25 -191
  57. package/dist/src/gate.js +3 -2
  58. package/dist/src/installer.d.ts +14 -7
  59. package/dist/src/installer.js +38 -141
  60. package/dist/src/legacy.js +1 -0
  61. package/dist/src/managed-content.d.ts +7 -0
  62. package/dist/src/managed-content.js +15 -4
  63. package/dist/src/managed-planning.d.ts +65 -0
  64. package/dist/src/managed-planning.js +219 -0
  65. package/dist/src/mcp-setup.js +3 -1
  66. package/dist/src/model/config-contracts.d.ts +65 -0
  67. package/dist/src/model/config-contracts.js +1 -0
  68. package/dist/src/model/install-contracts.d.ts +29 -0
  69. package/dist/src/model/install-contracts.js +1 -0
  70. package/dist/src/model/lifecycle-enums.d.ts +13 -0
  71. package/dist/src/model/lifecycle-enums.js +1 -0
  72. package/dist/src/model/loop.d.ts +20 -0
  73. package/dist/src/model/loop.js +1 -0
  74. package/dist/src/model/manifest.d.ts +16 -0
  75. package/dist/src/model/manifest.js +1 -0
  76. package/dist/src/model/records.d.ts +138 -0
  77. package/dist/src/model/records.js +1 -0
  78. package/dist/src/model.d.ts +11 -264
  79. package/dist/src/model.js +1 -1
  80. package/dist/src/profiles.d.ts +2 -0
  81. package/dist/src/profiles.js +53 -9
  82. package/dist/src/state/archive-record.d.ts +9 -0
  83. package/dist/src/state/archive-record.js +26 -0
  84. package/dist/src/state/commands.d.ts +18 -0
  85. package/dist/src/state/commands.js +27 -0
  86. package/dist/src/state/domain.d.ts +9 -0
  87. package/dist/src/state/domain.js +9 -0
  88. package/dist/src/state/guards.d.ts +7 -0
  89. package/dist/src/state/guards.js +17 -0
  90. package/dist/src/state/io.d.ts +10 -0
  91. package/dist/src/state/io.js +35 -0
  92. package/dist/src/state/items.d.ts +3 -4
  93. package/dist/src/state/items.js +58 -57
  94. package/dist/src/state/lessons.js +14 -7
  95. package/dist/src/state/lifecycle.js +2 -2
  96. package/dist/src/state/lock.js +7 -6
  97. package/dist/src/state/memory.js +24 -12
  98. package/dist/src/state/{archive.js → migrate.js} +7 -7
  99. package/dist/src/state/partition.d.ts +15 -0
  100. package/dist/src/state/partition.js +90 -0
  101. package/dist/src/state/paths.d.ts +11 -0
  102. package/dist/src/state/paths.js +12 -0
  103. package/dist/src/state/render.js +9 -6
  104. package/dist/src/state/store.js +9 -15
  105. package/dist/src/state/transitions.js +9 -7
  106. package/dist/src/state/validate.js +50 -7
  107. package/dist/src/state.d.ts +16 -11
  108. package/dist/src/state.js +16 -11
  109. package/dist/src/task-ui.d.ts +54 -0
  110. package/dist/src/task-ui.js +1570 -0
  111. package/dist/src/upgrade-board.js +10 -10
  112. package/dist/src/upgrade.d.ts +0 -1
  113. package/dist/src/upgrade.js +19 -83
  114. package/dist/src/workflow.js +3 -3
  115. package/docs/README.md +6 -2
  116. package/docs/architecture.md +9 -8
  117. package/docs/command-reference.md +81 -20
  118. package/docs/configuration.md +28 -8
  119. package/docs/coordination-service.md +18 -6
  120. package/docs/design-decisions.md +83 -0
  121. package/docs/development.md +27 -2
  122. package/docs/loop-local-run.md +495 -0
  123. package/docs/loop-runbook.md +235 -0
  124. package/docs/operating-workflow.md +2 -2
  125. package/docs/testing-and-release.md +5 -1
  126. package/docs/troubleshooting.md +125 -0
  127. package/package.json +4 -1
  128. package/dist/src/ledger.d.ts +0 -22
  129. package/dist/src/ledger.js +0 -119
  130. package/dist/src/state/shared.d.ts +0 -29
  131. package/dist/src/state/shared.js +0 -71
  132. package/dist/src/tracker.d.ts +0 -36
  133. package/dist/src/tracker.js +0 -44
  134. /package/dist/src/state/{archive.d.ts → migrate.d.ts} +0 -0
@@ -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-kit" | "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";
@@ -5,6 +5,8 @@ export declare const validateLoopConfig: (value: unknown) => LoopConfig;
5
5
  /** Validates a single profile file (built-in or local). */
6
6
  export declare const validateProfile: (value: unknown, label?: string) => Profile;
7
7
  export declare const defaultConfig: () => ProjectConfig;
8
+ /** Validates and normalizes a parsed `.agents/config.json` value. */
9
+ export declare const validateProjectConfig: (value: unknown) => ProjectConfig;
8
10
  /** Loads and validates `.agents/config.json`, falling back to the default config when absent. */
9
11
  export declare const loadProjectConfig: (root: string) => ProjectConfig;
10
12
  /** Resolves a list of profile ids into an ordered, cycle-free list of profiles. */
@@ -33,10 +33,15 @@ export const validateLoopConfig = (value) => {
33
33
  if (value === undefined)
34
34
  return { mode: "off" };
35
35
  const item = object(value, "config.loop");
36
- if (Object.keys(item).some((key) => !["mode", "agents"].includes(key)) || typeof item.mode !== "string" || !["off", "local", "fleet"].includes(item.mode))
36
+ if (Object.keys(item).some((key) => !["mode", "agents", "workerImage", "pollMs", "limits"].includes(key)) || typeof item.mode !== "string" || !["off", "local", "fleet"].includes(item.mode))
37
37
  throw new Error("config.loop requires a valid mode and only supported fields");
38
+ const workerImage = validateWorkerImageMap(item.workerImage);
39
+ if (item.pollMs !== undefined && (!Number.isSafeInteger(item.pollMs) || Number(item.pollMs) < 1_000 || Number(item.pollMs) > 300_000))
40
+ throw new Error("config.loop.pollMs must be an integer from 1000 to 300000");
41
+ const limits = validateLoopLimits(item.limits);
42
+ const optional = { ...(workerImage ? { workerImage } : {}), ...(item.pollMs !== undefined ? { pollMs: Number(item.pollMs) } : {}), ...(limits ? { limits } : {}) };
38
43
  if (item.agents === undefined)
39
- return { mode: item.mode };
44
+ return { mode: item.mode, ...optional };
40
45
  const agents = object(item.agents, "config.loop.agents");
41
46
  if (Object.keys(agents).some((key) => !["lead", "workers"].includes(key)) || !isLoopProvider(agents.lead))
42
47
  throw new Error(`config.loop.agents.lead must be ${loopProviderNames}`);
@@ -44,7 +49,34 @@ export const validateLoopConfig = (value) => {
44
49
  for (const [id, provider] of Object.entries(workers))
45
50
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id) || !isLoopProvider(provider))
46
51
  throw new Error(`config.loop.agents.workers must map worker IDs to ${loopProviderNames}`);
47
- return { mode: item.mode, agents: { lead: agents.lead, workers: workers } };
52
+ return { mode: item.mode, agents: { lead: agents.lead, workers: workers }, ...optional };
53
+ };
54
+ /** Bounds mirror `validateLimits` in assets/aidlc/scripts/lib/loop-runtime.mjs. */
55
+ const LOOP_LIMIT_BOUNDS = { maxTurns: 1_000, turnTimeoutMs: 3_600_000, maxStallTurns: 20 };
56
+ const validateLoopLimits = (value) => {
57
+ if (value === undefined)
58
+ return undefined;
59
+ const map = object(value, "config.loop.limits");
60
+ const result = {};
61
+ for (const [key, entry] of Object.entries(map)) {
62
+ if (!Object.hasOwn(LOOP_LIMIT_BOUNDS, key))
63
+ throw new Error(`config.loop.limits supports only ${Object.keys(LOOP_LIMIT_BOUNDS).join(", ")}`);
64
+ const max = LOOP_LIMIT_BOUNDS[key];
65
+ if (!Number.isSafeInteger(entry) || Number(entry) < 1 || Number(entry) > max)
66
+ throw new Error(`config.loop.limits.${key} must be an integer from 1 to ${max}`);
67
+ result[key] = Number(entry);
68
+ }
69
+ return result;
70
+ };
71
+ const validateWorkerImageMap = (value) => {
72
+ if (value === undefined)
73
+ return undefined;
74
+ const map = object(value, "config.loop.workerImage");
75
+ for (const [provider, tag] of Object.entries(map)) {
76
+ if (!isLoopProvider(provider) || typeof tag !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._/:@-]{0,255}$/.test(tag))
77
+ throw new Error(`config.loop.workerImage must map ${loopProviderNames} to image tags`);
78
+ }
79
+ return map;
48
80
  };
49
81
  const strings = (value, label) => {
50
82
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
@@ -127,6 +159,7 @@ export const validateProfile = (value, label = "profile") => {
127
159
  };
128
160
  };
129
161
  export const defaultConfig = () => ({
162
+ $schema: "./aidlc/schemas/config.schema.json",
130
163
  schemaVersion: 2,
131
164
  extends: ["topology/single"],
132
165
  specs: { roots: [] },
@@ -138,12 +171,9 @@ export const defaultConfig = () => ({
138
171
  mcp: { enabled: false },
139
172
  loop: { mode: "off" }
140
173
  });
141
- /** Loads and validates `.agents/config.json`, falling back to the default config when absent. */
142
- export const loadProjectConfig = (root) => {
143
- const path = join(resolve(root), ".agents/config.json");
144
- if (!existsSync(path))
145
- return defaultConfig();
146
- const item = object(JSON.parse(readFileSync(path, "utf8")), ".agents/config.json");
174
+ /** Validates and normalizes a parsed `.agents/config.json` value. */
175
+ export const validateProjectConfig = (value) => {
176
+ const item = object(value, ".agents/config.json");
147
177
  const risk = object(item.risk ?? {}, "config.risk");
148
178
  const context = object(item.context ?? {}, "config.context");
149
179
  const levels = ["low", "normal", "high", "regulated"];
@@ -159,7 +189,14 @@ export const loadProjectConfig = (root) => {
159
189
  const maxChars = Number(context.maxChars ?? 16_000);
160
190
  if (!Number.isInteger(maxChars) || maxChars < 4_000 || maxChars > 128_000)
161
191
  throw new Error("context.maxChars must be an integer from 4000 to 128000");
192
+ let schema;
193
+ if (item.$schema !== undefined) {
194
+ if (typeof item.$schema !== "string" || item.$schema.length > 512)
195
+ throw new Error("config.$schema must be a string of at most 512 characters");
196
+ schema = item.$schema;
197
+ }
162
198
  return {
199
+ $schema: schema,
163
200
  schemaVersion: Number(item.schemaVersion),
164
201
  extends: strings(item.extends ?? ["topology/single"], "config.extends"),
165
202
  specs: { roots: strings(object(item.specs ?? {}, "config.specs").roots ?? [], "config.specs.roots").map((entry) => safeRelative(entry, "config.specs.roots")) },
@@ -173,6 +210,13 @@ export const loadProjectConfig = (root) => {
173
210
  loop: validateLoopConfig(item.loop)
174
211
  };
175
212
  };
213
+ /** Loads and validates `.agents/config.json`, falling back to the default config when absent. */
214
+ export const loadProjectConfig = (root) => {
215
+ const path = join(resolve(root), ".agents/config.json");
216
+ if (!existsSync(path))
217
+ return defaultConfig();
218
+ return validateProjectConfig(JSON.parse(readFileSync(path, "utf8")));
219
+ };
176
220
  const readLocalProfile = (root, id) => {
177
221
  const path = join(resolve(root), ".agents/project/profiles", safeRelative(id, "profile id"), "profile.json");
178
222
  if (!existsSync(path))
@@ -0,0 +1,9 @@
1
+ import type { ArchivedTaskSummary, TaskState } from "../model.js";
2
+ export declare const taskRecordContent: (task: TaskState) => string;
3
+ /** Builds the immutable archive-catalog summary for a terminal task from its record content. */
4
+ export declare const archivedSummary: (task: TaskState, content?: string) => ArchivedTaskSummary;
5
+ /** Single terminal-to-archive policy shared by auto-archive on save and schema migration. */
6
+ export declare const buildArchiveRecord: (task: TaskState) => {
7
+ content: string;
8
+ summary: ArchivedTaskSummary;
9
+ };
@@ -0,0 +1,26 @@
1
+ import { digest } from "./domain.js";
2
+ import { archiveRecordPath } from "./paths.js";
3
+ export const taskRecordContent = (task) => `${JSON.stringify(task, null, 2)}\n`;
4
+ /** Builds the immutable archive-catalog summary for a terminal task from its record content. */
5
+ export const archivedSummary = (task, content = taskRecordContent(task)) => ({
6
+ id: task.id,
7
+ title: task.title,
8
+ type: task.type,
9
+ phase: task.phase,
10
+ gate: task.gate,
11
+ status: task.status,
12
+ risk: task.risk,
13
+ areas: task.areas,
14
+ record: archiveRecordPath(task.id),
15
+ digest: digest(content),
16
+ lessonCount: task.lessons?.length ?? 0,
17
+ predecessorTaskId: task.predecessorTaskId,
18
+ successorTaskId: task.successorTaskId,
19
+ createdAt: task.createdAt,
20
+ updatedAt: task.updatedAt
21
+ });
22
+ /** Single terminal-to-archive policy shared by auto-archive on save and schema migration. */
23
+ export const buildArchiveRecord = (task) => {
24
+ const content = taskRecordContent(task);
25
+ return { content, summary: archivedSummary(task, content) };
26
+ };
@@ -0,0 +1,18 @@
1
+ import type { TaskState } from "../model.js";
2
+ export type NextActionActions = Array<{
3
+ id: "reopen_g1" | "create_successor" | "supersede" | "close" | "record_handoff" | "resume";
4
+ command: string;
5
+ }>;
6
+ export interface CommandKit {
7
+ continueItem: (taskId: string, itemId: string) => string;
8
+ continueVerification: (taskId: string) => string;
9
+ continuePhase: (taskId: string, phase: string) => string;
10
+ recoverG1Wait: (taskId: string) => string;
11
+ recoverGatelessWrap: (taskId: string) => string;
12
+ approveGate: (taskId: string, gate: string) => string;
13
+ handoffActions: (task: TaskState) => NextActionActions;
14
+ pausedActions: (taskId: string) => NextActionActions;
15
+ repairHandoffAction: (taskId: string, kind: "review_exhausted" | "repair_exhausted") => NextActionActions;
16
+ }
17
+ /** Command-string synthesis for `nextAction`: script paths, `--root` suffixes, and CLI placeholders exist only here. */
18
+ export declare const commandKit: (root?: string) => CommandKit;
@@ -0,0 +1,27 @@
1
+ import { join, resolve } from "node:path";
2
+ /** Command-string synthesis for `nextAction`: script paths, `--root` suffixes, and CLI placeholders exist only here. */
3
+ export const commandKit = (root) => {
4
+ const suffix = root === undefined ? "" : ` --root ${JSON.stringify(resolve(root))}`;
5
+ const script = (name) => root === undefined ? `node .agents/aidlc/scripts/${name}` : `node ${JSON.stringify(join(resolve(root), ".agents/aidlc/scripts", name))}`;
6
+ const stateScript = script("state.mjs");
7
+ const contextScript = script("context.mjs");
8
+ return {
9
+ continueItem: (taskId, itemId) => `${contextScript} ${taskId} --phase build --item ${itemId}${suffix}`,
10
+ continueVerification: (taskId) => `${contextScript} ${taskId} --phase build${suffix}`,
11
+ continuePhase: (taskId, phase) => `${contextScript} ${taskId} --phase ${phase}${suffix}`,
12
+ recoverG1Wait: (taskId) => `${contextScript} ${taskId} --phase plan${suffix}`,
13
+ recoverGatelessWrap: (taskId) => `${stateScript} task status ${taskId} --status active${suffix}`,
14
+ approveGate: (taskId, gate) => `${stateScript} gate approve ${taskId} --gate ${gate} --source <explicit-user-approval>${suffix}`,
15
+ handoffActions: (task) => [
16
+ ...(task.phase === "build" ? [{ id: "reopen_g1", command: `${stateScript} task reopen ${task.id} --to plan --reason <reason> --source <explicit-user-request>${suffix}` }] : []),
17
+ { id: "create_successor", command: `${stateScript} task create <new-task-id> --title <title>${suffix}` },
18
+ { id: "supersede", command: `${stateScript} task supersede ${task.id} --successor <new-task-id> --reason <reason> --source <explicit-user-request>${suffix}` },
19
+ { id: "close", command: `${stateScript} task close ${task.id} --reason <reason> --source <explicit-user-request>${suffix}` }
20
+ ],
21
+ pausedActions: (taskId) => [
22
+ { id: "resume", command: `${stateScript} task status ${taskId} --status active${suffix}` },
23
+ { id: "record_handoff", command: `${stateScript} task handoff ${taskId} --kind <kind> --reason <reason> --source <source>${suffix}` }
24
+ ],
25
+ repairHandoffAction: (taskId, kind) => [{ id: "record_handoff", command: `${stateScript} task handoff ${taskId} --kind ${kind} --reason <summary-of-exhausted-repairs> --source <agent>${suffix}` }]
26
+ };
27
+ };
@@ -0,0 +1,9 @@
1
+ import type { Gate, HandoffKind, Phase, TaskState, WorkflowState } from "../model.js";
2
+ export declare const emptyState: () => WorkflowState;
3
+ export declare const legacyG2WaitSource = "schema-v4-migration";
4
+ export declare const phases: Phase[];
5
+ export declare const gates: Gate[];
6
+ export declare const handoffKinds: HandoffKind[];
7
+ export declare const terminal: (task: TaskState) => boolean;
8
+ export declare const phaseGate: (phase: Phase) => Gate;
9
+ export declare const digest: (content: string) => string;
@@ -0,0 +1,9 @@
1
+ import { createHash } from "node:crypto";
2
+ export const emptyState = () => ({ schemaVersion: 4, tasks: {}, archive: {} });
3
+ export const legacyG2WaitSource = "schema-v4-migration";
4
+ export const phases = ["clarify", "plan", "build", "wrap", "done"];
5
+ export const gates = ["none", "G0_confirm", "G1_review", "G2_codereview"];
6
+ export const handoffKinds = ["repair_exhausted", "review_exhausted", "g2_failed", "release_failed", "structural_change", "other"];
7
+ export const terminal = (task) => task.status === "done" || task.status === "closed" || task.status === "superseded";
8
+ export const phaseGate = (phase) => phase === "clarify" ? "G0_confirm" : phase === "plan" ? "G1_review" : phase === "build" ? "G2_codereview" : "none";
9
+ export const digest = (content) => createHash("sha256").update(content).digest("hex");
@@ -0,0 +1,7 @@
1
+ export declare const iso: (value: unknown) => value is string;
2
+ export declare const text: (value: unknown) => value is string;
3
+ export declare const idPattern: RegExp;
4
+ export declare const audited: (reason: string, source: string) => void;
5
+ export declare const tokens: (value: string) => string[];
6
+ /** Rejects artifact paths that escape the project-owned tasks directory. */
7
+ export declare const assertTaskArtifactPath: (id: string, path: string) => void;
@@ -0,0 +1,17 @@
1
+ import { isAbsolute, normalize } from "node:path";
2
+ export const iso = (value) => typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(Date.parse(value)).toISOString() === value;
3
+ export const text = (value) => typeof value === "string" && Boolean(value.trim());
4
+ export const idPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
5
+ export const audited = (reason, source) => {
6
+ if (!text(reason) || !text(source))
7
+ throw new Error("Lifecycle mutation requires non-empty --reason and --source");
8
+ };
9
+ export const tokens = (value) => [...new Set(value.toLocaleLowerCase().split(/[^\p{L}\p{N}]+/u).filter((item) => item.length > 1))];
10
+ /** Rejects artifact paths that escape the project-owned tasks directory. */
11
+ export const assertTaskArtifactPath = (id, path) => {
12
+ const separator = process.platform === "win32" ? "\\" : "/";
13
+ const cleaned = normalize(path);
14
+ if (isAbsolute(cleaned) || cleaned === ".." || cleaned.startsWith(`..${separator}`) || !cleaned.startsWith(`.agents${separator}data${separator}tasks${separator}`)) {
15
+ throw new Error(`Invalid artifact path for ${id}: ${path}`);
16
+ }
17
+ };
@@ -0,0 +1,10 @@
1
+ export declare const assertNoSymlinkPath: (root: string, relative: string) => void;
2
+ export declare const writeAtomic: (path: string, content: string) => void;
3
+ export declare const parseJsonText: (content: string, label: string, recovery?: string) => unknown;
4
+ export declare const parseJsonFile: (path: string, label: string, recovery?: string) => unknown;
5
+ /** Resolves a project-relative state path to its absolute form, rejecting symlink-crossing variants first. */
6
+ export declare const safeResolvedPath: (root: string, relative: string) => string;
7
+ /** Loads and validates a canonical JSON state file, or produces the fallback when the file does not exist. */
8
+ export declare const loadJsonOr: <T>(root: string, relative: string, validate: (value: unknown) => T, fallback: () => T, label: string, recovery?: string) => T;
9
+ /** Persists a JSON state file atomically in the canonical 2-space, trailing-newline formatting. */
10
+ export declare const saveJson: (root: string, relative: string, value: unknown) => void;
@@ -0,0 +1,35 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { resolveProjectPathWithoutSymlinks } from "../project-path.js";
4
+ export const assertNoSymlinkPath = (root, relative) => {
5
+ resolveProjectPathWithoutSymlinks(root, relative, "Unsafe state path", "State path crosses a symlink");
6
+ };
7
+ export const writeAtomic = (path, content) => {
8
+ mkdirSync(dirname(path), { recursive: true });
9
+ const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
10
+ writeFileSync(temporary, content, "utf8");
11
+ renameSync(temporary, path);
12
+ };
13
+ export const parseJsonText = (content, label, recovery = "") => {
14
+ try {
15
+ return JSON.parse(content);
16
+ }
17
+ catch (error) {
18
+ throw new Error(`${label} is corrupted or unreadable: ${error instanceof Error ? error.message : String(error)}${recovery ? `; ${recovery}` : ""}`);
19
+ }
20
+ };
21
+ export const parseJsonFile = (path, label, recovery = "") => parseJsonText(readFileSync(path, "utf8"), label, recovery);
22
+ /** Resolves a project-relative state path to its absolute form, rejecting symlink-crossing variants first. */
23
+ export const safeResolvedPath = (root, relative) => {
24
+ assertNoSymlinkPath(root, relative);
25
+ return join(resolve(root), relative);
26
+ };
27
+ /** Loads and validates a canonical JSON state file, or produces the fallback when the file does not exist. */
28
+ export const loadJsonOr = (root, relative, validate, fallback, label, recovery = "") => {
29
+ const path = safeResolvedPath(root, relative);
30
+ return existsSync(path) ? validate(parseJsonFile(path, label, recovery)) : fallback();
31
+ };
32
+ /** Persists a JSON state file atomically in the canonical 2-space, trailing-newline formatting. */
33
+ export const saveJson = (root, relative, value) => {
34
+ writeAtomic(safeResolvedPath(root, relative), `${JSON.stringify(value, null, 2)}\n`);
35
+ };
@@ -1,4 +1,5 @@
1
1
  import type { Gate, Phase, TaskState } from "../model.js";
2
+ import { type NextActionActions } from "./commands.js";
2
3
  export interface NextAction {
3
4
  classification: "await_user" | "run_phase" | "blocked" | "terminal" | "complete";
4
5
  phase: Phase;
@@ -8,10 +9,7 @@ export interface NextAction {
8
9
  successorTaskId?: string;
9
10
  itemId?: string;
10
11
  remainingItems?: number;
11
- actions?: Array<{
12
- id: "reopen_g1" | "create_successor" | "supersede" | "close" | "record_handoff" | "resume";
13
- command: string;
14
- }>;
12
+ actions?: NextActionActions;
15
13
  reason: string;
16
14
  }
17
15
  /** Validates execution dependencies and reports which pending items are ready to dispatch. */
@@ -22,4 +20,5 @@ export declare const readyTaskItems: (task: TaskState) => {
22
20
  waitingOn: string[];
23
21
  }>;
24
22
  };
23
+ /** Classifies the next lifecycle action for a task and attaches root-qualified command strings. */
25
24
  export declare const nextAction: (task: TaskState, root?: string) => NextAction;