@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
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { access, copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
@@ -5,6 +6,7 @@ import { copyDir } from "../shared/copy-dir.js";
5
6
  import { loadHarnessManifest } from "../governance/harness.js";
6
7
  const MANAGED_BLOCK_START = "<!-- LOOP_AGENT_INIT_START -->";
7
8
  const MANAGED_BLOCK_END = "<!-- LOOP_AGENT_INIT_END -->";
9
+ const INIT_SURFACE_STATE_PATH = ".harness/init-surface.json";
8
10
  const CORE_DOC_FILES = [
9
11
  "README.md",
10
12
  "architecture/runtime-boundaries.md",
@@ -88,474 +90,474 @@ const COMPAT_PROMPTS = {
88
90
  ].join("\n"),
89
91
  };
90
92
  const GOVERNANCE_ROOT_TOKEN = "__LOOP_AGENT_GOVERNANCE_ROOT__";
91
- const INIT_CHECK_ENGINEERING_STRUCTURE_SH = `#!/usr/bin/env bash
92
- set -euo pipefail
93
-
94
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
95
- cd "\${ROOT_DIR}"
96
-
97
- required_paths=(
98
- "README.md"
99
- "harness.json"
100
- "AGENTS.md"
101
- "${GOVERNANCE_ROOT_TOKEN}/README.md"
102
- "${GOVERNANCE_ROOT_TOKEN}/development-principles.md"
103
- "${GOVERNANCE_ROOT_TOKEN}/feature-workflow.md"
104
- "${GOVERNANCE_ROOT_TOKEN}/verification-matrix.md"
105
- "${GOVERNANCE_ROOT_TOKEN}/architecture/runtime-boundaries.md"
106
- "${GOVERNANCE_ROOT_TOKEN}/templates"
107
- "skills/loop-agent/SKILL.md"
108
- ".harness/prompts/analyze.md"
109
- ".harness/prompts/plan.md"
110
- ".harness/tasks"
111
- ".harness/dag-runs/active"
112
- ".harness/dag-runs/completed"
113
- ".harness/dag-runs/paused"
114
- ".harness/runs/active"
115
- ".harness/runs/completed"
116
- ".harness/runs/failed"
117
- "scripts/check-engineering-structure.sh"
118
- "scripts/check-doc-index.sh"
119
- "scripts/check-doc-links.sh"
120
- "scripts/check-active-plan-status.sh"
121
- "scripts/check-exec-plan-index-sync.sh"
122
- "scripts/check-harness-runtime-clean.sh"
123
- "scripts/check-architecture-boundaries.sh"
124
- "scripts/check-skill-entry.sh"
125
- "scripts/check-repo.sh"
126
- "scripts/ci-governance.sh"
127
- "scripts/ci-tests.sh"
128
- "scripts/ci.sh"
129
- )
130
-
131
- missing=()
132
- for item in "\${required_paths[@]}"; do
133
- if [[ ! -e "\${item}" ]]; then
134
- missing+=("\${item}")
135
- fi
136
- done
137
-
138
- if (( \${#missing[@]} > 0 )); then
139
- echo "loop-agent 初始化检查失败,缺少必要路径:"
140
- for item in "\${missing[@]}"; do
141
- echo " - \${item}"
142
- done
143
- exit 1
144
- fi
145
-
146
- echo "loop-agent 工程结构检查通过"
93
+ const INIT_CHECK_ENGINEERING_STRUCTURE_SH = `#!/usr/bin/env bash
94
+ set -euo pipefail
95
+
96
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
97
+ cd "\${ROOT_DIR}"
98
+
99
+ required_paths=(
100
+ "README.md"
101
+ "harness.json"
102
+ "AGENTS.md"
103
+ "${GOVERNANCE_ROOT_TOKEN}/README.md"
104
+ "${GOVERNANCE_ROOT_TOKEN}/development-principles.md"
105
+ "${GOVERNANCE_ROOT_TOKEN}/feature-workflow.md"
106
+ "${GOVERNANCE_ROOT_TOKEN}/verification-matrix.md"
107
+ "${GOVERNANCE_ROOT_TOKEN}/architecture/runtime-boundaries.md"
108
+ "${GOVERNANCE_ROOT_TOKEN}/templates"
109
+ "skills/loop-agent/SKILL.md"
110
+ ".harness/prompts/analyze.md"
111
+ ".harness/prompts/plan.md"
112
+ ".harness/tasks"
113
+ ".harness/dag-runs/active"
114
+ ".harness/dag-runs/completed"
115
+ ".harness/dag-runs/paused"
116
+ ".harness/runs/active"
117
+ ".harness/runs/completed"
118
+ ".harness/runs/failed"
119
+ "scripts/check-engineering-structure.sh"
120
+ "scripts/check-doc-index.sh"
121
+ "scripts/check-doc-links.sh"
122
+ "scripts/check-active-plan-status.sh"
123
+ "scripts/check-exec-plan-index-sync.sh"
124
+ "scripts/check-harness-runtime-clean.sh"
125
+ "scripts/check-architecture-boundaries.sh"
126
+ "scripts/check-skill-entry.sh"
127
+ "scripts/check-repo.sh"
128
+ "scripts/ci-governance.sh"
129
+ "scripts/ci-tests.sh"
130
+ "scripts/ci.sh"
131
+ )
132
+
133
+ missing=()
134
+ for item in "\${required_paths[@]}"; do
135
+ if [[ ! -e "\${item}" ]]; then
136
+ missing+=("\${item}")
137
+ fi
138
+ done
139
+
140
+ if (( \${#missing[@]} > 0 )); then
141
+ echo "loop-agent 初始化检查失败,缺少必要路径:"
142
+ for item in "\${missing[@]}"; do
143
+ echo " - \${item}"
144
+ done
145
+ exit 1
146
+ fi
147
+
148
+ echo "loop-agent 工程结构检查通过"
147
149
  `;
148
- const INIT_CHECK_DOC_INDEX_SH = `#!/usr/bin/env bash
149
- set -euo pipefail
150
-
151
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
152
- cd "\${ROOT_DIR}"
153
-
154
- required_docs=(
155
- "${GOVERNANCE_ROOT_TOKEN}/README.md"
156
- "${GOVERNANCE_ROOT_TOKEN}/development-principles.md"
157
- "${GOVERNANCE_ROOT_TOKEN}/feature-workflow.md"
158
- "${GOVERNANCE_ROOT_TOKEN}/verification-matrix.md"
159
- "${GOVERNANCE_ROOT_TOKEN}/architecture/runtime-boundaries.md"
160
- "${GOVERNANCE_ROOT_TOKEN}/loop-agent-harness.md"
161
- "${GOVERNANCE_ROOT_TOKEN}/harness-methodology-tdd.md"
162
- "${GOVERNANCE_ROOT_TOKEN}/harness-methodology-verification.md"
163
- "${GOVERNANCE_ROOT_TOKEN}/harness-methodology-debugging.md"
164
- "${GOVERNANCE_ROOT_TOKEN}/templates"
165
- "${GOVERNANCE_ROOT_TOKEN}/exec-plans/active/README.md"
166
- "${GOVERNANCE_ROOT_TOKEN}/exec-plans/completed/README.md"
167
- "${GOVERNANCE_ROOT_TOKEN}/progress/README.md"
168
- "${GOVERNANCE_ROOT_TOKEN}/reports/README.md"
169
- "${GOVERNANCE_ROOT_TOKEN}/decisions/README.md"
170
- )
171
-
172
- missing=()
173
- for item in "\${required_docs[@]}"; do
174
- if [[ ! -e "\${item}" ]]; then
175
- missing+=("\${item}")
176
- fi
177
- done
178
-
179
- if (( \${#missing[@]} > 0 )); then
180
- echo "docs 索引检查失败,缺少必要文档或目录:" >&2
181
- for item in "\${missing[@]}"; do
182
- echo " - \${item}" >&2
183
- done
184
- exit 1
185
- fi
186
-
187
- echo "docs 索引检查通过"
150
+ const INIT_CHECK_DOC_INDEX_SH = `#!/usr/bin/env bash
151
+ set -euo pipefail
152
+
153
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
154
+ cd "\${ROOT_DIR}"
155
+
156
+ required_docs=(
157
+ "${GOVERNANCE_ROOT_TOKEN}/README.md"
158
+ "${GOVERNANCE_ROOT_TOKEN}/development-principles.md"
159
+ "${GOVERNANCE_ROOT_TOKEN}/feature-workflow.md"
160
+ "${GOVERNANCE_ROOT_TOKEN}/verification-matrix.md"
161
+ "${GOVERNANCE_ROOT_TOKEN}/architecture/runtime-boundaries.md"
162
+ "${GOVERNANCE_ROOT_TOKEN}/loop-agent-harness.md"
163
+ "${GOVERNANCE_ROOT_TOKEN}/harness-methodology-tdd.md"
164
+ "${GOVERNANCE_ROOT_TOKEN}/harness-methodology-verification.md"
165
+ "${GOVERNANCE_ROOT_TOKEN}/harness-methodology-debugging.md"
166
+ "${GOVERNANCE_ROOT_TOKEN}/templates"
167
+ "${GOVERNANCE_ROOT_TOKEN}/exec-plans/active/README.md"
168
+ "${GOVERNANCE_ROOT_TOKEN}/exec-plans/completed/README.md"
169
+ "${GOVERNANCE_ROOT_TOKEN}/progress/README.md"
170
+ "${GOVERNANCE_ROOT_TOKEN}/reports/README.md"
171
+ "${GOVERNANCE_ROOT_TOKEN}/decisions/README.md"
172
+ )
173
+
174
+ missing=()
175
+ for item in "\${required_docs[@]}"; do
176
+ if [[ ! -e "\${item}" ]]; then
177
+ missing+=("\${item}")
178
+ fi
179
+ done
180
+
181
+ if (( \${#missing[@]} > 0 )); then
182
+ echo "docs 索引检查失败,缺少必要文档或目录:" >&2
183
+ for item in "\${missing[@]}"; do
184
+ echo " - \${item}" >&2
185
+ done
186
+ exit 1
187
+ fi
188
+
189
+ echo "docs 索引检查通过"
188
190
  `;
189
- const INIT_CHECK_DOC_LINKS_SH = `#!/usr/bin/env bash
190
- set -euo pipefail
191
-
192
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
193
- cd "\${ROOT_DIR}"
194
-
195
- if [[ ! -d "${GOVERNANCE_ROOT_TOKEN}" ]]; then
196
- echo "docs 链接检查失败:缺少 ${GOVERNANCE_ROOT_TOKEN}/" >&2
197
- exit 1
198
- fi
199
-
200
- missing=()
201
- while IFS= read -r -d '' file; do
202
- while IFS= read -r link; do
203
- [[ -z "\${link}" ]] && continue
204
- [[ "\${link}" == http://* || "\${link}" == https://* || "\${link}" == mailto:* || "\${link}" == "#"* ]] && continue
205
- [[ "\${link}" == *"://"* ]] && continue
206
- target="\${link%%#*}"
207
- [[ -z "\${target}" ]] && continue
208
- [[ "\${target}" == /* ]] && continue
209
- base="$(dirname "\${file}")"
210
- if [[ ! -e "\${base}/\${target}" ]]; then
211
- missing+=("\${file}: \${link}")
212
- fi
213
- done < <(grep -Eo '\\[[^]]+\\]\\([^)]+\\)' "\${file}" | sed -E 's/^.*\\(([^)]+)\\)$/\\1/' || true)
214
- done < <(find "${GOVERNANCE_ROOT_TOKEN}" -type f -name '*.md' -print0)
215
-
216
- if (( \${#missing[@]} > 0 )); then
217
- echo "docs 链接检查失败:" >&2
218
- for item in "\${missing[@]}"; do
219
- echo " - \${item}" >&2
220
- done
221
- exit 1
222
- fi
223
-
224
- echo "docs 链接检查通过"
191
+ const INIT_CHECK_DOC_LINKS_SH = `#!/usr/bin/env bash
192
+ set -euo pipefail
193
+
194
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
195
+ cd "\${ROOT_DIR}"
196
+
197
+ if [[ ! -d "${GOVERNANCE_ROOT_TOKEN}" ]]; then
198
+ echo "docs 链接检查失败:缺少 ${GOVERNANCE_ROOT_TOKEN}/" >&2
199
+ exit 1
200
+ fi
201
+
202
+ missing=()
203
+ while IFS= read -r -d '' file; do
204
+ while IFS= read -r link; do
205
+ [[ -z "\${link}" ]] && continue
206
+ [[ "\${link}" == http://* || "\${link}" == https://* || "\${link}" == mailto:* || "\${link}" == "#"* ]] && continue
207
+ [[ "\${link}" == *"://"* ]] && continue
208
+ target="\${link%%#*}"
209
+ [[ -z "\${target}" ]] && continue
210
+ [[ "\${target}" == /* ]] && continue
211
+ base="$(dirname "\${file}")"
212
+ if [[ ! -e "\${base}/\${target}" ]]; then
213
+ missing+=("\${file}: \${link}")
214
+ fi
215
+ done < <(grep -Eo '\\[[^]]+\\]\\([^)]+\\)' "\${file}" | sed -E 's/^.*\\(([^)]+)\\)$/\\1/' || true)
216
+ done < <(find "${GOVERNANCE_ROOT_TOKEN}" -type f -name '*.md' -print0)
217
+
218
+ if (( \${#missing[@]} > 0 )); then
219
+ echo "docs 链接检查失败:" >&2
220
+ for item in "\${missing[@]}"; do
221
+ echo " - \${item}" >&2
222
+ done
223
+ exit 1
224
+ fi
225
+
226
+ echo "docs 链接检查通过"
225
227
  `;
226
- const INIT_CHECK_ACTIVE_PLAN_STATUS_SH = `#!/usr/bin/env bash
227
- set -euo pipefail
228
-
229
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
230
- cd "\${ROOT_DIR}"
231
-
232
- active_dir="${GOVERNANCE_ROOT_TOKEN}/exec-plans/active"
233
- [[ -d "\${active_dir}" ]] || { echo "active plan 检查失败:缺少 \${active_dir}" >&2; exit 1; }
234
-
235
- bad=()
236
- while IFS= read -r -d '' file; do
237
- name="$(basename "\${file}")"
238
- [[ "\${name}" == "README.md" ]] && continue
239
- if grep -Eiq 'status:[[:space:]]*(completed|done|closed)|状态[::][[:space:]]*(已完成|完成|关闭)' "\${file}"; then
240
- bad+=("\${file}")
241
- fi
242
- done < <(find "\${active_dir}" -maxdepth 1 -type f -name '*.md' -print0)
243
-
244
- if (( \${#bad[@]} > 0 )); then
245
- echo "active plan 状态检查失败:已完成计划不应留在 active 目录:" >&2
246
- for item in "\${bad[@]}"; do
247
- echo " - \${item}" >&2
248
- done
249
- exit 1
250
- fi
251
-
252
- echo "active plan 状态检查通过"
228
+ const INIT_CHECK_ACTIVE_PLAN_STATUS_SH = `#!/usr/bin/env bash
229
+ set -euo pipefail
230
+
231
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
232
+ cd "\${ROOT_DIR}"
233
+
234
+ active_dir="${GOVERNANCE_ROOT_TOKEN}/exec-plans/active"
235
+ [[ -d "\${active_dir}" ]] || { echo "active plan 检查失败:缺少 \${active_dir}" >&2; exit 1; }
236
+
237
+ bad=()
238
+ while IFS= read -r -d '' file; do
239
+ name="$(basename "\${file}")"
240
+ [[ "\${name}" == "README.md" ]] && continue
241
+ if grep -Eiq 'status:[[:space:]]*(completed|done|closed)|状态[::][[:space:]]*(已完成|完成|关闭)' "\${file}"; then
242
+ bad+=("\${file}")
243
+ fi
244
+ done < <(find "\${active_dir}" -maxdepth 1 -type f -name '*.md' -print0)
245
+
246
+ if (( \${#bad[@]} > 0 )); then
247
+ echo "active plan 状态检查失败:已完成计划不应留在 active 目录:" >&2
248
+ for item in "\${bad[@]}"; do
249
+ echo " - \${item}" >&2
250
+ done
251
+ exit 1
252
+ fi
253
+
254
+ echo "active plan 状态检查通过"
253
255
  `;
254
- const INIT_CHECK_EXEC_PLAN_INDEX_SYNC_SH = `#!/usr/bin/env bash
255
- set -euo pipefail
256
-
257
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
258
- cd "\${ROOT_DIR}"
259
-
260
- required=(
261
- "${GOVERNANCE_ROOT_TOKEN}/exec-plans/README.md"
262
- "${GOVERNANCE_ROOT_TOKEN}/exec-plans/active/README.md"
263
- "${GOVERNANCE_ROOT_TOKEN}/exec-plans/completed/README.md"
264
- )
265
-
266
- for item in "\${required[@]}"; do
267
- if [[ ! -f "\${item}" ]]; then
268
- echo "exec-plan 索引同步检查失败:缺少 \${item}" >&2
269
- exit 1
270
- fi
271
- done
272
-
273
- echo "exec-plan 索引同步检查通过"
256
+ const INIT_CHECK_EXEC_PLAN_INDEX_SYNC_SH = `#!/usr/bin/env bash
257
+ set -euo pipefail
258
+
259
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
260
+ cd "\${ROOT_DIR}"
261
+
262
+ required=(
263
+ "${GOVERNANCE_ROOT_TOKEN}/exec-plans/README.md"
264
+ "${GOVERNANCE_ROOT_TOKEN}/exec-plans/active/README.md"
265
+ "${GOVERNANCE_ROOT_TOKEN}/exec-plans/completed/README.md"
266
+ )
267
+
268
+ for item in "\${required[@]}"; do
269
+ if [[ ! -f "\${item}" ]]; then
270
+ echo "exec-plan 索引同步检查失败:缺少 \${item}" >&2
271
+ exit 1
272
+ fi
273
+ done
274
+
275
+ echo "exec-plan 索引同步检查通过"
274
276
  `;
275
- const INIT_CHECK_HARNESS_RUNTIME_CLEAN_SH = `#!/usr/bin/env bash
276
- set -euo pipefail
277
-
278
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
279
- cd "\${ROOT_DIR}"
280
-
281
- failures=()
282
-
283
- count_entries() {
284
- local dir="$1"
285
- if [[ ! -d "\${dir}" ]]; then
286
- echo 0
287
- return
288
- fi
289
- find "\${dir}" -mindepth 1 -maxdepth 1 ! -name .gitkeep | wc -l | tr -d ' '
290
- }
291
-
292
- if [[ "\${HARNESS_ALLOW_ACTIVE_DAG_RUNS:-}" != "1" ]]; then
293
- active_dag_count="$(count_entries ".harness/dag-runs/active")"
294
- [[ "\${active_dag_count}" == "0" ]] || failures+=(".harness/dag-runs/active contains \${active_dag_count} entries")
295
- fi
296
-
297
- if [[ "\${HARNESS_ALLOW_ACTIVE_TOOL_RUNS:-\${HARNESS_ALLOW_ACTIVE_ONE_SHOT_RUNS:-}}" != "1" ]]; then
298
- active_run_count="$(count_entries ".harness/runs/active")"
299
- [[ "\${active_run_count}" == "0" ]] || failures+=(".harness/runs/active contains \${active_run_count} entries")
300
- fi
301
-
302
- if [[ -d artifacts ]]; then
303
- artifact_count="$(find artifacts -mindepth 1 -maxdepth 1 ! -name .gitkeep | wc -l | tr -d ' ')"
304
- [[ "\${artifact_count}" == "0" ]] || failures+=("root artifacts/ contains \${artifact_count} entries; use docs/reports or .harness run artifacts")
305
- fi
306
-
307
- if (( \${#failures[@]} > 0 )); then
308
- echo "[HARNESS RUNTIME DIRTY]" >&2
309
- for item in "\${failures[@]}"; do
310
- echo " - \${item}" >&2
311
- done
312
- exit 1
313
- fi
314
-
315
- echo "harness runtime clean check passed"
277
+ const INIT_CHECK_HARNESS_RUNTIME_CLEAN_SH = `#!/usr/bin/env bash
278
+ set -euo pipefail
279
+
280
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
281
+ cd "\${ROOT_DIR}"
282
+
283
+ failures=()
284
+
285
+ count_entries() {
286
+ local dir="$1"
287
+ if [[ ! -d "\${dir}" ]]; then
288
+ echo 0
289
+ return
290
+ fi
291
+ find "\${dir}" -mindepth 1 -maxdepth 1 ! -name .gitkeep | wc -l | tr -d ' '
292
+ }
293
+
294
+ if [[ "\${HARNESS_ALLOW_ACTIVE_DAG_RUNS:-}" != "1" ]]; then
295
+ active_dag_count="$(count_entries ".harness/dag-runs/active")"
296
+ [[ "\${active_dag_count}" == "0" ]] || failures+=(".harness/dag-runs/active contains \${active_dag_count} entries")
297
+ fi
298
+
299
+ if [[ "\${HARNESS_ALLOW_ACTIVE_TOOL_RUNS:-\${HARNESS_ALLOW_ACTIVE_ONE_SHOT_RUNS:-}}" != "1" ]]; then
300
+ active_run_count="$(count_entries ".harness/runs/active")"
301
+ [[ "\${active_run_count}" == "0" ]] || failures+=(".harness/runs/active contains \${active_run_count} entries")
302
+ fi
303
+
304
+ if [[ -d artifacts ]]; then
305
+ artifact_count="$(find artifacts -mindepth 1 -maxdepth 1 ! -name .gitkeep | wc -l | tr -d ' ')"
306
+ [[ "\${artifact_count}" == "0" ]] || failures+=("root artifacts/ contains \${artifact_count} entries; use docs/reports or .harness run artifacts")
307
+ fi
308
+
309
+ if (( \${#failures[@]} > 0 )); then
310
+ echo "[HARNESS RUNTIME DIRTY]" >&2
311
+ for item in "\${failures[@]}"; do
312
+ echo " - \${item}" >&2
313
+ done
314
+ exit 1
315
+ fi
316
+
317
+ echo "harness runtime clean check passed"
316
318
  `;
317
- const INIT_CHECK_ARCHITECTURE_BOUNDARIES_SH = `#!/usr/bin/env bash
318
- set -euo pipefail
319
-
320
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
321
- cd "\${ROOT_DIR}"
322
-
323
- boundary_doc="${GOVERNANCE_ROOT_TOKEN}/architecture/runtime-boundaries.md"
324
- if [[ ! -f "\${boundary_doc}" ]]; then
325
- echo "architecture boundary 检查失败:缺少 \${boundary_doc}" >&2
326
- exit 1
327
- fi
328
-
329
- violations=()
330
-
331
- check_ts_import_boundary() {
332
- local from_dir="$1"
333
- local forbidden_dir="$2"
334
- local label="$3"
335
- [[ -d "\${from_dir}" && -d "\${forbidden_dir}" ]] || return 0
336
- while IFS= read -r -d '' file; do
337
- if grep -Eq "from ['\\\"][^'\\\"]*\${forbidden_dir}/|import\\(['\\\"][^'\\\"]*\${forbidden_dir}/|require\\(['\\\"][^'\\\"]*\${forbidden_dir}/" "\${file}"; then
338
- violations+=("\${label}: \${file} imports \${forbidden_dir}")
339
- fi
340
- done < <(find "\${from_dir}" -type f \\( -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' -o -name '*.mjs' -o -name '*.cjs' \\) -print0)
341
- }
342
-
343
- # These checks are intentionally optional and stack-agnostic. They activate only
344
- # when a target repository has recognizable source directories.
345
- check_ts_import_boundary "src/workflows" "src/commands" "workflow-runtime"
346
- check_ts_import_boundary "src/executors" "src/commands" "executors"
347
- check_ts_import_boundary "src/domain" "src/infrastructure" "domain"
348
-
349
- if (( \${#violations[@]} > 0 )); then
350
- echo "architecture boundary 检查失败:" >&2
351
- for item in "\${violations[@]}"; do
352
- echo " - \${item}" >&2
353
- done
354
- echo "请更新 \${boundary_doc} 中的层边界,或修正反向依赖。" >&2
355
- exit 1
356
- fi
357
-
358
- echo "architecture boundary 检查通过"
319
+ const INIT_CHECK_ARCHITECTURE_BOUNDARIES_SH = `#!/usr/bin/env bash
320
+ set -euo pipefail
321
+
322
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
323
+ cd "\${ROOT_DIR}"
324
+
325
+ boundary_doc="${GOVERNANCE_ROOT_TOKEN}/architecture/runtime-boundaries.md"
326
+ if [[ ! -f "\${boundary_doc}" ]]; then
327
+ echo "architecture boundary 检查失败:缺少 \${boundary_doc}" >&2
328
+ exit 1
329
+ fi
330
+
331
+ violations=()
332
+
333
+ check_ts_import_boundary() {
334
+ local from_dir="$1"
335
+ local forbidden_dir="$2"
336
+ local label="$3"
337
+ [[ -d "\${from_dir}" && -d "\${forbidden_dir}" ]] || return 0
338
+ while IFS= read -r -d '' file; do
339
+ if grep -Eq "from ['\\\"][^'\\\"]*\${forbidden_dir}/|import\\(['\\\"][^'\\\"]*\${forbidden_dir}/|require\\(['\\\"][^'\\\"]*\${forbidden_dir}/" "\${file}"; then
340
+ violations+=("\${label}: \${file} imports \${forbidden_dir}")
341
+ fi
342
+ done < <(find "\${from_dir}" -type f \\( -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' -o -name '*.mjs' -o -name '*.cjs' \\) -print0)
343
+ }
344
+
345
+ # These checks are intentionally optional and stack-agnostic. They activate only
346
+ # when a target repository has recognizable source directories.
347
+ check_ts_import_boundary "src/workflows" "src/commands" "workflow-runtime"
348
+ check_ts_import_boundary "src/executors" "src/commands" "executors"
349
+ check_ts_import_boundary "src/domain" "src/infrastructure" "domain"
350
+
351
+ if (( \${#violations[@]} > 0 )); then
352
+ echo "architecture boundary 检查失败:" >&2
353
+ for item in "\${violations[@]}"; do
354
+ echo " - \${item}" >&2
355
+ done
356
+ echo "请更新 \${boundary_doc} 中的层边界,或修正反向依赖。" >&2
357
+ exit 1
358
+ fi
359
+
360
+ echo "architecture boundary 检查通过"
359
361
  `;
360
- const INIT_CHECK_SKILL_ENTRY_SH = `#!/usr/bin/env bash
361
- set -euo pipefail
362
-
363
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
364
- cd "\${ROOT_DIR}"
365
-
366
- skill_md="skills/loop-agent/SKILL.md"
367
- if [[ ! -f "\${skill_md}" ]]; then
368
- echo "skill entry 检查失败:缺少 \${skill_md}" >&2
369
- exit 1
370
- fi
371
-
372
- required_refs=(
373
- "references/harness-policy.md"
374
- "references/hybrid-dag.md"
375
- "references/verification-and-failure-handling.md"
376
- "references/command-reference.md"
377
- )
378
-
379
- missing=()
380
- for ref in "\${required_refs[@]}"; do
381
- [[ -f "skills/loop-agent/\${ref}" ]] || missing+=("\${ref}")
382
- done
383
-
384
- while IFS= read -r ref; do
385
- [[ -z "\${ref}" ]] && continue
386
- [[ -f "skills/loop-agent/\${ref}" ]] || missing+=("\${ref}")
387
- done < <(grep -Eo 'references/[A-Za-z0-9._/-]+\\.md' "\${skill_md}" | sort -u || true)
388
-
389
- if (( \${#missing[@]} > 0 )); then
390
- echo "skill entry 检查失败:引用的 reference 文件不存在:" >&2
391
- printf ' - %s\\n' "\${missing[@]}" >&2
392
- exit 1
393
- fi
394
-
395
- line_count="$(wc -l < "\${skill_md}" | tr -d ' ')"
396
- if [[ "\${line_count}" =~ ^[0-9]+$ && "\${line_count}" -gt 220 ]]; then
397
- echo "skill entry 检查警告:\${skill_md} 行数为 \${line_count},建议保持入口精简并把细节放入 references/" >&2
398
- fi
399
-
400
- echo "skill entry 检查通过:references ok, lines=\${line_count}"
362
+ const INIT_CHECK_SKILL_ENTRY_SH = `#!/usr/bin/env bash
363
+ set -euo pipefail
364
+
365
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
366
+ cd "\${ROOT_DIR}"
367
+
368
+ skill_md="skills/loop-agent/SKILL.md"
369
+ if [[ ! -f "\${skill_md}" ]]; then
370
+ echo "skill entry 检查失败:缺少 \${skill_md}" >&2
371
+ exit 1
372
+ fi
373
+
374
+ required_refs=(
375
+ "references/harness-policy.md"
376
+ "references/hybrid-dag.md"
377
+ "references/verification-and-failure-handling.md"
378
+ "references/command-reference.md"
379
+ )
380
+
381
+ missing=()
382
+ for ref in "\${required_refs[@]}"; do
383
+ [[ -f "skills/loop-agent/\${ref}" ]] || missing+=("\${ref}")
384
+ done
385
+
386
+ while IFS= read -r ref; do
387
+ [[ -z "\${ref}" ]] && continue
388
+ [[ -f "skills/loop-agent/\${ref}" ]] || missing+=("\${ref}")
389
+ done < <(grep -Eo 'references/[A-Za-z0-9._/-]+\\.md' "\${skill_md}" | sort -u || true)
390
+
391
+ if (( \${#missing[@]} > 0 )); then
392
+ echo "skill entry 检查失败:引用的 reference 文件不存在:" >&2
393
+ printf ' - %s\\n' "\${missing[@]}" >&2
394
+ exit 1
395
+ fi
396
+
397
+ line_count="$(wc -l < "\${skill_md}" | tr -d ' ')"
398
+ if [[ "\${line_count}" =~ ^[0-9]+$ && "\${line_count}" -gt 220 ]]; then
399
+ echo "skill entry 检查警告:\${skill_md} 行数为 \${line_count},建议保持入口精简并把细节放入 references/" >&2
400
+ fi
401
+
402
+ echo "skill entry 检查通过:references ok, lines=\${line_count}"
401
403
  `;
402
- const INIT_CHECK_REPO_SH = `#!/usr/bin/env bash
403
- set -euo pipefail
404
-
405
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
406
- cd "\${ROOT_DIR}"
407
-
408
- checks=(
409
- "scripts/check-engineering-structure.sh"
410
- "scripts/check-doc-index.sh"
411
- "scripts/check-doc-links.sh"
412
- "scripts/check-active-plan-status.sh"
413
- "scripts/check-exec-plan-index-sync.sh"
414
- "scripts/check-harness-runtime-clean.sh"
415
- "scripts/check-architecture-boundaries.sh"
416
- "scripts/check-skill-entry.sh"
417
- )
418
-
419
- for check in "\${checks[@]}"; do
420
- echo "==> bash \${check}"
421
- bash "\${check}"
422
- echo
423
- done
424
-
425
- echo "仓库治理检查全部通过"
404
+ const INIT_CHECK_REPO_SH = `#!/usr/bin/env bash
405
+ set -euo pipefail
406
+
407
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
408
+ cd "\${ROOT_DIR}"
409
+
410
+ checks=(
411
+ "scripts/check-engineering-structure.sh"
412
+ "scripts/check-doc-index.sh"
413
+ "scripts/check-doc-links.sh"
414
+ "scripts/check-active-plan-status.sh"
415
+ "scripts/check-exec-plan-index-sync.sh"
416
+ "scripts/check-harness-runtime-clean.sh"
417
+ "scripts/check-architecture-boundaries.sh"
418
+ "scripts/check-skill-entry.sh"
419
+ )
420
+
421
+ for check in "\${checks[@]}"; do
422
+ echo "==> bash \${check}"
423
+ bash "\${check}"
424
+ echo
425
+ done
426
+
427
+ echo "仓库治理检查全部通过"
426
428
  `;
427
- const INIT_CI_GOVERNANCE_SH = `#!/usr/bin/env bash
428
- set -euo pipefail
429
-
430
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
431
- cd "\${ROOT_DIR}"
432
-
433
- bash scripts/check-repo.sh
429
+ const INIT_CI_GOVERNANCE_SH = `#!/usr/bin/env bash
430
+ set -euo pipefail
431
+
432
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
433
+ cd "\${ROOT_DIR}"
434
+
435
+ bash scripts/check-repo.sh
434
436
  `;
435
- const INIT_CI_TESTS_SH = `#!/usr/bin/env bash
436
- set -euo pipefail
437
-
438
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
439
- cd "\${ROOT_DIR}"
440
-
441
- ran=0
442
- notes=()
443
-
444
- run_cmd() {
445
- echo "==> $*"
446
- "$@"
447
- ran=1
448
- }
449
-
450
- run_bash() {
451
- echo "==> $*"
452
- bash -lc "$*"
453
- ran=1
454
- }
455
-
456
- has_make_target() {
457
- local target="$1"
458
- [[ -f Makefile || -f makefile ]] || return 1
459
- grep -Eq "^[[:alnum:]_.-]*\${target}[[:alnum:]_.-]*:" Makefile makefile 2>/dev/null
460
- }
461
-
462
- has_npm_script() {
463
- local script="$1"
464
- [[ -f package.json ]] || return 1
465
- command -v node >/dev/null 2>&1 || return 1
466
- node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); process.exit(p.scripts && p.scripts[process.argv[1]] ? 0 : 1)" "\${script}" >/dev/null 2>&1
467
- }
468
-
469
- if [[ -f package.json ]]; then
470
- if command -v npm >/dev/null 2>&1; then
471
- for script in lint typecheck test build; do
472
- if has_npm_script "\${script}"; then
473
- run_cmd npm run "\${script}"
474
- fi
475
- done
476
- else
477
- notes+=("package.json exists but npm is not available")
478
- fi
479
- fi
480
-
481
- if has_make_target verify; then
482
- run_cmd make verify
483
- elif has_make_target test; then
484
- run_cmd make test
485
- fi
486
-
487
- if [[ -f go.mod ]]; then
488
- if command -v go >/dev/null 2>&1; then
489
- run_cmd go test ./...
490
- else
491
- notes+=("go.mod exists but go is not available")
492
- fi
493
- fi
494
-
495
- if [[ -f Cargo.toml ]]; then
496
- if command -v cargo >/dev/null 2>&1; then
497
- run_cmd cargo test
498
- else
499
- notes+=("Cargo.toml exists but cargo is not available")
500
- fi
501
- fi
502
-
503
- if [[ -f pyproject.toml || -f pytest.ini || -f tox.ini ]]; then
504
- if command -v pytest >/dev/null 2>&1; then
505
- run_cmd pytest
506
- elif command -v tox >/dev/null 2>&1 && [[ -f tox.ini ]]; then
507
- run_cmd tox
508
- else
509
- notes+=("Python project metadata found but pytest/tox is not available")
510
- fi
511
- fi
512
-
513
- if [[ -f pom.xml ]]; then
514
- if command -v mvn >/dev/null 2>&1; then
515
- run_cmd mvn test
516
- else
517
- notes+=("pom.xml exists but mvn is not available")
518
- fi
519
- fi
520
-
521
- if [[ -f build.gradle || -f build.gradle.kts || -f settings.gradle || -f settings.gradle.kts ]]; then
522
- if [[ -x ./gradlew ]]; then
523
- run_cmd ./gradlew test
524
- elif command -v gradle >/dev/null 2>&1; then
525
- run_cmd gradle test
526
- else
527
- notes+=("Gradle project metadata found but gradle/gradlew is not available")
528
- fi
529
- fi
530
-
531
- if compgen -G "*.sln" >/dev/null || compgen -G "*.csproj" >/dev/null; then
532
- if command -v dotnet >/dev/null 2>&1; then
533
- run_cmd dotnet test
534
- else
535
- notes+=(".NET project metadata found but dotnet is not available")
536
- fi
537
- fi
538
-
539
- if (( ran == 0 )); then
540
- echo "未检测到可安全自动运行的项目测试入口。"
541
- echo "loop-agent 治理检查仍可运行;请让初始化模型根据目标项目技术栈补充 scripts/ci-tests.sh 与 docs/verification-matrix.md。"
542
- fi
543
-
544
- if (( \${#notes[@]} > 0 )); then
545
- echo "项目验证探测备注:"
546
- for note in "\${notes[@]}"; do
547
- echo " - \${note}"
548
- done
549
- fi
437
+ const INIT_CI_TESTS_SH = `#!/usr/bin/env bash
438
+ set -euo pipefail
439
+
440
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
441
+ cd "\${ROOT_DIR}"
442
+
443
+ ran=0
444
+ notes=()
445
+
446
+ run_cmd() {
447
+ echo "==> $*"
448
+ "$@"
449
+ ran=1
450
+ }
451
+
452
+ run_bash() {
453
+ echo "==> $*"
454
+ bash -lc "$*"
455
+ ran=1
456
+ }
457
+
458
+ has_make_target() {
459
+ local target="$1"
460
+ [[ -f Makefile || -f makefile ]] || return 1
461
+ grep -Eq "^[[:alnum:]_.-]*\${target}[[:alnum:]_.-]*:" Makefile makefile 2>/dev/null
462
+ }
463
+
464
+ has_npm_script() {
465
+ local script="$1"
466
+ [[ -f package.json ]] || return 1
467
+ command -v node >/dev/null 2>&1 || return 1
468
+ node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); process.exit(p.scripts && p.scripts[process.argv[1]] ? 0 : 1)" "\${script}" >/dev/null 2>&1
469
+ }
470
+
471
+ if [[ -f package.json ]]; then
472
+ if command -v npm >/dev/null 2>&1; then
473
+ for script in lint typecheck test build; do
474
+ if has_npm_script "\${script}"; then
475
+ run_cmd npm run "\${script}"
476
+ fi
477
+ done
478
+ else
479
+ notes+=("package.json exists but npm is not available")
480
+ fi
481
+ fi
482
+
483
+ if has_make_target verify; then
484
+ run_cmd make verify
485
+ elif has_make_target test; then
486
+ run_cmd make test
487
+ fi
488
+
489
+ if [[ -f go.mod ]]; then
490
+ if command -v go >/dev/null 2>&1; then
491
+ run_cmd go test ./...
492
+ else
493
+ notes+=("go.mod exists but go is not available")
494
+ fi
495
+ fi
496
+
497
+ if [[ -f Cargo.toml ]]; then
498
+ if command -v cargo >/dev/null 2>&1; then
499
+ run_cmd cargo test
500
+ else
501
+ notes+=("Cargo.toml exists but cargo is not available")
502
+ fi
503
+ fi
504
+
505
+ if [[ -f pyproject.toml || -f pytest.ini || -f tox.ini ]]; then
506
+ if command -v pytest >/dev/null 2>&1; then
507
+ run_cmd pytest
508
+ elif command -v tox >/dev/null 2>&1 && [[ -f tox.ini ]]; then
509
+ run_cmd tox
510
+ else
511
+ notes+=("Python project metadata found but pytest/tox is not available")
512
+ fi
513
+ fi
514
+
515
+ if [[ -f pom.xml ]]; then
516
+ if command -v mvn >/dev/null 2>&1; then
517
+ run_cmd mvn test
518
+ else
519
+ notes+=("pom.xml exists but mvn is not available")
520
+ fi
521
+ fi
522
+
523
+ if [[ -f build.gradle || -f build.gradle.kts || -f settings.gradle || -f settings.gradle.kts ]]; then
524
+ if [[ -x ./gradlew ]]; then
525
+ run_cmd ./gradlew test
526
+ elif command -v gradle >/dev/null 2>&1; then
527
+ run_cmd gradle test
528
+ else
529
+ notes+=("Gradle project metadata found but gradle/gradlew is not available")
530
+ fi
531
+ fi
532
+
533
+ if compgen -G "*.sln" >/dev/null || compgen -G "*.csproj" >/dev/null; then
534
+ if command -v dotnet >/dev/null 2>&1; then
535
+ run_cmd dotnet test
536
+ else
537
+ notes+=(".NET project metadata found but dotnet is not available")
538
+ fi
539
+ fi
540
+
541
+ if (( ran == 0 )); then
542
+ echo "未检测到可安全自动运行的项目测试入口。"
543
+ echo "loop-agent 治理检查仍可运行;请让初始化模型根据目标项目技术栈补充 scripts/ci-tests.sh 与 docs/verification-matrix.md。"
544
+ fi
545
+
546
+ if (( \${#notes[@]} > 0 )); then
547
+ echo "项目验证探测备注:"
548
+ for note in "\${notes[@]}"; do
549
+ echo " - \${note}"
550
+ done
551
+ fi
550
552
  `;
551
- const INIT_CI_SH = `#!/usr/bin/env bash
552
- set -euo pipefail
553
-
554
- ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
555
- cd "\${ROOT_DIR}"
556
-
557
- bash scripts/ci-governance.sh
558
- bash scripts/ci-tests.sh
553
+ const INIT_CI_SH = `#!/usr/bin/env bash
554
+ set -euo pipefail
555
+
556
+ ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
557
+ cd "\${ROOT_DIR}"
558
+
559
+ bash scripts/ci-governance.sh
560
+ bash scripts/ci-tests.sh
559
561
  `;
560
562
  const INIT_SCRIPT_FILES = {
561
563
  "scripts/check-engineering-structure.sh": INIT_CHECK_ENGINEERING_STRUCTURE_SH,
@@ -846,6 +848,236 @@ async function writeCompatPrompts(input) {
846
848
  });
847
849
  }
848
850
  }
851
+ function sha256Text(value) {
852
+ return createHash("sha256").update(value).digest("hex");
853
+ }
854
+ function sha256Buffer(value) {
855
+ return createHash("sha256").update(value).digest("hex");
856
+ }
857
+ function isRecord(value) {
858
+ return typeof value === "object" && value !== null && !Array.isArray(value);
859
+ }
860
+ function normalizeRelativePath(relativePath) {
861
+ return relativePath.split(path.sep).join("/");
862
+ }
863
+ function manifestPathToTargetPath(relativePath, governanceRoot) {
864
+ if (governanceRoot !== "docs" && relativePath.startsWith("docs/")) {
865
+ return `${governanceRoot}/${relativePath.slice("docs/".length)}`;
866
+ }
867
+ return relativePath;
868
+ }
869
+ function targetPathToManifestPath(relativePath, governanceRoot) {
870
+ if (governanceRoot !== "docs" && relativePath.startsWith(`${governanceRoot}/`)) {
871
+ return `docs/${relativePath.slice(`${governanceRoot}/`.length)}`;
872
+ }
873
+ return relativePath;
874
+ }
875
+ async function readPackageVersion(assetRoot) {
876
+ const packageJson = JSON.parse(await readFile(path.join(assetRoot, "package.json"), "utf-8"));
877
+ return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
878
+ }
879
+ async function readInitSurfaceManifest(assetRoot) {
880
+ const manifestPath = path.join(assetRoot, "docs", "init-surface.manifest.json");
881
+ const raw = await readFile(manifestPath, "utf-8");
882
+ const manifest = JSON.parse(raw);
883
+ const initFullRequired = manifest.initFullRequired ?? [];
884
+ const initSurface = manifest.initSurface ?? {};
885
+ const entries = initFullRequired.map((relativePath) => ({
886
+ path: relativePath,
887
+ mode: initSurface[relativePath] ?? inferInitSurfaceMode(relativePath),
888
+ }));
889
+ if (!entries.some((entry) => entry.path === INIT_SURFACE_STATE_PATH)) {
890
+ entries.push({ path: INIT_SURFACE_STATE_PATH, mode: "state" });
891
+ }
892
+ return { manifest, raw, sha256: sha256Text(raw), entries };
893
+ }
894
+ function inferInitSurfaceMode(relativePath) {
895
+ if (relativePath === INIT_SURFACE_STATE_PATH)
896
+ return "state";
897
+ if (relativePath === ".harness/tasks" ||
898
+ relativePath === ".harness/dag-runs/active" ||
899
+ relativePath.endsWith("/")) {
900
+ return "directory";
901
+ }
902
+ if (relativePath === "README.md" || relativePath === "AGENTS.md")
903
+ return "managed-block";
904
+ if (relativePath === "harness.json" ||
905
+ relativePath.startsWith("scripts/") ||
906
+ relativePath.startsWith(".harness/prompts/") ||
907
+ relativePath.startsWith("docs/README.md") ||
908
+ relativePath.startsWith("docs/development-principles.md") ||
909
+ relativePath.startsWith("docs/feature-workflow.md") ||
910
+ relativePath.startsWith("docs/verification-matrix.md") ||
911
+ relativePath.startsWith("docs/loop-agent-harness.md") ||
912
+ relativePath.startsWith("docs/architecture/runtime-boundaries.md")) {
913
+ return "generated";
914
+ }
915
+ return "copied";
916
+ }
917
+ async function buildDesiredSurfaceContent(input) {
918
+ const manifestPath = targetPathToManifestPath(input.entry.path, input.governanceRoot);
919
+ if (input.entry.mode === "directory" || input.entry.mode === "state")
920
+ return {};
921
+ if (manifestPath === "README.md") {
922
+ return {
923
+ content: buildManagedReadmeBlock({
924
+ projectName: input.projectName,
925
+ governanceRoot: input.governanceRoot,
926
+ }),
927
+ };
928
+ }
929
+ if (manifestPath === "AGENTS.md") {
930
+ return {
931
+ content: buildManagedAgentsBlock({
932
+ projectName: input.projectName,
933
+ governanceRoot: input.governanceRoot,
934
+ }),
935
+ };
936
+ }
937
+ if (manifestPath === "harness.json") {
938
+ const template = await readJsonIfExists(path.join(input.assetRoot, "harness.json"));
939
+ return {
940
+ content: `${JSON.stringify(buildHarness({
941
+ existing: {},
942
+ template,
943
+ projectName: input.projectName,
944
+ governanceRoot: input.governanceRoot,
945
+ }), null, 2)}\n`,
946
+ };
947
+ }
948
+ if (manifestPath.startsWith("scripts/")) {
949
+ const scripts = buildInitScriptFiles(input.governanceRoot);
950
+ return { content: scripts[manifestPath] };
951
+ }
952
+ if (manifestPath.startsWith(".harness/prompts/")) {
953
+ const name = path.basename(manifestPath);
954
+ return { content: COMPAT_PROMPTS[name] };
955
+ }
956
+ if (manifestPath.startsWith("docs/")) {
957
+ const doc = manifestPath.slice("docs/".length);
958
+ const generated = buildGeneratedCoreDoc({
959
+ doc,
960
+ projectName: input.projectName,
961
+ governanceRoot: input.governanceRoot,
962
+ });
963
+ if (generated !== undefined)
964
+ return { content: generated };
965
+ }
966
+ const sourcePath = path.join(input.assetRoot, manifestPath);
967
+ if (await exists(sourcePath))
968
+ return { content: await readFile(sourcePath, "utf-8"), sourcePath };
969
+ return {};
970
+ }
971
+ function extractManagedBlock(content) {
972
+ const start = content.indexOf(MANAGED_BLOCK_START);
973
+ const end = content.indexOf(MANAGED_BLOCK_END);
974
+ if (start < 0 || end <= start)
975
+ return undefined;
976
+ return content.slice(start, end + MANAGED_BLOCK_END.length);
977
+ }
978
+ async function readExistingSurfaceState(repoRoot) {
979
+ const filePath = path.join(repoRoot, INIT_SURFACE_STATE_PATH);
980
+ if (!(await exists(filePath)))
981
+ return undefined;
982
+ const parsed = JSON.parse(await readFile(filePath, "utf-8"));
983
+ if (!isRecord(parsed) || parsed.schemaVersion !== 1)
984
+ return undefined;
985
+ return parsed;
986
+ }
987
+ async function buildCurrentSurfaceState(input) {
988
+ const assetRoot = await findPackageRoot();
989
+ const controllerVersion = await readPackageVersion(assetRoot);
990
+ const manifest = await readInitSurfaceManifest(assetRoot);
991
+ const files = {};
992
+ for (const manifestEntry of manifest.entries) {
993
+ const targetRelativePath = normalizeRelativePath(manifestPathToTargetPath(manifestEntry.path, input.governanceRoot));
994
+ const entry = { ...manifestEntry, path: targetRelativePath };
995
+ const targetPath = path.join(input.repoRoot, targetRelativePath);
996
+ const targetStat = await stat(targetPath).catch(() => null);
997
+ const desired = await buildDesiredSurfaceContent({
998
+ assetRoot,
999
+ repoRoot: input.repoRoot,
1000
+ projectName: input.projectName,
1001
+ governanceRoot: input.governanceRoot,
1002
+ entry,
1003
+ });
1004
+ const sourceSha256 = desired.content === undefined ? undefined : sha256Text(desired.content);
1005
+ const base = {
1006
+ status: targetStat ? "present" : "missing",
1007
+ relationship: "missing-from-target",
1008
+ sourceSha256,
1009
+ sourcePath: desired.sourcePath ? repoRelative(assetRoot, desired.sourcePath) : undefined,
1010
+ mode: entry.mode,
1011
+ };
1012
+ if (entry.mode === "state") {
1013
+ files[targetRelativePath] = {
1014
+ ...base,
1015
+ status: "present",
1016
+ relationship: "state-file",
1017
+ };
1018
+ continue;
1019
+ }
1020
+ if (!targetStat) {
1021
+ files[targetRelativePath] = base;
1022
+ continue;
1023
+ }
1024
+ if (entry.mode === "directory") {
1025
+ files[targetRelativePath] = {
1026
+ ...base,
1027
+ relationship: targetStat.isDirectory() ? "directory-present" : "local-existing-unknown",
1028
+ };
1029
+ continue;
1030
+ }
1031
+ const current = await readFile(targetPath);
1032
+ const currentSha256 = sha256Buffer(current);
1033
+ if (entry.mode === "managed-block") {
1034
+ const currentBlock = extractManagedBlock(current.toString("utf-8"));
1035
+ files[targetRelativePath] = {
1036
+ ...base,
1037
+ currentSha256,
1038
+ relationship: currentBlock && sourceSha256 && sha256Text(currentBlock) === sourceSha256
1039
+ ? "managed-block-current"
1040
+ : "managed-block-present",
1041
+ };
1042
+ continue;
1043
+ }
1044
+ files[targetRelativePath] = {
1045
+ ...base,
1046
+ currentSha256,
1047
+ relationship: sourceSha256 && currentSha256 === sourceSha256
1048
+ ? entry.mode === "generated"
1049
+ ? "matches-current-generated"
1050
+ : "matches-current-package"
1051
+ : "local-existing-unknown",
1052
+ };
1053
+ }
1054
+ return {
1055
+ schemaVersion: 1,
1056
+ controllerVersion,
1057
+ stateKind: input.stateKind,
1058
+ generatedAt: input.generatedAt ?? new Date().toISOString(),
1059
+ manifestSha256: manifest.sha256,
1060
+ files,
1061
+ };
1062
+ }
1063
+ async function writeInitSurfaceState(input) {
1064
+ const state = await buildCurrentSurfaceState(input);
1065
+ const target = path.join(input.repoRoot, INIT_SURFACE_STATE_PATH);
1066
+ await mkdir(path.dirname(target), { recursive: true });
1067
+ await writeFile(target, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
1068
+ return state;
1069
+ }
1070
+ async function resolveInitProjectContext(input) {
1071
+ const harness = await readJsonIfExists(path.join(input.repoRoot, "harness.json"));
1072
+ return {
1073
+ projectName: input.projectName ??
1074
+ (typeof harness.project === "string" ? harness.project : undefined) ??
1075
+ path.basename(input.repoRoot),
1076
+ governanceRoot: input.governanceRoot ??
1077
+ (typeof harness.governanceRoot === "string" ? harness.governanceRoot : undefined) ??
1078
+ "docs",
1079
+ };
1080
+ }
849
1081
  function buildManagedReadmeBlock(input) {
850
1082
  return [
851
1083
  MANAGED_BLOCK_START,
@@ -1389,8 +1621,277 @@ export async function initializeLoopAgentProject(options) {
1389
1621
  }
1390
1622
  await ensureHarnessDirs(repoRoot, written);
1391
1623
  await writeCompatPrompts({ repoRoot, merge, written, skipped });
1624
+ await writeInitSurfaceState({ repoRoot, projectName, governanceRoot, stateKind: "recorded" });
1625
+ written.push(INIT_SURFACE_STATE_PATH);
1392
1626
  return { repoRoot, projectName, governanceRoot, profile, written, skipped };
1393
1627
  }
1628
+ function actionForMissing(pathName, state) {
1629
+ if (state.mode === "state")
1630
+ return undefined;
1631
+ if (state.mode === "directory") {
1632
+ return { type: "create-directory", path: pathName, reason: "required init directory is missing" };
1633
+ }
1634
+ if (state.mode === "copied") {
1635
+ return { type: "copy-missing", path: pathName, reason: "required bundled init file is missing" };
1636
+ }
1637
+ if (state.mode === "generated") {
1638
+ return { type: "write-generated-missing", path: pathName, reason: "required generated init file is missing" };
1639
+ }
1640
+ return { type: "refresh-managed-block", path: pathName, reason: "managed block file is missing or stale" };
1641
+ }
1642
+ function modelMergeTaskFor(pathName, state, allPaths) {
1643
+ return {
1644
+ path: pathName,
1645
+ reason: "target file exists but does not match the current loop-agent initialization surface",
1646
+ sourcePath: state.sourcePath,
1647
+ allowedPaths: [pathName],
1648
+ forbiddenPaths: [
1649
+ ...allPaths.filter((candidate) => candidate !== pathName),
1650
+ ".harness/dag-runs/**",
1651
+ ".harness/runs/**",
1652
+ ".git/**",
1653
+ ],
1654
+ mergeRules: [
1655
+ "Preserve target-project user content and project-specific commands.",
1656
+ "Adopt the current loop-agent initialization structure where it does not conflict with local intent.",
1657
+ "Do not delete user-authored sections merely because they differ from the package template.",
1658
+ "Keep writes inside allowedPaths only.",
1659
+ ],
1660
+ verification: [
1661
+ "loop-agent init check-update --repo-root . --json",
1662
+ "loop-agent init doctor --repo-root .",
1663
+ "loop-agent inspect --repo-root .",
1664
+ "bash scripts/check-repo.sh",
1665
+ ],
1666
+ };
1667
+ }
1668
+ function recommendedNextFor(report) {
1669
+ const next = [];
1670
+ if (report.surfaceState === "missing") {
1671
+ next.push("loop-agent init update --repo-root <target> --bootstrap-surface");
1672
+ }
1673
+ if (report.deterministicActions.some((action) => action.type !== "bootstrap-surface")) {
1674
+ next.push("loop-agent init update --repo-root <target> --apply-safe");
1675
+ }
1676
+ if (report.modelMergeTasks.length > 0) {
1677
+ next.push("loop-agent init check-update --repo-root <target> --markdown");
1678
+ }
1679
+ if (report.humanDecisions.length > 0) {
1680
+ next.push("Review humanDecisions before applying semantic or policy-sensitive changes.");
1681
+ }
1682
+ if (next.length === 0)
1683
+ next.push("No init update actions are needed.");
1684
+ return next;
1685
+ }
1686
+ export async function checkInitUpdate(input) {
1687
+ const repoRoot = path.resolve(input.repoRoot);
1688
+ const { projectName, governanceRoot } = await resolveInitProjectContext({
1689
+ repoRoot,
1690
+ projectName: input.projectName,
1691
+ governanceRoot: input.governanceRoot,
1692
+ });
1693
+ const recordedState = await readExistingSurfaceState(repoRoot);
1694
+ const surfaceState = recordedState?.stateKind ?? "missing";
1695
+ const currentState = await buildCurrentSurfaceState({
1696
+ repoRoot,
1697
+ projectName,
1698
+ governanceRoot,
1699
+ stateKind: recordedState?.stateKind ?? "inferred-baseline",
1700
+ });
1701
+ const allPaths = Object.keys(currentState.files).sort();
1702
+ const deterministicActions = [];
1703
+ const modelMergeTasks = [];
1704
+ const humanDecisions = [];
1705
+ if (!recordedState) {
1706
+ deterministicActions.push({
1707
+ type: "bootstrap-surface",
1708
+ path: INIT_SURFACE_STATE_PATH,
1709
+ reason: "target project has no recorded init surface baseline",
1710
+ });
1711
+ }
1712
+ for (const [pathName, state] of Object.entries(currentState.files)) {
1713
+ if (state.relationship === "missing-from-target") {
1714
+ const action = actionForMissing(pathName, state);
1715
+ if (action)
1716
+ deterministicActions.push(action);
1717
+ continue;
1718
+ }
1719
+ if (state.relationship === "managed-block-present") {
1720
+ deterministicActions.push({
1721
+ type: "refresh-managed-block",
1722
+ path: pathName,
1723
+ reason: "managed block is missing or differs from the current package block",
1724
+ });
1725
+ continue;
1726
+ }
1727
+ if (state.relationship === "local-existing-unknown") {
1728
+ if (state.mode === "directory") {
1729
+ humanDecisions.push({
1730
+ path: pathName,
1731
+ reason: "required init directory path exists but is not a directory",
1732
+ });
1733
+ }
1734
+ else {
1735
+ modelMergeTasks.push(modelMergeTaskFor(pathName, state, allPaths));
1736
+ }
1737
+ }
1738
+ }
1739
+ const partial = {
1740
+ repoRoot,
1741
+ controllerVersion: currentState.controllerVersion,
1742
+ surfaceState,
1743
+ deterministicActions,
1744
+ modelMergeTasks,
1745
+ humanDecisions,
1746
+ summary: {
1747
+ missing: deterministicActions.filter((action) => action.type !== "bootstrap-surface").length,
1748
+ modelMerge: modelMergeTasks.length,
1749
+ humanDecision: humanDecisions.length,
1750
+ },
1751
+ };
1752
+ const recommendedNext = recommendedNextFor(partial);
1753
+ return {
1754
+ ...partial,
1755
+ ok: deterministicActions.length === 0 &&
1756
+ modelMergeTasks.length === 0 &&
1757
+ humanDecisions.length === 0,
1758
+ recommendedNext,
1759
+ };
1760
+ }
1761
+ async function applySafeAction(input) {
1762
+ const assetRoot = await findPackageRoot();
1763
+ const entry = {
1764
+ path: input.action.path,
1765
+ mode: input.action.type === "create-directory"
1766
+ ? "directory"
1767
+ : input.action.type === "copy-missing"
1768
+ ? "copied"
1769
+ : input.action.type === "refresh-managed-block"
1770
+ ? "managed-block"
1771
+ : "generated",
1772
+ };
1773
+ const target = path.join(input.repoRoot, input.action.path);
1774
+ if (input.action.type === "create-directory") {
1775
+ if (await exists(target))
1776
+ return false;
1777
+ await mkdir(target, { recursive: true });
1778
+ return true;
1779
+ }
1780
+ const desired = await buildDesiredSurfaceContent({
1781
+ assetRoot,
1782
+ repoRoot: input.repoRoot,
1783
+ projectName: input.projectName,
1784
+ governanceRoot: input.governanceRoot,
1785
+ entry,
1786
+ });
1787
+ if (desired.content === undefined)
1788
+ return false;
1789
+ await mkdir(path.dirname(target), { recursive: true });
1790
+ if (input.action.type === "refresh-managed-block") {
1791
+ const existing = (await exists(target)) ? await readFile(target, "utf-8") : "";
1792
+ const next = input.action.path === "README.md" && existing.trim().length === 0
1793
+ ? buildTargetReadme({ projectName: input.projectName, governanceRoot: input.governanceRoot })
1794
+ : input.action.path === "AGENTS.md" && existing.trim().length === 0
1795
+ ? `# AGENTS.md\n\n${desired.content}\n`
1796
+ : mergeManagedBlock(existing, desired.content);
1797
+ await writeFile(target, next, "utf-8");
1798
+ return true;
1799
+ }
1800
+ if (await exists(target))
1801
+ return false;
1802
+ await writeFile(target, desired.content, "utf-8");
1803
+ return true;
1804
+ }
1805
+ export async function applyInitUpdate(input) {
1806
+ const repoRoot = path.resolve(input.repoRoot);
1807
+ const { projectName, governanceRoot } = await resolveInitProjectContext({
1808
+ repoRoot,
1809
+ projectName: input.projectName,
1810
+ governanceRoot: input.governanceRoot,
1811
+ });
1812
+ const applied = [];
1813
+ const skipped = [];
1814
+ if (input.bootstrapSurface) {
1815
+ await writeInitSurfaceState({ repoRoot, projectName, governanceRoot, stateKind: "inferred-baseline" });
1816
+ applied.push({
1817
+ type: "bootstrap-surface",
1818
+ path: INIT_SURFACE_STATE_PATH,
1819
+ reason: "wrote inferred init surface baseline",
1820
+ });
1821
+ }
1822
+ if (input.applySafe) {
1823
+ const report = await checkInitUpdate({ repoRoot, projectName, governanceRoot });
1824
+ for (const action of report.deterministicActions) {
1825
+ if (action.type === "bootstrap-surface") {
1826
+ skipped.push(action);
1827
+ continue;
1828
+ }
1829
+ const currentState = await buildCurrentSurfaceState({
1830
+ repoRoot,
1831
+ projectName,
1832
+ governanceRoot,
1833
+ stateKind: "inferred-baseline",
1834
+ });
1835
+ const relationship = currentState.files[action.path]?.relationship;
1836
+ if (relationship === "local-existing-unknown") {
1837
+ skipped.push(action);
1838
+ continue;
1839
+ }
1840
+ if (await applySafeAction({ repoRoot, projectName, governanceRoot, action }))
1841
+ applied.push(action);
1842
+ else
1843
+ skipped.push(action);
1844
+ }
1845
+ await writeInitSurfaceState({ repoRoot, projectName, governanceRoot, stateKind: "inferred-baseline" });
1846
+ }
1847
+ return {
1848
+ applied,
1849
+ skipped,
1850
+ report: await checkInitUpdate({ repoRoot, projectName, governanceRoot }),
1851
+ };
1852
+ }
1853
+ function formatCheckUpdateText(report) {
1854
+ return [
1855
+ `loop-agent init update check for ${report.repoRoot}`,
1856
+ `controllerVersion: ${report.controllerVersion}`,
1857
+ `surfaceState: ${report.surfaceState}`,
1858
+ `deterministicActions: ${report.deterministicActions.length}`,
1859
+ `modelMergeTasks: ${report.modelMergeTasks.length}`,
1860
+ `humanDecisions: ${report.humanDecisions.length}`,
1861
+ "recommendedNext:",
1862
+ ...report.recommendedNext.map((item) => `- ${item}`),
1863
+ ].join("\n");
1864
+ }
1865
+ function formatCheckUpdateMarkdown(report) {
1866
+ const lines = [
1867
+ "# loop-agent init check-update",
1868
+ "",
1869
+ `- repoRoot: \`${report.repoRoot}\``,
1870
+ `- controllerVersion: \`${report.controllerVersion}\``,
1871
+ `- surfaceState: \`${report.surfaceState}\``,
1872
+ "",
1873
+ "## Deterministic Actions",
1874
+ "",
1875
+ ...(report.deterministicActions.length
1876
+ ? report.deterministicActions.map((action) => `- \`${action.type}\` \`${action.path}\`: ${action.reason}`)
1877
+ : ["- None"]),
1878
+ "",
1879
+ "## Model Merge Tasks",
1880
+ "",
1881
+ ];
1882
+ if (report.modelMergeTasks.length === 0) {
1883
+ lines.push("- None", "");
1884
+ }
1885
+ else {
1886
+ for (const task of report.modelMergeTasks) {
1887
+ lines.push(`### ${task.path}`, "", task.reason, "", "allowedPaths:", ...task.allowedPaths.map((item) => `- ${item}`), "", "forbiddenPaths:", ...task.forbiddenPaths.map((item) => `- ${item}`), "", "mergeRules:", ...task.mergeRules.map((item) => `- ${item}`), "", "verification:", ...task.verification.map((item) => `- ${item}`), "");
1888
+ }
1889
+ }
1890
+ lines.push("## Human Decisions", "", ...(report.humanDecisions.length
1891
+ ? report.humanDecisions.map((decision) => `- \`${decision.path}\`: ${decision.reason}`)
1892
+ : ["- None"]), "", "## Recommended Next", "", ...report.recommendedNext.map((item) => `- ${item}`), "");
1893
+ return lines.join("\n");
1894
+ }
1394
1895
  export async function runInitDoctor(input) {
1395
1896
  const repoRoot = path.resolve(input.repoRoot);
1396
1897
  const checks = [];
@@ -1432,9 +1933,12 @@ function parseInitArgs(repoRoot, args) {
1432
1933
  let provider;
1433
1934
  let model;
1434
1935
  let json = false;
1936
+ let markdown = false;
1937
+ let bootstrapSurface = false;
1938
+ let applySafe = false;
1435
1939
  for (let i = 0; i < args.length; i += 1) {
1436
1940
  const arg = args[i];
1437
- if ((arg === "instructions" || arg === "doctor") && !subcommand) {
1941
+ if ((arg === "instructions" || arg === "doctor" || arg === "check-update" || arg === "update") && !subcommand) {
1438
1942
  subcommand = arg;
1439
1943
  continue;
1440
1944
  }
@@ -1464,6 +1968,12 @@ function parseInitArgs(repoRoot, args) {
1464
1968
  model = arg.slice("--model=".length);
1465
1969
  else if (arg === "--json")
1466
1970
  json = true;
1971
+ else if (arg === "--markdown")
1972
+ markdown = true;
1973
+ else if (arg === "--bootstrap-surface")
1974
+ bootstrapSurface = true;
1975
+ else if (arg === "--apply-safe")
1976
+ applySafe = true;
1467
1977
  else if (arg.startsWith("-"))
1468
1978
  throw new Error(`unknown init flag: ${arg}`);
1469
1979
  else
@@ -1471,7 +1981,20 @@ function parseInitArgs(repoRoot, args) {
1471
1981
  }
1472
1982
  if (profile !== "full" && profile !== "minimal")
1473
1983
  throw new Error("init --profile must be full or minimal");
1474
- return { repoRoot, projectName, governanceRoot, profile, merge, provider, model, subcommand, json };
1984
+ return {
1985
+ repoRoot,
1986
+ projectName,
1987
+ governanceRoot,
1988
+ profile,
1989
+ merge,
1990
+ provider,
1991
+ model,
1992
+ subcommand,
1993
+ json,
1994
+ markdown,
1995
+ bootstrapSurface,
1996
+ applySafe,
1997
+ };
1475
1998
  }
1476
1999
  export async function runInit(repoRoot, rawArgs) {
1477
2000
  const parsed = parseInitArgs(repoRoot, rawArgs);
@@ -1486,9 +2009,38 @@ export async function runInit(repoRoot, rawArgs) {
1486
2009
  process.exitCode = 1;
1487
2010
  return;
1488
2011
  }
2012
+ if (parsed.subcommand === "check-update") {
2013
+ const report = await checkInitUpdate(parsed);
2014
+ if (parsed.json)
2015
+ console.log(JSON.stringify(report, null, 2));
2016
+ else if (parsed.markdown)
2017
+ console.log(formatCheckUpdateMarkdown(report));
2018
+ else
2019
+ console.log(formatCheckUpdateText(report));
2020
+ return;
2021
+ }
2022
+ if (parsed.subcommand === "update") {
2023
+ if (!parsed.bootstrapSurface && !parsed.applySafe) {
2024
+ throw new Error("usage: init update [--bootstrap-surface] [--apply-safe]");
2025
+ }
2026
+ const result = await applyInitUpdate(parsed);
2027
+ console.log(parsed.json ? JSON.stringify(result, null, 2) : formatInitUpdateResult(result));
2028
+ return;
2029
+ }
1489
2030
  const result = await initializeLoopAgentProject(parsed);
1490
2031
  console.log(parsed.json ? JSON.stringify(result, null, 2) : formatInitResult(result));
1491
2032
  }
2033
+ function formatInitUpdateResult(result) {
2034
+ return [
2035
+ "loop-agent init update complete",
2036
+ `applied: ${result.applied.length}`,
2037
+ `skipped: ${result.skipped.length}`,
2038
+ `remaining deterministicActions: ${result.report.deterministicActions.length}`,
2039
+ `remaining modelMergeTasks: ${result.report.modelMergeTasks.length}`,
2040
+ "recommendedNext:",
2041
+ ...result.report.recommendedNext.map((item) => `- ${item}`),
2042
+ ].join("\n");
2043
+ }
1492
2044
  function formatInitResult(result) {
1493
2045
  return [
1494
2046
  `Initialized loop-agent harness at ${result.repoRoot}`,