@chinhae/atw-cli 0.5.0 → 0.7.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 (257) hide show
  1. package/README.md +128 -451
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +24 -1
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/channel/adapters/claude.d.ts +44 -2
  6. package/dist/commands/channel/adapters/claude.d.ts.map +1 -1
  7. package/dist/commands/channel/adapters/claude.js +124 -23
  8. package/dist/commands/channel/adapters/claude.js.map +1 -1
  9. package/dist/commands/channel/adapters/codex.d.ts +36 -6
  10. package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
  11. package/dist/commands/channel/adapters/codex.js +150 -10
  12. package/dist/commands/channel/adapters/codex.js.map +1 -1
  13. package/dist/commands/channel/adapters/index.d.ts +33 -4
  14. package/dist/commands/channel/adapters/index.d.ts.map +1 -1
  15. package/dist/commands/channel/adapters/index.js +159 -6
  16. package/dist/commands/channel/adapters/index.js.map +1 -1
  17. package/dist/commands/channel/adapters/omp.d.ts +106 -0
  18. package/dist/commands/channel/adapters/omp.d.ts.map +1 -0
  19. package/dist/commands/channel/adapters/omp.js +405 -0
  20. package/dist/commands/channel/adapters/omp.js.map +1 -0
  21. package/dist/commands/channel/adapters/pi.d.ts +123 -0
  22. package/dist/commands/channel/adapters/pi.d.ts.map +1 -0
  23. package/dist/commands/channel/adapters/pi.js +387 -0
  24. package/dist/commands/channel/adapters/pi.js.map +1 -0
  25. package/dist/commands/channel/adapters/types.d.ts +16 -0
  26. package/dist/commands/channel/adapters/types.d.ts.map +1 -1
  27. package/dist/commands/channel/agent-loader.d.ts +1 -0
  28. package/dist/commands/channel/agent-loader.d.ts.map +1 -1
  29. package/dist/commands/channel/agent-loader.js +45 -5
  30. package/dist/commands/channel/agent-loader.js.map +1 -1
  31. package/dist/commands/channel/dev-parse-trace.d.ts +1 -1
  32. package/dist/commands/channel/dev-parse-trace.d.ts.map +1 -1
  33. package/dist/commands/channel/dev-parse-trace.js +25 -2
  34. package/dist/commands/channel/dev-parse-trace.js.map +1 -1
  35. package/dist/commands/channel/index.d.ts.map +1 -1
  36. package/dist/commands/channel/index.js +44 -12
  37. package/dist/commands/channel/index.js.map +1 -1
  38. package/dist/commands/channel/run.d.ts +39 -2
  39. package/dist/commands/channel/run.d.ts.map +1 -1
  40. package/dist/commands/channel/run.js +227 -68
  41. package/dist/commands/channel/run.js.map +1 -1
  42. package/dist/commands/channel/spawn.d.ts +7 -2
  43. package/dist/commands/channel/spawn.d.ts.map +1 -1
  44. package/dist/commands/channel/spawn.js +35 -1
  45. package/dist/commands/channel/spawn.js.map +1 -1
  46. package/dist/commands/channel/store/events.d.ts +2 -2
  47. package/dist/commands/channel/store/events.d.ts.map +1 -1
  48. package/dist/commands/channel/store/events.js +1 -1
  49. package/dist/commands/channel/store/events.js.map +1 -1
  50. package/dist/commands/channel/supervisor/shutdown.d.ts +2 -0
  51. package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -1
  52. package/dist/commands/channel/supervisor/shutdown.js +6 -1
  53. package/dist/commands/channel/supervisor/shutdown.js.map +1 -1
  54. package/dist/commands/channel/supervisor.d.ts +2 -2
  55. package/dist/commands/channel/supervisor.d.ts.map +1 -1
  56. package/dist/commands/channel/supervisor.js +26 -3
  57. package/dist/commands/channel/supervisor.js.map +1 -1
  58. package/dist/commands/init.d.ts.map +1 -1
  59. package/dist/commands/init.js +6 -1
  60. package/dist/commands/init.js.map +1 -1
  61. package/dist/commands/mem.d.ts +2 -1
  62. package/dist/commands/mem.d.ts.map +1 -1
  63. package/dist/commands/mem.js +162 -13
  64. package/dist/commands/mem.js.map +1 -1
  65. package/dist/commands/mode.d.ts.map +1 -1
  66. package/dist/commands/mode.js +5 -3
  67. package/dist/commands/mode.js.map +1 -1
  68. package/dist/commands/uninstall.d.ts +5 -3
  69. package/dist/commands/uninstall.d.ts.map +1 -1
  70. package/dist/commands/uninstall.js +40 -16
  71. package/dist/commands/uninstall.js.map +1 -1
  72. package/dist/commands/update.d.ts +34 -0
  73. package/dist/commands/update.d.ts.map +1 -1
  74. package/dist/commands/update.js +209 -11
  75. package/dist/commands/update.js.map +1 -1
  76. package/dist/commands/verify.d.ts +18 -0
  77. package/dist/commands/verify.d.ts.map +1 -0
  78. package/dist/commands/verify.js +75 -0
  79. package/dist/commands/verify.js.map +1 -0
  80. package/dist/configurators/index.d.ts.map +1 -1
  81. package/dist/configurators/index.js +3 -2
  82. package/dist/configurators/index.js.map +1 -1
  83. package/dist/configurators/omp.d.ts +22 -0
  84. package/dist/configurators/omp.d.ts.map +1 -1
  85. package/dist/configurators/omp.js +101 -2
  86. package/dist/configurators/omp.js.map +1 -1
  87. package/dist/configurators/shared.d.ts.map +1 -1
  88. package/dist/configurators/shared.js +39 -7
  89. package/dist/configurators/shared.js.map +1 -1
  90. package/dist/configurators/workflow.d.ts.map +1 -1
  91. package/dist/configurators/workflow.js +5 -1
  92. package/dist/configurators/workflow.js.map +1 -1
  93. package/dist/constants/paths.d.ts +4 -0
  94. package/dist/constants/paths.d.ts.map +1 -1
  95. package/dist/constants/paths.js +4 -0
  96. package/dist/constants/paths.js.map +1 -1
  97. package/dist/migrations/manifests/0.6.0.json +442 -0
  98. package/dist/migrations/manifests/0.7.0.json +1378 -0
  99. package/dist/templates/atw/definition-of-done.md +65 -0
  100. package/dist/templates/atw/gitignore.txt +7 -0
  101. package/dist/templates/atw/index.d.ts +11 -0
  102. package/dist/templates/atw/index.d.ts.map +1 -1
  103. package/dist/templates/atw/index.js +22 -0
  104. package/dist/templates/atw/index.js.map +1 -1
  105. package/dist/templates/atw/scripts/common/active_task.py +2 -2
  106. package/dist/templates/atw/scripts/common/workflow_phase.py +33 -0
  107. package/dist/templates/atw/scripts/cross_model_review.py +3312 -0
  108. package/dist/templates/atw/scripts/decision_log.py +2002 -0
  109. package/dist/templates/atw/scripts/floor_guard.py +642 -0
  110. package/dist/templates/atw/scripts/task.py +25 -6
  111. package/dist/templates/atw/scripts/tickets.py +480 -10
  112. package/dist/templates/atw/workflow-general.md +6 -1
  113. package/dist/templates/atw/workflow.md +346 -26
  114. package/dist/templates/claude/settings.json +12 -0
  115. package/dist/templates/codex/hooks/session-start.py +2 -2
  116. package/dist/templates/common/bundled-skills/atw-askme/SKILL.md +1 -1
  117. package/dist/templates/common/bundled-skills/atw-askme-with-docs/SKILL.md +1 -1
  118. package/dist/templates/common/bundled-skills/atw-channel/SKILL.md +5 -2
  119. package/dist/templates/common/bundled-skills/atw-channel/references/command-reference.md +44 -5
  120. package/dist/templates/common/bundled-skills/atw-channel/references/progress-debugging.md +9 -0
  121. package/dist/templates/common/bundled-skills/atw-channel/references/workers.md +48 -4
  122. package/dist/templates/common/bundled-skills/atw-code-review/SKILL.md +78 -3
  123. package/dist/templates/common/bundled-skills/atw-codebase-design/SKILL.md +2 -2
  124. package/dist/templates/common/bundled-skills/atw-codebase-design/{DESIGN-IT-TWICE.md → design-it-twice.md} +3 -3
  125. package/dist/templates/common/bundled-skills/atw-cross-model-review/SKILL.md +361 -0
  126. package/dist/templates/common/bundled-skills/atw-cross-model-review/agents/openai.yaml +5 -0
  127. package/dist/templates/common/bundled-skills/atw-cross-model-review/references/four-tiers.md +42 -0
  128. package/dist/templates/common/bundled-skills/atw-cross-model-review/references/log-review-questions.md +38 -0
  129. package/dist/templates/common/bundled-skills/atw-cross-model-review/references/questions.md +54 -0
  130. package/dist/templates/common/bundled-skills/atw-cross-model-review/references/recheck-questions.md +42 -0
  131. package/dist/templates/common/bundled-skills/atw-cross-model-review/references/report-format.md +22 -0
  132. package/dist/templates/common/bundled-skills/atw-diagnosing-bugs/SKILL.md +13 -2
  133. package/dist/templates/common/bundled-skills/atw-domain-modeling/SKILL.md +2 -2
  134. package/dist/templates/common/bundled-skills/atw-handoff/SKILL.md +1 -1
  135. package/dist/templates/common/bundled-skills/atw-implement/SKILL.md +37 -1
  136. package/dist/templates/common/bundled-skills/atw-implement-all/SKILL.md +219 -24
  137. package/dist/templates/common/bundled-skills/atw-implement-spec/SKILL.md +237 -18
  138. package/dist/templates/common/bundled-skills/atw-improve-codebase-architecture/SKILL.md +2 -2
  139. package/dist/templates/common/bundled-skills/atw-init-repo/SKILL.md +1 -1
  140. package/dist/templates/common/bundled-skills/atw-init-repo/references/issue-tracker-atw-local.md +7 -1
  141. package/dist/templates/common/bundled-skills/atw-init-repo/references/issue-tracker-atw.md +7 -1
  142. package/dist/templates/common/bundled-skills/atw-map/SKILL.md +1 -1
  143. package/dist/templates/common/bundled-skills/atw-meta/SKILL.md +1 -1
  144. package/dist/templates/common/bundled-skills/atw-meta/references/customize-local/add-project-local-conventions.md +4 -3
  145. package/dist/templates/common/bundled-skills/atw-meta/references/customize-local/change-skills-or-commands.md +21 -16
  146. package/dist/templates/common/bundled-skills/atw-meta/references/customize-local/change-task-lifecycle.md +1 -1
  147. package/dist/templates/common/bundled-skills/atw-meta/references/customize-local/overview.md +1 -0
  148. package/dist/templates/common/bundled-skills/atw-meta/references/local-architecture/task-system.md +2 -1
  149. package/dist/templates/common/bundled-skills/atw-meta/references/platform-files/hooks-and-settings.md +1 -0
  150. package/dist/templates/common/bundled-skills/atw-noob-mode/SKILL.md +1 -1
  151. package/dist/templates/common/bundled-skills/atw-performance/SKILL.md +134 -0
  152. package/dist/templates/common/bundled-skills/atw-performance/agents/openai.yaml +3 -0
  153. package/dist/templates/common/bundled-skills/atw-performance/references/optimization-patterns.md +250 -0
  154. package/dist/templates/common/bundled-skills/atw-performance/references/performance-checklist.md +238 -0
  155. package/dist/templates/common/bundled-skills/atw-performance/references/performance-optimization.md +264 -0
  156. package/dist/templates/common/bundled-skills/atw-prototype/SKILL.md +3 -3
  157. package/dist/templates/common/bundled-skills/atw-prototype/{LOGIC.md → logic.md} +1 -1
  158. package/dist/templates/common/bundled-skills/atw-prototype/references/design-direction.md +91 -58
  159. package/dist/templates/common/bundled-skills/atw-prototype/references/direction-persuasion.md +25 -0
  160. package/dist/templates/common/bundled-skills/atw-prototype/references/style-explorer.md +1 -1
  161. package/dist/templates/common/bundled-skills/atw-prototype/ui.md +194 -0
  162. package/dist/templates/common/bundled-skills/atw-reflect/SKILL.md +77 -0
  163. package/dist/templates/common/bundled-skills/atw-reflect/agents/openai.yaml +5 -0
  164. package/dist/templates/common/bundled-skills/atw-reflect/references/divergent-reviewer.md +38 -0
  165. package/dist/templates/common/bundled-skills/atw-reflect/references/judgment-reviewer.md +37 -0
  166. package/dist/templates/common/bundled-skills/atw-reflect/references/synthesizer.md +63 -0
  167. package/dist/templates/common/bundled-skills/atw-reflect/references/tooling-reviewer.md +48 -0
  168. package/dist/templates/common/bundled-skills/atw-security/SKILL.md +248 -0
  169. package/dist/templates/common/bundled-skills/atw-security/agents/openai.yaml +3 -0
  170. package/dist/templates/common/bundled-skills/atw-security/references/hardening-patterns.md +324 -0
  171. package/dist/templates/common/bundled-skills/atw-security/references/security-checklist.md +248 -0
  172. package/dist/templates/common/bundled-skills/atw-session-insight/SKILL.md +1 -1
  173. package/dist/templates/common/bundled-skills/atw-session-insight/references/cli-quick-reference.md +11 -2
  174. package/dist/templates/common/bundled-skills/atw-spec/SKILL.md +1 -1
  175. package/dist/templates/common/bundled-skills/atw-tdd/SKILL.md +25 -0
  176. package/dist/templates/common/bundled-skills/atw-tickets/SKILL.md +1 -1
  177. package/dist/templates/common/bundled-skills/atw-triage/SKILL.md +5 -5
  178. package/dist/templates/common/bundled-skills/atw-ui/SKILL.md +99 -31
  179. package/dist/templates/common/bundled-skills/atw-ui/references/accessibility-checklist.md +160 -0
  180. package/dist/templates/common/bundled-skills/atw-ui/references/layout-and-viewport.md +7 -5
  181. package/dist/templates/common/bundled-skills/atw-ui/references/media.md +117 -20
  182. package/dist/templates/common/bundled-skills/atw-ui/references/motion.md +47 -23
  183. package/dist/templates/common/bundled-skills/atw-ui/references/play.md +18 -0
  184. package/dist/templates/common/bundled-skills/atw-ui/references/reviewer-manual.md +73 -0
  185. package/dist/templates/common/bundled-skills/atw-ui/references/scroll-narrative.md +98 -0
  186. package/dist/templates/common/bundled-skills/atw-ui/references/tools.md +46 -0
  187. package/dist/templates/common/bundled-skills/atw-ui/references/visual-language.md +40 -24
  188. package/dist/templates/common/bundled-skills/atw-ui/references/visual-review.md +26 -82
  189. package/dist/templates/common/bundled-skills/atw-ui/scripts/shoot.mjs +1721 -0
  190. package/dist/templates/common/bundled-skills/atw-verify-skill/SKILL.md +58 -0
  191. package/dist/templates/common/bundled-skills/atw-verify-skill/agents/openai.yaml +5 -0
  192. package/dist/templates/common/bundled-skills/atw-verify-skill/references/feature-map-example/README.md +47 -0
  193. package/dist/templates/common/bundled-skills/atw-verify-skill/references/feature-map-example/create-note.md +39 -0
  194. package/dist/templates/common/bundled-skills/atw-verify-skill/references/feature-map-example/search.md +45 -0
  195. package/dist/templates/common/bundled-skills/atw-verify-skill/references/generate.md +107 -0
  196. package/dist/templates/common/bundled-skills/atw-verify-skill/references/health-check.md +100 -0
  197. package/dist/templates/common/bundled-skills/atw-verify-skill/references/verify-skill-template.md +99 -0
  198. package/dist/templates/common/bundled-skills/atw-what-happened/SKILL.md +49 -0
  199. package/dist/templates/common/bundled-skills/atw-what-happened/agents/openai.yaml +5 -0
  200. package/dist/templates/common/commands/finish-work.md +10 -1
  201. package/dist/templates/common/commands/start.md +3 -1
  202. package/dist/templates/common/skills/update-spec.md +40 -4
  203. package/dist/templates/copilot/hooks/session-start.py +2 -2
  204. package/dist/templates/dsh/DSH.md +2 -2
  205. package/dist/templates/markdown/spec/backend/quality-guidelines.md.txt +10 -0
  206. package/dist/templates/markdown/spec/frontend/quality-guidelines.md.txt +10 -0
  207. package/dist/templates/omp/config.yml +15 -0
  208. package/dist/templates/omp/extensions/atw/index.ts.txt +135 -18
  209. package/dist/templates/omp/index.d.ts +1 -0
  210. package/dist/templates/omp/index.d.ts.map +1 -1
  211. package/dist/templates/omp/index.js +3 -0
  212. package/dist/templates/omp/index.js.map +1 -1
  213. package/dist/templates/opencode/lib/session-utils.js +4 -2
  214. package/dist/templates/opencode/plugins/inject-workflow-state.js +5 -1
  215. package/dist/templates/pi/extensions/atw/index.ts.txt +80 -7
  216. package/dist/templates/shared-hooks/index.d.ts +8 -1
  217. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  218. package/dist/templates/shared-hooks/index.js +8 -0
  219. package/dist/templates/shared-hooks/index.js.map +1 -1
  220. package/dist/templates/shared-hooks/inject-workflow-state.py +131 -14
  221. package/dist/templates/shared-hooks/session-start.py +4 -2
  222. package/dist/templates/shared-hooks/show-notice.py +135 -0
  223. package/dist/templates/snow/SNOW.md +1 -1
  224. package/dist/utils/initialized.d.ts +9 -0
  225. package/dist/utils/initialized.d.ts.map +1 -0
  226. package/dist/utils/initialized.js +24 -0
  227. package/dist/utils/initialized.js.map +1 -0
  228. package/dist/utils/managed-removal.d.ts +2 -0
  229. package/dist/utils/managed-removal.d.ts.map +1 -1
  230. package/dist/utils/managed-removal.js +23 -3
  231. package/dist/utils/managed-removal.js.map +1 -1
  232. package/dist/utils/optional-skills.d.ts +2 -0
  233. package/dist/utils/optional-skills.d.ts.map +1 -1
  234. package/dist/utils/optional-skills.js +1 -1
  235. package/dist/utils/optional-skills.js.map +1 -1
  236. package/dist/utils/template-hash.d.ts.map +1 -1
  237. package/dist/utils/template-hash.js +3 -0
  238. package/dist/utils/template-hash.js.map +1 -1
  239. package/dist/utils/uninstall-scrubbers.d.ts +10 -0
  240. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
  241. package/dist/utils/uninstall-scrubbers.js +50 -0
  242. package/dist/utils/uninstall-scrubbers.js.map +1 -1
  243. package/dist/utils/verify-entries.d.ts +51 -0
  244. package/dist/utils/verify-entries.d.ts.map +1 -0
  245. package/dist/utils/verify-entries.js +399 -0
  246. package/dist/utils/verify-entries.js.map +1 -0
  247. package/package.json +2 -2
  248. package/dist/templates/common/bundled-skills/atw-prototype/UI.md +0 -174
  249. package/dist/templates/common/bundled-skills/atw-prototype/references/visual-language.md +0 -88
  250. package/dist/templates/common/bundled-skills/atw-prototype/references/visual-review.md +0 -63
  251. package/dist/templates/common/bundled-skills/atw-ui/references/imagery.md +0 -69
  252. /package/dist/templates/common/bundled-skills/atw-codebase-design/{DEEPENING.md → deepening.md} +0 -0
  253. /package/dist/templates/common/bundled-skills/atw-domain-modeling/{ADR-FORMAT.md → adr-format.md} +0 -0
  254. /package/dist/templates/common/bundled-skills/atw-domain-modeling/{GLOSSARY-FORMAT.md → glossary-format.md} +0 -0
  255. /package/dist/templates/common/bundled-skills/atw-improve-codebase-architecture/{HTML-REPORT.md → html-report.md} +0 -0
  256. /package/dist/templates/common/bundled-skills/atw-triage/{AGENT-BRIEF.md → agent-brief.md} +0 -0
  257. /package/dist/templates/common/bundled-skills/atw-triage/{OUT-OF-SCOPE.md → out-of-scope.md} +0 -0
