agentplane 0.3.2 → 0.3.3

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 (190) hide show
  1. package/assets/AGENTS.md +1 -1
  2. package/assets/agents/CODER.json +4 -3
  3. package/assets/agents/DOCS.json +1 -1
  4. package/assets/agents/INTEGRATOR.json +1 -1
  5. package/assets/agents/ORCHESTRATOR.json +1 -0
  6. package/assets/agents/PLANNER.json +1 -0
  7. package/assets/agents/TESTER.json +3 -1
  8. package/assets/policy/dod.code.md +2 -2
  9. package/assets/policy/dod.core.md +16 -2
  10. package/assets/policy/dod.docs.md +2 -2
  11. package/assets/policy/incidents.md +44 -1
  12. package/assets/policy/workflow.direct.md +8 -4
  13. package/bin/agentplane.js +59 -9
  14. package/bin/dist-guard.js +78 -10
  15. package/bin/runtime-context.d.ts +3 -0
  16. package/bin/runtime-context.js +13 -0
  17. package/bin/runtime-watch.d.ts +26 -0
  18. package/bin/runtime-watch.js +116 -0
  19. package/bin/stale-dist-policy.d.ts +6 -0
  20. package/bin/stale-dist-policy.js +44 -0
  21. package/dist/.build-manifest.json +2480 -5
  22. package/dist/backends/task-backend/local-backend.d.ts.map +1 -1
  23. package/dist/backends/task-backend/local-backend.js +9 -12
  24. package/dist/backends/task-backend/redmine-backend.d.ts.map +1 -1
  25. package/dist/backends/task-backend/redmine-backend.js +23 -18
  26. package/dist/backends/task-backend/shared/constants.d.ts +1 -0
  27. package/dist/backends/task-backend/shared/constants.d.ts.map +1 -1
  28. package/dist/backends/task-backend/shared/constants.js +1 -0
  29. package/dist/backends/task-backend/shared/doc.d.ts +1 -0
  30. package/dist/backends/task-backend/shared/doc.d.ts.map +1 -1
  31. package/dist/backends/task-backend/shared/doc.js +4 -1
  32. package/dist/backends/task-backend/shared/export.js +3 -3
  33. package/dist/cli/bootstrap-guide.d.ts +1 -3
  34. package/dist/cli/bootstrap-guide.d.ts.map +1 -1
  35. package/dist/cli/bootstrap-guide.js +13 -33
  36. package/dist/cli/command-guide.d.ts.map +1 -1
  37. package/dist/cli/command-guide.js +27 -34
  38. package/dist/cli/run-cli/catalog.d.ts +7 -0
  39. package/dist/cli/run-cli/catalog.d.ts.map +1 -0
  40. package/dist/cli/run-cli/catalog.js +22 -0
  41. package/dist/cli/run-cli/command-catalog.d.ts +1 -1
  42. package/dist/cli/run-cli/command-catalog.d.ts.map +1 -1
  43. package/dist/cli/run-cli/command-catalog.js +11 -0
  44. package/dist/cli/run-cli/commands/init/write-config.d.ts.map +1 -1
  45. package/dist/cli/run-cli/commands/init/write-config.js +2 -0
  46. package/dist/cli/run-cli/commands/init.js +5 -14
  47. package/dist/cli/run-cli/error-guidance.d.ts +9 -0
  48. package/dist/cli/run-cli/error-guidance.d.ts.map +1 -0
  49. package/dist/cli/run-cli/error-guidance.js +180 -0
  50. package/dist/cli/run-cli/globals.d.ts +22 -0
  51. package/dist/cli/run-cli/globals.d.ts.map +1 -0
  52. package/dist/cli/run-cli/globals.js +197 -0
  53. package/dist/cli/run-cli/update-warning.d.ts +6 -0
  54. package/dist/cli/run-cli/update-warning.d.ts.map +1 -0
  55. package/dist/cli/run-cli/update-warning.js +64 -0
  56. package/dist/cli/run-cli.d.ts.map +1 -1
  57. package/dist/cli/run-cli.js +5 -476
  58. package/dist/cli/spec/docs-render.d.ts.map +1 -1
  59. package/dist/cli/spec/docs-render.js +14 -1
  60. package/dist/commands/doctor/archive.d.ts +4 -0
  61. package/dist/commands/doctor/archive.d.ts.map +1 -0
  62. package/dist/commands/doctor/archive.js +211 -0
  63. package/dist/commands/doctor/fixes.d.ts +9 -0
  64. package/dist/commands/doctor/fixes.d.ts.map +1 -0
  65. package/dist/commands/doctor/fixes.js +40 -0
  66. package/dist/commands/doctor/layering.d.ts +2 -0
  67. package/dist/commands/doctor/layering.d.ts.map +1 -0
  68. package/dist/commands/doctor/layering.js +87 -0
  69. package/dist/commands/doctor/runtime.d.ts +4 -0
  70. package/dist/commands/doctor/runtime.d.ts.map +1 -0
  71. package/dist/commands/doctor/runtime.js +56 -0
  72. package/dist/commands/doctor/workflow.d.ts +6 -0
  73. package/dist/commands/doctor/workflow.d.ts.map +1 -0
  74. package/dist/commands/doctor/workflow.js +62 -0
  75. package/dist/commands/doctor/workspace.d.ts +2 -0
  76. package/dist/commands/doctor/workspace.d.ts.map +1 -0
  77. package/dist/commands/doctor/workspace.js +165 -0
  78. package/dist/commands/doctor.run.d.ts.map +1 -1
  79. package/dist/commands/doctor.run.js +16 -342
  80. package/dist/commands/doctor.spec.d.ts +1 -0
  81. package/dist/commands/doctor.spec.d.ts.map +1 -1
  82. package/dist/commands/doctor.spec.js +15 -1
  83. package/dist/commands/guard/impl/commands.d.ts.map +1 -1
  84. package/dist/commands/guard/impl/commands.js +19 -0
  85. package/dist/commands/release/apply.command.d.ts +2 -8
  86. package/dist/commands/release/apply.command.d.ts.map +1 -1
  87. package/dist/commands/release/apply.command.js +158 -387
  88. package/dist/commands/release/apply.mutation.d.ts +7 -0
  89. package/dist/commands/release/apply.mutation.d.ts.map +1 -0
  90. package/dist/commands/release/apply.mutation.js +107 -0
  91. package/dist/commands/release/apply.preflight.d.ts +25 -0
  92. package/dist/commands/release/apply.preflight.d.ts.map +1 -0
  93. package/dist/commands/release/apply.preflight.js +338 -0
  94. package/dist/commands/release/apply.reporting.d.ts +4 -0
  95. package/dist/commands/release/apply.reporting.d.ts.map +1 -0
  96. package/dist/commands/release/apply.reporting.js +24 -0
  97. package/dist/commands/release/apply.types.d.ts +46 -0
  98. package/dist/commands/release/apply.types.d.ts.map +1 -0
  99. package/dist/commands/release/apply.types.js +1 -0
  100. package/dist/commands/runtime.command.d.ts +28 -0
  101. package/dist/commands/runtime.command.d.ts.map +1 -0
  102. package/dist/commands/runtime.command.js +169 -0
  103. package/dist/commands/shared/task-store.d.ts.map +1 -1
  104. package/dist/commands/shared/task-store.js +7 -3
  105. package/dist/commands/task/add.d.ts.map +1 -1
  106. package/dist/commands/task/add.js +3 -33
  107. package/dist/commands/task/block.d.ts.map +1 -1
  108. package/dist/commands/task/block.js +2 -2
  109. package/dist/commands/task/close-duplicate.d.ts.map +1 -1
  110. package/dist/commands/task/close-duplicate.js +2 -2
  111. package/dist/commands/task/close-noop.d.ts.map +1 -1
  112. package/dist/commands/task/close-noop.js +2 -2
  113. package/dist/commands/task/comment.js +2 -2
  114. package/dist/commands/task/derive.d.ts.map +1 -1
  115. package/dist/commands/task/derive.js +3 -3
  116. package/dist/commands/task/doc-template.d.ts +10 -0
  117. package/dist/commands/task/doc-template.d.ts.map +1 -0
  118. package/dist/commands/task/doc-template.js +104 -0
  119. package/dist/commands/task/doc.d.ts.map +1 -1
  120. package/dist/commands/task/doc.js +36 -1
  121. package/dist/commands/task/finish.d.ts.map +1 -1
  122. package/dist/commands/task/finish.js +7 -4
  123. package/dist/commands/task/migrate-doc.command.d.ts.map +1 -1
  124. package/dist/commands/task/migrate-doc.command.js +5 -1
  125. package/dist/commands/task/migrate-doc.d.ts.map +1 -1
  126. package/dist/commands/task/migrate-doc.js +136 -2
  127. package/dist/commands/task/new.d.ts.map +1 -1
  128. package/dist/commands/task/new.js +4 -110
  129. package/dist/commands/task/new.spec.js +3 -3
  130. package/dist/commands/task/plan.d.ts.map +1 -1
  131. package/dist/commands/task/plan.js +5 -4
  132. package/dist/commands/task/scaffold.d.ts.map +1 -1
  133. package/dist/commands/task/scaffold.js +7 -52
  134. package/dist/commands/task/set-status.d.ts.map +1 -1
  135. package/dist/commands/task/set-status.js +2 -2
  136. package/dist/commands/task/shared/dependencies.d.ts +15 -0
  137. package/dist/commands/task/shared/dependencies.d.ts.map +1 -0
  138. package/dist/commands/task/shared/dependencies.js +143 -0
  139. package/dist/commands/task/shared/docs.d.ts +21 -0
  140. package/dist/commands/task/shared/docs.d.ts.map +1 -0
  141. package/dist/commands/task/shared/docs.js +121 -0
  142. package/dist/commands/task/shared/listing.d.ts +20 -0
  143. package/dist/commands/task/shared/listing.d.ts.map +1 -0
  144. package/dist/commands/task/shared/listing.js +127 -0
  145. package/dist/commands/task/shared/tags.d.ts +24 -0
  146. package/dist/commands/task/shared/tags.d.ts.map +1 -0
  147. package/dist/commands/task/shared/tags.js +177 -0
  148. package/dist/commands/task/shared/transitions.d.ts +42 -0
  149. package/dist/commands/task/shared/transitions.d.ts.map +1 -0
  150. package/dist/commands/task/shared/transitions.js +175 -0
  151. package/dist/commands/task/shared.d.ts +5 -106
  152. package/dist/commands/task/shared.d.ts.map +1 -1
  153. package/dist/commands/task/shared.js +5 -681
  154. package/dist/commands/task/start.d.ts.map +1 -1
  155. package/dist/commands/task/start.js +7 -5
  156. package/dist/commands/task/verify-record.d.ts.map +1 -1
  157. package/dist/commands/task/verify-record.js +9 -25
  158. package/dist/commands/task/verify-show.command.d.ts.map +1 -1
  159. package/dist/commands/task/verify-show.command.js +5 -1
  160. package/dist/commands/upgrade/apply.d.ts +44 -0
  161. package/dist/commands/upgrade/apply.d.ts.map +1 -0
  162. package/dist/commands/upgrade/apply.js +180 -0
  163. package/dist/commands/upgrade/report.d.ts +21 -0
  164. package/dist/commands/upgrade/report.d.ts.map +1 -0
  165. package/dist/commands/upgrade/report.js +81 -0
  166. package/dist/commands/upgrade/source.d.ts +35 -0
  167. package/dist/commands/upgrade/source.d.ts.map +1 -0
  168. package/dist/commands/upgrade/source.js +109 -0
  169. package/dist/commands/upgrade/types.d.ts +31 -0
  170. package/dist/commands/upgrade/types.d.ts.map +1 -0
  171. package/dist/commands/upgrade/types.js +1 -0
  172. package/dist/commands/upgrade.d.ts +1 -35
  173. package/dist/commands/upgrade.d.ts.map +1 -1
  174. package/dist/commands/upgrade.js +46 -331
  175. package/dist/shared/diagnostics.d.ts +23 -0
  176. package/dist/shared/diagnostics.d.ts.map +1 -0
  177. package/dist/shared/diagnostics.js +57 -0
  178. package/dist/shared/errors.d.ts +2 -0
  179. package/dist/shared/errors.d.ts.map +1 -1
  180. package/dist/shared/errors.js +2 -0
  181. package/dist/shared/repo-cli-version.d.ts +13 -0
  182. package/dist/shared/repo-cli-version.d.ts.map +1 -0
  183. package/dist/shared/repo-cli-version.js +63 -0
  184. package/dist/shared/runtime-source.d.ts +33 -0
  185. package/dist/shared/runtime-source.d.ts.map +1 -0
  186. package/dist/shared/runtime-source.js +156 -0
  187. package/dist/shared/version-compare.d.ts +7 -0
  188. package/dist/shared/version-compare.d.ts.map +1 -0
  189. package/dist/shared/version-compare.js +30 -0
  190. package/package.json +2 -2
