@tea-agent/loop-agent 0.1.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 (264) hide show
  1. package/AGENTS.md +121 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +144 -0
  4. package/bin/loop-agent.js +21 -0
  5. package/dist/adapters/aimax.js +91 -0
  6. package/dist/adapters/context.js +32 -0
  7. package/dist/adapters/index.js +28 -0
  8. package/dist/adapters/loop-agent.js +98 -0
  9. package/dist/adapters/types.js +1 -0
  10. package/dist/cli/catalog.js +259 -0
  11. package/dist/cli/help.js +55 -0
  12. package/dist/cli/index.js +3 -0
  13. package/dist/cli/program.js +505 -0
  14. package/dist/cli.js +12 -0
  15. package/dist/commands/closeout.js +13 -0
  16. package/dist/commands/coverage-audit.js +14 -0
  17. package/dist/commands/cursor-prompt.js +222 -0
  18. package/dist/commands/cursor-worker.js +43 -0
  19. package/dist/commands/dag-approve.js +102 -0
  20. package/dist/commands/dag-final-verification.js +76 -0
  21. package/dist/commands/dag-init-hybrid.js +56 -0
  22. package/dist/commands/dag-reconcile-tasks.js +51 -0
  23. package/dist/commands/dag-reject.js +91 -0
  24. package/dist/commands/dag-report.js +177 -0
  25. package/dist/commands/dag-resume.js +34 -0
  26. package/dist/commands/dag-run-task.js +470 -0
  27. package/dist/commands/dag-validate.js +186 -0
  28. package/dist/commands/dag-workflow-compile.js +91 -0
  29. package/dist/commands/dag-workflow-plan.js +130 -0
  30. package/dist/commands/dag-workflow-validate.js +66 -0
  31. package/dist/commands/delegate.js +132 -0
  32. package/dist/commands/docs-archive.js +5 -0
  33. package/dist/commands/docs-audit.js +5 -0
  34. package/dist/commands/doctor.js +50 -0
  35. package/dist/commands/goal.js +92 -0
  36. package/dist/commands/handoff-check.js +5 -0
  37. package/dist/commands/harvest.js +44 -0
  38. package/dist/commands/inspect.js +11 -0
  39. package/dist/commands/instructions.js +195 -0
  40. package/dist/commands/knowledge.js +64 -0
  41. package/dist/commands/loop-benchmark.js +72 -0
  42. package/dist/commands/loop.js +241 -0
  43. package/dist/commands/new-task.js +5 -0
  44. package/dist/commands/pi-prompt.js +181 -0
  45. package/dist/commands/pi-reuse-benchmark.js +153 -0
  46. package/dist/commands/plan-list.js +5 -0
  47. package/dist/commands/promote-run.js +29 -0
  48. package/dist/commands/reference-index.js +16 -0
  49. package/dist/commands/run-dag.js +184 -0
  50. package/dist/commands/spine.js +38 -0
  51. package/dist/commands/stats.js +84 -0
  52. package/dist/commands/status.js +56 -0
  53. package/dist/commands/study-init.js +192 -0
  54. package/dist/commands/workflow.js +259 -0
  55. package/dist/commands/worktree-create.js +31 -0
  56. package/dist/commands/worktree-list.js +5 -0
  57. package/dist/commands/worktree-remove.js +26 -0
  58. package/dist/cursor-worker-entry.js +8 -0
  59. package/dist/executors/config-core.js +55 -0
  60. package/dist/executors/config.js +2 -0
  61. package/dist/executors/cursor-artifacts.js +33 -0
  62. package/dist/executors/cursor-execution-log.js +81 -0
  63. package/dist/executors/cursor-executor-artifacts.js +135 -0
  64. package/dist/executors/cursor-executor.js +468 -0
  65. package/dist/executors/cursor-run.js +115 -0
  66. package/dist/executors/cursor-tool.js +94 -0
  67. package/dist/executors/cursor-worker-client.js +213 -0
  68. package/dist/executors/cursor-worker-protocol.js +18 -0
  69. package/dist/executors/cursor-worker-server.js +54 -0
  70. package/dist/executors/cursor-worker.js +3 -0
  71. package/dist/executors/cursor.js +6 -0
  72. package/dist/executors/dag-cursor-executor.js +88 -0
  73. package/dist/executors/dag-pi-executor.js +322 -0
  74. package/dist/executors/dag-static-executor.js +45 -0
  75. package/dist/executors/dag.js +4 -0
  76. package/dist/executors/index.js +8 -0
  77. package/dist/executors/model-routing.js +60 -0
  78. package/dist/executors/pi-event-serializer.js +43 -0
  79. package/dist/executors/pi-executor.js +606 -0
  80. package/dist/executors/pi-reuse-benchmark.js +316 -0
  81. package/dist/executors/pi-runtime-reuse.js +29 -0
  82. package/dist/executors/pi-sdk-executor.js +255 -0
  83. package/dist/executors/pi-sdk.js +1 -0
  84. package/dist/executors/pi.js +3 -0
  85. package/dist/executors/shell-executor.js +300 -0
  86. package/dist/executors/shell-presets.js +47 -0
  87. package/dist/executors/shell-verification.js +251 -0
  88. package/dist/executors/shell-write-guard.js +126 -0
  89. package/dist/executors/shell.js +3 -0
  90. package/dist/executors/static.js +1 -0
  91. package/dist/governance/checks.js +434 -0
  92. package/dist/governance/harness.js +9 -0
  93. package/dist/governance/index.js +3 -0
  94. package/dist/governance/manifest-types.js +128 -0
  95. package/dist/governance/manifest.js +2 -0
  96. package/dist/governance/path-guard.js +69 -0
  97. package/dist/governance/path-guards.js +2 -0
  98. package/dist/governance/profiles.js +3 -0
  99. package/dist/governance/requirement-coverage.js +425 -0
  100. package/dist/governance/skill-safety.js +135 -0
  101. package/dist/governance/spine-audit.js +152 -0
  102. package/dist/records/closeout.js +2 -0
  103. package/dist/records/harvest.js +236 -0
  104. package/dist/records/index.js +3 -0
  105. package/dist/records/one-shot-runs.js +421 -0
  106. package/dist/records/promotion.js +199 -0
  107. package/dist/shared/artifacts-core.js +88 -0
  108. package/dist/shared/artifacts.js +2 -0
  109. package/dist/shared/context-files.js +32 -0
  110. package/dist/shared/context.js +2 -0
  111. package/dist/shared/copy-dir.js +17 -0
  112. package/dist/shared/git-progress.js +165 -0
  113. package/dist/shared/index.js +5 -0
  114. package/dist/shared/logger.js +23 -0
  115. package/dist/shared/one-shot-prompt-args.js +98 -0
  116. package/dist/shared/path-refs.js +31 -0
  117. package/dist/shared/prompts.js +26 -0
  118. package/dist/shared/reference-context.js +238 -0
  119. package/dist/shared/timeout-policy.js +19 -0
  120. package/dist/shared/timeout.js +1 -0
  121. package/dist/shared/types.js +5 -0
  122. package/dist/task/config-types.js +97 -0
  123. package/dist/task/config.js +2 -0
  124. package/dist/task/delegate.js +220 -0
  125. package/dist/task/goal-audit.js +51 -0
  126. package/dist/task/goal-policy.js +8 -0
  127. package/dist/task/goal.js +3 -0
  128. package/dist/task/ids.js +1 -0
  129. package/dist/task/index.js +9 -0
  130. package/dist/task/lifecycle.js +1 -0
  131. package/dist/task/paths.js +1 -0
  132. package/dist/task/read-model.js +149 -0
  133. package/dist/task/runtime.js +699 -0
  134. package/dist/task/source-state.js +1 -0
  135. package/dist/task/state.js +55 -0
  136. package/dist/task/subagent-guidance.js +1 -0
  137. package/dist/task/workflow-state-types.js +92 -0
  138. package/dist/task/worktree-cleanup.js +140 -0
  139. package/dist/task/worktree.js +171 -0
  140. package/dist/workflows/dag/authoring.js +8 -0
  141. package/dist/workflows/dag/authority-surface.js +138 -0
  142. package/dist/workflows/dag/canvas-observer.js +474 -0
  143. package/dist/workflows/dag/decision-envelope.js +502 -0
  144. package/dist/workflows/dag/decision-evidence.js +153 -0
  145. package/dist/workflows/dag/decision-gates.js +1 -0
  146. package/dist/workflows/dag/executor-registry.js +25 -0
  147. package/dist/workflows/dag/facts.js +4 -0
  148. package/dist/workflows/dag/failure-category.js +111 -0
  149. package/dist/workflows/dag/final-verification.js +180 -0
  150. package/dist/workflows/dag/governance-constants.js +5 -0
  151. package/dist/workflows/dag/governance-profile.js +405 -0
  152. package/dist/workflows/dag/index.js +6 -0
  153. package/dist/workflows/dag/init-hybrid.js +855 -0
  154. package/dist/workflows/dag/knowledge-curator.js +162 -0
  155. package/dist/workflows/dag/lifecycle.js +484 -0
  156. package/dist/workflows/dag/prompt-source.js +88 -0
  157. package/dist/workflows/dag/prompt.js +130 -0
  158. package/dist/workflows/dag/reconcile-tasks.js +404 -0
  159. package/dist/workflows/dag/recovery-recommendation.js +226 -0
  160. package/dist/workflows/dag/repair-artifact.js +136 -0
  161. package/dist/workflows/dag/report.js +1019 -0
  162. package/dist/workflows/dag/runner.js +1677 -0
  163. package/dist/workflows/dag/runtime.js +5 -0
  164. package/dist/workflows/dag/skill-instructions.js +471 -0
  165. package/dist/workflows/dag/skills.js +41 -0
  166. package/dist/workflows/dag/spec.js +3 -0
  167. package/dist/workflows/dag/topo.js +30 -0
  168. package/dist/workflows/dag/types.js +275 -0
  169. package/dist/workflows/dag/upstream-artifacts.js +95 -0
  170. package/dist/workflows/dag/validate.js +527 -0
  171. package/dist/workflows/dynamic/artifacts.js +65 -0
  172. package/dist/workflows/dynamic/compile.js +360 -0
  173. package/dist/workflows/dynamic/compileTypes.js +1 -0
  174. package/dist/workflows/dynamic/errors.js +5 -0
  175. package/dist/workflows/dynamic/index.js +7 -0
  176. package/dist/workflows/dynamic/profiles.js +156 -0
  177. package/dist/workflows/dynamic/spec.js +114 -0
  178. package/dist/workflows/dynamic/validate.js +275 -0
  179. package/dist/workflows/loop/actions.js +1334 -0
  180. package/dist/workflows/loop/benchmark.js +510 -0
  181. package/dist/workflows/loop/closeout.js +134 -0
  182. package/dist/workflows/loop/context.js +48 -0
  183. package/dist/workflows/loop/events.js +25 -0
  184. package/dist/workflows/loop/hash.js +32 -0
  185. package/dist/workflows/loop/index.js +8 -0
  186. package/dist/workflows/loop/paths.js +17 -0
  187. package/dist/workflows/loop/rounds.js +81 -0
  188. package/dist/workflows/loop/signals.js +55 -0
  189. package/dist/workflows/loop/state.js +116 -0
  190. package/dist/workflows/loop/templates.js +54 -0
  191. package/dist/workflows/loop/types.js +28 -0
  192. package/docs/README.md +62 -0
  193. package/docs/agent-dag-recovery-playbook.md +158 -0
  194. package/docs/agent-dag-runner.md +40 -0
  195. package/docs/cursor-executor-usage.md +25 -0
  196. package/docs/decisions/README.md +3 -0
  197. package/docs/design/README.md +36 -0
  198. package/docs/development-principles.md +71 -0
  199. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
  200. package/docs/exec-plans/README.md +6 -0
  201. package/docs/exec-plans/active/README.md +5 -0
  202. package/docs/exec-plans/completed/README.md +5 -0
  203. package/docs/feature-workflow.md +184 -0
  204. package/docs/harness-methodology-debugging.md +153 -0
  205. package/docs/harness-methodology-tdd.md +130 -0
  206. package/docs/harness-methodology-verification.md +27 -0
  207. package/docs/loop-agent-harness.md +42 -0
  208. package/docs/progress/README.md +3 -0
  209. package/docs/reports/README.md +3 -0
  210. package/docs/templates/adr.md +60 -0
  211. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
  212. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
  213. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
  214. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
  215. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
  216. package/docs/templates/agent-dag-report.schema.json +423 -0
  217. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
  218. package/docs/templates/agent-dag.base.json +195 -0
  219. package/docs/templates/agent-dag.final-verification.json +190 -0
  220. package/docs/templates/agent-dag.schema.json +316 -0
  221. package/docs/templates/agent-dag.supervised-implementation.json +500 -0
  222. package/docs/templates/exec-plan.md +64 -0
  223. package/docs/templates/feature-spec.md +53 -0
  224. package/docs/templates/hybrid-dag.json +193 -0
  225. package/docs/templates/progress-log.md +17 -0
  226. package/docs/templates/project-start-checklist.md +9 -0
  227. package/docs/templates/qa-report.md +42 -0
  228. package/docs/templates/sprint-contract.md +29 -0
  229. package/docs/verification-matrix.md +30 -0
  230. package/examples/decision-gate-agent-dag.json +123 -0
  231. package/examples/example-dag.json +51 -0
  232. package/examples/hybrid-loop-agent-dag.json +194 -0
  233. package/harness.json +92 -0
  234. package/package.json +61 -0
  235. package/skills/ai-engineering-context/SKILL.md +48 -0
  236. package/skills/loop-agent/SKILL.md +260 -0
  237. package/skills/loop-agent/references/README.md +63 -0
  238. package/skills/loop-agent/references/command-reference.md +315 -0
  239. package/skills/loop-agent/references/harness-policy.md +258 -0
  240. package/skills/loop-agent/references/hybrid-dag.md +216 -0
  241. package/skills/loop-agent/references/learned/README.md +21 -0
  242. package/skills/loop-agent/references/model-routing.md +36 -0
  243. package/skills/loop-agent/references/multi-worktree.md +54 -0
  244. package/skills/loop-agent/references/one-shot-runs.md +85 -0
  245. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
  246. package/skills/loop-agent/references/pi-prompt.md +23 -0
  247. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
  248. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
  249. package/skills/loop-agent/references/task-workflow.md +84 -0
  250. package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
  251. package/skills/requesting-code-review/SKILL.md +101 -0
  252. package/skills/requesting-code-review/code-reviewer.md +168 -0
  253. package/skills/systematic-debugging/CREATION-LOG.md +119 -0
  254. package/skills/systematic-debugging/SKILL.md +296 -0
  255. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  256. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  257. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  258. package/skills/systematic-debugging/find-polluter.sh +63 -0
  259. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  260. package/skills/systematic-debugging/test-academic.md +14 -0
  261. package/skills/systematic-debugging/test-pressure-1.md +58 -0
  262. package/skills/systematic-debugging/test-pressure-2.md +68 -0
  263. package/skills/systematic-debugging/test-pressure-3.md +69 -0
  264. package/skills/verification-before-completion/SKILL.md +154 -0
