@tea-agent/loop-agent 0.3.0 → 0.4.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 (119) hide show
  1. package/AGENTS.md +135 -133
  2. package/CHANGELOG.md +88 -63
  3. package/README.md +171 -168
  4. package/bin/agent-worker.js +22 -0
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/commands/init.js +457 -457
  7. package/dist/commands/loop-benchmark.js +11 -11
  8. package/dist/commands/pi-reuse-benchmark.js +16 -16
  9. package/dist/executors/cursor-executor.js +1 -1
  10. package/dist/executors/dag-pi-executor.js +8 -1
  11. package/dist/task/runtime.js +27 -27
  12. package/dist/worker/cli.js +119 -0
  13. package/dist/worker/loop-agent/command-result.js +1 -0
  14. package/dist/worker/loop-agent/loop-agent-client.js +105 -0
  15. package/dist/worker/loop-agent/parse-json.js +14 -0
  16. package/dist/worker/materialize/harness-task-materializer.js +157 -0
  17. package/dist/worker/pool/failure-routing.js +98 -0
  18. package/dist/worker/pool/run-store.js +117 -0
  19. package/dist/worker/pool/types.js +1 -0
  20. package/dist/worker/preflight.js +108 -0
  21. package/dist/worker/profile-mapping.js +76 -0
  22. package/dist/worker/progress-reporter.js +81 -0
  23. package/dist/worker/report/morning-report.js +69 -0
  24. package/dist/worker/repos/repo-resolver.js +23 -0
  25. package/dist/worker/run-task/run-task.js +359 -0
  26. package/dist/worker/runner/run-ready.js +216 -0
  27. package/dist/worker/task-graph/acceptance-schema.js +25 -0
  28. package/dist/worker/task-graph/ready-queue.js +23 -0
  29. package/dist/worker/task-graph/task-graph-schema.js +28 -0
  30. package/dist/worker/task-graph/types.js +1 -0
  31. package/dist/worker/task-graph/validate.js +188 -0
  32. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  33. package/dist/worker/task-spec/schema.js +116 -0
  34. package/dist/worker/task-spec/types.js +1 -0
  35. package/dist/worker/task-spec/validate.js +352 -0
  36. package/dist/workflows/dag/canvas-observer.js +275 -275
  37. package/docs/README.md +65 -61
  38. package/docs/agent-dag-recovery-playbook.md +184 -184
  39. package/docs/agent-dag-runner.md +42 -42
  40. package/docs/architecture/runtime-boundaries.md +147 -147
  41. package/docs/cursor-executor-usage.md +25 -25
  42. package/docs/decisions/README.md +3 -3
  43. package/docs/design/README.md +36 -36
  44. package/docs/development-principles.md +73 -71
  45. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  46. package/docs/exec-plans/README.md +6 -6
  47. package/docs/exec-plans/active/README.md +7 -7
  48. package/docs/exec-plans/completed/README.md +19 -11
  49. package/docs/feature-workflow.md +186 -186
  50. package/docs/harness-methodology-debugging.md +153 -153
  51. package/docs/harness-methodology-tdd.md +130 -130
  52. package/docs/harness-methodology-verification.md +27 -27
  53. package/docs/loop-agent-harness.md +42 -42
  54. package/docs/production-readiness.md +96 -96
  55. package/docs/progress/README.md +3 -3
  56. package/docs/reports/README.md +5 -5
  57. package/docs/skills/README.md +6 -6
  58. package/docs/skills/vetted-skill-registry.md +22 -22
  59. package/docs/templates/adr.md +60 -60
  60. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  61. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  62. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  63. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  64. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  65. package/docs/templates/agent-dag-report.schema.json +454 -454
  66. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  67. package/docs/templates/agent-dag.base.json +195 -195
  68. package/docs/templates/agent-dag.final-verification.json +190 -190
  69. package/docs/templates/agent-dag.schema.json +316 -316
  70. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  71. package/docs/templates/exec-plan.md +64 -64
  72. package/docs/templates/feature-spec.md +53 -53
  73. package/docs/templates/hybrid-dag.json +193 -193
  74. package/docs/templates/production-readiness-checklist.md +57 -57
  75. package/docs/templates/progress-log.md +17 -17
  76. package/docs/templates/project-start-checklist.md +9 -9
  77. package/docs/templates/qa-report.md +48 -48
  78. package/docs/templates/sprint-contract.md +29 -29
  79. package/docs/verification-matrix.md +41 -41
  80. package/examples/decision-gate-agent-dag.json +123 -123
  81. package/examples/example-dag.json +51 -51
  82. package/examples/hybrid-loop-agent-dag.json +194 -194
  83. package/harness.json +89 -89
  84. package/package.json +60 -58
  85. package/skills/ai-engineering-context/SKILL.md +48 -48
  86. package/skills/code-review-core/SKILL.md +20 -20
  87. package/skills/codebase-scout/SKILL.md +19 -19
  88. package/skills/loop-agent/SKILL.md +147 -145
  89. package/skills/loop-agent/references/README.md +67 -67
  90. package/skills/loop-agent/references/command-reference.md +368 -340
  91. package/skills/loop-agent/references/harness-policy.md +259 -258
  92. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  93. package/skills/loop-agent/references/learned/README.md +21 -21
  94. package/skills/loop-agent/references/long-running-loop.md +59 -59
  95. package/skills/loop-agent/references/model-routing.md +36 -36
  96. package/skills/loop-agent/references/multi-worktree.md +54 -54
  97. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  98. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  99. package/skills/loop-agent/references/pi-prompt.md +23 -23
  100. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  101. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  102. package/skills/loop-agent/references/task-workflow.md +84 -84
  103. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  104. package/skills/requesting-code-review/SKILL.md +101 -101
  105. package/skills/requesting-code-review/code-reviewer.md +168 -168
  106. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  107. package/skills/systematic-debugging/SKILL.md +296 -296
  108. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  109. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  110. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  111. package/skills/systematic-debugging/find-polluter.sh +63 -63
  112. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  113. package/skills/systematic-debugging/test-academic.md +14 -14
  114. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  115. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  116. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  117. package/skills/test-driven-development/SKILL.md +20 -20
  118. package/skills/verification-before-completion/SKILL.md +154 -154
  119. package/skills/webapp-testing/SKILL.md +19 -19
