@tea-agent/loop-agent 0.2.1 → 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 (135) hide show
  1. package/AGENTS.md +91 -87
  2. package/CHANGELOG.md +89 -52
  3. package/README.md +195 -180
  4. package/bin/agent-worker.js +22 -0
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/args.js +6 -0
  7. package/dist/application/dag/generate-task-dag.js +2 -0
  8. package/dist/application/dag/run-dag.js +3 -0
  9. package/dist/application/dag/validate-dag.js +40 -0
  10. package/dist/cli/command-definitions.js +2 -2
  11. package/dist/cli/program.js +24 -4
  12. package/dist/commands/init.js +1011 -459
  13. package/dist/commands/loop-benchmark.js +11 -11
  14. package/dist/commands/pi-reuse-benchmark.js +16 -16
  15. package/dist/executors/cursor-executor.js +1 -1
  16. package/dist/executors/dag-pi-executor.js +8 -1
  17. package/dist/task/runtime.js +27 -27
  18. package/dist/worker/cli.js +119 -0
  19. package/dist/worker/loop-agent/command-result.js +1 -0
  20. package/dist/worker/loop-agent/loop-agent-client.js +105 -0
  21. package/dist/worker/loop-agent/parse-json.js +14 -0
  22. package/dist/worker/materialize/harness-task-materializer.js +157 -0
  23. package/dist/worker/pool/failure-routing.js +98 -0
  24. package/dist/worker/pool/run-store.js +117 -0
  25. package/dist/worker/pool/types.js +1 -0
  26. package/dist/worker/preflight.js +108 -0
  27. package/dist/worker/profile-mapping.js +76 -0
  28. package/dist/worker/progress-reporter.js +81 -0
  29. package/dist/worker/report/morning-report.js +69 -0
  30. package/dist/worker/repos/repo-resolver.js +23 -0
  31. package/dist/worker/run-task/run-task.js +359 -0
  32. package/dist/worker/runner/run-ready.js +216 -0
  33. package/dist/worker/task-graph/acceptance-schema.js +25 -0
  34. package/dist/worker/task-graph/ready-queue.js +23 -0
  35. package/dist/worker/task-graph/task-graph-schema.js +28 -0
  36. package/dist/worker/task-graph/types.js +1 -0
  37. package/dist/worker/task-graph/validate.js +188 -0
  38. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  39. package/dist/worker/task-spec/schema.js +116 -0
  40. package/dist/worker/task-spec/types.js +1 -0
  41. package/dist/worker/task-spec/validate.js +352 -0
  42. package/dist/workflows/dag/canvas-observer.js +275 -275
  43. package/dist/workflows/dag/dynamic-runtime/loop-until.js +2 -1
  44. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  45. package/dist/workflows/dag/init-hybrid.js +3 -3
  46. package/dist/workflows/dag/skills.js +3 -3
  47. package/dist/workflows/dag/types.js +2 -0
  48. package/dist/workflows/dynamic/compile.js +11 -0
  49. package/dist/workflows/dynamic/spec.js +1 -0
  50. package/docs/README.md +72 -65
  51. package/docs/agent-dag-recovery-playbook.md +184 -184
  52. package/docs/agent-dag-runner.md +42 -40
  53. package/docs/architecture/runtime-boundaries.md +147 -147
  54. package/docs/cursor-executor-usage.md +25 -25
  55. package/docs/decisions/README.md +3 -3
  56. package/docs/design/README.md +36 -36
  57. package/docs/development-principles.md +73 -71
  58. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  59. package/docs/exec-plans/README.md +6 -6
  60. package/docs/exec-plans/active/README.md +7 -10
  61. package/docs/exec-plans/completed/README.md +19 -9
  62. package/docs/feature-workflow.md +186 -186
  63. package/docs/harness-methodology-debugging.md +153 -153
  64. package/docs/harness-methodology-tdd.md +130 -130
  65. package/docs/harness-methodology-verification.md +27 -27
  66. package/docs/init-surface.manifest.json +175 -0
  67. package/docs/loop-agent-harness.md +42 -42
  68. package/docs/production-readiness.md +96 -96
  69. package/docs/progress/README.md +3 -3
  70. package/docs/reports/README.md +5 -5
  71. package/docs/skills/README.md +6 -0
  72. package/docs/skills/vetted-skill-registry.md +26 -0
  73. package/docs/templates/adr.md +60 -60
  74. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  75. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  76. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  77. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  78. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  79. package/docs/templates/agent-dag-report.schema.json +454 -454
  80. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  81. package/docs/templates/agent-dag.base.json +195 -195
  82. package/docs/templates/agent-dag.final-verification.json +190 -190
  83. package/docs/templates/agent-dag.schema.json +316 -316
  84. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  85. package/docs/templates/exec-plan.md +64 -64
  86. package/docs/templates/feature-spec.md +53 -53
  87. package/docs/templates/hybrid-dag.json +193 -193
  88. package/docs/templates/init-evolution-review.md +33 -0
  89. package/docs/templates/production-readiness-checklist.md +57 -57
  90. package/docs/templates/progress-log.md +17 -17
  91. package/docs/templates/project-start-checklist.md +9 -9
  92. package/docs/templates/qa-report.md +48 -48
  93. package/docs/templates/sprint-contract.md +29 -29
  94. package/docs/verification-matrix.md +41 -41
  95. package/examples/decision-gate-agent-dag.json +123 -123
  96. package/examples/example-dag.json +51 -51
  97. package/examples/hybrid-loop-agent-dag.json +194 -194
  98. package/harness.json +94 -92
  99. package/package.json +66 -62
  100. package/skills/ai-engineering-context/SKILL.md +48 -48
  101. package/skills/code-review-core/SKILL.md +20 -0
  102. package/skills/codebase-scout/SKILL.md +19 -0
  103. package/skills/init-capability-evolution/SKILL.md +69 -0
  104. package/skills/loop-agent/SKILL.md +147 -145
  105. package/skills/loop-agent/references/README.md +67 -67
  106. package/skills/loop-agent/references/command-reference.md +403 -357
  107. package/skills/loop-agent/references/harness-policy.md +259 -258
  108. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  109. package/skills/loop-agent/references/learned/README.md +21 -21
  110. package/skills/loop-agent/references/long-running-loop.md +59 -59
  111. package/skills/loop-agent/references/model-routing.md +36 -36
  112. package/skills/loop-agent/references/multi-worktree.md +54 -54
  113. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  114. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  115. package/skills/loop-agent/references/pi-prompt.md +23 -23
  116. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  117. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  118. package/skills/loop-agent/references/task-workflow.md +84 -84
  119. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  120. package/skills/requesting-code-review/SKILL.md +101 -101
  121. package/skills/requesting-code-review/code-reviewer.md +168 -168
  122. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  123. package/skills/systematic-debugging/SKILL.md +296 -296
  124. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  125. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  126. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  127. package/skills/systematic-debugging/find-polluter.sh +63 -63
  128. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  129. package/skills/systematic-debugging/test-academic.md +14 -14
  130. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  131. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  132. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  133. package/skills/test-driven-development/SKILL.md +20 -0
  134. package/skills/verification-before-completion/SKILL.md +154 -154
  135. package/skills/webapp-testing/SKILL.md +19 -0
@@ -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
+ }