@@ -0,0 +1,126 @@
1
+ import { spawn } from "node:child_process";
2
+ import { pathMatchesPattern } from "../shared/git-progress.js";
3
+ export function snapshotGitStatusPorcelain(porcelain) {
4
+ const snapshot = new Map();
5
+ for (const line of porcelain.split("\n")) {
6
+ const trimmed = line.trimEnd();
7
+ if (!trimmed)
8
+ continue;
9
+ const x = trimmed[0] ?? " ";
10
+ const y = trimmed[1] ?? " ";
11
+ const rawPath = trimmed.slice(3);
12
+ const path = normalizeGitPath(rawPath);
13
+ snapshot.set(path, `${x}${y}`);
14
+ }
15
+ return snapshot;
16
+ }
17
+ export function pathsChangedDuringRun(before, after) {
18
+ const changed = new Set();
19
+ for (const [filePath, afterCode] of after) {
20
+ const beforeCode = before.get(filePath);
21
+ if (beforeCode === undefined || beforeCode !== afterCode) {
22
+ changed.add(filePath);
23
+ }
24
+ }
25
+ for (const filePath of before.keys()) {
26
+ if (!after.has(filePath)) {
27
+ changed.add(filePath);
28
+ }
29
+ }
30
+ return Array.from(changed).sort();
31
+ }
32
+ const DAG_RUNS_PREFIX = ".harness/dag-runs/";
33
+ /** MVP post-run guard: only paths that changed during the shell node are checked. */
34
+ export function validateShellWriteGuard(input) {
35
+ const violations = [];
36
+ const writePolicy = input.writePolicy ?? "read-only";
37
+ const writeSet = input.writeSet ?? [];
38
+ const allowedPaths = input.allowedPaths ?? [];
39
+ const forbiddenPaths = input.forbiddenPaths ?? [];
40
+ for (const rawPath of input.changedPaths) {
41
+ const filePath = normalizePath(rawPath);
42
+ if (filePath.startsWith(DAG_RUNS_PREFIX)) {
43
+ continue;
44
+ }
45
+ if (matchesAnyPattern(filePath, forbiddenPaths)) {
46
+ violations.push(filePath);
47
+ continue;
48
+ }
49
+ if (writePolicy === "read-only" || writePolicy === "none") {
50
+ violations.push(filePath);
51
+ continue;
52
+ }
53
+ if (writePolicy === "exclusive") {
54
+ if (writeSet.length > 0 && matchesAnyPattern(filePath, writeSet)) {
55
+ continue;
56
+ }
57
+ if (input.allowSiblingWriteSetAttribution === true &&
58
+ matchesAnySiblingWriteSet(filePath, input.concurrentSiblingWriteSets ?? [])) {
59
+ continue;
60
+ }
61
+ violations.push(filePath);
62
+ continue;
63
+ }
64
+ if (allowedPaths.length > 0 && !matchesAnyPattern(filePath, allowedPaths)) {
65
+ violations.push(filePath);
66
+ }
67
+ }
68
+ return { ok: violations.length === 0, violations };
69
+ }
70
+ export async function readGitStatusPorcelain(cwd) {
71
+ return new Promise((resolve, reject) => {
72
+ const child = spawn("git", ["status", "--porcelain=v1", "--untracked-files=all"], {
73
+ cwd,
74
+ stdio: ["ignore", "pipe", "pipe"],
75
+ });
76
+ let stdout = "";
77
+ let stderr = "";
78
+ child.stdout.setEncoding("utf-8");
79
+ child.stderr.setEncoding("utf-8");
80
+ child.stdout.on("data", (chunk) => {
81
+ stdout += chunk;
82
+ });
83
+ child.stderr.on("data", (chunk) => {
84
+ stderr += chunk;
85
+ });
86
+ child.on("error", reject);
87
+ child.on("close", (code) => {
88
+ if (code === 0) {
89
+ resolve(stdout);
90
+ return;
91
+ }
92
+ reject(new Error(`git status failed: ${stderr.trim() || stdout.trim()}`));
93
+ });
94
+ });
95
+ }
96
+ export async function runPostRunWriteGuard(input) {
97
+ const afterStatus = await readGitStatusPorcelain(input.rootCwd);
98
+ const beforeSnapshot = snapshotGitStatusPorcelain(input.beforeStatus);
99
+ const afterSnapshot = snapshotGitStatusPorcelain(afterStatus);
100
+ const changedPaths = pathsChangedDuringRun(beforeSnapshot, afterSnapshot);
101
+ return validateShellWriteGuard({
102
+ changedPaths,
103
+ writePolicy: input.writePolicy,
104
+ writeSet: input.writeSet,
105
+ allowedPaths: input.allowedPaths,
106
+ forbiddenPaths: input.forbiddenPaths,
107
+ concurrentSiblingWriteSets: input.concurrentSiblingWriteSets,
108
+ allowSiblingWriteSetAttribution: input.writePolicy === "exclusive" &&
109
+ (input.concurrentSiblingWriteSets?.length ?? 0) > 0,
110
+ });
111
+ }
112
+ function matchesAnyPattern(filePath, patterns) {
113
+ return patterns.some((pattern) => pathMatchesPattern(filePath, pattern));
114
+ }
115
+ function matchesAnySiblingWriteSet(filePath, siblingWriteSets) {
116
+ return siblingWriteSets.some((writeSet) => writeSet.length > 0 && matchesAnyPattern(filePath, writeSet));
117
+ }
118
+ function normalizeGitPath(value) {
119
+ const target = value.includes(" -> ")
120
+ ? (value.split(" -> ").at(-1) ?? value)
121
+ : value;
122
+ return normalizePath(target);
123
+ }
124
+ function normalizePath(value) {
125
+ return value.replace(/\\/g, "/").replace(/^\.\//, "");
126
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./shell-executor.js";
2
+ export * from "./shell-presets.js";
3
+ export * from "./shell-write-guard.js";
@@ -0,0 +1 @@
1
+ export * from "./dag-static-executor.js";
@@ -0,0 +1,434 @@
1
+ import { access, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { repoRelativePath } from '../shared/path-refs.js';
4
+ import { loadHarnessManifest } from './harness.js';
5
+ import { auditLoopAgentSkillSafety } from './skill-safety.js';
6
+ import { getTaskPaths, getTaskStatus, loadTaskConfig, shouldRunRetrospective } from '../task/runtime.js';
7
+ import { buildTaskReadModel } from '../task/read-model.js';
8
+ const SKIP_DIRS = new Set([
9
+ '.git',
10
+ '.harness',
11
+ 'node_modules',
12
+ 'dist',
13
+ 'coverage',
14
+ ]);
15
+ export async function resolveGovernancePaths(repoRoot) {
16
+ const manifest = await loadHarnessManifest(repoRoot);
17
+ const governanceRoot = path.join(repoRoot, manifest.governanceRoot);
18
+ const execPlansDir = path.join(repoRoot, manifest.artifacts.execPlansDir ?? path.join(manifest.governanceRoot, 'exec-plans'));
19
+ return {
20
+ readmePath: path.join(repoRoot, manifest.entrypoints.readme ?? 'README.md'),
21
+ governanceRoot,
22
+ governanceIndexPath: manifest.entrypoints.governanceIndex
23
+ ? path.join(repoRoot, manifest.entrypoints.governanceIndex)
24
+ : null,
25
+ execPlansDir,
26
+ activePlansDir: path.join(execPlansDir, 'active'),
27
+ completedPlansDir: path.join(execPlansDir, 'completed'),
28
+ };
29
+ }
30
+ export async function listActivePlans(repoRoot) {
31
+ const paths = await resolveGovernancePaths(repoRoot);
32
+ const entries = await safeReadDir(paths.activePlansDir);
33
+ const plans = [];
34
+ for (const entry of entries) {
35
+ if (!entry.isFile() || !entry.name.endsWith('.md') || entry.name === 'README.md')
36
+ continue;
37
+ const absolutePath = path.join(paths.activePlansDir, entry.name);
38
+ const content = await readFile(absolutePath, 'utf-8');
39
+ plans.push({
40
+ file: entry.name,
41
+ relativePath: toRepoRelative(repoRoot, absolutePath),
42
+ title: extractTitle(content) ?? entry.name,
43
+ statusLine: extractStatusLine(content),
44
+ normalizedStatus: normalizeStatus(extractStatusLine(content)),
45
+ });
46
+ }
47
+ return plans.sort((a, b) => a.file.localeCompare(b.file));
48
+ }
49
+ export async function auditDocs(repoRoot) {
50
+ const paths = await resolveGovernancePaths(repoRoot);
51
+ const manifest = await loadHarnessManifest(repoRoot);
52
+ const issues = [];
53
+ const plans = await listActivePlans(repoRoot);
54
+ for (const plan of plans) {
55
+ if (plan.normalizedStatus !== 'active') {
56
+ issues.push({
57
+ type: 'active-status-mismatch',
58
+ severity: 'warning',
59
+ file: plan.relativePath,
60
+ message: `active 目录中的计划状态为 ${plan.statusLine ?? 'unknown'},建议迁入 completed/ 或修正文档状态`,
61
+ });
62
+ }
63
+ }
64
+ for (const [name, entry] of Object.entries(manifest.entrypoints ?? {})) {
65
+ if (!entry || !entry.endsWith('.md'))
66
+ continue;
67
+ const target = path.join(repoRoot, entry);
68
+ if (!await exists(target)) {
69
+ issues.push({
70
+ type: 'missing-harness-entrypoint',
71
+ severity: 'error',
72
+ file: 'harness.json',
73
+ message: `entrypoints.${name} 指向缺失文档:${entry}`,
74
+ });
75
+ }
76
+ }
77
+ const markdownFiles = await listMarkdownFiles(repoRoot);
78
+ for (const markdownFile of markdownFiles) {
79
+ const content = await readFile(markdownFile, 'utf-8');
80
+ for (const link of extractMarkdownLinks(content)) {
81
+ if (!isLocalMarkdownLink(link))
82
+ continue;
83
+ const resolved = path.resolve(path.dirname(markdownFile), stripAnchor(link));
84
+ if (!await exists(resolved)) {
85
+ issues.push({
86
+ type: 'missing-link-target',
87
+ severity: 'error',
88
+ file: toRepoRelative(repoRoot, markdownFile),
89
+ message: `存在失效文档链接:${link}`,
90
+ });
91
+ }
92
+ }
93
+ const relative = toRepoRelative(repoRoot, markdownFile);
94
+ if (path.basename(markdownFile).startsWith('aimax-memory-plugin-host-gap-')
95
+ && /Closeout/i.test(content)
96
+ && /(已不再是宿主阻塞项|已完成能力|closeout)/i.test(content)) {
97
+ issues.push({
98
+ type: 'host-gap-closeout',
99
+ severity: 'warning',
100
+ file: relative,
101
+ message: '该 host-gap 文档内容已 closeout,建议重命名或降级为历史记录入口',
102
+ });
103
+ }
104
+ }
105
+ const skillSafety = await auditLoopAgentSkillSafety(repoRoot);
106
+ for (const finding of skillSafety.findings) {
107
+ issues.push({
108
+ type: `skill-safety:${finding.ruleId}`,
109
+ severity: finding.severity,
110
+ file: `${finding.file}:${finding.line}`,
111
+ message: `${finding.message}: ${finding.snippet}`,
112
+ });
113
+ }
114
+ return {
115
+ governanceRoot: toRepoRelative(repoRoot, paths.governanceRoot),
116
+ issues,
117
+ plans,
118
+ summary: {
119
+ activePlanCount: plans.length,
120
+ issueCount: issues.length,
121
+ errorCount: issues.filter((issue) => issue.severity === 'error').length,
122
+ warningCount: issues.filter((issue) => issue.severity === 'warning').length,
123
+ },
124
+ };
125
+ }
126
+ export async function archivePlan(repoRoot, inputPath) {
127
+ const paths = await resolveGovernancePaths(repoRoot);
128
+ const sourcePath = await resolveArchiveSourcePath(repoRoot, paths.activePlansDir, inputPath);
129
+ const destinationPath = path.join(paths.completedPlansDir, path.basename(sourcePath));
130
+ await mkdir(paths.completedPlansDir, { recursive: true });
131
+ const sourceContent = await readFile(sourcePath, 'utf-8');
132
+ const archivedContent = ensureArchiveMetadata(normalizeArchivedStatus(sourceContent), path.basename(sourcePath));
133
+ await writeFile(destinationPath, archivedContent, 'utf-8');
134
+ await rm(sourcePath, { force: true });
135
+ const oldRelativePath = toRepoRelative(repoRoot, sourcePath);
136
+ const newRelativePath = toRepoRelative(repoRoot, destinationPath);
137
+ const updatedFiles = await rewriteMarkdownReferences(repoRoot, oldRelativePath, newRelativePath);
138
+ const activeIndexPath = path.join(paths.activePlansDir, 'README.md');
139
+ if (await exists(activeIndexPath)) {
140
+ const activeIndex = await readFile(activeIndexPath, 'utf-8');
141
+ const rewritten = activeIndex
142
+ .split(/\r?\n/)
143
+ .filter((line) => !line.includes(`(${path.basename(sourcePath)})`) && !line.includes(path.basename(sourcePath)))
144
+ .join('\n')
145
+ .replace(/\n{3,}/g, '\n\n');
146
+ if (rewritten !== activeIndex) {
147
+ await writeFile(activeIndexPath, rewritten.trimEnd() + '\n', 'utf-8');
148
+ updatedFiles.push(toRepoRelative(repoRoot, activeIndexPath));
149
+ }
150
+ }
151
+ const completedIndexPath = path.join(paths.completedPlansDir, 'README.md');
152
+ if (await exists(completedIndexPath)) {
153
+ const completedIndex = await readFile(completedIndexPath, 'utf-8');
154
+ const marker = `- [${path.basename(destinationPath)}](${path.basename(destinationPath)})`;
155
+ if (!completedIndex.includes(marker)) {
156
+ const next = appendArchivedMarker(completedIndex, marker);
157
+ await writeFile(completedIndexPath, next, 'utf-8');
158
+ updatedFiles.push(toRepoRelative(repoRoot, completedIndexPath));
159
+ }
160
+ }
161
+ return {
162
+ from: oldRelativePath,
163
+ to: newRelativePath,
164
+ updatedFiles: Array.from(new Set(updatedFiles)).sort(),
165
+ };
166
+ }
167
+ export async function checkTaskHandoff(repoRoot, taskId) {
168
+ const tasksDir = path.join(repoRoot, '.harness', 'tasks');
169
+ const taskIds = taskId ? [taskId] : await listTaskIds(tasksDir);
170
+ const tasks = [];
171
+ for (const currentTaskId of taskIds) {
172
+ const issues = [];
173
+ try {
174
+ const state = await getTaskStatus(repoRoot, currentTaskId);
175
+ const taskConfig = await loadTaskConfig(repoRoot, currentTaskId);
176
+ const taskPaths = getTaskPaths(repoRoot, currentTaskId);
177
+ const readModel = await buildTaskReadModel(repoRoot, currentTaskId);
178
+ const requirementPath = path.join(taskPaths.sourceDir, '需求.md');
179
+ const constraintPath = path.join(taskPaths.sourceDir, '执行约束.md');
180
+ if (!await exists(requirementPath)) {
181
+ issues.push({
182
+ type: 'missing-requirement',
183
+ severity: 'error',
184
+ file: toRepoRelative(repoRoot, requirementPath),
185
+ message: '缺少 source/需求.md',
186
+ });
187
+ }
188
+ if (!await exists(constraintPath)) {
189
+ issues.push({
190
+ type: 'missing-constraint',
191
+ severity: 'error',
192
+ file: toRepoRelative(repoRoot, constraintPath),
193
+ message: '缺少 source/执行约束.md',
194
+ });
195
+ }
196
+ if (taskConfig.autoCommitAfterVerify && taskConfig.allowedPaths.length === 0) {
197
+ issues.push({
198
+ type: 'unsafe-auto-commit-scope',
199
+ severity: 'warning',
200
+ file: toRepoRelative(repoRoot, taskPaths.taskConfigPath),
201
+ message: 'autoCommitAfterVerify=true 但 allowedPaths 为空,后续自动提交范围可能过宽',
202
+ });
203
+ }
204
+ const executorLogPath = path.join(taskPaths.logsDir, 'executor.jsonl');
205
+ const decisionLogPath = path.join(taskPaths.logsDir, 'decisions.jsonl');
206
+ const modifyLogPath = path.join(taskPaths.taskDir, 'artifacts', '修改记录.md');
207
+ if (state.decisionLog.length === 0) {
208
+ issues.push({
209
+ type: 'missing-decision-log',
210
+ severity: 'warning',
211
+ file: toRepoRelative(repoRoot, decisionLogPath),
212
+ message: '任务尚未记录结构化 decision log',
213
+ });
214
+ }
215
+ if (!await exists(executorLogPath)) {
216
+ issues.push({
217
+ type: 'missing-executor-log',
218
+ severity: 'warning',
219
+ file: toRepoRelative(repoRoot, executorLogPath),
220
+ message: '缺少 logs/executor.jsonl',
221
+ });
222
+ }
223
+ if (!await exists(decisionLogPath)) {
224
+ issues.push({
225
+ type: 'missing-decision-log-file',
226
+ severity: 'warning',
227
+ file: toRepoRelative(repoRoot, decisionLogPath),
228
+ message: '缺少 logs/decisions.jsonl',
229
+ });
230
+ }
231
+ if (state.sourceState.stalePlan) {
232
+ issues.push({
233
+ type: 'stale-plan',
234
+ severity: state.currentStep === 'implement' || state.status === 'active' ? 'error' : 'warning',
235
+ file: toRepoRelative(repoRoot, taskPaths.statePath),
236
+ message: 'source materials 已变更,但最近一次 plan/spec baseline 未更新',
237
+ });
238
+ }
239
+ if (state.status === 'completed' && !state.artifacts.verifyResult) {
240
+ issues.push({
241
+ type: 'missing-verify-artifact',
242
+ severity: 'error',
243
+ file: toRepoRelative(repoRoot, path.join(taskPaths.taskDir, 'artifacts', '验证结果.md')),
244
+ message: '任务已 completed,但验证工件未标记完成',
245
+ });
246
+ }
247
+ if (state.status === 'completed' && shouldRunRetrospective(taskConfig) && !state.artifacts.retrospective) {
248
+ issues.push({
249
+ type: 'missing-retrospective-artifact',
250
+ severity: 'warning',
251
+ file: toRepoRelative(repoRoot, path.join(taskPaths.taskDir, 'artifacts', '复盘报告.md')),
252
+ message: '任务应包含 retrospective,但复盘工件未完成',
253
+ });
254
+ }
255
+ if (readModel.promotion.required && !readModel.promotion.ready) {
256
+ issues.push({
257
+ type: 'missing-promotion-artifacts',
258
+ severity: 'error',
259
+ file: toRepoRelative(repoRoot, path.join(taskPaths.taskDir, 'artifacts')),
260
+ message: '已存在 completed DAG/one-shot run evidence,但任务工件尚未完成 promotion',
261
+ });
262
+ }
263
+ if (readModel.promotion.ready && !readModel.closeout.exists) {
264
+ issues.push({
265
+ type: 'missing-closeout-record',
266
+ severity: 'warning',
267
+ file: toRepoRelative(repoRoot, readModel.closeout.path),
268
+ message: '任务工件已 promotion,但缺少 docs/progress closeout 记录',
269
+ });
270
+ }
271
+ if (state.artifacts.modifyLog && await exists(modifyLogPath)) {
272
+ const modifyLog = (await readFile(modifyLogPath, 'utf-8')).trim();
273
+ if (modifyLog === '# 修改记录\n\n## 1. 修改文件清单\n\n## 2. 关键改动说明') {
274
+ issues.push({
275
+ type: 'empty-modify-log',
276
+ severity: 'warning',
277
+ file: toRepoRelative(repoRoot, modifyLogPath),
278
+ message: '修改记录仍是模板内容,尚未写入实际改动说明',
279
+ });
280
+ }
281
+ }
282
+ tasks.push({
283
+ taskId: currentTaskId,
284
+ status: state.status,
285
+ issues,
286
+ });
287
+ }
288
+ catch (error) {
289
+ issues.push({
290
+ type: 'task-load-failed',
291
+ severity: 'error',
292
+ file: path.join('.harness', 'tasks', currentTaskId),
293
+ message: error instanceof Error ? error.message : String(error),
294
+ });
295
+ tasks.push({
296
+ taskId: currentTaskId,
297
+ status: 'unknown',
298
+ issues,
299
+ });
300
+ }
301
+ }
302
+ const flatIssues = tasks.flatMap((task) => task.issues);
303
+ return {
304
+ tasks,
305
+ summary: {
306
+ taskCount: tasks.length,
307
+ issueCount: flatIssues.length,
308
+ errorCount: flatIssues.filter((issue) => issue.severity === 'error').length,
309
+ warningCount: flatIssues.filter((issue) => issue.severity === 'warning').length,
310
+ },
311
+ };
312
+ }
313
+ function extractTitle(content) {
314
+ const match = content.match(/^#\s+(.+)$/m);
315
+ return match?.[1]?.trim() ?? null;
316
+ }
317
+ function extractStatusLine(content) {
318
+ const match = content.match(/^## 状态\s*\n\s*\n([^\n]+)$/m);
319
+ return match?.[1]?.trim() ?? null;
320
+ }
321
+ function normalizeStatus(statusLine) {
322
+ if (!statusLine)
323
+ return 'unknown';
324
+ const value = statusLine.replace(/^- /, '').trim();
325
+ if (value.startsWith('active'))
326
+ return 'active';
327
+ if (value.startsWith('completed'))
328
+ return 'completed';
329
+ if (value.startsWith('blocked'))
330
+ return 'blocked';
331
+ if (value.startsWith('draft'))
332
+ return 'draft';
333
+ return 'unknown';
334
+ }
335
+ function normalizeArchivedStatus(content) {
336
+ return content.replace(/^## 状态\s*\n\s*\n([^\n]+)$/m, (_whole, line) => `## 状态\n\n${line.replace(/^-\s*active\b/, '- completed').replace(/^active\b/, 'completed')}`);
337
+ }
338
+ function ensureArchiveMetadata(content, fileName) {
339
+ if (content.includes('## 归档说明'))
340
+ return content;
341
+ const replacement = `## 状态\n\n- completed\n\n## 归档说明\n\n- 由 \`loop-agent docs archive\` 自动归档\n- 归档文件:\`${fileName}\`\n`;
342
+ if (/^## 状态\s*\n\s*\n[^\n]+$/m.test(content)) {
343
+ return content.replace(/^## 状态\s*\n\s*\n[^\n]+$/m, replacement);
344
+ }
345
+ return `${content.trimEnd()}\n\n## 归档说明\n\n- 由 \`loop-agent docs archive\` 自动归档\n- 归档文件:\`${fileName}\`\n`;
346
+ }
347
+ async function rewriteMarkdownReferences(repoRoot, oldRelativePath, newRelativePath) {
348
+ const markdownFiles = await listMarkdownFiles(repoRoot);
349
+ const updatedFiles = [];
350
+ for (const markdownFile of markdownFiles) {
351
+ const original = await readFile(markdownFile, 'utf-8');
352
+ const rewritten = original.replaceAll(oldRelativePath, newRelativePath);
353
+ if (rewritten !== original) {
354
+ await writeFile(markdownFile, rewritten, 'utf-8');
355
+ updatedFiles.push(toRepoRelative(repoRoot, markdownFile));
356
+ }
357
+ }
358
+ return updatedFiles;
359
+ }
360
+ function appendArchivedMarker(content, marker) {
361
+ const header = '## Recently Archived';
362
+ if (content.includes(header)) {
363
+ return content.replace(header, `${header}\n\n${marker}`);
364
+ }
365
+ return `${content.trimEnd()}\n\n${header}\n\n${marker}\n`;
366
+ }
367
+ async function resolveArchiveSourcePath(repoRoot, activePlansDir, inputPath) {
368
+ if (path.isAbsolute(inputPath))
369
+ return inputPath;
370
+ if (inputPath.includes('/'))
371
+ return path.resolve(repoRoot, inputPath);
372
+ return path.join(activePlansDir, inputPath);
373
+ }
374
+ async function listTaskIds(tasksDir) {
375
+ const entries = await safeReadDir(tasksDir);
376
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
377
+ }
378
+ async function listMarkdownFiles(root) {
379
+ const files = [];
380
+ await walk(root, files);
381
+ return files.sort();
382
+ }
383
+ async function walk(targetDir, files) {
384
+ const entries = await safeReadDir(targetDir);
385
+ for (const entry of entries) {
386
+ if (entry.isDirectory()) {
387
+ if (SKIP_DIRS.has(entry.name))
388
+ continue;
389
+ await walk(path.join(targetDir, entry.name), files);
390
+ continue;
391
+ }
392
+ if (entry.isFile() && entry.name.endsWith('.md')) {
393
+ files.push(path.join(targetDir, entry.name));
394
+ }
395
+ }
396
+ }
397
+ function extractMarkdownLinks(content) {
398
+ const links = [];
399
+ const pattern = /\[[^\]]+\]\(([^)]+)\)/g;
400
+ for (const match of content.matchAll(pattern)) {
401
+ if (match[1])
402
+ links.push(match[1]);
403
+ }
404
+ return links;
405
+ }
406
+ function isLocalMarkdownLink(link) {
407
+ if (link.startsWith('http://') || link.startsWith('https://') || link.startsWith('mailto:') || link.startsWith('#')) {
408
+ return false;
409
+ }
410
+ return stripAnchor(link).endsWith('.md');
411
+ }
412
+ function stripAnchor(link) {
413
+ return link.split('#')[0] ?? link;
414
+ }
415
+ async function exists(targetPath) {
416
+ try {
417
+ await access(targetPath);
418
+ return true;
419
+ }
420
+ catch {
421
+ return false;
422
+ }
423
+ }
424
+ async function safeReadDir(targetPath) {
425
+ try {
426
+ return await readdir(targetPath, { withFileTypes: true });
427
+ }
428
+ catch {
429
+ return [];
430
+ }
431
+ }
432
+ function toRepoRelative(repoRoot, absolutePath) {
433
+ return repoRelativePath(repoRoot, absolutePath);
434
+ }
@@ -0,0 +1,9 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { harnessManifestSchema } from '../shared/types.js';
4
+ export async function loadHarnessManifest(repoRoot) {
5
+ const manifestPath = path.join(repoRoot, 'harness.json');
6
+ const raw = await readFile(manifestPath, 'utf-8');
7
+ const parsed = JSON.parse(raw);
8
+ return harnessManifestSchema.parse(parsed);
9
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./manifest.js";
2
+ export * from "./profiles.js";
3
+ export * from "./path-guards.js";