@@ -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 {};
@@ -0,0 +1,352 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ import { resolveLoopAgentProfile } from "../profile-mapping.js";
5
+ import { mapRiskLevelToComplexity } from "./complexity-mapping.js";
6
+ import { taskSpecSchema } from "./schema.js";
7
+ const CODE_WRITING_TYPES = new Set([
8
+ "backend-feature",
9
+ "frontend-feature",
10
+ "bugfix",
11
+ "qa-testcode",
12
+ ]);
13
+ const VERIFY_OPTIONAL_TYPES = new Set(["qa-analysis", "reviewer-gate"]);
14
+ const QA_TYPES = new Set([
15
+ "qa-analysis",
16
+ "qa-casegen",
17
+ "qa-testcode",
18
+ "qa-execute",
19
+ "qa-failure-analysis",
20
+ ]);
21
+ export async function validateTaskSpec(input, options = {}) {
22
+ const schemaResult = taskSpecSchema.safeParse(input);
23
+ const fallback = fallbackIdentity(input);
24
+ if (!schemaResult.success) {
25
+ const errors = schemaResult.error.issues.map((issue) => ({
26
+ layer: "schema",
27
+ code: "schema-invalid",
28
+ message: issue.message,
29
+ path: issue.path.join("."),
30
+ }));
31
+ const failure = firstFailure(errors, "schema-invalid", "TaskSpec schema validation failed");
32
+ return buildResult({
33
+ taskId: fallback.taskId,
34
+ featureId: fallback.featureId,
35
+ businessProfile: fallback.businessProfile,
36
+ riskLevel: fallback.riskLevel,
37
+ checks: {
38
+ schema: failure,
39
+ sourceDocs: failureFor("schema-invalid", "Skipped because schema validation failed"),
40
+ acceptanceRefs: failureFor("schema-invalid", "Skipped because schema validation failed"),
41
+ dependsOn: "skipped",
42
+ pathBoundary: failureFor("schema-invalid", "Skipped because schema validation failed"),
43
+ verifyCommands: failureFor("schema-invalid", "Skipped because schema validation failed"),
44
+ profileMapping: failureFor("schema-invalid", "Skipped because schema validation failed"),
45
+ },
46
+ errors,
47
+ warnings: [],
48
+ });
49
+ }
50
+ const taskSpec = schemaResult.data;
51
+ const errors = [];
52
+ const warnings = [];
53
+ const sourceDocsCheck = await checkSourceDocs(taskSpec, options, errors);
54
+ const acceptanceRefsCheck = await checkAcceptanceRefs(taskSpec, options, errors);
55
+ const dependsOnCheck = await checkDependsOn(taskSpec, options, errors);
56
+ const pathBoundaryCheck = checkPathBoundaries(taskSpec, errors, warnings);
57
+ const verifyCommandsCheck = checkVerifyCommands(taskSpec, errors);
58
+ const profileMapping = resolveLoopAgentProfile(taskSpec);
59
+ return buildResult({
60
+ taskId: taskSpec.id,
61
+ featureId: taskSpec.feature_id,
62
+ businessProfile: taskSpec.type,
63
+ riskLevel: taskSpec.risk_level,
64
+ loopAgentProfilePreview: profileMapping.loopAgentProfile,
65
+ checks: {
66
+ schema: "ok",
67
+ sourceDocs: sourceDocsCheck,
68
+ acceptanceRefs: acceptanceRefsCheck,
69
+ dependsOn: dependsOnCheck,
70
+ pathBoundary: pathBoundaryCheck,
71
+ verifyCommands: verifyCommandsCheck,
72
+ profileMapping: "ok",
73
+ },
74
+ errors,
75
+ warnings,
76
+ });
77
+ }
78
+ function buildResult(input) {
79
+ const riskLevel = input.riskLevel === "low" ||
80
+ input.riskLevel === "medium" ||
81
+ input.riskLevel === "high"
82
+ ? input.riskLevel
83
+ : "medium";
84
+ return {
85
+ schemaVersion: 1,
86
+ ok: input.errors.length === 0 && checksAreOk(input.checks),
87
+ taskId: input.taskId,
88
+ featureId: input.featureId,
89
+ businessProfile: input.businessProfile,
90
+ riskLevel: input.riskLevel,
91
+ complexityPreview: mapRiskLevelToComplexity(riskLevel),
92
+ loopAgentProfilePreview: input.loopAgentProfilePreview,
93
+ checks: input.checks,
94
+ errors: input.errors,
95
+ warnings: input.warnings,
96
+ };
97
+ }
98
+ function checksAreOk(checks) {
99
+ return Object.values(checks).every((check) => check === "ok" || check === "skipped");
100
+ }
101
+ async function checkSourceDocs(taskSpec, options, errors) {
102
+ const baseDir = getTaskSpecDir(options);
103
+ if (!baseDir)
104
+ return "ok";
105
+ for (const [key, relativePath] of Object.entries(taskSpec.source_docs)) {
106
+ if (!relativePath)
107
+ continue;
108
+ if (!(await exists(path.resolve(baseDir, relativePath)))) {
109
+ const error = {
110
+ layer: "file-existence",
111
+ code: "source-doc-missing",
112
+ message: `source_docs.${key} does not exist: ${relativePath}`,
113
+ path: `source_docs.${key}`,
114
+ };
115
+ errors.push(error);
116
+ return toFailure(error);
117
+ }
118
+ }
119
+ return "ok";
120
+ }
121
+ async function checkAcceptanceRefs(taskSpec, options, errors) {
122
+ const baseDir = getTaskSpecDir(options);
123
+ if (!baseDir)
124
+ return "ok";
125
+ const acceptancePath = path.resolve(baseDir, taskSpec.source_docs.acceptance);
126
+ if (!(await exists(acceptancePath))) {
127
+ const error = {
128
+ layer: "file-existence",
129
+ code: "acceptance-source-missing",
130
+ message: `acceptance source does not exist: ${taskSpec.source_docs.acceptance}`,
131
+ path: "source_docs.acceptance",
132
+ };
133
+ errors.push(error);
134
+ return toFailure(error);
135
+ }
136
+ const parsed = YAML.parse(await readFile(acceptancePath, "utf-8"));
137
+ const ids = collectStringIds(parsed);
138
+ for (const acceptanceRef of taskSpec.acceptance_refs) {
139
+ if (!ids.has(acceptanceRef)) {
140
+ const error = {
141
+ layer: "file-existence",
142
+ code: "acceptance-ref-missing",
143
+ message: `acceptance ref not found in acceptance source: ${acceptanceRef}`,
144
+ path: "acceptance_refs",
145
+ };
146
+ errors.push(error);
147
+ return toFailure(error);
148
+ }
149
+ }
150
+ return "ok";
151
+ }
152
+ async function checkDependsOn(taskSpec, options, errors) {
153
+ if (taskSpec.depends_on.length === 0)
154
+ return "skipped";
155
+ const baseDir = getTaskSpecDir(options);
156
+ if (!baseDir)
157
+ return "ok";
158
+ for (const dependency of taskSpec.depends_on) {
159
+ const dependencyPath = path.resolve(baseDir, `${dependency}.yaml`);
160
+ if (!(await exists(dependencyPath))) {
161
+ const error = {
162
+ layer: "file-existence",
163
+ code: "depends-on-missing",
164
+ message: `depends_on task file does not exist: ${dependency}.yaml`,
165
+ path: "depends_on",
166
+ };
167
+ errors.push(error);
168
+ return toFailure(error);
169
+ }
170
+ }
171
+ return "ok";
172
+ }
173
+ function checkPathBoundaries(taskSpec, errors, warnings) {
174
+ const start = errors.length;
175
+ for (const [index, allowedPath] of taskSpec.constraints.allowed_paths.entries()) {
176
+ if (isBroadAllowedPath(allowedPath)) {
177
+ errors.push({
178
+ layer: "path-boundary",
179
+ code: "broad-allowed-paths",
180
+ message: `allowed_paths entry is too broad: ${allowedPath}`,
181
+ path: `constraints.allowed_paths[${index}]`,
182
+ });
183
+ }
184
+ for (const [forbiddenIndex, forbiddenPath] of taskSpec.constraints.forbidden_paths.entries()) {
185
+ if (patternsOverlap(allowedPath, forbiddenPath)) {
186
+ errors.push({
187
+ layer: "path-boundary",
188
+ code: "allowed-forbidden-overlap",
189
+ message: `allowed path overlaps forbidden path: ${allowedPath} / ${forbiddenPath}`,
190
+ path: `constraints.forbidden_paths[${forbiddenIndex}]`,
191
+ });
192
+ }
193
+ }
194
+ }
195
+ if (CODE_WRITING_TYPES.has(taskSpec.type) &&
196
+ taskSpec.constraints.allowed_paths.length === 0) {
197
+ errors.push({
198
+ layer: "path-boundary",
199
+ code: "code-task-empty-allowed-paths",
200
+ message: `${taskSpec.type} requires non-empty allowed_paths`,
201
+ path: "constraints.allowed_paths",
202
+ });
203
+ }
204
+ if (taskSpec.constraints.forbidden_paths.length === 0) {
205
+ errors.push({
206
+ layer: "path-boundary",
207
+ code: "forbidden-paths-empty",
208
+ message: "TaskSpec requires non-empty forbidden_paths",
209
+ path: "constraints.forbidden_paths",
210
+ });
211
+ }
212
+ if (QA_TYPES.has(taskSpec.type) && !hasQaBusinessCodeForbiddenPath(taskSpec)) {
213
+ errors.push({
214
+ layer: "path-boundary",
215
+ code: "qa-business-code-not-forbidden",
216
+ message: "QA tasks must forbid common business code directories",
217
+ path: "constraints.forbidden_paths",
218
+ });
219
+ }
220
+ const complexity = mapRiskLevelToComplexity(taskSpec.risk_level);
221
+ if ((complexity === "medium" || complexity === "large") &&
222
+ taskSpec.constraints.allowed_paths.length === 0) {
223
+ errors.push({
224
+ layer: "path-boundary",
225
+ code: "empty-allowed-paths-for-complexity",
226
+ message: `${complexity} tasks require non-empty allowed_paths before materialize`,
227
+ path: "constraints.allowed_paths",
228
+ });
229
+ }
230
+ if (complexity === "small" && taskSpec.constraints.allowed_paths.length === 0) {
231
+ warnings.push({
232
+ code: "empty-allowed-paths-path-guard-risk",
233
+ message: "empty allowed_paths will make non-harness writes fail path guard",
234
+ path: "constraints.allowed_paths",
235
+ });
236
+ }
237
+ const pathErrors = errors.slice(start).filter((error) => error.layer === "path-boundary");
238
+ if (pathErrors.length === 0)
239
+ return "ok";
240
+ return toFailure(pathErrors[0]);
241
+ }
242
+ function checkVerifyCommands(taskSpec, errors) {
243
+ if (!VERIFY_OPTIONAL_TYPES.has(taskSpec.type) &&
244
+ taskSpec.verify.commands.length === 0) {
245
+ const error = {
246
+ layer: "file-existence",
247
+ code: "verify-commands-empty",
248
+ message: `${taskSpec.type} requires verify.commands`,
249
+ path: "verify.commands",
250
+ };
251
+ errors.push(error);
252
+ return toFailure(error);
253
+ }
254
+ return "ok";
255
+ }
256
+ function getTaskSpecDir(options) {
257
+ return options.taskSpecPath ? path.dirname(options.taskSpecPath) : undefined;
258
+ }
259
+ async function exists(filePath) {
260
+ try {
261
+ await access(filePath);
262
+ return true;
263
+ }
264
+ catch {
265
+ return false;
266
+ }
267
+ }
268
+ function collectStringIds(value, ids = new Set()) {
269
+ if (Array.isArray(value)) {
270
+ for (const item of value)
271
+ collectStringIds(item, ids);
272
+ return ids;
273
+ }
274
+ if (value && typeof value === "object") {
275
+ for (const [key, child] of Object.entries(value)) {
276
+ if (key === "id" && typeof child === "string")
277
+ ids.add(child);
278
+ collectStringIds(child, ids);
279
+ }
280
+ }
281
+ return ids;
282
+ }
283
+ function isBroadAllowedPath(pattern) {
284
+ const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
285
+ return normalized === "**" || normalized === "./**" || normalized === ".";
286
+ }
287
+ function probeForPattern(pattern) {
288
+ return pattern
289
+ .replace(/\\/g, "/")
290
+ .replace(/\/\*\*$/, "/_probe_")
291
+ .replace(/\*\*$/, "_probe_")
292
+ .replace(/\*/g, "_")
293
+ .replace(/^\.\//, "");
294
+ }
295
+ function patternsOverlap(left, right) {
296
+ const leftProbe = probeForPattern(left);
297
+ const rightProbe = probeForPattern(right);
298
+ return (pathMatchesPattern(leftProbe, right) ||
299
+ pathMatchesPattern(rightProbe, left) ||
300
+ leftProbe === rightProbe);
301
+ }
302
+ function pathMatchesPattern(filePath, pattern) {
303
+ const normalizedPath = filePath.replace(/\\/g, "/");
304
+ const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
305
+ if (normalizedPattern.endsWith("/**")) {
306
+ const prefix = normalizedPattern.slice(0, -3);
307
+ return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`);
308
+ }
309
+ if (normalizedPattern.includes("*")) {
310
+ const escaped = normalizedPattern
311
+ .split("*")
312
+ .map((part) => part.replace(/[|\\{}()[\]^$+?.]/g, "\\$&"))
313
+ .join(".*");
314
+ return new RegExp(`^${escaped}$`).test(normalizedPath);
315
+ }
316
+ return normalizedPath === normalizedPattern;
317
+ }
318
+ function hasQaBusinessCodeForbiddenPath(taskSpec) {
319
+ return taskSpec.constraints.forbidden_paths.some((forbiddenPath) => {
320
+ const normalized = forbiddenPath.replace(/\\/g, "/");
321
+ return (normalized.includes("/src/") ||
322
+ normalized.includes("src/**") ||
323
+ normalized.includes("/services/") ||
324
+ normalized.includes("services/**") ||
325
+ normalized.includes("/app/") ||
326
+ normalized.includes("app/**") ||
327
+ normalized.includes("/apps/"));
328
+ });
329
+ }
330
+ function fallbackIdentity(input) {
331
+ const record = input && typeof input === "object" ? input : {};
332
+ return {
333
+ taskId: typeof record.id === "string" ? record.id : "",
334
+ featureId: typeof record.feature_id === "string" ? record.feature_id : "",
335
+ businessProfile: typeof record.type === "string" ? record.type : "",
336
+ riskLevel: typeof record.risk_level === "string" ? record.risk_level : "medium",
337
+ };
338
+ }
339
+ function failureFor(code, message, pathValue) {
340
+ return { status: "failed", code, message, path: pathValue };
341
+ }
342
+ function firstFailure(errors, code, message) {
343
+ return errors[0] ? toFailure(errors[0]) : failureFor(code, message);
344
+ }
345
+ function toFailure(error) {
346
+ return {
347
+ status: "failed",
348
+ code: error.code,
349
+ message: error.message,
350
+ path: error.path,
351
+ };
352
+ }