@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.
- package/README.md +188 -15
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +25 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +154 -17
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mode.d.ts +21 -0
- package/dist/commands/mode.d.ts.map +1 -0
- package/dist/commands/mode.js +401 -0
- package/dist/commands/mode.js.map +1 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +20 -13
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/workflow.d.ts +17 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +9 -3
- package/dist/commands/workflow.js.map +1 -1
- package/dist/configurators/claude.d.ts +3 -2
- package/dist/configurators/claude.d.ts.map +1 -1
- package/dist/configurators/claude.js +3 -3
- package/dist/configurators/claude.js.map +1 -1
- package/dist/configurators/codex.d.ts +4 -3
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +10 -10
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/index.d.ts +9 -3
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +95 -17
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/shared.d.ts +13 -0
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +25 -0
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/workflow.d.ts +15 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +23 -5
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/configurators/zcode.d.ts +1 -1
- package/dist/configurators/zcode.d.ts.map +1 -1
- package/dist/configurators/zcode.js +2 -2
- package/dist/configurators/zcode.js.map +1 -1
- package/dist/constants/paths.d.ts +4 -0
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +4 -0
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.4.0.json +9 -0
- package/dist/templates/atw/index.d.ts +10 -1
- package/dist/templates/atw/index.d.ts.map +1 -1
- package/dist/templates/atw/index.js +11 -1
- package/dist/templates/atw/index.js.map +1 -1
- package/dist/templates/atw/scripts/add_session.py +34 -13
- package/dist/templates/atw/scripts/common/active_task.py +2 -2
- package/dist/templates/atw/scripts/common/config.py +13 -0
- package/dist/templates/atw/scripts/common/developer.py +15 -4
- package/dist/templates/atw/scripts/common/noob_mode.py +35 -15
- package/dist/templates/atw/scripts/common/packages_context.py +16 -3
- package/dist/templates/atw/scripts/common/session_context.py +77 -19
- package/dist/templates/atw/scripts/common/task_store.py +78 -27
- package/dist/templates/atw/scripts/noob_mode.py +16 -4
- package/dist/templates/atw/scripts/task.py +99 -38
- package/dist/templates/atw/workflow-general.md +554 -0
- package/dist/templates/atw/workflow.md +1 -1
- package/dist/templates/codex/hooks/session-start.py +69 -18
- package/dist/templates/common/bundled-skills/atw-noob-mode/SKILL.md +6 -4
- package/dist/templates/common/commands/start.md +43 -0
- package/dist/templates/copilot/hooks/session-start.py +69 -18
- package/dist/templates/dsh/DSH.md +43 -0
- package/dist/templates/markdown/agents.md +11 -0
- package/dist/templates/markdown/index.d.ts +9 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +14 -1
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/conventions.md.txt +13 -0
- package/dist/templates/opencode/lib/atw-context.js +19 -0
- package/dist/templates/opencode/lib/session-utils.js +79 -24
- package/dist/templates/opencode/plugins/inject-workflow-state.js +18 -8
- package/dist/templates/pi/extensions/atw/index.ts.txt +20 -9
- package/dist/templates/shared-hooks/inject-shell-session-context.py +3 -3
- package/dist/templates/shared-hooks/inject-workflow-state.py +21 -2
- package/dist/templates/shared-hooks/session-start.py +70 -22
- package/dist/templates/snow/SNOW.md +30 -0
- package/dist/templates/snow/hooks/write-atw-context.py +64 -0
- package/dist/utils/manifest-prune.d.ts +2 -1
- package/dist/utils/manifest-prune.d.ts.map +1 -1
- package/dist/utils/manifest-prune.js +10 -7
- package/dist/utils/manifest-prune.js.map +1 -1
- package/dist/utils/optional-skills.d.ts.map +1 -1
- package/dist/utils/optional-skills.js +2 -1
- package/dist/utils/optional-skills.js.map +1 -1
- package/dist/utils/project-mode.d.ts +30 -0
- package/dist/utils/project-mode.d.ts.map +1 -0
- package/dist/utils/project-mode.js +86 -0
- package/dist/utils/project-mode.js.map +1 -0
- package/dist/utils/workflow-resolver.d.ts +9 -5
- package/dist/utils/workflow-resolver.d.ts.map +1 -1
- package/dist/utils/workflow-resolver.js +25 -11
- package/dist/utils/workflow-resolver.js.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
.
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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 {
|
|
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
|
|
181
|
-
* Mirrors noob_mode_reminder() in
|
|
182
|
-
*
|
|
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
|
|
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
|
-
|
|
199
|
+
// No identity file: this developer never chose.
|
|
190
200
|
}
|
|
191
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
|
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;
|
|
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"}
|