@chinhae/atw-cli 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +188 -15
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +25 -1
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/init.d.ts +3 -0
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +154 -17
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/mode.d.ts +21 -0
  10. package/dist/commands/mode.d.ts.map +1 -0
  11. package/dist/commands/mode.js +401 -0
  12. package/dist/commands/mode.js.map +1 -0
  13. package/dist/commands/update.d.ts +7 -0
  14. package/dist/commands/update.d.ts.map +1 -1
  15. package/dist/commands/update.js +20 -13
  16. package/dist/commands/update.js.map +1 -1
  17. package/dist/commands/workflow.d.ts +17 -0
  18. package/dist/commands/workflow.d.ts.map +1 -1
  19. package/dist/commands/workflow.js +9 -3
  20. package/dist/commands/workflow.js.map +1 -1
  21. package/dist/configurators/claude.d.ts +3 -2
  22. package/dist/configurators/claude.d.ts.map +1 -1
  23. package/dist/configurators/claude.js +3 -3
  24. package/dist/configurators/claude.js.map +1 -1
  25. package/dist/configurators/codex.d.ts +4 -3
  26. package/dist/configurators/codex.d.ts.map +1 -1
  27. package/dist/configurators/codex.js +10 -10
  28. package/dist/configurators/codex.js.map +1 -1
  29. package/dist/configurators/index.d.ts +9 -3
  30. package/dist/configurators/index.d.ts.map +1 -1
  31. package/dist/configurators/index.js +95 -17
  32. package/dist/configurators/index.js.map +1 -1
  33. package/dist/configurators/shared.d.ts +13 -0
  34. package/dist/configurators/shared.d.ts.map +1 -1
  35. package/dist/configurators/shared.js +25 -0
  36. package/dist/configurators/shared.js.map +1 -1
  37. package/dist/configurators/workflow.d.ts +15 -1
  38. package/dist/configurators/workflow.d.ts.map +1 -1
  39. package/dist/configurators/workflow.js +23 -5
  40. package/dist/configurators/workflow.js.map +1 -1
  41. package/dist/configurators/zcode.d.ts +1 -1
  42. package/dist/configurators/zcode.d.ts.map +1 -1
  43. package/dist/configurators/zcode.js +2 -2
  44. package/dist/configurators/zcode.js.map +1 -1
  45. package/dist/constants/paths.d.ts +4 -0
  46. package/dist/constants/paths.d.ts.map +1 -1
  47. package/dist/constants/paths.js +4 -0
  48. package/dist/constants/paths.js.map +1 -1
  49. package/dist/migrations/manifests/0.4.0.json +9 -0
  50. package/dist/templates/atw/index.d.ts +10 -1
  51. package/dist/templates/atw/index.d.ts.map +1 -1
  52. package/dist/templates/atw/index.js +11 -1
  53. package/dist/templates/atw/index.js.map +1 -1
  54. package/dist/templates/atw/scripts/add_session.py +34 -13
  55. package/dist/templates/atw/scripts/common/active_task.py +2 -2
  56. package/dist/templates/atw/scripts/common/config.py +13 -0
  57. package/dist/templates/atw/scripts/common/developer.py +15 -4
  58. package/dist/templates/atw/scripts/common/noob_mode.py +35 -15
  59. package/dist/templates/atw/scripts/common/packages_context.py +16 -3
  60. package/dist/templates/atw/scripts/common/session_context.py +77 -19
  61. package/dist/templates/atw/scripts/common/task_store.py +78 -27
  62. package/dist/templates/atw/scripts/noob_mode.py +16 -4
  63. package/dist/templates/atw/scripts/task.py +99 -38
  64. package/dist/templates/atw/workflow-general.md +554 -0
  65. package/dist/templates/atw/workflow.md +1 -1
  66. package/dist/templates/codex/hooks/session-start.py +69 -18
  67. package/dist/templates/common/bundled-skills/atw-noob-mode/SKILL.md +6 -4
  68. package/dist/templates/common/commands/start.md +43 -0
  69. package/dist/templates/copilot/hooks/session-start.py +69 -18
  70. package/dist/templates/dsh/DSH.md +43 -0
  71. package/dist/templates/markdown/agents.md +11 -0
  72. package/dist/templates/markdown/index.d.ts +9 -0
  73. package/dist/templates/markdown/index.d.ts.map +1 -1
  74. package/dist/templates/markdown/index.js +14 -1
  75. package/dist/templates/markdown/index.js.map +1 -1
  76. package/dist/templates/markdown/spec/conventions.md.txt +13 -0
  77. package/dist/templates/opencode/lib/atw-context.js +19 -0
  78. package/dist/templates/opencode/lib/session-utils.js +79 -24
  79. package/dist/templates/opencode/plugins/inject-workflow-state.js +18 -8
  80. package/dist/templates/pi/extensions/atw/index.ts.txt +20 -9
  81. package/dist/templates/shared-hooks/inject-shell-session-context.py +3 -3
  82. package/dist/templates/shared-hooks/inject-workflow-state.py +21 -2
  83. package/dist/templates/shared-hooks/session-start.py +70 -22
  84. package/dist/templates/snow/SNOW.md +30 -0
  85. package/dist/templates/snow/hooks/write-atw-context.py +64 -0
  86. package/dist/utils/manifest-prune.d.ts +2 -1
  87. package/dist/utils/manifest-prune.d.ts.map +1 -1
  88. package/dist/utils/manifest-prune.js +10 -7
  89. package/dist/utils/manifest-prune.js.map +1 -1
  90. package/dist/utils/optional-skills.d.ts.map +1 -1
  91. package/dist/utils/optional-skills.js +2 -1
  92. package/dist/utils/optional-skills.js.map +1 -1
  93. package/dist/utils/project-mode.d.ts +30 -0
  94. package/dist/utils/project-mode.d.ts.map +1 -0
  95. package/dist/utils/project-mode.js +86 -0
  96. package/dist/utils/project-mode.js.map +1 -0
  97. package/dist/utils/workflow-resolver.d.ts +9 -5
  98. package/dist/utils/workflow-resolver.d.ts.map +1 -1
  99. package/dist/utils/workflow-resolver.js +25 -11
  100. package/dist/utils/workflow-resolver.js.map +1 -1
  101. package/package.json +3 -3
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from "fs"
3
3
  import { join } from "path"