@@ -4,6 +4,7 @@ import {
4
4
  openSync,
5
5
  readFileSync,
6
6
  readSync,
7
+ readdirSync,
7
8
  realpathSync,
8
9
  renameSync,
9
10
  statSync,
@@ -1180,6 +1181,55 @@ function inheritSessionPointer(
1180
1181
  // ── Workflow State Breadcrumb ─────────────────────────────────────────
1181
1182
  const WF_RE =
1182
1183
  /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
1184
+ // Mirrors select_platform_blocks() in .atw/scripts/common/workflow_phase.py:
1185
+ // keep only the `[Platform, ...]` blocks naming this platform, or every block
1186
+ // when none does; marker lines are always dropped.
1187
+ const PLATFORM_MARKER_RE = /^\[(\/?)([A-Za-z][^[\]]*)\]\s*$/;
1188
+ function selectPlatformBlocks(body: string, platform: string): string {
1189
+ const norm = (s: string) => s.toLowerCase().replace(/[-_ ]/g, "");
1190
+ const names = (m: RegExpMatchArray) => (m[2] ?? "").split(",").map((n) => norm(n.trim()));
1191
+ const lines = body.split("\n");
1192
+ const anyMatch = lines.some((l) => {
1193
+ const m = l.match(PLATFORM_MARKER_RE);
1194
+ return !!m && m[1] !== "/" && names(m).includes(norm(platform));
1195
+ });
1196
+ const out: string[] = [];
1197
+ let keep = true;
1198
+ for (const line of lines) {
1199
+ const m = line.match(PLATFORM_MARKER_RE);
1200
+ if (m) {
1201
+ keep = m[1] === "/" || !anyMatch || names(m).includes(norm(platform));
1202
+ continue;
1203
+ }
1204
+ if (keep) out.push(line);
1205
+ }
1206
+ return out.join("\n");
1207
+ }
1208
+ // Mirrors DECISION_LOG_REMINDER / has_decision_log() in the shared per-turn
1209
+ // Python hook: same sentence, implement or accept only.
1210
+ const DECISION_LOG_REMINDER =
1211
+ "This task has a decision log: keep recording it at key points with `python3 ./.atw/scripts/decision_log.py`; in this conversation, the start row (`decision_log.py start`) goes in just before the first row you log — `add` and the other writing commands write it first when it is missing, so a conversation with nothing to log writes none; read the last rows they print, and when another conversation wrote in between they write only the start row and stop: read it, then run the same command again.";
1212
+ // Mirrors DECISION_LOG_ARCHIVE_REMINDER in the shared per-turn Python hook:
1213
+ // same sentence, accept only (with a log), placed before DECISION_LOG_REMINDER.
1214
+ const DECISION_LOG_ARCHIVE_REMINDER =
1215
+ "This task has a decision log: once accepted, after the acceptance row and before `task.py archive`, log one row for the archive (`decision_log.py add`); it goes into the archive commit with the task.";
1216
+ // Mirrors DECISION_LOG_REPORT_REMINDER in the shared per-turn Python hook:
1217
+ // same sentence, implement or accept (with a log), just before DECISION_LOG_REMINDER.
1218
+ const DECISION_LOG_REPORT_REMINDER =
1219
+ "This task has a decision log: when the user asks how things stand or for a report (汇报一下, 现在什么情况, what's the status, report), rebuild it from the task directory in four blocks — `python3 ./.atw/scripts/decision_log.py pending --report` prints the draft; post it in plain words, keep the plain-words lines it carries with noob mode on, and fill in what it marks. This is the task's report, not `atw-what-happened`: that skill only the user starts, and it tells what changed in this chat since their last message.";
1220
+ // Mirrors DECISION_LOG_NO_TASK_REPORT_REMINDER / any_task_has_decision_log() in
1221
+ // the shared per-turn Python hook: same sentence, no current task only.
1222
+ const DECISION_LOG_NO_TASK_REPORT_REMINDER =
1223
+ "No task is current, but some task under `.atw/tasks/` has a decision log: when the user asks how things stand or for a report (汇报一下, 现在什么情况, what's the status, report), rebuild it from that task's directory in four blocks — list the active tasks with `python3 ./.atw/scripts/task.py list` and see which of their directories hold a `decision-log.md` (if the user named the task or said enough to tell which one, take that one; if not, only one, take it; several, ask the user which), then `python3 ./.atw/scripts/decision_log.py pending --report --task-dir <dir>` prints the draft; post it in plain words, keep the plain-words lines it carries with noob mode on, and fill in what it marks. This is the task's report, not `atw-what-happened`: that skill only the user starts, and it tells what changed in this chat since their last message.";
1224
+ // 只看 .atw/tasks/ 下一层,归档里的不算
1225
+ function anyTaskHasDecisionLog(root: string): boolean {
1226
+ const tasks = join(root, ".atw", "tasks");
1227
+ try {
1228
+ return readdirSync(tasks).some((name) => exists(join(tasks, name, "decision-log.md")));
1229
+ } catch {
1230
+ return false;
1231
+ }
1232
+ }
1183
1233
  function workflowBreadcrumb(root: string, key: string | null): string {
1184
1234
  const wf = readText(join(root, ".atw", "workflow.md"));
1185
1235
  if (!wf) return "";
@@ -1191,7 +1241,9 @@ function workflowBreadcrumb(root: string, key: string | null): string {
1191
1241
  }
1192
1242
  const dir = readTaskDir(root, key);
1193
1243
  let header = "Status: no_task",
1194
- lookup = "no_task";
1244
+ lookup = "no_task",
1245
+ decisionLog = false,
1246
+ accept = false;
1195
1247
  if (dir) {
1196
1248
  try {
1197
1249
  const d = JSON.parse(readText(join(dir, "task.json"))) as JsonObject;
@@ -1200,11 +1252,23 @@ function workflowBreadcrumb(root: string, key: string | null): string {
1200
1252
  if (status) {
1201
1253
  header = `Task: ${id} (${status})`;
1202
1254
  lookup = status;
1255
+ decisionLog =
1256
+ (status === "implement" || status === "accept") &&
1257
+ exists(join(dir, "decision-log.md"));
1258
+ accept = status === "accept";
1203
1259
  }
1204
1260
  } catch {}
1205
1261
  }
1206
- const body = templates[lookup] ?? "Refer to workflow.md for current step.";
1207
- return `<workflow-state>\n${header}\n${body}\n</workflow-state>`;
1262
+ const body = selectPlatformBlocks(
1263
+ templates[lookup] ?? "Refer to workflow.md for current step.",
1264
+ "Pi",
1265
+ );
1266
+ const tail = decisionLog
1267
+ ? `${accept ? `\n${DECISION_LOG_ARCHIVE_REMINDER}` : ""}\n${DECISION_LOG_REPORT_REMINDER}\n${DECISION_LOG_REMINDER}`
1268
+ : lookup === "no_task" && anyTaskHasDecisionLog(root)
1269
+ ? `\n${DECISION_LOG_NO_TASK_REPORT_REMINDER}`
1270
+ : "";
1271
+ return `<workflow-state>\n${header}\n${body}${tail}\n</workflow-state>`;
1208
1272
  }
1209
1273
 
1210
1274
  // ── Session Overview ───────────────────────────────────────────────────
@@ -1250,16 +1314,16 @@ function buildStartupContext(
1250
1314
  ): string {
1251
1315
  const workflow = workflowOverview(root, key);
1252
1316
  // get_context.py puts a <conventions> block in the overview only for a
1253
- // `general` project. Its workflow has its own way to pick up unfinished
1254
- // work; the bare `task.py start` below is one the script refuses there.
1317
+ // `general` project: there <ready> defers to <atw-workflow>, which has its
1318
+ // own way to pick up unfinished work. In dev mode <ready> points to the
1319
+ // per-turn no-task breadcrumb for binding an already-existing task.
1255
1320
  const ready = overview.includes("\n<conventions>\n")
1256
1321
  ? [
1257
1322
  "Do what <atw-workflow> above says for the current task's status. With no current task, it also says when to create one and how to pick up unfinished work.",
1258
1323
  ]
1259
1324
  : [
1260
1325
  "Use the current workflow state to decide whether to create, continue, or skip an ATW task.",
1261
- "When CURRENT TASK is (none) but ACTIVE TASKS lists exactly one task assigned to you that is already at the implement or accept stage, this window is merely not bound to it yet: run `python3 ./.atw/scripts/task.py start <task-dir>` for that task, then continue from its stage. Do not ask whether to create a new task in that case.",
1262
- "If several tasks qualify, or the only candidate is still in a planning stage (discover / specify / slice), ask which task to bind instead of running start: start moves a planning-stage task straight into implement.",
1326
+ "With no current task, if the request is about a task that already exists — the user names it, or asks to continue or accept and only one open task fits — make it this session's task first, as the no-task breadcrumb says; do not ask whether to create a new one.",
1263
1327
  ];
1264
1328
  return [
1265
1329
  "<session-context>\nATW compact SessionStart context. Use it to orient the session; load details on demand.\n</session-context>",
@@ -2032,6 +2096,10 @@ export default function atwExtension(pi: {
2032
2096
  return undefined;
2033
2097
  });
2034
2098
  pi.on?.("before_agent_start", (event, ctx) => {
2099
+ // Same switch as the other platforms' hooks: channel sets ATW_HOOKS=0 on
2100
+ // its helpers, which must not receive prompts written for the main AI.
2101
+ if (process.env.ATW_HOOKS === "0" || process.env.ATW_DISABLE_HOOKS === "1")
2102
+ return undefined;
2035
2103
  const k = getKey(event, ctx);
2036
2104
  const key = cacheKey(k, ctx);
2037
2105
  const cur = (event as { systemPrompt?: string }).systemPrompt ?? "";
@@ -2089,4 +2157,9 @@ export default function atwExtension(pi: {
2089
2157
  pi.on?.("context", (event, ctx) => {
2090
2158
  getKey(event, ctx);
2091
2159
  });
2160
+ // Compaction can fold the persisted runtime-context message into its
2161
+ // summary. Forget what was sent so the next turn re-sends it unchanged.
2162
+ pi.on?.("session_compact", () => {
2163
+ lastSentRuntimeCtx.clear();
2164
+ });
2092
2165
  }
@@ -11,7 +11,7 @@ export interface HookScript {
11
11
  /** Script content — no placeholders, ready to write directly */
12
12
  content: string;
13
13
  }
14
- export type SharedHookName = "session-start.py" | "inject-shell-session-context.py" | "inject-workflow-state.py" | "inject-subagent-context.py";
14
+ export type SharedHookName = "session-start.py" | "inject-shell-session-context.py" | "inject-workflow-state.py" | "inject-subagent-context.py" | "show-notice.py";
15
15
  export type SharedHookPlatform = "claude" | "cursor" | "codex" | "gemini" | "qoder" | "copilot" | "codebuddy" | "droid" | "kiro" | "trae" | "zcode";
16
16
  /**
17
17
  * Which shared hooks each platform actually invokes. Single source of truth
@@ -51,6 +51,13 @@ export type SharedHookPlatform = "claude" | "cursor" | "codex" | "gemini" | "qod
51
51
  * (per-turn breadcrumb), and `inject-subagent-context.py` (sub-agent
52
52
  * spawn). The scripts emit a plain-text Kiro branch — Kiro adds a hook's
53
53
  * stdout directly to the conversation context (no JSON envelope).
54
+ * - `show-notice.py` — Claude Code only (PostToolUse, matcher `Bash`): after
55
+ * `cross_model_review.py afk-notice|prepare|prepare-log` it hands the
56
+ * `notice` the script wrote to `.atw/.runtime/hook-notice.json` to Claude
57
+ * Code as `systemMessage`, which Claude Code shows the user without the
58
+ * model in between (task decision-log-afk ticket 26: Claude Code 2.1.302
59
+ * rewrote the paragraph instead of posting it). Other platforms keep the
60
+ * skills' "post it word for word".
54
61
  * - Claude Code `statusLine` is intentionally not installed by default.
55
62
  * Users can add their own statusLine command in `.claude/settings.json`,
56
63
  * or opt in to the ATW one via `atw init --with-statusline`
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,iCAAiC,GACjC,0BAA0B,GAC1B,4BAA4B,CAAC;AAEjC,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,OAAO,GACP,SAAS,GACT,WAAW,GACX,OAAO,GACP,MAAM,GACN,MAAM,GACN,OAAO,CAAC;AAEZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAC3C,kBAAkB,EAClB,SAAS,cAAc,EAAE,CAoD1B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,kBAAkB,GAC3B,UAAU,EAAE,CAGd"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,iCAAiC,GACjC,0BAA0B,GAC1B,4BAA4B,GAC5B,gBAAgB,CAAC;AAErB,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,OAAO,GACP,SAAS,GACT,WAAW,GACX,OAAO,GACP,MAAM,GACN,MAAM,GACN,OAAO,CAAC;AAEZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAC3C,kBAAkB,EAClB,SAAS,cAAc,EAAE,CAqD1B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,kBAAkB,GAC3B,UAAU,EAAE,CAGd"}
@@ -51,6 +51,13 @@ function readTemplate(relativePath) {
51
51
  * (per-turn breadcrumb), and `inject-subagent-context.py` (sub-agent
52
52
  * spawn). The scripts emit a plain-text Kiro branch — Kiro adds a hook's
53
53
  * stdout directly to the conversation context (no JSON envelope).
54
+ * - `show-notice.py` — Claude Code only (PostToolUse, matcher `Bash`): after
55
+ * `cross_model_review.py afk-notice|prepare|prepare-log` it hands the
56
+ * `notice` the script wrote to `.atw/.runtime/hook-notice.json` to Claude
57
+ * Code as `systemMessage`, which Claude Code shows the user without the
58
+ * model in between (task decision-log-afk ticket 26: Claude Code 2.1.302
59
+ * rewrote the paragraph instead of posting it). Other platforms keep the
60
+ * skills' "post it word for word".
54
61
  * - Claude Code `statusLine` is intentionally not installed by default.
55
62
  * Users can add their own statusLine command in `.claude/settings.json`,
56
63
  * or opt in to the ATW one via `atw init --with-statusline`
@@ -67,6 +74,7 @@ export const SHARED_HOOKS_BY_PLATFORM = {
67
74
  "session-start.py",
68
75
  "inject-workflow-state.py",
69
76
  "inject-subagent-context.py",
77
+ "show-notice.py",
70
78
  ],
71
79
  cursor: [
72
80
  "session-start.py",
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AA4BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAGjC;IACF,MAAM,EAAE;QACN,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,4BAA4B;KAC7B;IACD,KAAK,EAAE,CAAC,0BAA0B,EAAE,4BAA4B,CAAC;IACjE,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,OAAO,EAAE,CAAC,0BAA0B,CAAC;IACrC,SAAS,EAAE;QACT,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,IAAI,EAAE;QACJ,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,IAAI,EAAE;QACJ,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;SACjC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SAChC,IAAI,EAAE,CAAC;IAEV,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAA4B;IAE5B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,wBAAwB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,OAAO,oBAAoB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACnE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AA6BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAGjC;IACF,MAAM,EAAE;QACN,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;QAC5B,gBAAgB;KACjB;IACD,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,4BAA4B;KAC7B;IACD,KAAK,EAAE,CAAC,0BAA0B,EAAE,4BAA4B,CAAC;IACjE,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,OAAO,EAAE,CAAC,0BAA0B,CAAC;IACrC,SAAS,EAAE;QACT,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,IAAI,EAAE;QACJ,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,IAAI,EAAE;QACJ,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;SACjC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SAChC,IAAI,EAAE,CAAC;IAEV,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAA4B;IAE5B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,wBAAwB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,OAAO,oBAAoB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACnE,CAAC"}
@@ -160,13 +160,13 @@ def _resolve_active_task(root: Path, input_data: dict):
160
160
 
161
161
  def get_active_task(
162
162
  root: Path, input_data: dict
163
- ) -> tuple[str, str, str] | None:
163
+ ) -> tuple[str, str, str, Path] | None:
164
164
  """Return active task data, a task-record error, or no task pointer.
165
165
 
166
- ``(task_id, "task_error", source)`` is distinct from ``None``: a session
167
- pointer can exist even when its task record is missing or unreadable, and
168
- that state needs a diagnostic breadcrumb rather than the normal ``no_task``
169
- prompt.
166
+ ``(task_id, "task_error", source, task_dir)`` is distinct from ``None``: a
167
+ session pointer can exist even when its task record is missing or
168
+ unreadable, and that state needs a diagnostic breadcrumb rather than the
169
+ normal ``no_task`` prompt.
170
170
  """
171
171
  active = _resolve_active_task(root, input_data)
172
172
  if not active.task_path:
@@ -176,23 +176,23 @@ def get_active_task(
176
176
  if not task_dir.is_absolute():
177
177
  task_dir = root / task_dir
178
178
  if active.stale:
179
- return task_dir.name, f"stale_{active.source_type}", active.source
179
+ return task_dir.name, f"stale_{active.source_type}", active.source, task_dir
180
180
 
181
181
  task_json = task_dir / "task.json"
182
182
  if not task_json.is_file():
183
- return task_dir.name, "task_error", active.source
183
+ return task_dir.name, "task_error", active.source, task_dir
184
184
  try:
185
185
  data = json.loads(task_json.read_text(encoding="utf-8"))
186
186
  except (json.JSONDecodeError, OSError):
187
- return task_dir.name, "task_error", active.source
187
+ return task_dir.name, "task_error", active.source, task_dir
188
188
  if not isinstance(data, dict):
189
- return task_dir.name, "task_error", active.source
189
+ return task_dir.name, "task_error", active.source, task_dir
190
190
 
191
191
  task_id = data.get("id") or task_dir.name
192
192
  status = data.get("status", "")
193
193
  if not isinstance(status, str) or not status:
194
- return task_dir.name, "task_error", active.source
195
- return task_id, status, active.source
194
+ return task_dir.name, "task_error", active.source, task_dir
195
+ return task_id, status, active.source, task_dir
196
196
 
197
197
 
198
198
  # ---------------------------------------------------------------------------
@@ -359,12 +359,80 @@ def resolve_breadcrumb_key(
359
359
  return status
360
360
 
361
361
 
362
+ # 决定日志提醒:pi、omp 扩展里的 DECISION_LOG_REMINDER 与这里逐字相同。
363
+ DECISION_LOG_REMINDER = (
364
+ "This task has a decision log: keep recording it at key points with "
365
+ "`python3 ./.atw/scripts/decision_log.py`; in this conversation, the "
366
+ "start row (`decision_log.py start`) goes in just before the first row you "
367
+ "log — `add` and the other writing commands write it first when it is "
368
+ "missing, so a conversation with nothing to log writes none; read the last "
369
+ "rows they print, and when another conversation wrote in between they "
370
+ "write only the start row and stop: read it, then run the same command "
371
+ "again."
372
+ )
373
+ # accept 阶段另加归档那一行的提醒,接在「Once accepted, run task.py archive」之后;pi、omp 扩展同名常量逐字相同
374
+ DECISION_LOG_ARCHIVE_REMINDER = (
375
+ "This task has a decision log: once accepted, after the acceptance row "
376
+ "and before `task.py archive`, log one row for the archive "
377
+ "(`decision_log.py add`); it goes into the archive commit with the task."
378
+ )
379
+ # 用户问进度、要汇报时怎么拼(prd L 节):implement、accept 有日志时都出,紧挨在 DECISION_LOG_REMINDER 之前;pi、omp 扩展同名常量逐字相同
380
+ DECISION_LOG_REPORT_REMINDER = (
381
+ "This task has a decision log: when the user asks how things stand or for "
382
+ "a report (汇报一下, 现在什么情况, what's the status, report), rebuild it "
383
+ "from the task directory in four blocks — "
384
+ "`python3 ./.atw/scripts/decision_log.py pending --report` prints the "
385
+ "draft; post it in plain words, keep the plain-words lines it carries "
386
+ "with noob mode on, and fill in what it marks. This is the task's report, "
387
+ "not `atw-what-happened`: that skill only the user starts, and it tells "
388
+ "what changed in this chat since their last message."
389
+ )
390
+ # 没绑任务时问进度、要汇报(工单 27):.atw/tasks/ 下有任务带日志时出;pi、omp 扩展同名常量逐字相同
391
+ DECISION_LOG_NO_TASK_REPORT_REMINDER = (
392
+ "No task is current, but some task under `.atw/tasks/` has a decision "
393
+ "log: when the user asks how things stand or for a report (汇报一下, "
394
+ "现在什么情况, what's the status, report), rebuild it from that task's "
395
+ "directory in four blocks — list the active tasks with "
396
+ "`python3 ./.atw/scripts/task.py list` and see which of their directories "
397
+ "hold a `decision-log.md` (if the user named the task or said enough to "
398
+ "tell which one, take that one; if not, only one, take it; several, ask "
399
+ "the user which), then `python3 ./.atw/scripts/decision_log.py pending "
400
+ "--report --task-dir <dir>` prints the draft; post it in plain words, "
401
+ "keep the plain-words lines it carries with noob mode on, and fill in what "
402
+ "it marks. This is the task's report, not `atw-what-happened`: that skill "
403
+ "only the user starts, and it tells what changed in this chat since their "
404
+ "last message."
405
+ )
406
+ DECISION_LOG_STATUSES = ("implement", "accept")
407
+
408
+
409
+ def has_decision_log(status: str, task_dir: Path) -> bool:
410
+ """Whether the breadcrumb carries DECISION_LOG_REMINDER: an implement or
411
+ accept task whose directory holds ``decision-log.md``. Planning stages
412
+ never do — their decisions are the user's and live in prd.md and tickets.
413
+ """
414
+ return status in DECISION_LOG_STATUSES and (task_dir / "decision-log.md").is_file()
415
+
416
+
417
+ def any_task_has_decision_log(root: Path) -> bool:
418
+ """Whether a task directly under ``.atw/tasks/`` holds a ``decision-log.md``
419
+ file. One level only, so archived tasks never count; cheap enough to run
420
+ every turn.
421
+ """
422
+ try:
423
+ return any((d / "decision-log.md").is_file() for d in (root / ".atw" / "tasks").iterdir())
424
+ except OSError:
425
+ return False
426
+
427
+
362
428
  def build_breadcrumb(
363
429
  task_id: Optional[str],
364
430
  status: str,
365
431
  templates: dict[str, str],
366
432
  source: str | None = None,
367
433
  breadcrumb_key: str | None = None,
434
+ task_has_log: bool = False,
435
+ repo_has_task_log: bool = False,
368
436
  ) -> str:
369
437
  """Build the <workflow-state>...</workflow-state> block.
370
438
 
@@ -372,6 +440,14 @@ def build_breadcrumb(
372
440
  - Unknown status (no tag, or workflow.md missing) → generic
373
441
  "Refer to workflow.md for current step." line
374
442
  - `no_task` pseudo-status (task_id is None) → header omits task info
443
+ - ``task_has_log`` (the current task carries its decision log, see
444
+ ``has_decision_log``) →
445
+ DECISION_LOG_REMINDER as the block's last line,
446
+ DECISION_LOG_REPORT_REMINDER just before it, and in accept
447
+ DECISION_LOG_ARCHIVE_REMINDER before that
448
+ - ``repo_has_task_log`` (no current task, but some task under
449
+ ``.atw/tasks/`` has a decision log) →
450
+ DECISION_LOG_NO_TASK_REPORT_REMINDER as the block's last line
375
451
  """
376
452
  lookup_key = breadcrumb_key or status
377
453
  body = templates.get(lookup_key)
@@ -379,10 +455,38 @@ def build_breadcrumb(
379
455
  body = templates.get(status)
380
456
  if body is None:
381
457
  body = "Refer to workflow.md for current step."
458
+ if repo_has_task_log:
459
+ body = f"{body}\n{DECISION_LOG_NO_TASK_REPORT_REMINDER}"
460
+ if task_has_log:
461
+ if status == "accept":
462
+ body = f"{body}\n{DECISION_LOG_ARCHIVE_REMINDER}"
463
+ body = f"{body}\n{DECISION_LOG_REPORT_REMINDER}\n{DECISION_LOG_REMINDER}"
382
464
  header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})"
383
465
  return f"<workflow-state>\n{header}\n{body}\n</workflow-state>"
384
466
 
385
467
 
468
+ def _select_platform_blocks(root: Path, body: str, platform: str | None, config: dict) -> str:
469
+ """Keep only this platform's `[Platform]` block in a breadcrumb body.
470
+
471
+ The accept breadcrumb gives the cross-model review input once per tool;
472
+ a host handed all of them has picked another tool's form. A platform
473
+ with no block of its own gets every line (each names its tool). An
474
+ install whose scripts predate the helper gets the body unchanged.
475
+ """
476
+ scripts_dir = root / ".atw" / "scripts"
477
+ if str(scripts_dir) not in sys.path:
478
+ sys.path.insert(0, str(scripts_dir))
479
+ try:
480
+ from common.workflow_phase import ( # type: ignore[import-not-found]
481
+ resolve_effective_platform,
482
+ select_platform_blocks,
483
+ )
484
+ except Exception:
485
+ return body
486
+ effective = resolve_effective_platform(platform, config) if platform else None
487
+ return select_platform_blocks(body, effective)
488
+
489
+
386
490
  def _is_general_project(root: Path) -> bool:
387
491
  """Whether this is a `general` project, as `common.config` reads the mode.
388
492
 
@@ -473,20 +577,33 @@ def main() -> int:
473
577
 
474
578
  templates = load_breadcrumbs(root)
475
579
  platform = _detect_platform(data)
580
+ templates = {
581
+ key: _select_platform_blocks(root, body, platform, config)
582
+ for key, body in templates.items()
583
+ }
476
584
  task = get_active_task(root, data)
477
585
  if task is None:
478
586
  # No active task — still emit a breadcrumb nudging AI toward
479
587
  # atw-brainstorm + task.py create when user describes real work.
480
588
  no_task_key = resolve_breadcrumb_key("no_task", platform, config)
481
589
  breadcrumb = build_breadcrumb(
482
- None, "no_task", templates, breadcrumb_key=no_task_key
590
+ None,
591
+ "no_task",
592
+ templates,
593
+ breadcrumb_key=no_task_key,
594
+ repo_has_task_log=any_task_has_decision_log(root),
483
595
  )
484
596
  else:
485
- task_id, status, source = task
597
+ task_id, status, source, task_dir = task
486
598
  status_key = resolve_breadcrumb_key(status, platform, config)
487
599
  source_for_breadcrumb = None if platform == "codex" else source
488
600
  breadcrumb = build_breadcrumb(
489
- task_id, status, templates, source_for_breadcrumb, breadcrumb_key=status_key
601
+ task_id,
602
+ status,
603
+ templates,
604
+ source_for_breadcrumb,
605
+ breadcrumb_key=status_key,
606
+ task_has_log=has_decision_log(status, task_dir),
490
607
  )
491
608
  if platform == "codex":
492
609
  parts: list[str] = []
@@ -454,8 +454,10 @@ def _get_task_status(atw_dir: Path, input_data: dict, general: bool = False) ->
454
454
  )
455
455
  return (
456
456
  "Status: NO ACTIVE TASK\n"
457
- "Next-Action: Classify the current turn before creating any ATW task. "
458
- "Simple conversation / small task asks only whether this turn should create an ATW task. "
457
+ "Next-Action: If the request is about a task that already exists — the user names it, or asks to continue or accept and only one open task fits — make it this session's task first, as the no-task breadcrumb says; do not ask whether to create a new one. "
458
+ "Otherwise classify the current turn before creating any ATW task. "
459
+ "Simple conversation / small task asks whether this turn should create an ATW task. "
460
+ "In that ask, say what each answer means: without a task there is no requirements document (`prd.md`) and no formal acceptance (each requirement checked with evidence, the user signs off); a task adds writing the requirements, the user confirming the spec, implementing against it, and that acceptance before archiving. "
459
461
  "Complex task asks whether task creation and planning are allowed."
460
462
  )
461
463
 
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ """ATW start-notice hook (Claude Code PostToolUse, matcher ``Bash``).
3
+
4
+ Claude Code 2.1.302 as the host rewrote the paragraph that
5
+ ``cross_model_review.py afk-notice`` / ``prepare`` / ``prepare-log`` print
6
+ as ``notice`` instead of posting it word for word, however the skills put it
7
+ (task decision-log-afk tickets 20, 23, 28, 26). So the script also writes the
8
+ paragraph to ``.atw/.runtime/hook-notice.json``, and this hook, right after
9
+ one of those commands ends, hands it to Claude Code as ``systemMessage``:
10
+ Claude Code shows that to the user itself, the model never sees it. An
11
+ ``additionalContext`` line tells the model the paragraph has been shown, so it
12
+ does not post it again.
13
+
14
+ Verified on Claude Code 2.1.302 (``claude -p --output-format stream-json
15
+ --verbose``): the message arrives as ``{"type":"system","subtype":
16
+ "informational","content":"PostToolUse:Bash says: <text>"}`` (every line of
17
+ the text gets that prefix), the session record keeps it as an
18
+ ``attachment`` of type ``hook_system_message`` with the text unchanged, and the
19
+ model, asked to quote any hook message it saw, saw none.
20
+
21
+ The file is shown once and then removed. Nothing is printed when the command
22
+ is not one of the three, when the file was written by another of them, when
23
+ it was written in another conversation (its ``session`` is the ATW context key
24
+ ``resolve_context_key`` gave the script; this hook computes the same key from
25
+ its own input — when either side has none, it is shown as before), when there
26
+ is no file, outside an ATW project, or with ATW hooks turned off.
27
+
28
+ Claude Code caps ``systemMessage`` at 10,000 characters and replaces a longer
29
+ one with a file path and a 2,000-character preview. A paragraph over
30
+ ``SHOW_LIMIT`` is therefore not shown at all: ``additionalContext`` tells the
31
+ model to post it itself, word for word.
32
+
33
+ The command names in ``COMMAND_RE`` are the script's ``NOTICE_COMMANDS``:
34
+ change one, change the other.
35
+ """
36
+ from __future__ import annotations
37
+
38
+ import json
39
+ import os
40
+ import re
41
+ import sys
42
+ from pathlib import Path
43
+ from typing import Optional
44
+
45
+ NOTICE_FILE = Path(".atw") / ".runtime" / "hook-notice.json"
46
+ # Claude Code 的上限是 10,000 字,留出余量
47
+ SHOW_LIMIT = 8000
48
+ COMMAND_RE = re.compile(r"cross_model_review\.py['\"]?\s+(afk-notice|prepare-log|prepare)(?![\w-])")
49
+ SHOWN = (
50
+ "ATW: Claude Code has just shown the user the `notice` that `cross_model_review.py {command}` printed "
51
+ "({which}), word for word, as a message of its own. Do not post it again and do not reword it; "
52
+ "go on with the next step."
53
+ )
54
+ TOO_LONG = (
55
+ "ATW: the `notice` that `cross_model_review.py {command}` printed ({which}) is too long for Claude Code "
56
+ "to show whole ({length} characters, over {limit}), so it was not shown. Post it yourself, word for word, "
57
+ "as a message of its own, as the skill says."
58
+ )
59
+
60
+
61
+ def find_atw_root(start: Path) -> Optional[Path]:
62
+ cur = start.resolve()
63
+ for d in (cur, *cur.parents):
64
+ if (d / ".atw").is_dir():
65
+ return d
66
+ return None
67
+
68
+
69
+ def _context_key(root: Path, data: dict) -> Optional[str]:
70
+ """ATW's own key for this conversation, the same way the script got it."""
71
+ scripts_dir = root / ".atw" / "scripts"
72
+ if str(scripts_dir) not in sys.path:
73
+ sys.path.insert(0, str(scripts_dir))
74
+ try:
75
+ from common.active_task import resolve_context_key # type: ignore[import-not-found]
76
+ except ImportError:
77
+ return None
78
+ return resolve_context_key(data, platform="claude")
79
+
80
+
81
+ def main() -> int:
82
+ if os.environ.get("ATW_HOOKS") == "0" or os.environ.get("ATW_DISABLE_HOOKS") == "1":
83
+ return 0
84
+ try:
85
+ # 按字节读、输出全用 ASCII 转义:Windows 上的默认编码碰不到中文
86
+ data = json.loads(sys.stdin.buffer.read().decode("utf-8", errors="replace"))
87
+ except (ValueError, OSError):
88
+ return 0
89
+ if not isinstance(data, dict) or data.get("tool_name") != "Bash":
90
+ return 0
91
+ tool_input = data.get("tool_input")
92
+ command = tool_input.get("command") if isinstance(tool_input, dict) else None
93
+ match = COMMAND_RE.search(command) if isinstance(command, str) else None
94
+ if match is None:
95
+ return 0
96
+ cwd = data.get("cwd")
97
+ root = find_atw_root(Path(cwd) if isinstance(cwd, str) and cwd else Path.cwd())
98
+ if root is None:
99
+ return 0
100
+ path = root / NOTICE_FILE
101
+ try:
102
+ saved = json.loads(path.read_text(encoding="utf-8"))
103
+ except (OSError, ValueError):
104
+ return 0
105
+ if not isinstance(saved, dict) or saved.get("command") != match.group(1):
106
+ return 0
107
+ text = saved.get("text")
108
+ if not isinstance(text, str) or not text:
109
+ return 0
110
+ written_in = saved.get("session")
111
+ here = _context_key(root, data) if written_in else None
112
+ if written_in and here and written_in != here:
113
+ # 别的对话写的:留给它,不在这里显示
114
+ return 0
115
+ path.unlink(missing_ok=True)
116
+ command_name = match.group(1)
117
+ which = {"zh": "the Chinese paragraph", "en": "the English paragraph"}.get(
118
+ saved.get("lang"), "both the Chinese and the English paragraph"
119
+ )
120
+ if len(text) > SHOW_LIMIT:
121
+ out: dict = {"hookSpecificOutput": {
122
+ "hookEventName": "PostToolUse",
123
+ "additionalContext": TOO_LONG.format(command=command_name, which=which, length=len(text), limit=SHOW_LIMIT),
124
+ }}
125
+ else:
126
+ out = {"systemMessage": text, "hookSpecificOutput": {
127
+ "hookEventName": "PostToolUse",
128
+ "additionalContext": SHOWN.format(command=command_name, which=which),
129
+ }}
130
+ print(json.dumps(out))
131
+ return 0
132
+
133
+
134
+ if __name__ == "__main__":
135
+ sys.exit(main())
@@ -50,7 +50,7 @@ the AI do the rest. There are no prompt commands to run and no project agents.
50
50
 
51
51
  | Capability | Status |
52
52
  | ------------------------------------------ | ---------------------------------------------------------------------------- |
53
- | Skills (`.snow/skills/atw-*/SKILL.md`) | Works — the six skills of `general` mode |
53
+ | Skills (`.snow/skills/atw-*/SKILL.md`) | Works — the seven skills of `general` mode |
54
54
  | Context hooks (`.snow/hooks/`) | Inject model context via stdout JSON + write `.snow/log/atw-context.txt` |
55
55
  | Prompt commands, project agents | None in `general` mode |
56
56
 
@@ -0,0 +1,9 @@
1
+ /** `.atw/` 里归用户、卸载后仍留着的条目。只剩它们时项目不算装了 ATW。 */
2
+ export declare const ATW_ENTRIES_KEPT_BY_UNINSTALL: readonly string[];
3
+ /**
4
+ * 项目里装没装 ATW:`.atw/` 存在,且不是只剩卸载后留下的用户条目。
5
+ * 不看 `.atw/.version`——早期版本装的项目可能没有它;空的 `.atw/` 照旧算装过
6
+ * (由各命令自己报清单缺失)。
7
+ */
8
+ export declare function isAtwInitialized(cwd: string): boolean;
9
+ //# sourceMappingURL=initialized.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"initialized.d.ts","sourceRoot":"","sources":["../../src/utils/initialized.ts"],"names":[],"mappings":"AAKA,8CAA8C;AAC9C,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAE1D,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWrD"}
@@ -0,0 +1,24 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { DIR_NAMES } from "../constants/paths.js";
4
+ /** `.atw/` 里归用户、卸载后仍留着的条目。只剩它们时项目不算装了 ATW。 */
5
+ export const ATW_ENTRIES_KEPT_BY_UNINSTALL = [
6
+ DIR_NAMES.SKILL_NOTES,
7
+ ];
8
+ /**
9
+ * 项目里装没装 ATW:`.atw/` 存在,且不是只剩卸载后留下的用户条目。
10
+ * 不看 `.atw/.version`——早期版本装的项目可能没有它;空的 `.atw/` 照旧算装过
11
+ * (由各命令自己报清单缺失)。
12
+ */
13
+ export function isAtwInitialized(cwd) {
14
+ let entries;
15
+ try {
16
+ entries = fs.readdirSync(path.join(cwd, DIR_NAMES.WORKFLOW));
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ return (entries.length === 0 ||
22
+ entries.some((entry) => !ATW_ENTRIES_KEPT_BY_UNINSTALL.includes(entry)));
23
+ }
24
+ //# sourceMappingURL=initialized.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"initialized.js","sourceRoot":"","sources":["../../src/utils/initialized.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD,8CAA8C;AAC9C,MAAM,CAAC,MAAM,6BAA6B,GAAsB;IAC9D,SAAS,CAAC,WAAW;CACtB,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,CACL,OAAO,CAAC,MAAM,KAAK,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,6BAA6B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CACxE,CAAC;AACJ,CAAC"}
@@ -30,6 +30,8 @@ export interface ManagedRemovalPlan {
30
30
  deletions: PlannedDeletion[];
31
31
  modifications: PlannedModification[];
32
32
  removeAtwDir: boolean;
33
+ /** `.atw/` 里归用户、卸载留着的条目(磁盘上有的);非空时 `.atw/` 只删其余条目。 */
34
+ keptAtwEntries: string[];
33
35
  allPosixPaths: string[];
34
36
  }
35
37
  export interface BuildManagedRemovalPlanOptions {