@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,105 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { parseCommandJson } from "./parse-json.js";
5
+ export const DEFAULT_WORKER_COMMAND_TIMEOUT_MS = 120_000;
6
+ export class LoopAgentClient {
7
+ loopAgentBin;
8
+ baseArgs;
9
+ artifactRoot;
10
+ defaultTimeoutMs;
11
+ env;
12
+ constructor(options) {
13
+ this.loopAgentBin = options.loopAgentBin;
14
+ this.baseArgs = options.baseArgs ?? [];
15
+ this.artifactRoot = options.artifactRoot;
16
+ this.defaultTimeoutMs =
17
+ options.defaultTimeoutMs ?? DEFAULT_WORKER_COMMAND_TIMEOUT_MS;
18
+ this.env = options.env;
19
+ }
20
+ async run(args, options) {
21
+ return this.runCommand(this.loopAgentBin, [...this.baseArgs, ...args], args, options);
22
+ }
23
+ async runExternal(command, args, options) {
24
+ return this.runCommand(command, args, args, options);
25
+ }
26
+ async runCommand(command, commandArgs, resultArgs, options) {
27
+ const startedAt = Date.now();
28
+ const artifactDir = path.join(this.artifactRoot, sanitizeName(options.artifactName));
29
+ await mkdir(artifactDir, { recursive: true });
30
+ const { stdout, stderr, exitCode, timedOut } = await spawnCommand({
31
+ command,
32
+ args: commandArgs,
33
+ cwd: options.cwd,
34
+ timeoutMs: options.timeoutMs ?? this.defaultTimeoutMs,
35
+ env: { ...process.env, ...this.env, ...options.env },
36
+ });
37
+ const result = {
38
+ ok: exitCode === 0 && !timedOut,
39
+ args: resultArgs,
40
+ command,
41
+ cwd: options.cwd,
42
+ durationMs: Date.now() - startedAt,
43
+ exitCode,
44
+ stdout,
45
+ stderr,
46
+ timedOut,
47
+ artifacts: {
48
+ dir: artifactDir,
49
+ stdoutPath: path.join(artifactDir, "stdout.txt"),
50
+ stderrPath: path.join(artifactDir, "stderr.txt"),
51
+ resultPath: path.join(artifactDir, "result.json"),
52
+ },
53
+ };
54
+ if (options.expectJson) {
55
+ const parsed = parseCommandJson(stdout);
56
+ if (parsed.ok) {
57
+ result.json = parsed.value;
58
+ }
59
+ else {
60
+ result.ok = false;
61
+ result.parseFailure = parsed.failure;
62
+ }
63
+ }
64
+ await writeFile(result.artifacts.stdoutPath, stdout, "utf-8");
65
+ await writeFile(result.artifacts.stderrPath, stderr, "utf-8");
66
+ await writeFile(result.artifacts.resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
67
+ return result;
68
+ }
69
+ }
70
+ function spawnCommand(input) {
71
+ return new Promise((resolve, reject) => {
72
+ const child = spawn(input.command, input.args, {
73
+ cwd: input.cwd,
74
+ env: input.env,
75
+ shell: false,
76
+ stdio: ["ignore", "pipe", "pipe"],
77
+ });
78
+ let stdout = "";
79
+ let stderr = "";
80
+ let timedOut = false;
81
+ const timeout = setTimeout(() => {
82
+ timedOut = true;
83
+ child.kill("SIGTERM");
84
+ }, input.timeoutMs);
85
+ child.stdout.setEncoding("utf8");
86
+ child.stderr.setEncoding("utf8");
87
+ child.stdout.on("data", (chunk) => {
88
+ stdout += chunk;
89
+ });
90
+ child.stderr.on("data", (chunk) => {
91
+ stderr += chunk;
92
+ });
93
+ child.on("error", (error) => {
94
+ clearTimeout(timeout);
95
+ reject(error);
96
+ });
97
+ child.on("close", (exitCode) => {
98
+ clearTimeout(timeout);
99
+ resolve({ stdout, stderr, exitCode, timedOut });
100
+ });
101
+ });
102
+ }
103
+ function sanitizeName(name) {
104
+ return name.replace(/[^a-zA-Z0-9._-]+/g, "-");
105
+ }
@@ -0,0 +1,14 @@
1
+ export function parseCommandJson(stdout) {
2
+ try {
3
+ return { ok: true, value: JSON.parse(stdout) };
4
+ }
5
+ catch (error) {
6
+ return {
7
+ ok: false,
8
+ failure: {
9
+ code: "json-parse-failed",
10
+ message: error instanceof Error ? error.message : String(error),
11
+ },
12
+ };
13
+ }
14
+ }
@@ -0,0 +1,157 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ import { writeTaskConfig, writeTaskArtifactFile } from "../../infrastructure/harness/task-store.js";
5
+ import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
6
+ import { resolveLoopAgentProfile } from "../profile-mapping.js";
7
+ import { mapRiskLevelToComplexity } from "../task-spec/complexity-mapping.js";
8
+ import { validateTaskSpec } from "../task-spec/validate.js";
9
+ export async function materializeTaskSpec(options) {
10
+ const now = options.now ?? new Date();
11
+ const validation = await validateTaskSpec(options.taskSpec, {
12
+ taskSpecPath: options.taskSpecPath,
13
+ });
14
+ if (!validation.ok) {
15
+ const codes = validation.errors.map((error) => error.code).join(", ");
16
+ throw new Error(`TaskSpec validation failed: ${codes}`);
17
+ }
18
+ const harnessTaskId = buildHarnessTaskId(options.taskSpec, now);
19
+ const existingConfig = await loadExistingTaskConfig(options.repoRoot, harnessTaskId);
20
+ if (!existingConfig) {
21
+ const newTaskResult = await options.client.run(["new-task", harnessTaskId, options.taskSpec.title], {
22
+ cwd: options.repoRoot,
23
+ artifactName: `new-task-${harnessTaskId}`,
24
+ });
25
+ if (!newTaskResult.ok) {
26
+ const racedConfig = await loadExistingTaskConfig(options.repoRoot, harnessTaskId);
27
+ if (!racedConfig) {
28
+ throw new Error(`loop-agent new-task failed with exit ${newTaskResult.exitCode}: ${newTaskResult.stderr}`);
29
+ }
30
+ }
31
+ }
32
+ const paths = getTaskPaths(options.repoRoot, harnessTaskId);
33
+ const baseConfig = await loadTaskConfig(options.repoRoot, harnessTaskId);
34
+ const taskConfig = {
35
+ ...baseConfig,
36
+ taskId: harnessTaskId,
37
+ title: options.taskSpec.title,
38
+ allowedPaths: options.taskSpec.constraints.allowed_paths,
39
+ forbiddenPaths: options.taskSpec.constraints.forbidden_paths,
40
+ hardConstraints: options.taskSpec.constraints.hard_constraints,
41
+ complexity: mapRiskLevelToComplexity(options.taskSpec.risk_level),
42
+ verifyMode: options.taskSpec.verify.mode,
43
+ verifyPreset: options.taskSpec.verify.preset,
44
+ verifyQuota: options.taskSpec.verify.quota,
45
+ autoCommitAfterVerify: false,
46
+ };
47
+ await writeTaskConfig(options.repoRoot, harnessTaskId, taskConfig);
48
+ const sourcePaths = {
49
+ requirementPath: path.join(paths.sourceDir, "需求.md"),
50
+ constraintsPath: path.join(paths.sourceDir, "执行约束.md"),
51
+ taskYamlPath: path.join(paths.sourceDir, "task.yaml"),
52
+ };
53
+ await mkdir(paths.sourceDir, { recursive: true });
54
+ await writeFile(sourcePaths.requirementPath, renderRequirementMarkdown(options.taskSpec), "utf-8");
55
+ await writeFile(sourcePaths.constraintsPath, renderConstraintsMarkdown(options.taskSpec), "utf-8");
56
+ await writeFile(sourcePaths.taskYamlPath, YAML.stringify(options.taskSpec), "utf-8");
57
+ const profileMapping = resolveLoopAgentProfile(options.taskSpec);
58
+ const manifest = {
59
+ schemaVersion: 1,
60
+ businessId: options.taskSpec.id,
61
+ harnessTaskId,
62
+ featureId: options.taskSpec.feature_id,
63
+ loopAgentProfile: profileMapping.loopAgentProfile,
64
+ taskConfigPath: paths.taskConfigPath,
65
+ source: sourcePaths,
66
+ };
67
+ await writeTaskArtifactFile(options.repoRoot, harnessTaskId, "materialize-manifest.json", `${JSON.stringify(manifest, null, 2)}\n`);
68
+ return manifest;
69
+ }
70
+ export function buildHarnessTaskId(taskSpec, now) {
71
+ const date = now.toISOString().slice(0, 10);
72
+ const slug = slugify(`${taskSpec.id}-${taskSpec.title}`);
73
+ return `${date}-${slug}`;
74
+ }
75
+ function slugify(value) {
76
+ return value
77
+ .normalize("NFKD")
78
+ .replace(/[\u0300-\u036f]/g, "")
79
+ .toLowerCase()
80
+ .replace(/[^a-z0-9]+/g, "-")
81
+ .replace(/^-+|-+$/g, "")
82
+ .replace(/-{2,}/g, "-");
83
+ }
84
+ function renderRequirementMarkdown(taskSpec) {
85
+ const lines = [
86
+ `# ${taskSpec.title}`,
87
+ "",
88
+ `TaskSpec business id: ${taskSpec.id}`,
89
+ `Feature id: ${taskSpec.feature_id}`,
90
+ `Task type: ${taskSpec.type}`,
91
+ `Risk level: ${taskSpec.risk_level}`,
92
+ "",
93
+ ];
94
+ if (taskSpec.description) {
95
+ lines.push("## Description", "", taskSpec.description, "");
96
+ }
97
+ lines.push("## Goals", "", ...bulletLines(taskSpec.scope.goals), "");
98
+ if (taskSpec.scope.non_goals.length > 0) {
99
+ lines.push("## Non Goals", "", ...bulletLines(taskSpec.scope.non_goals), "");
100
+ }
101
+ if (taskSpec.scope.assumptions.length > 0) {
102
+ lines.push("## Assumptions", "", ...bulletLines(taskSpec.scope.assumptions), "");
103
+ }
104
+ if (taskSpec.scope.open_questions.length > 0) {
105
+ lines.push("## Open Questions", "", ...bulletLines(taskSpec.scope.open_questions), "");
106
+ }
107
+ lines.push("## Acceptance References", "", ...bulletLines(taskSpec.acceptance_refs), "");
108
+ lines.push("## Required Outputs", "", ...bulletLines(taskSpec.outputs.required), "");
109
+ return `${lines.join("\n").trimEnd()}\n`;
110
+ }
111
+ function renderConstraintsMarkdown(taskSpec) {
112
+ const lines = [
113
+ `# Execution Constraints for ${taskSpec.id}`,
114
+ "",
115
+ "## Allowed Paths",
116
+ "",
117
+ ...bulletLines(taskSpec.constraints.allowed_paths),
118
+ "",
119
+ "## Forbidden Paths",
120
+ "",
121
+ ...bulletLines(taskSpec.constraints.forbidden_paths),
122
+ "",
123
+ "## Hard Constraints",
124
+ "",
125
+ ...bulletLines(taskSpec.constraints.hard_constraints),
126
+ "",
127
+ "## Verification Commands",
128
+ "",
129
+ ...bulletLines(taskSpec.verify.commands.map((command) => command.command)),
130
+ "",
131
+ "## Source Docs",
132
+ "",
133
+ ...bulletLines(Object.entries(taskSpec.source_docs).map(([key, value]) => `${key}: ${value}`)),
134
+ ];
135
+ return `${lines.join("\n").trimEnd()}\n`;
136
+ }
137
+ function bulletLines(values) {
138
+ if (values.length === 0)
139
+ return ["- None"];
140
+ return values.map((value) => `- ${value}`);
141
+ }
142
+ async function loadExistingTaskConfig(repoRoot, taskId) {
143
+ try {
144
+ return await loadTaskConfig(repoRoot, taskId);
145
+ }
146
+ catch (error) {
147
+ if (isNotFound(error))
148
+ return undefined;
149
+ throw error;
150
+ }
151
+ }
152
+ function isNotFound(error) {
153
+ return Boolean(error &&
154
+ typeof error === "object" &&
155
+ "code" in error &&
156
+ error.code === "ENOENT");
157
+ }
@@ -0,0 +1,98 @@
1
+ const PRODUCT_CATEGORIES = new Set([
2
+ "SpecUnclear",
3
+ "ContractMismatch",
4
+ "ProductBug",
5
+ "TestBug",
6
+ "EnvFailure",
7
+ "FlakyTest",
8
+ "RiskyChange",
9
+ "DependencyFailure",
10
+ "NeedsHuman",
11
+ "Unknown",
12
+ ]);
13
+ const FOLLOW_UP_BY_CATEGORY = {
14
+ SpecUnclear: "spec-clarification",
15
+ ContractMismatch: "architecture-contract-fix",
16
+ ProductBug: "dev-fix",
17
+ TestBug: "qa-fix-test",
18
+ EnvFailure: "env-fix",
19
+ FlakyTest: "flaky-test-analysis",
20
+ RiskyChange: "human-review",
21
+ DependencyFailure: "unblock-dependency",
22
+ NeedsHuman: "human-review",
23
+ Unknown: "human-triage",
24
+ };
25
+ const FOLLOW_UP_PREFIX_BY_CATEGORY = {
26
+ SpecUnclear: "SPEC",
27
+ ContractMismatch: "CONTRACT",
28
+ ProductBug: "FIX",
29
+ TestBug: "QA-FIX",
30
+ EnvFailure: "ENV",
31
+ FlakyTest: "FLAKY",
32
+ RiskyChange: "REVIEW",
33
+ DependencyFailure: "UNBLOCK",
34
+ NeedsHuman: "REVIEW",
35
+ Unknown: "TRIAGE",
36
+ };
37
+ const DAG_TO_PRODUCT_CATEGORY = {
38
+ validation: "SpecUnclear",
39
+ executor: "EnvFailure",
40
+ "write-guard": "RiskyChange",
41
+ timeout: "EnvFailure",
42
+ auth: "EnvFailure",
43
+ "human-required": "NeedsHuman",
44
+ "human-rejected": "NeedsHuman",
45
+ "shell-command": "ProductBug",
46
+ "static-error": "SpecUnclear",
47
+ "decision-envelope": "NeedsHuman",
48
+ skipped: "DependencyFailure",
49
+ unknown: "Unknown",
50
+ };
51
+ export function deriveFailureRoute(result) {
52
+ if (result.status === "succeeded")
53
+ return undefined;
54
+ const primaryFailure = readObject(result.reportDecision.primaryFailure);
55
+ const category = readProductCategory(primaryFailure, "productLineFailureCategory") ??
56
+ readProductCategory(primaryFailure, "productLineCategory") ??
57
+ readProductCategory(primaryFailure, "product_line_failure_category") ??
58
+ mapDagCategory(readString(primaryFailure, "failureCategory")) ??
59
+ mapDagCategory(readString(primaryFailure, "normalizedFailureCategory")) ??
60
+ categoryFromDecisionReason(result.reportDecision.reason);
61
+ const recommendedFollowUpKind = readString(primaryFailure, "recommendedFollowUpKind") ??
62
+ readString(primaryFailure, "recommendedFollowUp") ??
63
+ FOLLOW_UP_BY_CATEGORY[category];
64
+ return {
65
+ category,
66
+ recommendedFollowUpKind,
67
+ derivedFollowUpTaskId: `${FOLLOW_UP_PREFIX_BY_CATEGORY[category]}-${result.businessId}`,
68
+ source: primaryFailure ? "report-primary-failure" : "fallback",
69
+ };
70
+ }
71
+ function readProductCategory(value, key) {
72
+ const candidate = readString(value, key);
73
+ return candidate && PRODUCT_CATEGORIES.has(candidate)
74
+ ? candidate
75
+ : undefined;
76
+ }
77
+ function mapDagCategory(value) {
78
+ if (!value || value === "success")
79
+ return undefined;
80
+ return DAG_TO_PRODUCT_CATEGORY[value] ?? "Unknown";
81
+ }
82
+ function categoryFromDecisionReason(reason) {
83
+ if (reason === "report-json-unavailable" || reason === "report-run-missing") {
84
+ return "Unknown";
85
+ }
86
+ return "Unknown";
87
+ }
88
+ function readObject(value) {
89
+ if (!value || typeof value !== "object" || Array.isArray(value))
90
+ return undefined;
91
+ return value;
92
+ }
93
+ function readString(value, key) {
94
+ if (!value)
95
+ return undefined;
96
+ const child = value[key];
97
+ return typeof child === "string" ? child : undefined;
98
+ }
@@ -0,0 +1,125 @@
1
+ import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export function getTaskPoolRoot(repoRoot) {
4
+ return path.join(repoRoot, ".task-pool");
5
+ }
6
+ export function getRunsJsonlPath(repoRoot) {
7
+ return path.join(getTaskPoolRoot(repoRoot), "runs", "runs.jsonl");
8
+ }
9
+ export function getEventsJsonlPath(repoRoot) {
10
+ return path.join(getTaskPoolRoot(repoRoot), "events", "events.jsonl");
11
+ }
12
+ export function getTaskStatePath(repoRoot, taskId) {
13
+ return path.join(getTaskPoolRoot(repoRoot), "state", `${taskId}.json`);
14
+ }
15
+ export async function recordTaskPoolRun(input) {
16
+ await ensurePoolDirs(input.repoRoot);
17
+ await appendJsonlFile(getRunsJsonlPath(input.repoRoot), input.run);
18
+ await writeTaskPoolState(input.repoRoot, stateFromRun(input.run));
19
+ await appendJsonlFile(getEventsJsonlPath(input.repoRoot), eventFromRun(input.run));
20
+ }
21
+ export async function findRunByWorkerRunId(repoRoot, workerRunId) {
22
+ const runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
23
+ return runs.find((run) => run.workerRunId === workerRunId);
24
+ }
25
+ export async function readTaskPoolState(repoRoot, taskId) {
26
+ const statePath = getTaskStatePath(repoRoot, taskId);
27
+ try {
28
+ return JSON.parse(await readFile(statePath, "utf-8"));
29
+ }
30
+ catch (error) {
31
+ if (isNotFound(error))
32
+ return undefined;
33
+ throw error;
34
+ }
35
+ }
36
+ export async function readAllTaskPoolStates(repoRoot) {
37
+ const stateDir = path.join(getTaskPoolRoot(repoRoot), "state");
38
+ try {
39
+ const entries = await readdir(stateDir);
40
+ const states = {};
41
+ for (const entry of entries) {
42
+ if (!entry.endsWith(".json"))
43
+ continue;
44
+ const raw = await readFile(path.join(stateDir, entry), "utf-8");
45
+ const state = JSON.parse(raw);
46
+ states[state.taskId] = state;
47
+ }
48
+ return states;
49
+ }
50
+ catch (error) {
51
+ if (isNotFound(error))
52
+ return {};
53
+ throw error;
54
+ }
55
+ }
56
+ export async function writeTaskPoolState(repoRoot, state) {
57
+ const statePath = getTaskStatePath(repoRoot, state.taskId);
58
+ await mkdir(path.dirname(statePath), { recursive: true });
59
+ await writeFile(statePath, `${JSON.stringify({ schemaVersion: 1, ...state }, null, 2)}\n`, "utf-8");
60
+ }
61
+ export async function readJsonlFile(filePath) {
62
+ try {
63
+ const raw = await readFile(filePath, "utf-8");
64
+ return raw
65
+ .split(/\r?\n/)
66
+ .filter((line) => line.trim().length > 0)
67
+ .map((line) => JSON.parse(line));
68
+ }
69
+ catch (error) {
70
+ if (isNotFound(error))
71
+ return [];
72
+ throw error;
73
+ }
74
+ }
75
+ async function appendJsonlFile(filePath, value) {
76
+ await mkdir(path.dirname(filePath), { recursive: true });
77
+ try {
78
+ await appendFile(filePath, `${JSON.stringify(value)}\n`, "utf-8");
79
+ }
80
+ catch (error) {
81
+ const detail = error instanceof Error ? error.message : String(error);
82
+ // Include path so callers/tests can identify which JSONL file failed
83
+ // (Node EISDIR messages on Windows often omit the path).
84
+ throw new Error(`failed to append ${filePath}: ${detail}`, { cause: error });
85
+ }
86
+ }
87
+ async function ensurePoolDirs(repoRoot) {
88
+ await mkdir(path.join(getTaskPoolRoot(repoRoot), "runs"), { recursive: true });
89
+ await mkdir(path.join(getTaskPoolRoot(repoRoot), "events"), { recursive: true });
90
+ await mkdir(path.join(getTaskPoolRoot(repoRoot), "state"), { recursive: true });
91
+ }
92
+ function stateFromRun(run) {
93
+ return {
94
+ taskId: run.taskId,
95
+ status: stateStatusFromRun(run),
96
+ updatedAt: run.recordedAt,
97
+ workerRunId: run.workerRunId,
98
+ ...(run.runRecordPath ? { lastRunRecordPath: run.runRecordPath } : {}),
99
+ ...(run.failure ? { failure: run.failure } : {}),
100
+ };
101
+ }
102
+ function stateStatusFromRun(run) {
103
+ if (run.status === "succeeded")
104
+ return "Done";
105
+ if (run.status === "run-error")
106
+ return "Blocked";
107
+ return "Failed";
108
+ }
109
+ function eventFromRun(run) {
110
+ return {
111
+ schemaVersion: 1,
112
+ at: run.recordedAt,
113
+ type: "task-run-recorded",
114
+ batchRunId: run.batchRunId,
115
+ workerRunId: run.workerRunId,
116
+ taskId: run.taskId,
117
+ status: run.status,
118
+ };
119
+ }
120
+ function isNotFound(error) {
121
+ return Boolean(error &&
122
+ typeof error === "object" &&
123
+ "code" in error &&
124
+ error.code === "ENOENT");
125
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,108 @@
1
+ import { access } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export const DEFAULT_CHECK_REPO_TIMEOUT_MS = 300_000;
4
+ export async function preflightTargetRepo(input) {
5
+ const commands = [];
6
+ if (!(await exists(path.join(input.repoRoot, "harness.json")))) {
7
+ return {
8
+ ok: false,
9
+ code: "target-not-initialized",
10
+ message: "target repo is missing harness.json",
11
+ commands,
12
+ };
13
+ }
14
+ if (!(await exists(path.join(input.repoRoot, ".harness")))) {
15
+ return {
16
+ ok: false,
17
+ code: "target-not-initialized",
18
+ message: "target repo is missing .harness",
19
+ commands,
20
+ };
21
+ }
22
+ const version = await input.client.run(["--version"], {
23
+ cwd: input.repoRoot,
24
+ artifactName: "loop-agent-version",
25
+ });
26
+ commands.push({ artifactName: "loop-agent-version", result: version });
27
+ if (!version.ok) {
28
+ return {
29
+ ok: false,
30
+ code: "version-failed",
31
+ message: "loop-agent --version failed",
32
+ commands,
33
+ };
34
+ }
35
+ const inspect = await input.client.run(["inspect"], {
36
+ cwd: input.repoRoot,
37
+ artifactName: "inspect",
38
+ expectJson: true,
39
+ });
40
+ commands.push({ artifactName: "inspect", result: inspect });
41
+ if (!inspect.ok || !inspect.json || typeof inspect.json !== "object") {
42
+ return {
43
+ ok: false,
44
+ code: "inspect-failed",
45
+ message: "loop-agent inspect failed or did not return JSON",
46
+ commands,
47
+ };
48
+ }
49
+ const docsAudit = await input.client.run(["docs", "audit"], {
50
+ cwd: input.repoRoot,
51
+ artifactName: "docs-audit",
52
+ });
53
+ commands.push({ artifactName: "docs-audit", result: docsAudit });
54
+ if (!docsAudit.ok) {
55
+ return {
56
+ ok: false,
57
+ code: "docs-audit-failed",
58
+ message: "loop-agent docs audit failed",
59
+ commands,
60
+ };
61
+ }
62
+ const gitStatus = await input.client.runExternal("git", ["status", "--short", "--branch"], {
63
+ cwd: input.repoRoot,
64
+ artifactName: "git-status",
65
+ });
66
+ commands.push({ artifactName: "git-status", result: gitStatus });
67
+ if (!gitStatus.ok) {
68
+ return {
69
+ ok: false,
70
+ code: "git-status-failed",
71
+ message: "git status --short --branch failed",
72
+ commands,
73
+ };
74
+ }
75
+ if (input.runCheckRepo) {
76
+ const [command, ...args] = input.checkRepoCommand ?? ["bash", "scripts/check-repo.sh"];
77
+ const checkRepo = await input.client.runExternal(command, args, {
78
+ cwd: input.repoRoot,
79
+ artifactName: "check-repo",
80
+ timeoutMs: DEFAULT_CHECK_REPO_TIMEOUT_MS,
81
+ });
82
+ commands.push({ artifactName: "check-repo", result: checkRepo });
83
+ if (!checkRepo.ok) {
84
+ return {
85
+ ok: false,
86
+ code: "check-repo-failed",
87
+ message: `${[command, ...args].join(" ")} failed`,
88
+ commands,
89
+ };
90
+ }
91
+ }
92
+ return {
93
+ ok: true,
94
+ version: version.stdout.trim(),
95
+ inspect: inspect.json,
96
+ gitStatus: gitStatus.stdout,
97
+ commands,
98
+ };
99
+ }
100
+ async function exists(filePath) {
101
+ try {
102
+ await access(filePath);
103
+ return true;
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
@@ -0,0 +1,76 @@
1
+ const MINIMAL_TYPES = new Set([
2
+ "doc-update",
3
+ "qa-analysis",
4
+ "qa-casegen",
5
+ "qa-execute",
6
+ ]);
7
+ const SENSITIVE_PATH_MARKERS = [
8
+ "auth",
9
+ "payment",
10
+ "database",
11
+ "migration",
12
+ "migrations",
13
+ "infra",
14
+ "secret",
15
+ "public-contract",
16
+ "public_contract",
17
+ ];
18
+ export function resolveLoopAgentProfile(taskSpec) {
19
+ if (taskSpec.risk_level === "high") {
20
+ return mapping(taskSpec, "supervised", "risk-high", [
21
+ "risk_level=high requires supervised governance",
22
+ ]);
23
+ }
24
+ if (MINIMAL_TYPES.has(taskSpec.type)) {
25
+ return mapping(taskSpec, "minimal", `${taskSpec.type}-minimal`, [
26
+ `type=${taskSpec.type}`,
27
+ ]);
28
+ }
29
+ const sensitiveMarkers = collectSensitiveMarkers(taskSpec);
30
+ if (sensitiveMarkers.length > 0) {
31
+ return mapping(taskSpec, "reviewed", "sensitive-path", [
32
+ `sensitive path markers: ${sensitiveMarkers.join(", ")}`,
33
+ ]);
34
+ }
35
+ if (taskSpec.type === "backend-feature" ||
36
+ taskSpec.type === "frontend-feature") {
37
+ if (taskSpec.risk_level === "medium") {
38
+ return mapping(taskSpec, "reviewed", `${taskSpec.type}-medium`, [
39
+ `type=${taskSpec.type}`,
40
+ "risk_level=medium",
41
+ ]);
42
+ }
43
+ return mapping(taskSpec, "standard", `${taskSpec.type}-low`, [
44
+ `type=${taskSpec.type}`,
45
+ "risk_level=low",
46
+ ]);
47
+ }
48
+ if (taskSpec.type === "qa-testcode") {
49
+ return mapping(taskSpec, "standard", "qa-testcode-standard", [
50
+ "type=qa-testcode",
51
+ ]);
52
+ }
53
+ if (taskSpec.type === "ci-fix" || taskSpec.type === "reviewer-gate") {
54
+ return mapping(taskSpec, "reviewed", `${taskSpec.type}-reviewed`, [
55
+ `type=${taskSpec.type}`,
56
+ ]);
57
+ }
58
+ return mapping(taskSpec, "reviewed", "default-reviewed", [
59
+ `type=${taskSpec.type}`,
60
+ "default governance profile",
61
+ ]);
62
+ }
63
+ function mapping(taskSpec, loopAgentProfile, ruleId, reasons) {
64
+ return {
65
+ taskId: taskSpec.id,
66
+ businessProfile: taskSpec.type,
67
+ riskLevel: taskSpec.risk_level,
68
+ loopAgentProfile,
69
+ ruleId,
70
+ reasons,
71
+ };
72
+ }
73
+ function collectSensitiveMarkers(taskSpec) {
74
+ const haystack = taskSpec.constraints.allowed_paths.map((value) => value.toLowerCase());
75
+ return SENSITIVE_PATH_MARKERS.filter((marker) => haystack.some((value) => value.includes(marker)));
76
+ }