4
4
  import { execFileSync } from "child_process"
5
5
  import { platform } from "os"
6
- import { debugLog } from "./atw-context.js"
6
+ import { debugLog, isGeneralProject } from "./atw-context.js"
7
7
 
8
8
  const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
9
9
 
@@ -18,10 +18,22 @@ The acknowledgment must not alter the language used for the remainder of the res
18
18
  This notice is one-shot: do not repeat it after the first visible assistant reply in this session.
19
19
  </first-reply-notice>`
20
20
 
21
- function getTaskStatus(ctx, platformInput = null) {
21
+ /**
22
+ * Compact active-task status and next action. In a `general` project the AI
23
+ * decides on its own whether a request gets a task, so the no-task line defers
24
+ * to the workflow instead of telling it to ask first — keep the wording in step
25
+ * with `_get_task_status` in the shared session-start hook.
26
+ */
27
+ function getTaskStatus(ctx, platformInput = null, general = false) {
22
28
  const active = ctx.getActiveTask(platformInput)
23
29
  const taskRef = active.taskPath
24
30
  if (!taskRef) {
31
+ if (general) {
32
+ return (
33
+ "Status: NO ACTIVE TASK\n" +
34
+ "Next-Action: Do what <atw-workflow> above says for a session with no current task."
35
+ )
36
+ }
25
37
  return (
26
38
  "Status: NO ACTIVE TASK\n" +
27
39
  "Next-Action: Classify the current turn before creating any ATW task. " +
@@ -50,6 +62,11 @@ function getTaskStatus(ctx, platformInput = null) {
50
62
  const taskStatus = taskData.status || "unknown"
51
63
 
52
64
  if (taskStatus === "completed") {
65
+ if (general) {
66
+ // A general task records no branch; without the switch the archive is
67
+ // refused in a repository that has a remote.
68
+ return `Status: COMPLETED\nTask: ${taskTitle}\nNext-Action: The archive was interrupted — re-run \`task.py archive <task> --skip-branch-validation\` to finish it.`
69
+ }
53
70
  return `Status: COMPLETED\nTask: ${taskTitle}\nNext-Action: The archive was interrupted — re-run \`task.py archive <task>\` to finish it. If the working tree is dirty, commit or set the task back to \`implement\` first; there is no wrap-up commit step.`
54
71
  }
55
72
 
