@tea-agent/loop-agent 0.3.0 → 0.5.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 (57) hide show
  1. package/AGENTS.md +16 -14
  2. package/CHANGELOG.md +70 -53
  3. package/README.md +28 -25
  4. package/bin/agent-worker.js +22 -0
  5. package/dist/application/dag/validate-dag.js +14 -1
  6. package/dist/commands/init.js +220 -32
  7. package/dist/executors/config-core.js +3 -2
  8. package/dist/executors/dag-pi-executor.js +8 -1
  9. package/dist/executors/model-routing.js +43 -0
  10. package/dist/governance/manifest-types.js +9 -1
  11. package/dist/worker/cli.js +119 -0
  12. package/dist/worker/loop-agent/command-result.js +1 -0
  13. package/dist/worker/loop-agent/loop-agent-client.js +105 -0
  14. package/dist/worker/loop-agent/parse-json.js +14 -0
  15. package/dist/worker/materialize/harness-task-materializer.js +157 -0
  16. package/dist/worker/pool/failure-routing.js +98 -0
  17. package/dist/worker/pool/run-store.js +125 -0
  18. package/dist/worker/pool/types.js +1 -0
  19. package/dist/worker/preflight.js +108 -0
  20. package/dist/worker/profile-mapping.js +76 -0
  21. package/dist/worker/progress-reporter.js +81 -0
  22. package/dist/worker/report/morning-report.js +69 -0
  23. package/dist/worker/repos/repo-resolver.js +23 -0
  24. package/dist/worker/run-task/run-task.js +359 -0
  25. package/dist/worker/runner/run-ready.js +216 -0
  26. package/dist/worker/task-graph/acceptance-schema.js +25 -0
  27. package/dist/worker/task-graph/ready-queue.js +23 -0
  28. package/dist/worker/task-graph/task-graph-schema.js +28 -0
  29. package/dist/worker/task-graph/types.js +1 -0
  30. package/dist/worker/task-graph/validate.js +188 -0
  31. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  32. package/dist/worker/task-spec/schema.js +116 -0
  33. package/dist/worker/task-spec/types.js +1 -0
  34. package/dist/worker/task-spec/validate.js +352 -0
  35. package/dist/workflows/dag/init-hybrid.js +4 -13
  36. package/dist/workflows/dag/skill-instructions.js +4 -0
  37. package/dist/workflows/dag/types.js +1 -1
  38. package/dist/workflows/dag/validate.js +3 -2
  39. package/docs/README.md +11 -7
  40. package/docs/development-principles.md +2 -0
  41. package/docs/exec-plans/active/README.md +1 -1
  42. package/docs/exec-plans/completed/README.md +8 -0
  43. package/docs/init-surface.manifest.json +199 -175
  44. package/docs/skills/vetted-skill-registry.md +4 -4
  45. package/docs/templates/agent-dag.base.json +1 -1
  46. package/docs/templates/agent-dag.final-verification.json +1 -1
  47. package/docs/templates/agent-dag.supervised-implementation.json +1 -1
  48. package/docs/templates/hybrid-dag.json +1 -1
  49. package/docs/templates/init-evolution-review.md +33 -33
  50. package/examples/example-dag.json +1 -1
  51. package/examples/hybrid-loop-agent-dag.json +1 -1
  52. package/harness.json +7 -32
  53. package/package.json +14 -12
  54. package/skills/init-capability-evolution/SKILL.md +69 -69
  55. package/skills/loop-agent/SKILL.md +2 -0
  56. package/skills/loop-agent/references/command-reference.md +63 -35
  57. package/skills/loop-agent/references/harness-policy.md +2 -1