@@ -0,0 +1,211 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { execFileAsync, gitEnv } from "../shared/git.js";
4
+ const DEFAULT_RECENT_DONE_TASK_LIMIT = 200;
5
+ function pluralize(count, singular, plural = `${singular}s`) {
6
+ return count === 1 ? singular : plural;
7
+ }
8
+ function formatIdExamples(ids, maxExamples = 3) {
9
+ const shown = ids.slice(0, maxExamples);
10
+ const remainder = ids.length - shown.length;
11
+ return remainder > 0 ? `${shown.join(", ")}; +${remainder} more` : shown.join(", ");
12
+ }
13
+ function taskShortId(taskId) {
14
+ const shortId = taskId.split("-").at(-1) ?? taskId;
15
+ return shortId.trim().toUpperCase();
16
+ }
17
+ function textOrEmpty(value) {
18
+ return typeof value === "string" ? value.trim() : "";
19
+ }
20
+ function collectTaskEvidenceText(task) {
21
+ const chunks = [
22
+ textOrEmpty(task.title),
23
+ textOrEmpty(task.result_summary),
24
+ textOrEmpty(task.verification?.note),
25
+ textOrEmpty(task.commit?.message),
26
+ ...(Array.isArray(task.comments)
27
+ ? task.comments.map((entry) => textOrEmpty(entry?.body)).filter(Boolean)
28
+ : []),
29
+ ...(Array.isArray(task.tags) ? task.tags.filter((tag) => typeof tag === "string") : []),
30
+ ];
31
+ return chunks.filter(Boolean).join("\n");
32
+ }
33
+ function classifyUnknownHistoricalHash(task) {
34
+ void task;
35
+ return "INFO";
36
+ }
37
+ function classifyCloseCommitReference(task, subject) {
38
+ if (/\bclose:\s*record task doc\b/iu.test(subject)) {
39
+ return "INFO";
40
+ }
41
+ const evidence = collectTaskEvidenceText(task);
42
+ if (/(no-op|already implemented|already fixed|already present|without changes|task deemed not актуальна|obsolete|no changes)/iu.test(evidence)) {
43
+ return "INFO";
44
+ }
45
+ const subjectTaskId = /^\s*✅\s+([A-Z0-9]{6})\s+close:/u.exec(subject)?.[1] ?? "";
46
+ const currentTaskId = typeof task.id === "string" ? taskShortId(task.id) : "";
47
+ if (subjectTaskId && currentTaskId && subjectTaskId !== currentTaskId) {
48
+ return "INFO";
49
+ }
50
+ if (/\b(epic|roadmap|spike|meta)\b/iu.test(evidence)) {
51
+ return "INFO";
52
+ }
53
+ return "WARN";
54
+ }
55
+ function summarizeHistoricalFindings(findings, opts) {
56
+ if (findings.length === 0)
57
+ return [];
58
+ const prefix = `[${opts.severity}]`;
59
+ if (findings.length === 1) {
60
+ const [finding] = findings;
61
+ const subjectSuffix = opts.includeSubject && finding.subject ? ` (${finding.subject})` : "";
62
+ return [`${prefix} ${opts.singlePrefix}: ${finding.id} -> ${finding.hash}${subjectSuffix}`];
63
+ }
64
+ const grouped = new Map();
65
+ for (const finding of findings) {
66
+ const existing = grouped.get(finding.hash);
67
+ if (existing) {
68
+ existing.ids.push(finding.id);
69
+ if (!existing.subject && finding.subject)
70
+ existing.subject = finding.subject;
71
+ continue;
72
+ }
73
+ grouped.set(finding.hash, {
74
+ hash: finding.hash,
75
+ ids: [finding.id],
76
+ subject: finding.subject,
77
+ });
78
+ }
79
+ const groups = [...grouped.values()].toSorted((left, right) => {
80
+ const countDelta = right.ids.length - left.ids.length;
81
+ if (countDelta !== 0)
82
+ return countDelta;
83
+ return left.hash.localeCompare(right.hash);
84
+ });
85
+ const exampleGroups = groups.slice(0, 3).map((group) => {
86
+ const subjectSuffix = opts.includeSubject && group.subject ? `; subject: ${group.subject}` : "";
87
+ return `${group.hash} (${group.ids.length} ${pluralize(group.ids.length, "task")}: ${formatIdExamples(group.ids)}${subjectSuffix})`;
88
+ });
89
+ const remainingGroups = groups.length - exampleGroups.length;
90
+ const groupedSuffix = remainingGroups > 0 ? `; +${remainingGroups} more hash groups` : "";
91
+ return [
92
+ `${prefix} Historical task archive contains ${findings.length} DONE tasks with ${opts.summaryLabel} across ${groups.length} distinct commit ${opts.groupLabel}. Examples: ${exampleGroups.join("; ")}${groupedSuffix}`,
93
+ ];
94
+ }
95
+ export async function checkDoneTaskCommitInvariants(repoRoot, opts = {}) {
96
+ const tasksPath = path.join(repoRoot, ".agentplane", "tasks.json");
97
+ let raw = "";
98
+ try {
99
+ raw = await fs.readFile(tasksPath, "utf8");
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ let parsed;
105
+ try {
106
+ parsed = JSON.parse(raw);
107
+ }
108
+ catch {
109
+ return [`Invalid JSON snapshot: ${path.relative(repoRoot, tasksPath)}`];
110
+ }
111
+ const all = Array.isArray(parsed.tasks) ? parsed.tasks : [];
112
+ const allDone = all.filter((t) => {
113
+ const status = typeof t.status === "string" ? t.status : "";
114
+ return status.toUpperCase() === "DONE";
115
+ });
116
+ const done = opts.fullArchive ? allDone : allDone.slice(-DEFAULT_RECENT_DONE_TASK_LIMIT);
117
+ if (done.length === 0)
118
+ return [];
119
+ const problems = [];
120
+ const unknownHashFindings = [];
121
+ const closeCommitWarnFindings = [];
122
+ const closeCommitInfoFindings = [];
123
+ const hashes = new Set();
124
+ for (const task of done) {
125
+ const id = typeof task.id === "string" ? task.id : "<unknown>";
126
+ const hash = typeof task.commit?.hash === "string" ? task.commit.hash.trim() : "";
127
+ if (!hash) {
128
+ problems.push(`DONE task is missing implementation commit hash: ${id} (finish with --commit <hash>).`);
129
+ continue;
130
+ }
131
+ hashes.add(hash);
132
+ }
133
+ if (hashes.size === 0)
134
+ return problems;
135
+ const subjectByHash = await resolveCommitSubjects(repoRoot, [...hashes]);
136
+ for (const task of done) {
137
+ const id = typeof task.id === "string" ? task.id : "<unknown>";
138
+ const hash = typeof task.commit?.hash === "string" ? task.commit.hash.trim() : "";
139
+ if (!hash)
140
+ continue;
141
+ const subject = subjectByHash.get(hash) ?? "";
142
+ if (!subject) {
143
+ unknownHashFindings.push({
144
+ id,
145
+ hash,
146
+ severity: classifyUnknownHistoricalHash(task),
147
+ });
148
+ continue;
149
+ }
150
+ if (/\bclose:/iu.test(subject)) {
151
+ const severity = classifyCloseCommitReference(task, subject);
152
+ const finding = { id, hash, subject, severity };
153
+ if (severity === "WARN")
154
+ closeCommitWarnFindings.push(finding);
155
+ else
156
+ closeCommitInfoFindings.push(finding);
157
+ }
158
+ }
159
+ problems.push(...summarizeHistoricalFindings(unknownHashFindings, {
160
+ singlePrefix: "DONE task references unknown historical commit hash",
161
+ groupLabel: "hashes",
162
+ summaryLabel: "unknown implementation commit hashes",
163
+ includeSubject: false,
164
+ severity: "INFO",
165
+ }), ...summarizeHistoricalFindings(closeCommitInfoFindings, {
166
+ singlePrefix: "DONE task implementation commit resolves to a historical close commit reference",
167
+ groupLabel: "hashes",
168
+ summaryLabel: "historical close-commit references that were classified as non-actionable archive records",
169
+ includeSubject: true,
170
+ severity: "INFO",
171
+ }), ...summarizeHistoricalFindings(closeCommitWarnFindings, {
172
+ singlePrefix: "DONE task implementation commit points to a close commit",
173
+ groupLabel: "hashes",
174
+ summaryLabel: "implementation commits that point to close commits",
175
+ includeSubject: true,
176
+ severity: "WARN",
177
+ }));
178
+ return problems;
179
+ }
180
+ async function resolveCommitSubjects(repoRoot, hashes) {
181
+ const subjectByHash = new Map();
182
+ if (hashes.length === 0)
183
+ return subjectByHash;
184
+ for (const chunk of chunked(hashes, 200)) {
185
+ try {
186
+ const { stdout } = await execFileAsync("git", ["show", "-s", "--no-patch", "--format=%H%x00%s%x00", "--ignore-missing", ...chunk], {
187
+ cwd: repoRoot,
188
+ env: gitEnv(),
189
+ });
190
+ const parts = String(stdout ?? "").split("\u0000");
191
+ for (let i = 0; i + 1 < parts.length; i += 2) {
192
+ const hash = parts[i]?.trim() ?? "";
193
+ const subject = parts[i + 1]?.trim() ?? "";
194
+ if (!hash)
195
+ continue;
196
+ subjectByHash.set(hash, subject);
197
+ }
198
+ }
199
+ catch {
200
+ // Preserve old behavior: unknown/unreadable hashes are reported later as empty subject.
201
+ }
202
+ }
203
+ return subjectByHash;
204
+ }
205
+ function chunked(items, size) {
206
+ const chunks = [];
207
+ for (let i = 0; i < items.length; i += size) {
208
+ chunks.push(items.slice(i, i + size));
209
+ }
210
+ return chunks;
211
+ }
@@ -0,0 +1,9 @@
1
+ export declare function safeFixGitignore(repoRoot: string): Promise<{
2
+ changed: boolean;
3
+ note: string;
4
+ }>;
5
+ export declare function safeFixTaskIndex(repoRoot: string): Promise<{
6
+ changed: boolean;
7
+ note: string;
8
+ }>;
9
+ //# sourceMappingURL=fixes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixes.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/fixes.ts"],"names":[],"mappings":"AAMA,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAyB7C;AAED,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAQ7C"}
@@ -0,0 +1,40 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { RUNTIME_GITIGNORE_LINES } from "../../shared/runtime-artifacts.js";
4
+ import { loadCommandContext } from "../shared/task-backend.js";
5
+ export async function safeFixGitignore(repoRoot) {
6
+ const gitignorePath = path.join(repoRoot, ".gitignore");
7
+ let existing = "";
8
+ try {
9
+ existing = await fs.readFile(gitignorePath, "utf8");
10
+ }
11
+ catch {
12
+ return { changed: false, note: "Skip: .gitignore not found." };
13
+ }
14
+ const lines = existing.split(/\r?\n/);
15
+ const existingSet = new Set(lines.map((line) => line.trimEnd()));
16
+ const missing = RUNTIME_GITIGNORE_LINES.filter((line) => !existingSet.has(line));
17
+ if (missing.length === 0) {
18
+ return { changed: false, note: "OK: .gitignore already contains agentplane runtime ignores." };
19
+ }
20
+ const nextLines = [...lines];
21
+ if (nextLines.length > 0 && nextLines.at(-1) !== "")
22
+ nextLines.push("");
23
+ nextLines.push(...missing, "");
24
+ const next = `${nextLines.join("\n")}`.replaceAll(/\n{2,}$/g, "\n");
25
+ await fs.writeFile(gitignorePath, next, "utf8");
26
+ return {
27
+ changed: true,
28
+ note: `Fixed: added agentplane runtime ignores to .gitignore (${missing.length} lines).`,
29
+ };
30
+ }
31
+ export async function safeFixTaskIndex(repoRoot) {
32
+ try {
33
+ const ctx = await loadCommandContext({ cwd: repoRoot, rootOverride: null });
34
+ await ctx.taskBackend.listTasks();
35
+ return { changed: true, note: "OK: rebuilt tasks index cache (best-effort)." };
36
+ }
37
+ catch {
38
+ return { changed: false, note: "Skip: could not rebuild tasks index cache." };
39
+ }
40
+ }
@@ -0,0 +1,2 @@
1
+ export declare function checkLayering(repoRoot: string): Promise<string[]>;
2
+ //# sourceMappingURL=layering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layering.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/layering.ts"],"names":[],"mappings":"AA6CA,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAmDvE"}
@@ -0,0 +1,87 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ async function listTsFiles(rootDir) {
4
+ const out = [];
5
+ async function walk(absDir) {
6
+ const entries = await fs.readdir(absDir, { withFileTypes: true });
7
+ for (const ent of entries) {
8
+ if (ent.name.startsWith("."))
9
+ continue;
10
+ if (ent.name === "__snapshots__")
11
+ continue;
12
+ if (ent.name === "node_modules")
13
+ continue;
14
+ const abs = path.join(absDir, ent.name);
15
+ if (ent.isDirectory()) {
16
+ await walk(abs);
17
+ continue;
18
+ }
19
+ if (ent.isFile() && ent.name.endsWith(".ts")) {
20
+ out.push({ absPath: abs, relPath: path.relative(rootDir, abs) });
21
+ }
22
+ }
23
+ }
24
+ await walk(rootDir);
25
+ return out;
26
+ }
27
+ function extractImports(source) {
28
+ const imports = [];
29
+ const re = /^\s*import\s+(?:type\s+)?(?:[^"']*?\s+from\s+)?["']([^"']+)["']\s*;?/gm;
30
+ for (const match of source.matchAll(re)) {
31
+ imports.push(match[1] ?? "");
32
+ }
33
+ return imports.filter(Boolean);
34
+ }
35
+ async function isDirectory(absPath) {
36
+ try {
37
+ const st = await fs.stat(absPath);
38
+ return st.isDirectory();
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ export async function checkLayering(repoRoot) {
45
+ const problems = [];
46
+ const agentplaneSrcRoot = path.join(repoRoot, "packages", "agentplane", "src");
47
+ if (!(await isDirectory(agentplaneSrcRoot))) {
48
+ problems.push("Dev source checks requested but packages/agentplane/src was not found in this workspace.");
49
+ return problems;
50
+ }
51
+ const cliRoot = path.join(agentplaneSrcRoot, "cli");
52
+ const cliFiles = await listTsFiles(cliRoot);
53
+ for (const f of cliFiles) {
54
+ const src = await fs.readFile(f.absPath, "utf8");
55
+ const imports = extractImports(src);
56
+ const hits = imports.filter((s) => s.includes("/adapters/") ||
57
+ s.includes("../adapters") ||
58
+ s.includes("../../adapters") ||
59
+ s.includes("../../../adapters"));
60
+ if (hits.length > 0) {
61
+ problems.push(`${f.relPath} imports adapters directly: ${hits.join(", ")}`);
62
+ }
63
+ }
64
+ const roots = [path.join(agentplaneSrcRoot, "usecases"), path.join(agentplaneSrcRoot, "ports")];
65
+ const banned = [
66
+ "node:fs",
67
+ "node:fs/promises",
68
+ "fs",
69
+ "fs/promises",
70
+ "node:path",
71
+ "path",
72
+ "simple-git",
73
+ "isomorphic-git",
74
+ ];
75
+ for (const root of roots) {
76
+ const files = await listTsFiles(root);
77
+ for (const f of files) {
78
+ const src = await fs.readFile(f.absPath, "utf8");
79
+ const imports = extractImports(src);
80
+ const hits = imports.filter((s) => banned.some((b) => s === b || s.startsWith(`${b}/`)));
81
+ if (hits.length > 0) {
82
+ problems.push(`${f.relPath} imports banned modules: ${hits.join(", ")}`);
83
+ }
84
+ }
85
+ }
86
+ return problems;
87
+ }
@@ -0,0 +1,4 @@
1
+ import type { AgentplaneConfig } from "@agentplaneorg/core";
2
+ export declare function checkRuntimeSourceFacts(cwd: string, config?: AgentplaneConfig): string[];
3
+ export declare function findingSeverity(problem: string): "ERROR" | "WARN" | "INFO";
4
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAyB5D,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,gBAAgB,GAAG,MAAM,EAAE,CA+BxF;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAM1E"}
@@ -0,0 +1,56 @@
1
+ import { evaluateRepoCliVersionExpectation, } from "../../shared/repo-cli-version.js";
2
+ import { describeRuntimeMode, resolveRuntimeSourceInfo } from "../../shared/runtime-source.js";
3
+ function renderCliVersionFacts(expectation) {
4
+ if (!expectation.expectedVersion)
5
+ return [];
6
+ if (expectation.state === "older_than_expected" ||
7
+ expectation.state === "active_version_unresolved") {
8
+ return [
9
+ `[WARN] ${expectation.summary}`,
10
+ ...(expectation.recovery ? [`[WARN] Recovery: ${expectation.recovery}`] : []),
11
+ ];
12
+ }
13
+ return [
14
+ `[INFO] Repository expected agentplane CLI: ${expectation.expectedVersion}`,
15
+ ...(expectation.summary ? [`[INFO] ${expectation.summary}`] : []),
16
+ ];
17
+ }
18
+ export function checkRuntimeSourceFacts(cwd, config) {
19
+ const report = resolveRuntimeSourceInfo({ cwd, entryModuleUrl: import.meta.url });
20
+ const cliVersionFacts = config
21
+ ? renderCliVersionFacts(evaluateRepoCliVersionExpectation(config, report))
22
+ : [];
23
+ if (!report.framework.inFrameworkCheckout) {
24
+ return cliVersionFacts;
25
+ }
26
+ const warning = report.mode === "global-in-framework"
27
+ ? "[WARN] Framework checkout detected but the active runtime is still a global installed binary. " +
28
+ "Update or reinstall agentplane to pick up repo-local handoff, or run the repo-local binary directly."
29
+ : report.mode === "global-forced-in-framework"
30
+ ? "[WARN] Framework checkout is forcing the global installed binary via AGENTPLANE_USE_GLOBAL_IN_FRAMEWORK=1."
31
+ : null;
32
+ return [
33
+ ...(warning ? [warning] : []),
34
+ ...cliVersionFacts,
35
+ `[INFO] Runtime mode: ${report.mode} (${describeRuntimeMode(report.mode)})`,
36
+ `[INFO] Active binary: ${report.activeBinaryPath ?? "unresolved"}`,
37
+ ...(report.handoffFromBinaryPath
38
+ ? [`[INFO] Handoff source binary: ${report.handoffFromBinaryPath}`]
39
+ : []),
40
+ `[INFO] Resolved agentplane: ${report.agentplane.version ?? "unknown"} @ ${report.agentplane.packageRoot ?? "unresolved"}`,
41
+ `[INFO] Resolved @agentplaneorg/core: ${report.core.version ?? "unknown"} @ ${report.core.packageRoot ?? "unresolved"}`,
42
+ `[INFO] Framework repo root: ${report.frameworkSources.repoRoot ?? "unresolved"}`,
43
+ `[INFO] Framework agentplane root: ${report.frameworkSources.agentplaneRoot ?? "unresolved"}`,
44
+ `[INFO] Framework core root: ${report.frameworkSources.coreRoot ?? "unresolved"}`,
45
+ ];
46
+ }
47
+ export function findingSeverity(problem) {
48
+ const normalized = problem.trimStart();
49
+ if (normalized.startsWith("[WARN]"))
50
+ return "WARN";
51
+ if (normalized.startsWith("[INFO]"))
52
+ return "INFO";
53
+ if (normalized.startsWith("[ERROR]"))
54
+ return "ERROR";
55
+ return "ERROR";
56
+ }
@@ -0,0 +1,6 @@
1
+ export declare function checkWorkflowContract(repoRoot: string): Promise<string[]>;
2
+ export declare function safeFixWorkflow(repoRoot: string): Promise<{
3
+ changed: boolean;
4
+ note: string;
5
+ }>;
6
+ //# sourceMappingURL=workflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/workflow.ts"],"names":[],"mappings":"AAUA,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAU/E;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAiD7C"}
@@ -0,0 +1,62 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { emitWorkflowEvent, resolveWorkflowPaths, safeAutofixWorkflowText, validateWorkflowAtPath, } from "../../workflow-runtime/index.js";
4
+ export async function checkWorkflowContract(repoRoot) {
5
+ const result = await validateWorkflowAtPath(repoRoot);
6
+ const findings = result.diagnostics.map((d) => `[${d.severity}] ${d.code} ${d.path}: ${d.message}`);
7
+ emitWorkflowEvent({
8
+ event: "workflow_doctor_check",
9
+ details: { ok: result.ok, findings: result.diagnostics.length },
10
+ });
11
+ return findings;
12
+ }
13
+ export async function safeFixWorkflow(repoRoot) {
14
+ const paths = resolveWorkflowPaths(repoRoot);
15
+ let current = "";
16
+ let sourcePath = paths.workflowPath;
17
+ try {
18
+ current = await fs.readFile(paths.workflowPath, "utf8");
19
+ }
20
+ catch {
21
+ try {
22
+ current = await fs.readFile(paths.legacyWorkflowPath, "utf8");
23
+ sourcePath = paths.legacyWorkflowPath;
24
+ }
25
+ catch {
26
+ return { changed: false, note: "Skip: workflow contract file not found." };
27
+ }
28
+ }
29
+ const fixed = safeAutofixWorkflowText(current);
30
+ if (fixed.diagnostics.some((d) => d.code === "WF_FIX_SKIPPED_UNSAFE")) {
31
+ const details = fixed.diagnostics.map((d) => `${d.path}`).join(", ");
32
+ return {
33
+ changed: false,
34
+ note: `Skip: unsafe workflow autofix required (unknown keys). Proposed manual review: ${details}`,
35
+ };
36
+ }
37
+ if (!fixed.changed) {
38
+ if (sourcePath === paths.workflowPath) {
39
+ return { changed: false, note: "OK: workflow contract already normalized." };
40
+ }
41
+ await fs.mkdir(path.dirname(paths.workflowPath), { recursive: true });
42
+ await fs.writeFile(paths.workflowPath, current, "utf8");
43
+ await fs.rm(paths.legacyWorkflowPath, { force: true });
44
+ await fs.mkdir(paths.workflowDir, { recursive: true });
45
+ await fs.copyFile(paths.workflowPath, paths.lastKnownGoodPath);
46
+ return {
47
+ changed: true,
48
+ note: "Fixed: moved legacy WORKFLOW.md into .agentplane and refreshed last-known-good.",
49
+ };
50
+ }
51
+ await fs.mkdir(path.dirname(paths.workflowPath), { recursive: true });
52
+ await fs.writeFile(paths.workflowPath, fixed.text, "utf8");
53
+ if (sourcePath === paths.legacyWorkflowPath) {
54
+ await fs.rm(paths.legacyWorkflowPath, { force: true });
55
+ }
56
+ await fs.mkdir(paths.workflowDir, { recursive: true });
57
+ await fs.copyFile(paths.workflowPath, paths.lastKnownGoodPath);
58
+ return {
59
+ changed: true,
60
+ note: "Fixed: normalized workflow contract and refreshed last-known-good.",
61
+ };
62
+ }
@@ -0,0 +1,2 @@
1
+ export declare function checkWorkspace(repoRoot: string): Promise<string[]>;
2
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/workspace.ts"],"names":[],"mappings":"AAqIA,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAsDxE"}
@@ -0,0 +1,165 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { renderDiagnosticFinding } from "../../shared/diagnostics.js";
5
+ import { resolvePolicyGatewayForRepo } from "../../shared/policy-gateway.js";
6
+ async function pathExists(absPath) {
7
+ try {
8
+ await fs.access(absPath);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ async function isDirectory(absPath) {
16
+ try {
17
+ const st = await fs.stat(absPath);
18
+ return st.isDirectory();
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ async function listMissingManagedPolicyFiles(repoRoot) {
25
+ const manifestPath = fileURLToPath(new URL("../../../assets/framework.manifest.json", import.meta.url));
26
+ let parsed = {};
27
+ try {
28
+ parsed = JSON.parse(await fs.readFile(manifestPath, "utf8"));
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ const relPaths = Array.isArray(parsed.files)
34
+ ? parsed.files
35
+ .filter((entry) => entry?.required === true && typeof entry.path === "string")
36
+ .map((entry) => String(entry.path).replaceAll("\\", "/").trim())
37
+ .filter((relPath) => relPath.startsWith(".agentplane/policy/"))
38
+ : [];
39
+ const missing = [];
40
+ for (const relPath of relPaths) {
41
+ if (!(await pathExists(path.join(repoRoot, relPath)))) {
42
+ missing.push(relPath);
43
+ }
44
+ }
45
+ return missing.toSorted();
46
+ }
47
+ async function checkTaskReadmeMigrationState(repoRoot) {
48
+ const tasksPath = path.join(repoRoot, ".agentplane", "tasks.json");
49
+ let raw = "";
50
+ try {
51
+ raw = await fs.readFile(tasksPath, "utf8");
52
+ }
53
+ catch {
54
+ return [];
55
+ }
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(raw);
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ const tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
64
+ if (tasks.length === 0)
65
+ return [];
66
+ const legacy = tasks.filter((task) => task.doc_version !== 3);
67
+ if (legacy.length === 0)
68
+ return [];
69
+ const legacyActive = legacy.filter((task) => {
70
+ const status = typeof task.status === "string" ? task.status.trim().toUpperCase() : "";
71
+ return status !== "DONE";
72
+ });
73
+ const v3Count = tasks.length - legacy.length;
74
+ const exampleIds = legacy
75
+ .map((task) => (typeof task.id === "string" ? task.id : ""))
76
+ .filter(Boolean)
77
+ .slice(0, 5)
78
+ .join(", ");
79
+ const hasMixedVersions = v3Count > 0;
80
+ if (legacyActive.length > 0) {
81
+ return [
82
+ renderDiagnosticFinding({
83
+ severity: "WARN",
84
+ state: hasMixedVersions
85
+ ? "task README migration is incomplete (active v2/v3 mixed state)"
86
+ : "task README format is still on legacy v2",
87
+ likelyCause: "the workspace still contains active task READMEs that were never migrated to the README v3 contract",
88
+ nextAction: {
89
+ command: "agentplane task migrate-doc --all",
90
+ reason: "upgrade all task READMEs to doc_version=3 before continuing active work",
91
+ },
92
+ details: [
93
+ `Legacy tasks: ${legacy.length}; active legacy tasks: ${legacyActive.length}; README v3 tasks: ${v3Count}`,
94
+ exampleIds ? `Examples: ${exampleIds}` : "Examples unavailable in tasks snapshot.",
95
+ ],
96
+ }),
97
+ ];
98
+ }
99
+ return [
100
+ renderDiagnosticFinding({
101
+ severity: "INFO",
102
+ state: hasMixedVersions
103
+ ? "historical task archive still mixes README v2 and v3"
104
+ : "historical task archive still uses README v2",
105
+ likelyCause: "older DONE tasks were never backfilled to README v3 after the task-document contract changed",
106
+ nextAction: {
107
+ command: "agentplane task migrate-doc --all",
108
+ reason: "normalize archived task READMEs to the README v3 contract when convenient",
109
+ },
110
+ details: [
111
+ `Legacy tasks: ${legacy.length}; active legacy tasks: 0; README v3 tasks: ${v3Count}`,
112
+ exampleIds ? `Examples: ${exampleIds}` : "Examples unavailable in tasks snapshot.",
113
+ ],
114
+ }),
115
+ ];
116
+ }
117
+ export async function checkWorkspace(repoRoot) {
118
+ const problems = [];
119
+ const requiredFiles = [path.join(repoRoot, ".agentplane", "config.json")];
120
+ for (const filePath of requiredFiles) {
121
+ if (!(await pathExists(filePath))) {
122
+ problems.push(`Missing required file: ${path.relative(repoRoot, filePath)}`);
123
+ }
124
+ }
125
+ const gateway = await resolvePolicyGatewayForRepo({
126
+ gitRoot: repoRoot,
127
+ fallbackFlavor: "codex",
128
+ });
129
+ if (!(await pathExists(gateway.absPath))) {
130
+ problems.push("Missing required policy gateway file: AGENTS.md or CLAUDE.md");
131
+ }
132
+ if (await pathExists(gateway.absPath)) {
133
+ const missingManagedPolicy = await listMissingManagedPolicyFiles(repoRoot);
134
+ if (missingManagedPolicy.length > 0) {
135
+ const listed = missingManagedPolicy.slice(0, 8).join(", ");
136
+ const more = missingManagedPolicy.length > 8 ? ` (+${missingManagedPolicy.length - 8} more)` : "";
137
+ problems.push(renderDiagnosticFinding({
138
+ severity: "ERROR",
139
+ state: "framework-managed policy tree is incomplete",
140
+ likelyCause: "the active AGENTS.md/CLAUDE.md gateway expects required policy files that are not installed in this workspace",
141
+ nextAction: {
142
+ command: "agentplane upgrade --yes",
143
+ reason: "reinstall the managed policy tree from the currently active framework bundle",
144
+ },
145
+ details: [
146
+ `Missing required files: ${listed}${more}`,
147
+ "If the installed CLI is older than the gateway, update or reinstall agentplane first and then rerun `agentplane upgrade --yes` (or `agentplane upgrade --remote --yes`).",
148
+ "Recovery guide: docs/help/legacy-upgrade-recovery.mdx",
149
+ ],
150
+ }));
151
+ }
152
+ }
153
+ const agentsDir = path.join(repoRoot, ".agentplane", "agents");
154
+ if (!(await isDirectory(agentsDir))) {
155
+ problems.push("Missing required directory: .agentplane/agents");
156
+ return problems;
157
+ }
158
+ const entries = await fs.readdir(agentsDir);
159
+ const hasJson = entries.some((name) => name.endsWith(".json"));
160
+ if (!hasJson) {
161
+ problems.push("No agent profiles found in .agentplane/agents (*.json expected).");
162
+ }
163
+ problems.push(...(await checkTaskReadmeMigrationState(repoRoot)));
164
+ return problems;
165
+ }