@@ -253,11 +270,16 @@ function buildCompactCurrentState(ctx, platformInput, specIndexPaths) {
253
270
  const lines = []
254
271
  lines.push(`Developer: ${readDeveloper(directory)}`)
255
272
 
256
- const branch = runGit(directory, ["branch", "--show-current"]) || "(detached)"
257
- const dirtyCount = runGit(directory, ["status", "--porcelain"])
258
- .split(/\r?\n/)
259
- .filter(line => line.trim()).length
260
- lines.push(`Git: branch ${branch}; ${dirtyCount === 0 ? "clean" : `dirty ${dirtyCount} paths`}.`)
273
+ // 普通文件夹没有分支,也谈不上干净:照实说,不编一个状态出来。
274
+ if (runGit(directory, ["rev-parse", "--is-inside-work-tree"]) !== "true") {
275
+ lines.push("Git: not a git repository.")
276
+ } else {
277
+ const branch = runGit(directory, ["branch", "--show-current"]) || "(detached)"
278
+ const dirtyCount = runGit(directory, ["status", "--porcelain"])
279
+ .split(/\r?\n/)
280
+ .filter(line => line.trim()).length
281
+ lines.push(`Git: branch ${branch}; ${dirtyCount === 0 ? "clean" : `dirty ${dirtyCount} paths`}.`)
282
+ }
261
283
 
262
284
  const active = ctx.getActiveTask(platformInput)
263
285
  if (active.taskPath) {
@@ -312,6 +334,32 @@ function buildCompactCurrentState(ctx, platformInput, specIndexPaths) {
312
334
  return lines.join("\n")
313
335
  }
314
336
 
337
+ const CONVENTIONS_PATH = ".atw/spec/conventions.md"
338
+
339
+ /**
340
+ * The <conventions> block of a `general` project, else "". Mirrors
341
+ * conventions_context() in .atw/scripts/common/session_context.py — keep the two
342
+ * in step: a general project has one flat conventions file instead of
343
+ * per-package, per-layer guideline indexes, and always gets the block, so the
344
+ * AI knows where the first entry goes.
345
+ */
346
+ function buildConventionsContext(directory) {
347
+ if (!isGeneralProject(directory)) return ""
348
+ const header =
349
+ `Source: ${CONVENTIONS_PATH} — what this project's user asked for more than ` +
350
+ "once. Follow it in every reply."
351
+ let body = ""
352
+ try {
353
+ body = readFileSync(join(directory, CONVENTIONS_PATH), "utf-8").trim()
354
+ } catch {
355
+ // Not written yet.
356
+ }
357
+ if (!body) {
358
+ return `<conventions>\n${header} Nothing is written there yet.\n</conventions>`
359
+ }
360
+ return `<conventions>\n${header}\n\n${body}\n</conventions>`
361
+ }
362
+
315
363
  export function buildSessionContext(ctx, platformInput = null) {
316
364
  const directory = ctx.directory
317
365
  const contextKey = typeof ctx.getContextKey === "function"
@@ -320,7 +368,10 @@ export function buildSessionContext(ctx, platformInput = null) {
320
368
 
321
369
  const config = loadAtwConfig(directory, contextKey)
322
370
  const allowedPkgs = resolveSpecScope(config)
323
- const paths = collectSpecIndexPaths(directory, allowedPkgs)
371
+ // A general project keeps no guideline indexes — whatever a switch from dev
372
+ // left under `.atw/spec/` is not offered there.
373
+ const conventions = buildConventionsContext(directory)
374
+ const paths = conventions ? [] : collectSpecIndexPaths(directory, allowedPkgs)
324
375
 
325
376
  const parts = []
326
377
 
@@ -374,26 +425,30 @@ ATW compact SessionStart context. Use it to orient the session; load details on
374
425
  parts.push("</atw-workflow>")
375
426
  }
376
427
 
377
- parts.push("<guidelines>")
378
- parts.push(
379
- "Task context order for implementation/check: jsonl entries -> `prd.md`.\n"
380
- )
428
+ if (conventions) {
429
+ parts.push(conventions)
430
+ } else {
431
+ parts.push("<guidelines>")
432
+ parts.push(
433
+ "Task context order for implementation/check: jsonl entries -> `prd.md`.\n"
434
+ )
381
435
 
382
- if (paths.length > 0) {
383
- parts.push("## Available indexes (read on demand)")
384
- for (const p of paths) {
385
- parts.push(`- ${p}`)
436
+ if (paths.length > 0) {
437
+ parts.push("## Available indexes (read on demand)")
438
+ for (const p of paths) {
439
+ parts.push(`- ${p}`)
440
+ }
441
+ parts.push("")
386
442
  }
387
- parts.push("")
388
- }
389
443
 
390
- parts.push(
391
- "Discover more via: " +
392
- "`python3 ./.atw/scripts/get_context.py --mode packages`"
393
- )
394
- parts.push("</guidelines>")
444
+ parts.push(
445
+ "Discover more via: " +
446
+ "`python3 ./.atw/scripts/get_context.py --mode packages`"
447
+ )
448
+ parts.push("</guidelines>")
449
+ }
395
450
 
396
- const taskStatus = getTaskStatus(ctx, platformInput)
451
+ const taskStatus = getTaskStatus(ctx, platformInput, Boolean(conventions))
397
452
  parts.push(`<task-status>\n${taskStatus}\n</task-status>`)
398
453
 
399
454
  parts.push(`<ready>
@@ -29,7 +29,12 @@ import {
29
29
  platformInputFromMessages,
30
30
  prependEphemeralText,
31
31
  } from "../lib/context-visibility.js"
32
- import { AtwContext, debugLog, isAtwSubagent } from "../lib/atw-context.js"
32
+ import {
33
+ AtwContext,
34
+ debugLog,
35
+ isAtwSubagent,
36
+ isGeneralProject,
37
+ } from "../lib/atw-context.js"
33
38
 
34
39
  // Supports STATUS values with letters, digits, underscores, hyphens
35
40
  // (so "in-review" / "blocked-by-team" work alongside "task_error").
@@ -177,18 +182,23 @@ const NOOB_MODE_FALLBACK =
177
182
  "language, and every term, command or path explained where it appears."
178
183
 
179
184
  /**
180
- * The <noob-mode> block for a developer who switched noob mode on, else "".
181
- * Mirrors noob_mode_reminder() in .atw/scripts/common/noob_mode.py: the
182
- * switch is a `noob_mode=on` line in the gitignored .atw/.developer.
185
+ * The <noob-mode> block while noob mode is on for this developer, else "".
186
+ * Mirrors is_noob_mode_on() / noob_mode_reminder() in
187
+ * .atw/scripts/common/noob_mode.py — keep the two in step: a `noob_mode=on`
188
+ * or `noob_mode=off` line in the gitignored .atw/.developer is the
189
+ * developer's own choice and wins; without one the project mode decides
190
+ * (on for `general`, off for `dev`).
183
191
  */
184
192
  function buildNoobModeReminder(directory, templates) {
185
- let developer
193
+ let choice = null
186
194
  try {
187
- developer = readFileSync(join(directory, ".atw", ".developer"), "utf-8")
195
+ const developer = readFileSync(join(directory, ".atw", ".developer"), "utf-8")
196
+ const m = developer.match(/^noob_mode=(.*)$/m)
197
+ if (m) choice = m[1].trim() === "on"
188
198
  } catch {
189
- return ""
199
+ // No identity file: this developer never chose.
190
200
  }
191
- if (!/^noob_mode=on\s*$/m.test(developer)) return ""
201
+ if (!(choice ?? isGeneralProject(directory))) return ""
192
202
  return `<noob-mode>\n${templates.noob_mode || NOOB_MODE_FALLBACK}\n</noob-mode>`
193
203
  }
194
204
 
@@ -1249,18 +1249,24 @@ function buildStartupContext(
1249
1249
  overview: string,
1250
1250
  ): string {
1251
1251
  const workflow = workflowOverview(root, key);
1252
+ // 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.
1255
+ const ready = overview.includes("\n<conventions>\n")
1256
+ ? [
1257
+ "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
+ ]
1259
+ : [
1260
+ "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.",
1263
+ ];
1252
1264
  return [
1253
1265
  "<session-context>\nATW compact SessionStart context. Use it to orient the session; load details on demand.\n</session-context>",
1254
1266
  FIRST_REPLY_NOTICE,
1255
1267
  overview,
1256
1268
  workflow,
1257
- [
1258
- "<ready>",
1259
- "Use the current workflow state to decide whether to create, continue, or skip an ATW task.",
1260
- "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.",
1261
- "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.",
1262
- "</ready>",
1263
- ].join("\n"),
1269
+ ["<ready>", ...ready, "</ready>"].join("\n"),
1264
1270
  ]
1265
1271
  .filter(Boolean)
1266
1272
  .join("\n\n");
@@ -1983,9 +1989,14 @@ export default function atwExtension(pi: {
1983
1989
  // Events
1984
1990
  pi.on?.("session_start", (event, ctx) => {
1985
1991
  const k = getKey(event, ctx);
1986
- inheritSessionPointer(resolveRoot(ctx), k, event);
1992
+ const root = resolveRoot(ctx);
1993
+ inheritSessionPointer(root, k, event);
1994
+ // A `general` project installs no /atw-continue prompt.
1995
+ const canContinue = exists(join(root, ".pi", "prompts", "atw-continue.md"));
1987
1996
  ctx?.ui?.notify?.(
1988
- "ATW project context is available. Use /atw-start to bootstrap or /atw-continue to resume.",
1997
+ canContinue
1998
+ ? "ATW project context is available. Use /atw-start to bootstrap or /atw-continue to resume."
1999
+ : "ATW project context is available. Use /atw-start to bootstrap.",
1989
2000
  "info",
1990
2001
  );
1991
2002
  });
@@ -4,7 +4,7 @@
4
4
  No researched platform exports a session id into its shell tool's child
5
5
  process, but every hook-capable one puts that id on hook stdin. So the hook
6
6
  that fires just before a shell command writes a short-lived runtime ticket
7
- whenever the pending command calls `task.py start/current/finish`, and the
7
+ whenever the pending command calls `task.py start/resume/current/finish`, and the
8
8
  task script consumes it when it has no native session environment.
9
9
 
10
10
  Registered on whichever pre-shell event the host provides — Cursor's
@@ -35,7 +35,7 @@ if callable(_stdin_reconfigure):
35
35
  DIR_WORKFLOW = ".atw"
36
36
  DIR_RUNTIME = ".runtime"
37
37
  DIR_SHELL_TICKETS = "shell-tickets"
38
- SESSION_SUBCOMMANDS = {"start", "current", "finish"}
38
+ SESSION_SUBCOMMANDS = {"start", "resume", "current", "finish"}
39
39
  TICKET_TTL_SECONDS = 30
40
40
  CONTEXT_IDENTITY_KEYS = (
41
41
  "session_id",
@@ -177,7 +177,7 @@ def _extract_task_subcommands(command: str) -> list[dict[str, str]]:
177
177
  if name not in SESSION_SUBCOMMANDS:
178
178
  continue
179
179
  item = {"name": name}
180
- if name == "start" and index + 2 < len(tokens):
180
+ if name in ("start", "resume") and index + 2 < len(tokens):
181
181
  item["task_ref"] = tokens[index + 2]
182
182
  subcommands.append(item)
183
183
  return subcommands
@@ -383,8 +383,24 @@ def build_breadcrumb(
383
383
  return f"<workflow-state>\n{header}\n{body}\n</workflow-state>"
384
384
 
385
385
 
386
+ def _is_general_project(root: Path) -> bool:
387
+ """Whether this is a `general` project, as `common.config` reads the mode.
388
+
389
+ An install whose scripts predate project modes is a dev project.
390
+ """
391
+ scripts_dir = root / ".atw" / "scripts"
392
+ if str(scripts_dir) not in sys.path:
393
+ sys.path.insert(0, str(scripts_dir))
394
+ try:
395
+ from common.config import get_project_mode # type: ignore[import-not-found]
396
+
397
+ return get_project_mode(root) == "general"
398
+ except Exception:
399
+ return False
400
+
401
+
386
402
  def _noob_mode_reminder(root: Path) -> str:
387
- """The `<noob-mode>` block for a developer who switched noob mode on, else "".
403
+ """The `<noob-mode>` block while noob mode is on for this developer, else "".
388
404
 
389
405
  Riding on the per-turn breadcrumb is the point of the feature: a skill
390
406
  loaded once fades over a long conversation, this is said again every turn.
@@ -476,7 +492,10 @@ def main() -> int:
476
492
  parts: list[str] = []
477
493
  if task is None:
478
494
  parts.append(CODEX_NO_TASK_BOOTSTRAP_NOTICE)
479
- parts.append(_codex_mode_banner(config))
495
+ # The banner is about dispatching review sub-agents and the dev
496
+ # implement commands; a general project installs neither.
497
+ if not _is_general_project(root):
498
+ parts.append(_codex_mode_banner(config))
480
499
  parts.append(breadcrumb)
481
500
  breadcrumb = "\n\n".join(parts)
482
501
 
@@ -83,7 +83,7 @@ FIRST_REPLY_NOTICE = f"{_FIRST_REPLY_NOTICE_HEAD}\n{_FIRST_REPLY_NOTICE_TAIL}"
83
83
 
84
84
 
85
85
  def _noob_mode_reminder(project_dir: Path) -> str:
86
- """The `<noob-mode>` block for a developer who switched noob mode on, else "".
86
+ """The `<noob-mode>` block while noob mode is on for this developer, else "".
87
87
 
88
88
  Platforms with a per-turn hook hear this again every turn; here it covers
89
89
  the ones that only have SessionStart (Cursor) and the first reply of a
@@ -101,6 +101,24 @@ def _noob_mode_reminder(project_dir: Path) -> str:
101
101
  return ""
102
102
 
103
103
 
104
+ def _conventions_context(project_dir: Path) -> str:
105
+ """The `<conventions>` block of a `general` project, else "".
106
+
107
+ A general project has one flat conventions file instead of per-package,
108
+ per-layer guideline indexes. An install whose scripts predate the helper is
109
+ treated as a dev project.
110
+ """
111
+ scripts_dir = project_dir / ".atw" / "scripts"
112
+ if str(scripts_dir) not in sys.path:
113
+ sys.path.insert(0, str(scripts_dir))
114
+ try:
115
+ from common.session_context import conventions_context # type: ignore[import-not-found]
116
+
117
+ return conventions_context(project_dir)
118
+ except Exception:
119
+ return ""
120
+
121
+
104
122
  def _build_first_reply_notice(update_hint: str | None) -> str:
105
123
  """First-reply notice, carrying the ATW update reminder when there is one.
106
124
 
@@ -201,6 +219,9 @@ def _run_git(repo_root: Path, args: list[str]) -> str:
201
219
 
202
220
 
203
221
  def _format_git_state(repo_root: Path) -> str:
222
+ # 普通文件夹没有分支,也谈不上干净:照实说,不编一个状态出来。
223
+ if _run_git(repo_root, ["rev-parse", "--is-inside-work-tree"]) != "true":
224
+ return "Git: not a git repository."
204
225
  branch = _run_git(repo_root, ["branch", "--show-current"]) or "(detached)"
205
226
  dirty_lines = [
206
227
  line for line in _run_git(repo_root, ["status", "--porcelain"]).splitlines()
@@ -416,11 +437,21 @@ def _resolve_task_dir(atw_dir: Path, task_ref: str) -> Path:
416
437
  return atw_dir / "tasks" / path_obj
417
438
 
418
439
 
419
- def _get_task_status(atw_dir: Path, input_data: dict) -> str:
420
- """Return compact active-task status, artifact presence, and next action."""
440
+ def _get_task_status(atw_dir: Path, input_data: dict, general: bool = False) -> str:
441
+ """Return compact active-task status, artifact presence, and next action.
442
+
443
+ In a `general` project the AI decides on its own whether a request gets a
444
+ task, so the no-task line defers to the workflow instead of telling it to
445
+ ask first.
446
+ """
421
447
  active = _resolve_active_task(atw_dir, input_data)
422
448
 
423
449
  if not active.task_path:
450
+ if general:
451
+ return (
452
+ "Status: NO ACTIVE TASK\n"
453
+ "Next-Action: Do what <atw-workflow> above says for a session with no current task."
454
+ )
424
455
  return (
425
456
  "Status: NO ACTIVE TASK\n"
426
457
  "Next-Action: Classify the current turn before creating any ATW task. "
@@ -454,6 +485,15 @@ def _get_task_status(atw_dir: Path, input_data: dict) -> str:
454
485
  present_line = ", ".join(present) if present else "(none)"
455
486
 
456
487
  if task_status == "completed":
488
+ if general:
489
+ # A general task records no branch; without the switch the archive
490
+ # is refused in a repository that has a remote.
491
+ return (
492
+ f"Status: COMPLETED\nTask: {task_title}\n"
493
+ f"Present: {present_line}\n"
494
+ "Next-Action: The archive was interrupted — re-run "
495
+ "`task.py archive <task> --skip-branch-validation` to finish it."
496
+ )
457
497
  return (
458
498
  f"Status: COMPLETED\nTask: {task_title}\n"
459
499
  f"Present: {present_line}\n"
@@ -824,7 +864,12 @@ def main():
824
864
 
825
865
  output = StringIO()
826
866
 
827
- spec_index_paths = _collect_spec_index_paths(atw_dir, allowed_pkgs)
867
+ # A general project keeps no guideline indexes — whatever a switch from dev
868
+ # left under `.atw/spec/` is not offered there.
869
+ conventions = _conventions_context(project_dir)
870
+ spec_index_paths = (
871
+ [] if conventions else _collect_spec_index_paths(atw_dir, allowed_pkgs)
872
+ )
828
873
 
829
874
  output.write("""<session-context>
830
875
  ATW compact SessionStart context. Use it to orient the session; load details on demand.
@@ -851,27 +896,30 @@ ATW compact SessionStart context. Use it to orient the session; load details on
851
896
  output.write(_build_workflow_overview(atw_dir / "workflow.md"))
852
897
  output.write("\n</atw-workflow>\n\n")
853
898
 
854
- output.write("<guidelines>\n")
855
- # D32/C23: same as `_get_task_status` — the injected order is jsonl then
856
- # `prd.md`; `design.md` / `implement.md` are no longer materialized.
857
- output.write(
858
- "Task context order for implementation/check: jsonl entries -> `prd.md`.\n\n"
859
- )
899
+ if conventions:
900
+ output.write(f"{conventions}\n\n")
901
+ else:
902
+ output.write("<guidelines>\n")
903
+ # D32/C23: same as `_get_task_status` — the injected order is jsonl then
904
+ # `prd.md`; `design.md` / `implement.md` are no longer materialized.
905
+ output.write(
906
+ "Task context order for implementation/check: jsonl entries -> `prd.md`.\n\n"
907
+ )
860
908
 
861
- if spec_index_paths:
862
- output.write("## Available indexes (read on demand)\n")
863
- for p in spec_index_paths:
864
- output.write(f"- {p}\n")
865
- output.write("\n")
866
-
867
- output.write(
868
- "Discover more via: "
869
- "`python3 ./.atw/scripts/get_context.py --mode packages`\n"
870
- )
871
- output.write("</guidelines>\n\n")
909
+ if spec_index_paths:
910
+ output.write("## Available indexes (read on demand)\n")
911
+ for p in spec_index_paths:
912
+ output.write(f"- {p}\n")
913
+ output.write("\n")
914
+
915
+ output.write(
916
+ "Discover more via: "
917
+ "`python3 ./.atw/scripts/get_context.py --mode packages`\n"
918
+ )
919
+ output.write("</guidelines>\n\n")
872
920
 
873
921
  # Check task status and inject structured tag
874
- task_status = _get_task_status(atw_dir, hook_input)
922
+ task_status = _get_task_status(atw_dir, hook_input, general=bool(conventions))
875
923
  output.write(f"<task-status>\n{task_status}\n</task-status>\n\n")
876
924
 
877
925
  output.write("""<ready>
@@ -1,5 +1,6 @@
1
1
  # ATW on Snow CLI
2
2
 
3
+ {{^MODE_GENERAL}}
3
4
  Snow is a **class-1** ATW host: auto context inject + project agent discovery +
4
5
  `beforeSubAgentStart` prompt enrichment.
5
6
 
@@ -38,6 +39,35 @@ Primary path: project agent discovery — no manual merge required. Do not use l
38
39
 
39
40
  Context loading is class-1 hook inject (`beforeSubAgentStart` / session / user). Agents are **not** shipped with class-2 pull-based prelude text. Hooks already inject context; agents still re-read task artifacts for correctness.
40
41
 
42
+ {{/MODE_GENERAL}}
43
+ {{#MODE_GENERAL}}
44
+ Snow is a **class-1** ATW host: hooks inject ATW context into every session
45
+ and every message.
46
+
47
+ This project is in ATW's `general` mode, for work that is not code: you say
48
+ what you want in your own words, and the workflow in `.atw/workflow.md` has
49
+ the AI do the rest. There are no prompt commands to run and no project agents.
50
+
51
+ | Capability | Status |
52
+ | ------------------------------------------ | ---------------------------------------------------------------------------- |
53
+ | Skills (`.snow/skills/atw-*/SKILL.md`) | Works — the six skills of `general` mode |
54
+ | Context hooks (`.snow/hooks/`) | Inject model context via stdout JSON + write `.snow/log/atw-context.txt` |
55
+ | Prompt commands, project agents | None in `general` mode |
56
+
57
+ ## Quick start
58
+
59
+ ```bash
60
+ atw init --snow --mode general -u your-name
61
+ snow
62
+ ```
63
+
64
+ In Snow:
65
+
66
+ 1. Open a session in the project root — `onSessionStart` injects ATW context automatically, including what `.atw/spec/conventions.md` says and, unless you turned it off, the plain-language reminder.
67
+ 2. Say what you want done. The AI keeps the task record, shows you what it wrote down before it starts, checks the result with you, and files it when you accept.
68
+ 3. Debug injects: set `SNOW_DEBUG_HOOKS=1` and inspect `.snow/log/hooks-inject.txt`.
69
+
70
+ {{/MODE_GENERAL}}
41
71
  ## Tool names (Snow-native)
42
72
 
43
73
  - `filesystem-read` / `filesystem-create` / `filesystem-replaceedit` / `filesystem-edit`
@@ -434,6 +434,40 @@ def _workflow_phase_summary(
434
434
  return lines
435
435
 
436
436
 
437
+ def _conventions_context(repo: Path) -> str:
438
+ """The `<conventions>` block of a `general` project, else "".
439
+
440
+ Same entry the shared session-start hook uses. An install whose scripts
441
+ predate the helper is treated as a dev project.
442
+ """
443
+ scripts_dir = repo / ".atw" / "scripts"
444
+ if str(scripts_dir) not in sys.path:
445
+ sys.path.insert(0, str(scripts_dir))
446
+ try:
447
+ from common.session_context import conventions_context # type: ignore[import-not-found]
448
+
449
+ return conventions_context(repo)
450
+ except Exception:
451
+ return ""
452
+
453
+
454
+ def _noob_mode_reminder(repo: Path) -> str:
455
+ """The `<noob-mode>` block while noob mode is on for this developer, else "".
456
+
457
+ Same entry the shared hooks use. An install whose scripts predate the
458
+ helper simply has no noob mode.
459
+ """
460
+ scripts_dir = repo / ".atw" / "scripts"
461
+ if str(scripts_dir) not in sys.path:
462
+ sys.path.insert(0, str(scripts_dir))
463
+ try:
464
+ from common.noob_mode import noob_mode_reminder # type: ignore[import-not-found]
465
+
466
+ return noob_mode_reminder(repo)
467
+ except Exception:
468
+ return ""
469
+
470
+
437
471
  def build_context(
438
472
  repo: Path,
439
473
  *,
@@ -468,6 +502,20 @@ def build_context(
468
502
  lines.append(f"Dispatch prompt (truncated): {p}")
469
503
  lines.append("")
470
504
 
505
+ # A general project has no session-start entry on Snow (the hooks replace
506
+ # it), so its conventions and the plain-language reminder reach the main
507
+ # session here or not at all. A dev project's context stays as it was,
508
+ # which on Snow has never included the plain-language reminder.
509
+ conventions = _conventions_context(repo) if mode != "subagent" else ""
510
+ general = bool(conventions)
511
+ if general:
512
+ # Ahead of the task state and the excerpts, so the byte limit can never
513
+ # cut it off; said again on every user message, like the per-turn hook
514
+ # of other platforms.
515
+ noob_mode = _noob_mode_reminder(repo)
516
+ if noob_mode:
517
+ lines.extend(["", noob_mode, ""])
518
+
471
519
  task_py = repo / ".atw" / "scripts" / "task.py"
472
520
  current = ""
473
521
  task_dir: Path | None = None
@@ -492,6 +540,11 @@ def build_context(
492
540
  lines.append("(could not resolve active task directory from task.py output)")
493
541
  lines.append("")
494
542
 
543
+ if general and not compact:
544
+ # Ahead of the excerpts below: when the file is long, they are what the
545
+ # byte limit drops, and the block's first line still says where to read.
546
+ lines.extend([conventions, ""])
547
+
495
548
  if not compact:
496
549
  lines.extend(_workflow_phase_summary(repo, stdin_ctx))
497
550
 
@@ -546,6 +599,17 @@ def build_context(
546
599
  "",
547
600
  ]
548
601
  )
602
+ elif general:
603
+ # No sub-agents and no start entry are installed in a general project.
604
+ lines.extend(
605
+ [
606
+ "## Main-session checklist",
607
+ "1. Session/user hooks auto-inject ATW context."
608
+ if not compact
609
+ else "1. Compact inject — full context also at `.snow/log/atw-context.txt`.",
610
+ "",
611
+ ]
612
+ )
549
613
  else:
550
614
  lines.extend(
551
615
  [
@@ -49,7 +49,8 @@ export interface PruneOptions {
49
49
  /**
50
50
  * Walk the manifest and split it into kept vs pruned entries.
51
51
  *
52
- * @param cwd Project root — used to save the rewritten manifest.
52
+ * @param cwd Project root — used to read the project's mode and to save the
53
+ * rewritten manifest.
53
54
  * @param configuredPlatforms Output of `getConfiguredPlatforms(cwd)` — caller
54
55
  * resolves this so we don't have to re-walk the filesystem.
55
56
  * @param hashes Already-loaded manifest contents. Passing it in (vs reading
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-prune.d.ts","sourceRoot":"","sources":["../../src/utils/manifest-prune.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAWH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,0DAA0D;IAC1D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,6EAA6E;IAC7E,MAAM,EAAE,cAAc,CAAC;CACxB;AAoDD,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,mBAAmB,EAAE,SAAS,MAAM,EAAE,EACtC,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,YAAiB,GACzB,WAAW,CAmCb"}
1
+ {"version":3,"file":"manifest-prune.d.ts","sourceRoot":"","sources":["../../src/utils/manifest-prune.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAYH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,0DAA0D;IAC1D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,6EAA6E;IAC7E,MAAM,EAAE,cAAc,CAAC;CACxB;AAsDD,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,mBAAmB,EAAE,SAAS,MAAM,EAAE,EACtC,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,YAAiB,GACzB,WAAW,CAmCb"}