@@ -0,0 +1,216 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import YAML from "yaml";
5
+ import { deriveFailureRoute } from "../pool/failure-routing.js";
6
+ import { findRunByWorkerRunId, getTaskPoolRoot, readAllTaskPoolStates, recordTaskPoolRun, writeTaskPoolState, } from "../pool/run-store.js";
7
+ import { runTaskSpec, } from "../run-task/run-task.js";
8
+ import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
9
+ import { computeReadyQueue } from "../task-graph/ready-queue.js";
10
+ import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
11
+ import { taskSpecSchema } from "../task-spec/schema.js";
12
+ export async function runReadyTasks(options) {
13
+ const now = options.now ?? new Date();
14
+ const startedAt = now.toISOString();
15
+ const batchRunId = options.batchRunId ?? buildBatchRunId(now);
16
+ const graph = await loadTaskGraph(options.featureDir);
17
+ const states = await readAllTaskPoolStates(options.repoRoot);
18
+ const readyTaskIds = computeReadyQueue(graph, toGraphState(states)).filter((taskId) => isPoolReady(states[taskId]));
19
+ const limitedTaskIds = readyTaskIds.slice(0, options.limit ?? readyTaskIds.length);
20
+ const tasks = [];
21
+ const runner = options.runTask ?? runTaskSpec;
22
+ const progress = options.progress ?? noopProgressReporter;
23
+ const total = limitedTaskIds.length;
24
+ let index = 0;
25
+ const batchStartedAt = Date.now();
26
+ progress.batch(`batch ${batchRunId}: feature=${graph.feature_id}, ${total} task${total === 1 ? "" : "s"} ready → starting`);
27
+ for (const taskId of limitedTaskIds) {
28
+ index += 1;
29
+ const node = graph.nodes.find((candidate) => candidate.id === taskId);
30
+ const taskSpecPath = path.join(options.featureDir, "tasks", node?.task ?? `${taskId}.yaml`);
31
+ const taskSpec = await loadTaskSpec(taskSpecPath);
32
+ const workerRunId = options.workerRunIdForTask?.(taskId, taskSpec) ??
33
+ buildStableWorkerRunId(taskId, taskSpec, now);
34
+ if (workerRunId) {
35
+ const existing = await findRunByWorkerRunId(options.repoRoot, workerRunId);
36
+ if (existing) {
37
+ progress.task(`task ${index}/${total} ${taskId}: reuse existing run ${workerRunId}`);
38
+ tasks.push({
39
+ taskId,
40
+ workerRunId,
41
+ status: "reused",
42
+ runRecordPath: existing.runRecordPath,
43
+ });
44
+ continue;
45
+ }
46
+ }
47
+ const taskStartedAt = Date.now();
48
+ progress.task(`task ${index}/${total} ${taskId} "${taskSpec.title}" (${workerRunId})`);
49
+ progress.step(`preflight → materialize → dag-run-task → validate → run-dag → report`);
50
+ let result;
51
+ try {
52
+ await writeTaskPoolState(options.repoRoot, {
53
+ taskId,
54
+ status: "Running",
55
+ updatedAt: new Date().toISOString(),
56
+ workerRunId,
57
+ });
58
+ result = await runner({
59
+ repoRoot: options.repoRoot,
60
+ taskSpec,
61
+ taskSpecPath,
62
+ client: options.client,
63
+ now,
64
+ workerRunId,
65
+ preflight: {
66
+ runCheckRepo: options.runCheckRepo,
67
+ ...(options.checkRepoCommand ? { checkRepoCommand: options.checkRepoCommand } : {}),
68
+ },
69
+ progress,
70
+ ...(options.piModel ? { piModel: options.piModel } : {}),
71
+ });
72
+ }
73
+ catch (error) {
74
+ const message = errorMessage(error);
75
+ progress.note(`task ${taskId}: ERROR ${message}`);
76
+ try {
77
+ await recordTaskPoolRun({
78
+ repoRoot: options.repoRoot,
79
+ run: {
80
+ schemaVersion: 1,
81
+ batchRunId,
82
+ workerRunId,
83
+ taskId,
84
+ featureId: taskSpec.feature_id,
85
+ status: "run-error",
86
+ recordedAt: new Date().toISOString(),
87
+ error: message,
88
+ },
89
+ });
90
+ }
91
+ catch (recordError) {
92
+ tasks.push({
93
+ taskId,
94
+ workerRunId,
95
+ status: "record-error",
96
+ error: errorMessage(recordError),
97
+ });
98
+ continue;
99
+ }
100
+ tasks.push({
101
+ taskId,
102
+ workerRunId,
103
+ status: "run-error",
104
+ error: message,
105
+ });
106
+ continue;
107
+ }
108
+ const failure = deriveFailureRoute(result);
109
+ if (result.status === "succeeded") {
110
+ progress.step(`✓ ${taskId} succeeded in ${formatDuration(Date.now() - taskStartedAt)} (promoted + closed out)`);
111
+ }
112
+ else {
113
+ progress.step(`✗ ${taskId} failed in ${formatDuration(Date.now() - taskStartedAt)} — see failure artifacts`);
114
+ }
115
+ const run = {
116
+ schemaVersion: 1,
117
+ batchRunId,
118
+ workerRunId: result.workerRunId,
119
+ taskId: result.businessId,
120
+ featureId: taskSpec.feature_id,
121
+ status: result.status,
122
+ harnessTaskId: result.harnessTaskId,
123
+ runRecordPath: result.runRecordPath,
124
+ dagPath: result.dagPath,
125
+ recordedAt: new Date().toISOString(),
126
+ ...(failure ? { failure } : {}),
127
+ ...(result.failureArtifacts ? { failureArtifacts: result.failureArtifacts } : {}),
128
+ };
129
+ try {
130
+ await recordTaskPoolRun({ repoRoot: options.repoRoot, run });
131
+ tasks.push({
132
+ taskId,
133
+ workerRunId: result.workerRunId,
134
+ status: result.status,
135
+ runRecordPath: result.runRecordPath,
136
+ });
137
+ }
138
+ catch (error) {
139
+ tasks.push({
140
+ taskId,
141
+ workerRunId: result.workerRunId,
142
+ status: "record-error",
143
+ error: errorMessage(error),
144
+ runRecordPath: result.runRecordPath,
145
+ });
146
+ }
147
+ }
148
+ const summary = summarize(tasks);
149
+ const batchRunPath = path.join(getTaskPoolRoot(options.repoRoot), "runs", `${batchRunId}.json`);
150
+ const output = {
151
+ schemaVersion: 1,
152
+ status: summary.recordErrors > 0 || summary.runErrors > 0 ? "failed" : "completed",
153
+ batchRunId,
154
+ featureId: graph.feature_id,
155
+ startedAt,
156
+ summary,
157
+ tasks,
158
+ batchRunPath,
159
+ };
160
+ await mkdir(path.dirname(batchRunPath), { recursive: true });
161
+ await writeFile(batchRunPath, `${JSON.stringify(output, null, 2)}\n`, "utf-8");
162
+ progress.batch(`batch ${batchRunId} ${output.status}: ${summary.succeeded} succeeded, ${summary.failed} failed, ${summary.reused} reused in ${formatDuration(Date.now() - batchStartedAt)} → ${batchRunPath}`);
163
+ return output;
164
+ }
165
+ export function buildBatchRunId(now) {
166
+ return `batch-${now.toISOString().replace(/[-:.]/g, "").slice(0, 15)}`;
167
+ }
168
+ export function buildStableWorkerRunId(taskId, taskSpec, now) {
169
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
170
+ const slug = taskId
171
+ .toLowerCase()
172
+ .replace(/[^a-z0-9]+/g, "-")
173
+ .replace(/^-+|-+$/g, "");
174
+ const hash = createHash("sha256")
175
+ .update(`${taskSpec.feature_id}:${taskSpec.id}:${taskSpec.title}`)
176
+ .digest("hex")
177
+ .slice(0, 10);
178
+ return `wr-${date}-${slug}-${hash}`;
179
+ }
180
+ async function loadTaskGraph(featureDir) {
181
+ const graphPath = path.join(featureDir, "tasks", "task-graph.yaml");
182
+ return taskGraphSpecSchema.parse(YAML.parse(await readFile(graphPath, "utf-8")));
183
+ }
184
+ async function loadTaskSpec(taskSpecPath) {
185
+ return taskSpecSchema.parse(YAML.parse(await readFile(taskSpecPath, "utf-8")));
186
+ }
187
+ function toGraphState(states) {
188
+ const graphState = {};
189
+ for (const [taskId, state] of Object.entries(states)) {
190
+ if (state.status === "Done")
191
+ graphState[taskId] = "completed";
192
+ if (state.status === "Running" || state.status === "Queued")
193
+ graphState[taskId] = "running";
194
+ if (state.status === "Failed")
195
+ graphState[taskId] = "failed";
196
+ if (state.status === "Blocked")
197
+ graphState[taskId] = "blocked";
198
+ }
199
+ return graphState;
200
+ }
201
+ function isPoolReady(state) {
202
+ return !state || state.status === "Ready";
203
+ }
204
+ function summarize(tasks) {
205
+ return {
206
+ total: tasks.length,
207
+ succeeded: tasks.filter((task) => task.status === "succeeded").length,
208
+ failed: tasks.filter((task) => task.status === "failed").length,
209
+ reused: tasks.filter((task) => task.status === "reused").length,
210
+ recordErrors: tasks.filter((task) => task.status === "record-error").length,
211
+ runErrors: tasks.filter((task) => task.status === "run-error").length,
212
+ };
213
+ }
214
+ function errorMessage(error) {
215
+ return error instanceof Error ? error.message : String(error);
216
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ export const acceptanceItemSchema = z
3
+ .object({
4
+ id: z.string().min(1),
5
+ title: z.string().min(1),
6
+ priority: z.enum(["must", "should", "could", "wont"]).optional().default("must"),
7
+ type: z.string().min(1),
8
+ given: z.string().min(1),
9
+ when: z.string().min(1),
10
+ then: z.string().min(1),
11
+ verification: z
12
+ .object({
13
+ expected_task_refs: z.array(z.string().min(1)).min(1),
14
+ suggested_tests: z.array(z.string().min(1)).optional().default([]),
15
+ })
16
+ .strict(),
17
+ })
18
+ .strict();
19
+ export const acceptanceSpecSchema = z
20
+ .object({
21
+ schema_version: z.literal(1),
22
+ feature_id: z.string().min(1),
23
+ acceptance: z.array(acceptanceItemSchema).min(1),
24
+ })
25
+ .strict();
@@ -0,0 +1,23 @@
1
+ const TERMINAL_OR_ACTIVE = new Set([
2
+ "running",
3
+ "completed",
4
+ "failed",
5
+ "blocked",
6
+ ]);
7
+ export function computeReadyQueue(graph, state) {
8
+ return graph.nodes
9
+ .filter((node) => {
10
+ const ownStatus = getStatus(state[node.id]);
11
+ if (ownStatus && TERMINAL_OR_ACTIVE.has(ownStatus))
12
+ return false;
13
+ return node.depends_on.every((dependencyId) => getStatus(state[dependencyId]) === "completed");
14
+ })
15
+ .map((node) => node.id);
16
+ }
17
+ function getStatus(value) {
18
+ if (!value)
19
+ return undefined;
20
+ if (typeof value === "string")
21
+ return value;
22
+ return value.status;
23
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import { taskSpecTypeSchema } from "../task-spec/schema.js";
3
+ export const taskGraphNodeSchema = z
4
+ .object({
5
+ id: z.string().min(1),
6
+ task: z.string().min(1),
7
+ type: taskSpecTypeSchema,
8
+ depends_on: z.array(z.string().min(1)).optional().default([]),
9
+ })
10
+ .strict();
11
+ export const taskGraphSpecSchema = z
12
+ .object({
13
+ schema_version: z.literal(1),
14
+ feature_id: z.string().min(1),
15
+ nodes: z.array(taskGraphNodeSchema).min(1),
16
+ parallel_policy: z
17
+ .object({
18
+ max_parallel_tasks: z.number().int().positive().optional().default(1),
19
+ disallow_same_file_parallel_writes: z.boolean().optional().default(true),
20
+ })
21
+ .strict()
22
+ .optional()
23
+ .default({
24
+ max_parallel_tasks: 1,
25
+ disallow_same_file_parallel_writes: true,
26
+ }),
27
+ })
28
+ .strict();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,188 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ import { taskSpecSchema } from "../task-spec/schema.js";
5
+ import { acceptanceSpecSchema } from "./acceptance-schema.js";
6
+ import { computeReadyQueue } from "./ready-queue.js";
7
+ import { taskGraphSpecSchema } from "./task-graph-schema.js";
8
+ export async function validateFeatureTaskGraph(featureDir, options = {}) {
9
+ const errors = [];
10
+ const acceptance = await loadAcceptanceSpec(featureDir, options, errors);
11
+ const graph = await loadTaskGraphSpec(featureDir, options, errors);
12
+ if (!acceptance || !graph) {
13
+ return result("", acceptance, graph, errors);
14
+ }
15
+ checkDuplicateNodeIds(graph, errors);
16
+ checkAcceptanceTaskRefs(acceptance, graph, errors);
17
+ checkUnknownDependencies(graph, errors);
18
+ checkCycles(graph, errors);
19
+ await checkTaskFiles(featureDir, graph, errors);
20
+ return result(graph.feature_id, acceptance, graph, errors);
21
+ }
22
+ async function loadAcceptanceSpec(featureDir, options, errors) {
23
+ const raw = options.acceptanceOverride ??
24
+ YAML.parse(await readFile(path.join(featureDir, "acceptance.yaml"), "utf-8"));
25
+ const parsed = acceptanceSpecSchema.safeParse(raw);
26
+ if (!parsed.success) {
27
+ errors.push({
28
+ code: "acceptance-schema-invalid",
29
+ message: parsed.error.issues.map((issue) => issue.message).join("; "),
30
+ path: "acceptance.yaml",
31
+ });
32
+ return undefined;
33
+ }
34
+ return parsed.data;
35
+ }
36
+ async function loadTaskGraphSpec(featureDir, options, errors) {
37
+ const raw = options.graphOverride ??
38
+ YAML.parse(await readFile(path.join(featureDir, "tasks", "task-graph.yaml"), "utf-8"));
39
+ const parsed = taskGraphSpecSchema.safeParse(raw);
40
+ if (!parsed.success) {
41
+ errors.push({
42
+ code: "task-graph-schema-invalid",
43
+ message: parsed.error.issues.map((issue) => issue.message).join("; "),
44
+ path: "tasks/task-graph.yaml",
45
+ });
46
+ return undefined;
47
+ }
48
+ return parsed.data;
49
+ }
50
+ function checkDuplicateNodeIds(graph, errors) {
51
+ const seen = new Set();
52
+ for (const node of graph.nodes) {
53
+ if (seen.has(node.id)) {
54
+ errors.push({
55
+ code: "duplicate-node-id",
56
+ message: `duplicate task graph node id: ${node.id}`,
57
+ path: `nodes.${node.id}`,
58
+ });
59
+ }
60
+ seen.add(node.id);
61
+ }
62
+ }
63
+ function checkAcceptanceTaskRefs(acceptance, graph, errors) {
64
+ const nodeIds = new Set(graph.nodes.map((node) => node.id));
65
+ for (const item of acceptance.acceptance) {
66
+ for (const taskRef of item.verification.expected_task_refs) {
67
+ if (!nodeIds.has(taskRef)) {
68
+ errors.push({
69
+ code: "acceptance-task-ref-missing",
70
+ message: `acceptance ${item.id} references missing task ${taskRef}`,
71
+ path: `acceptance.${item.id}.verification.expected_task_refs`,
72
+ });
73
+ }
74
+ }
75
+ }
76
+ }
77
+ function checkUnknownDependencies(graph, errors) {
78
+ const nodeIds = new Set(graph.nodes.map((node) => node.id));
79
+ for (const node of graph.nodes) {
80
+ for (const dependency of node.depends_on) {
81
+ if (!nodeIds.has(dependency)) {
82
+ errors.push({
83
+ code: "unknown-dependency",
84
+ message: `${node.id} depends on unknown task ${dependency}`,
85
+ path: `nodes.${node.id}.depends_on`,
86
+ });
87
+ }
88
+ }
89
+ }
90
+ }
91
+ function checkCycles(graph, errors) {
92
+ const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
93
+ const visiting = new Set();
94
+ const visited = new Set();
95
+ const visit = (nodeId, trail) => {
96
+ if (visiting.has(nodeId)) {
97
+ errors.push({
98
+ code: "cycle-detected",
99
+ message: `cycle detected: ${[...trail, nodeId].join(" -> ")}`,
100
+ path: `nodes.${nodeId}.depends_on`,
101
+ });
102
+ return;
103
+ }
104
+ if (visited.has(nodeId))
105
+ return;
106
+ const node = nodes.get(nodeId);
107
+ if (!node)
108
+ return;
109
+ visiting.add(nodeId);
110
+ for (const dependency of node.depends_on)
111
+ visit(dependency, [...trail, nodeId]);
112
+ visiting.delete(nodeId);
113
+ visited.add(nodeId);
114
+ };
115
+ for (const node of graph.nodes)
116
+ visit(node.id, []);
117
+ }
118
+ async function checkTaskFiles(featureDir, graph, errors) {
119
+ for (const node of graph.nodes) {
120
+ const taskPath = path.join(featureDir, "tasks", node.task);
121
+ if (!(await exists(taskPath))) {
122
+ errors.push({
123
+ code: "task-file-missing",
124
+ message: `task file does not exist: ${node.task}`,
125
+ path: `nodes.${node.id}.task`,
126
+ });
127
+ continue;
128
+ }
129
+ const parsedTask = taskSpecSchema.safeParse(YAML.parse(await readFile(taskPath, "utf-8")));
130
+ if (!parsedTask.success) {
131
+ errors.push({
132
+ code: "task-spec-schema-invalid",
133
+ message: `task file is not a valid TaskSpec: ${node.task}`,
134
+ path: `nodes.${node.id}.task`,
135
+ });
136
+ continue;
137
+ }
138
+ const taskSpec = parsedTask.data;
139
+ if (taskSpec.id !== node.id) {
140
+ errors.push({
141
+ code: "task-id-mismatch",
142
+ message: `graph node ${node.id} points at TaskSpec ${taskSpec.id}`,
143
+ path: `nodes.${node.id}.id`,
144
+ });
145
+ }
146
+ if (taskSpec.type !== node.type) {
147
+ errors.push({
148
+ code: "task-type-mismatch",
149
+ message: `graph node ${node.id} type ${node.type} differs from TaskSpec ${taskSpec.type}`,
150
+ path: `nodes.${node.id}.type`,
151
+ });
152
+ }
153
+ if (!sameStringSet(taskSpec.depends_on, node.depends_on)) {
154
+ errors.push({
155
+ code: "task-depends-on-mismatch",
156
+ message: `graph node ${node.id} depends_on differs from TaskSpec`,
157
+ path: `nodes.${node.id}.depends_on`,
158
+ });
159
+ }
160
+ }
161
+ }
162
+ function result(featureId, acceptance, graph, errors) {
163
+ return {
164
+ ok: errors.length === 0,
165
+ featureId,
166
+ summary: {
167
+ acceptanceCount: acceptance?.acceptance.length ?? 0,
168
+ nodeCount: graph?.nodes.length ?? 0,
169
+ readyWithoutState: graph ? computeReadyQueue(graph, {}) : [],
170
+ },
171
+ errors,
172
+ };
173
+ }
174
+ async function exists(filePath) {
175
+ try {
176
+ await access(filePath);
177
+ return true;
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ }
183
+ function sameStringSet(left, right) {
184
+ if (left.length !== right.length)
185
+ return false;
186
+ const rightSet = new Set(right);
187
+ return left.every((value) => rightSet.has(value));
188
+ }
@@ -0,0 +1,8 @@
1
+ const RISK_TO_COMPLEXITY = {
2
+ low: "small",
3
+ medium: "medium",
4
+ high: "large",
5
+ };
6
+ export function mapRiskLevelToComplexity(riskLevel) {
7
+ return RISK_TO_COMPLEXITY[riskLevel];
8
+ }
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import { verifyModeSchema, verifyPresetSchema, verifyQuotaSchema, } from "../../task/config-types.js";
3
+ export const taskSpecTypeSchema = z.enum([
4
+ "architecture",
5
+ "backend-feature",
6
+ "frontend-feature",
7
+ "bugfix",
8
+ "ci-fix",
9
+ "doc-update",
10
+ "qa-analysis",
11
+ "qa-casegen",
12
+ "qa-testcode",
13
+ "qa-execute",
14
+ "qa-failure-analysis",
15
+ "reviewer-gate",
16
+ "fix-from-failure",
17
+ ]);
18
+ export const taskSpecRiskLevelSchema = z.enum(["low", "medium", "high"]);
19
+ const verifyCommandSchema = z
20
+ .object({
21
+ id: z.string().min(1),
22
+ label: z.string().min(1),
23
+ command: z.string().min(1),
24
+ required: z.boolean().optional().default(true),
25
+ timeout_ms: z.number().int().positive().optional(),
26
+ })
27
+ .strict();
28
+ export const taskSpecSchema = z
29
+ .object({
30
+ schema_version: z.literal(1),
31
+ id: z.string().min(1),
32
+ feature_id: z.string().min(1),
33
+ title: z.string().min(1),
34
+ description: z.string().optional().default(""),
35
+ repo: z
36
+ .object({
37
+ name: z.string().min(1),
38
+ path_ref: z.string().min(1),
39
+ default_branch: z.string().min(1).optional(),
40
+ })
41
+ .strict(),
42
+ type: taskSpecTypeSchema,
43
+ priority: z.enum(["P0", "P1", "P2", "P3"]).optional().default("P2"),
44
+ risk_level: taskSpecRiskLevelSchema,
45
+ depends_on: z.array(z.string().min(1)).optional().default([]),
46
+ source_docs: z
47
+ .object({
48
+ requirement: z.string().min(1),
49
+ acceptance: z.string().min(1),
50
+ design: z.string().min(1).optional(),
51
+ test_plan: z.string().min(1).optional(),
52
+ test_cases: z.string().min(1).optional(),
53
+ })
54
+ .strict(),
55
+ acceptance_refs: z.array(z.string().min(1)).min(1),
56
+ scope: z
57
+ .object({
58
+ goals: z.array(z.string().min(1)).min(1),
59
+ non_goals: z.array(z.string().min(1)).optional().default([]),
60
+ assumptions: z.array(z.string().min(1)).optional().default([]),
61
+ open_questions: z.array(z.string().min(1)).optional().default([]),
62
+ })
63
+ .strict(),
64
+ constraints: z
65
+ .object({
66
+ allowed_paths: z.array(z.string().min(1)).optional().default([]),
67
+ forbidden_paths: z.array(z.string().min(1)).optional().default([]),
68
+ hard_constraints: z.array(z.string().min(1)).optional().default([]),
69
+ })
70
+ .strict(),
71
+ verify: z
72
+ .object({
73
+ preset: verifyPresetSchema,
74
+ mode: verifyModeSchema,
75
+ quota: verifyQuotaSchema.optional().default("full"),
76
+ commands: z.array(verifyCommandSchema).optional().default([]),
77
+ })
78
+ .strict(),
79
+ worker: z
80
+ .object({
81
+ execution_mode: z.enum([
82
+ "dry-run",
83
+ "generate-only",
84
+ "generate-validate",
85
+ "generate-validate-run",
86
+ ]),
87
+ max_attempts: z.number().int().positive().optional().default(1),
88
+ timeout_ms: z.number().int().positive().optional(),
89
+ require_clean_worktree: z.boolean().optional().default(true),
90
+ require_human_review: z.boolean().optional().default(true),
91
+ create_worktree: z.boolean().optional().default(false),
92
+ })
93
+ .strict(),
94
+ loop_agent: z
95
+ .object({
96
+ profile_policy: z.literal("mapped"),
97
+ strict_models: z.boolean().optional().default(true),
98
+ strict_governance: z.boolean().optional().default(true),
99
+ no_cursor: z.boolean().optional().default(false),
100
+ max_concurrent: z.number().int().positive().optional().default(1),
101
+ })
102
+ .strict(),
103
+ outputs: z
104
+ .object({
105
+ required: z.array(z.string().min(1)).min(1),
106
+ })
107
+ .strict(),
108
+ failure_policy: z
109
+ .object({
110
+ failed_run_promote: z.boolean().optional().default(false),
111
+ generate_closeout_draft: z.boolean().optional().default(true),
112
+ create_followup_task: z.boolean().optional().default(true),
113
+ })
114
+ .strict(),
115
+ })
116
+ .strict();
@@ -0,0 +1 @@
1
+ export {};