@chinhae/atw-cli 0.3.6 → 0.5.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 +191 -17
- 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/migrations/manifests/0.5.0.json +9 -0
- package/dist/templates/atw/config.yaml +7 -6
- 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 +49 -19
- package/dist/templates/codex/hooks/session-start.py +69 -18
- package/dist/templates/common/bundled-skills/atw-implement/SKILL.md +8 -0
- package/dist/templates/common/bundled-skills/atw-implement-all/SKILL.md +93 -0
- package/dist/templates/common/bundled-skills/atw-implement-all/agents/openai.yaml +5 -0
- package/dist/templates/common/bundled-skills/atw-init-repo/references/issue-tracker-atw-local.md +3 -1
- package/dist/templates/common/bundled-skills/atw-init-repo/references/issue-tracker-atw.md +3 -1
- package/dist/templates/common/bundled-skills/atw-noob-mode/SKILL.md +7 -5
- package/dist/templates/common/bundled-skills/atw-tickets/SKILL.md +2 -0
- package/dist/templates/common/commands/continue.md +1 -1
- 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 +23 -4
- package/dist/templates/shared-hooks/session-start.py +70 -22
- package/dist/templates/snow/SNOW.md +31 -1
- 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
|
@@ -10,6 +10,7 @@ Provides:
|
|
|
10
10
|
output_json - Print JSON
|
|
11
11
|
output_text - Print text
|
|
12
12
|
get_update_hint - Once-per-session "update available" line
|
|
13
|
+
conventions_context - A general project's `<conventions>` block
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
16
|
from __future__ import annotations
|
|
@@ -22,7 +23,7 @@ import sys
|
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
|
|
24
25
|
from .active_task import resolve_context_key
|
|
25
|
-
from .config import get_git_packages
|
|
26
|
+
from .config import get_git_packages, get_project_mode
|
|
26
27
|
from .git import run_git
|
|
27
28
|
from .noob_mode import noob_mode_reminder
|
|
28
29
|
from .packages_context import get_packages_section
|
|
@@ -264,12 +265,18 @@ def _collect_package_git_info(
|
|
|
264
265
|
return discovered
|
|
265
266
|
|
|
266
267
|
|
|
267
|
-
def _append_root_git_context(
|
|
268
|
+
def _append_root_git_context(
|
|
269
|
+
lines: list[str], root_git_info: dict, package_git_info: list[dict]
|
|
270
|
+
) -> None:
|
|
268
271
|
"""Append root Git status without misleading non-Git roots."""
|
|
269
272
|
lines.append("## GIT STATUS")
|
|
270
273
|
if not root_git_info["isRepo"]:
|
|
271
274
|
lines.append("Root is not a Git repository.")
|
|
272
|
-
|
|
275
|
+
# 普通文件夹下面没有列出任何仓库,这句指路只在确实列了仓库时才说。
|
|
276
|
+
if package_git_info:
|
|
277
|
+
lines.append(
|
|
278
|
+
"Run Git commands from the package repository paths listed below."
|
|
279
|
+
)
|
|
273
280
|
else:
|
|
274
281
|
lines.append(f"Branch: {root_git_info['branch']}")
|
|
275
282
|
if root_git_info["isClean"]:
|
|
@@ -488,6 +495,54 @@ def get_update_hint(repo_root: Path, context_key: str | None = None) -> str | No
|
|
|
488
495
|
)
|
|
489
496
|
|
|
490
497
|
|
|
498
|
+
# =============================================================================
|
|
499
|
+
# Conventions (general projects)
|
|
500
|
+
# =============================================================================
|
|
501
|
+
|
|
502
|
+
# The one flat place a `general` project keeps what its user asked for more
|
|
503
|
+
# than once. A `dev` project spreads coding guidelines over per-package,
|
|
504
|
+
# per-layer directories under spec/ and lists their indexes at session start;
|
|
505
|
+
# a general project has this single file, handed to the AI in full instead.
|
|
506
|
+
#
|
|
507
|
+
# It sits under spec/ on purpose: that directory is already the user's own
|
|
508
|
+
# content everywhere it matters — never overwritten by `atw update`, never
|
|
509
|
+
# hash-tracked, checked for uncommitted work before `atw uninstall`.
|
|
510
|
+
FILE_CONVENTIONS = "conventions.md"
|
|
511
|
+
|
|
512
|
+
# Project-relative, as the AI should read and write it.
|
|
513
|
+
CONVENTIONS_PATH = f"{DIR_WORKFLOW}/{DIR_SPEC}/{FILE_CONVENTIONS}"
|
|
514
|
+
|
|
515
|
+
_CONVENTIONS_HEADER = (
|
|
516
|
+
f"Source: {CONVENTIONS_PATH} — what this project's user asked for more than "
|
|
517
|
+
"once. Follow it in every reply."
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def conventions_context(repo_root: Path) -> str:
|
|
522
|
+
"""The `<conventions>` block for a `general` project, "" for a `dev` one.
|
|
523
|
+
|
|
524
|
+
The session-start hooks and `get_context.py` all take the block from here,
|
|
525
|
+
so every platform says the same thing; the OpenCode plugin has its own port
|
|
526
|
+
(`lib/session-utils.js`) — keep the two in step.
|
|
527
|
+
|
|
528
|
+
A general project always gets the block: with the file missing or empty it
|
|
529
|
+
still names the place, so the AI knows where the first entry goes.
|
|
530
|
+
"""
|
|
531
|
+
if get_project_mode(repo_root) != "general":
|
|
532
|
+
return ""
|
|
533
|
+
body = ""
|
|
534
|
+
try:
|
|
535
|
+
body = (repo_root / CONVENTIONS_PATH).read_text(encoding="utf-8").strip()
|
|
536
|
+
except OSError:
|
|
537
|
+
pass
|
|
538
|
+
if not body:
|
|
539
|
+
return (
|
|
540
|
+
f"<conventions>\n{_CONVENTIONS_HEADER} Nothing is written there yet."
|
|
541
|
+
"\n</conventions>"
|
|
542
|
+
)
|
|
543
|
+
return f"<conventions>\n{_CONVENTIONS_HEADER}\n\n{body}\n</conventions>"
|
|
544
|
+
|
|
545
|
+
|
|
491
546
|
# =============================================================================
|
|
492
547
|
# JSON Output
|
|
493
548
|
# =============================================================================
|
|
@@ -607,6 +662,13 @@ def get_context_text(repo_root: Path | None = None) -> str:
|
|
|
607
662
|
lines.append(f"Name: {developer}")
|
|
608
663
|
lines.append("")
|
|
609
664
|
|
|
665
|
+
# A general project's conventions reach platforms without a hook the same
|
|
666
|
+
# way the noob-mode reminder below does.
|
|
667
|
+
conventions = conventions_context(repo_root)
|
|
668
|
+
if conventions:
|
|
669
|
+
lines.append(conventions)
|
|
670
|
+
lines.append("")
|
|
671
|
+
|
|
610
672
|
# Platforms without a hook learn about noob mode from this output alone.
|
|
611
673
|
noob_mode = noob_mode_reminder(repo_root)
|
|
612
674
|
if noob_mode:
|
|
@@ -614,16 +676,14 @@ def get_context_text(repo_root: Path | None = None) -> str:
|
|
|
614
676
|
lines.append("")
|
|
615
677
|
|
|
616
678
|
root_git_info = _collect_root_git_info(repo_root)
|
|
617
|
-
|
|
679
|
+
package_git_info = _collect_package_git_info(
|
|
680
|
+
repo_root,
|
|
681
|
+
discover_unconfigured=not root_git_info["isRepo"],
|
|
682
|
+
)
|
|
683
|
+
_append_root_git_context(lines, root_git_info, package_git_info)
|
|
618
684
|
|
|
619
685
|
# Package git repos — independent sub-repositories
|
|
620
|
-
_append_package_git_context(
|
|
621
|
-
lines,
|
|
622
|
-
_collect_package_git_info(
|
|
623
|
-
repo_root,
|
|
624
|
-
discover_unconfigured=not root_git_info["isRepo"],
|
|
625
|
-
),
|
|
626
|
-
)
|
|
686
|
+
_append_package_git_context(lines, package_git_info)
|
|
627
687
|
|
|
628
688
|
# Current task
|
|
629
689
|
lines.append("## CURRENT TASK")
|
|
@@ -851,16 +911,14 @@ def get_context_text_record(repo_root: Path | None = None) -> str:
|
|
|
851
911
|
lines.append("")
|
|
852
912
|
|
|
853
913
|
root_git_info = _collect_root_git_info(repo_root)
|
|
854
|
-
|
|
914
|
+
package_git_info = _collect_package_git_info(
|
|
915
|
+
repo_root,
|
|
916
|
+
discover_unconfigured=not root_git_info["isRepo"],
|
|
917
|
+
)
|
|
918
|
+
_append_root_git_context(lines, root_git_info, package_git_info)
|
|
855
919
|
|
|
856
920
|
# Package git repos — independent sub-repositories
|
|
857
|
-
_append_package_git_context(
|
|
858
|
-
lines,
|
|
859
|
-
_collect_package_git_info(
|
|
860
|
-
repo_root,
|
|
861
|
-
discover_unconfigured=not root_git_info["isRepo"],
|
|
862
|
-
),
|
|
863
|
-
)
|
|
921
|
+
_append_package_git_context(lines, package_git_info)
|
|
864
922
|
|
|
865
923
|
# CURRENT TASK
|
|
866
924
|
lines.append("## CURRENT TASK")
|
|
@@ -28,6 +28,7 @@ from pathlib import Path
|
|
|
28
28
|
from .config import (
|
|
29
29
|
get_codex_dispatch_mode,
|
|
30
30
|
get_packages,
|
|
31
|
+
get_project_mode,
|
|
31
32
|
get_session_auto_commit,
|
|
32
33
|
is_monorepo,
|
|
33
34
|
resolve_package,
|
|
@@ -263,10 +264,30 @@ def _parse_meta_pairs(pairs: list[str] | None) -> dict[str, str] | None:
|
|
|
263
264
|
return meta
|
|
264
265
|
|
|
265
266
|
|
|
266
|
-
def _default_prd_content(
|
|
267
|
+
def _default_prd_content(
|
|
268
|
+
title: str, description: str | None = None, general: bool = False
|
|
269
|
+
) -> str:
|
|
267
270
|
"""Return the default PRD skeleton created with every task."""
|
|
268
|
-
goal = (description or "").strip() or "TBD."
|
|
269
271
|
heading = title.strip() or "Untitled task"
|
|
272
|
+
if general:
|
|
273
|
+
# The user of a general project opens this file themselves: the three
|
|
274
|
+
# parts its workflow has the AI write, and no word about tickets.
|
|
275
|
+
unwritten = "(Not written yet.)"
|
|
276
|
+
return f"""# {heading}
|
|
277
|
+
|
|
278
|
+
## What will be done
|
|
279
|
+
|
|
280
|
+
{(description or "").strip() or unwritten}
|
|
281
|
+
|
|
282
|
+
## What will not be done
|
|
283
|
+
|
|
284
|
+
{unwritten}
|
|
285
|
+
|
|
286
|
+
## What counts as done
|
|
287
|
+
|
|
288
|
+
{unwritten}
|
|
289
|
+
"""
|
|
290
|
+
goal = (description or "").strip() or "TBD."
|
|
270
291
|
return f"""# {heading}
|
|
271
292
|
|
|
272
293
|
## Goal
|
|
@@ -471,7 +492,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
471
492
|
# back to the checked-out branch when the default can't be resolved
|
|
472
493
|
# (no remote configured, offline, etc.) — the pre-existing behavior.
|
|
473
494
|
# --base-branch lets the caller override both when neither is correct.
|
|
474
|
-
|
|
495
|
+
branch_rc, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
|
|
475
496
|
current_branch = branch_out.strip() or "main"
|
|
476
497
|
explicit_base_branch: str | None = getattr(args, "base_branch", None)
|
|
477
498
|
if explicit_base_branch:
|
|
@@ -482,15 +503,17 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
482
503
|
base_branch = resolved_base_branch
|
|
483
504
|
else:
|
|
484
505
|
base_branch = current_branch
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
506
|
+
# 普通文件夹里没有仓库也没有检出的分支,这条提醒说的事并不存在。
|
|
507
|
+
if branch_rc == 0:
|
|
508
|
+
print(
|
|
509
|
+
colored(
|
|
510
|
+
f"warning: could not resolve the repository's default branch "
|
|
511
|
+
f"(no remote configured, offline, etc.); stamping base_branch as "
|
|
512
|
+
f"the checked-out branch '{base_branch}'. Pass --base-branch to override.",
|
|
513
|
+
Colors.YELLOW,
|
|
514
|
+
),
|
|
515
|
+
file=sys.stderr,
|
|
516
|
+
)
|
|
494
517
|
|
|
495
518
|
task_data = {
|
|
496
519
|
"id": slug,
|
|
@@ -535,10 +558,12 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
535
558
|
)
|
|
536
559
|
return 1
|
|
537
560
|
|
|
561
|
+
general = get_project_mode(repo_root) == "general"
|
|
562
|
+
|
|
538
563
|
prd_path = task_dir / "prd.md"
|
|
539
564
|
if not prd_path.exists():
|
|
540
565
|
prd_path.write_text(
|
|
541
|
-
_default_prd_content(args.title, description),
|
|
566
|
+
_default_prd_content(args.title, description, general),
|
|
542
567
|
encoding="utf-8",
|
|
543
568
|
)
|
|
544
569
|
|
|
@@ -554,8 +579,11 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
554
579
|
# check.jsonl), so a seeded-but-empty implement manifest would only be a
|
|
555
580
|
# file nobody curates. The `start` gate only refuses manifests that exist
|
|
556
581
|
# with zero curated entries, so an absent one is not gated.
|
|
582
|
+
#
|
|
583
|
+
# A general project installs no sub-agents, so nobody would curate or read
|
|
584
|
+
# the manifest there either: it is not seeded, and `start` needs no flag.
|
|
557
585
|
created_jsonl = False
|
|
558
|
-
if _has_subagent_platform(repo_root):
|
|
586
|
+
if not general and _has_subagent_platform(repo_root):
|
|
559
587
|
jsonl_path = task_dir / "check.jsonl"
|
|
560
588
|
if not jsonl_path.exists():
|
|
561
589
|
jsonl_path.write_text("", encoding="utf-8")
|
|
@@ -654,28 +682,42 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
654
682
|
print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
|
|
655
683
|
print("", file=sys.stderr)
|
|
656
684
|
print(colored("Next steps:", Colors.BLUE), file=sys.stderr)
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
685
|
+
if general:
|
|
686
|
+
# A general project has no tickets, no sub-agents to curate context
|
|
687
|
+
# for and no `continue` command: its workflow has the AI do the rest.
|
|
688
|
+
print(" - Talk it through with the user until what they want is clear", file=sys.stderr)
|
|
661
689
|
print(
|
|
662
|
-
" -
|
|
663
|
-
"
|
|
690
|
+
" - Write prd.md: what will be done, what will not, what counts as done; "
|
|
691
|
+
"show it to the user and wait for them to confirm",
|
|
664
692
|
file=sys.stderr,
|
|
665
693
|
)
|
|
666
694
|
print(
|
|
667
|
-
|
|
668
|
-
"spec/research docs only, no code paths",
|
|
695
|
+
" - Once confirmed: task.py start <task>, then do the work",
|
|
669
696
|
file=sys.stderr,
|
|
670
697
|
)
|
|
698
|
+
else:
|
|
699
|
+
print(" - Fill prd.md with requirements and acceptance criteria", file=sys.stderr)
|
|
700
|
+
print(" - Lightweight task: PRD-only is valid", file=sys.stderr)
|
|
701
|
+
print(" - Complex task: slice it into issues/NN-slug.md tickets before task.py start", file=sys.stderr)
|
|
702
|
+
if created_jsonl:
|
|
703
|
+
print(
|
|
704
|
+
" - Curate check.jsonl (created empty) as the spec/research "
|
|
705
|
+
"manifest before task.py start when sub-agents need context:",
|
|
706
|
+
file=sys.stderr,
|
|
707
|
+
)
|
|
708
|
+
print(
|
|
709
|
+
' one JSON object per line — {"file": "<path>", "reason": "<why>"}; '
|
|
710
|
+
"spec/research docs only, no code paths",
|
|
711
|
+
file=sys.stderr,
|
|
712
|
+
)
|
|
713
|
+
print(
|
|
714
|
+
" list available specs: python3 .atw/scripts/get_context.py --mode packages",
|
|
715
|
+
file=sys.stderr,
|
|
716
|
+
)
|
|
671
717
|
print(
|
|
672
|
-
"
|
|
718
|
+
" - Use the ATW `continue` command or phase context to decide the next step",
|
|
673
719
|
file=sys.stderr,
|
|
674
720
|
)
|
|
675
|
-
print(
|
|
676
|
-
" - Use the ATW `continue` command or phase context to decide the next step",
|
|
677
|
-
file=sys.stderr,
|
|
678
|
-
)
|
|
679
721
|
print("", file=sys.stderr)
|
|
680
722
|
|
|
681
723
|
# Output relative path for script chaining
|
|
@@ -1442,6 +1484,15 @@ def _auto_commit_archive(
|
|
|
1442
1484
|
)
|
|
1443
1485
|
return True
|
|
1444
1486
|
|
|
1487
|
+
# 普通文件夹没有可提交的地方:这不是故障,说一句就够了,不把 git 的报错原文打出来。
|
|
1488
|
+
rc, _, _ = run_git(["rev-parse", "--is-inside-work-tree"], cwd=repo_root)
|
|
1489
|
+
if rc != 0:
|
|
1490
|
+
print(
|
|
1491
|
+
"[OK] Not a git repository — task archived, skipping auto-commit.",
|
|
1492
|
+
file=sys.stderr,
|
|
1493
|
+
)
|
|
1494
|
+
return True
|
|
1495
|
+
|
|
1445
1496
|
source_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}"
|
|
1446
1497
|
rc, tracked_out, _ = run_git(
|
|
1447
1498
|
["ls-files", "--", source_rel],
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
Switch noob mode (plain-language replies) for the current developer.
|
|
4
4
|
|
|
5
5
|
Usage:
|
|
6
|
-
python3 noob_mode.py on Turn it on
|
|
7
|
-
python3 noob_mode.py off Turn it off
|
|
8
|
-
python3 noob_mode.py status Show whether it is on
|
|
6
|
+
python3 noob_mode.py on Turn it on, whatever the project default
|
|
7
|
+
python3 noob_mode.py off Turn it off, whatever the project default
|
|
8
|
+
python3 noob_mode.py status Show whether it is on, and whether that is
|
|
9
|
+
this developer's setting or the project default
|
|
9
10
|
|
|
10
11
|
The switch is personal — see common/noob_mode.py.
|
|
11
12
|
"""
|
|
@@ -14,7 +15,8 @@ from __future__ import annotations
|
|
|
14
15
|
|
|
15
16
|
import sys
|
|
16
17
|
|
|
17
|
-
from common.
|
|
18
|
+
from common.config import get_project_mode
|
|
19
|
+
from common.noob_mode import is_noob_mode_on, noob_mode_choice, set_noob_mode
|
|
18
20
|
from common.paths import DIR_SCRIPTS, DIR_WORKFLOW, FILE_DEVELOPER, get_repo_root
|
|
19
21
|
|
|
20
22
|
|
|
@@ -35,6 +37,16 @@ def main() -> None:
|
|
|
35
37
|
sys.exit(1)
|
|
36
38
|
|
|
37
39
|
print(f"Noob mode: {'on' if is_noob_mode_on(repo_root) else 'off'}")
|
|
40
|
+
if action == "status":
|
|
41
|
+
# stdout stays the one-line answer; where it comes from goes to stderr.
|
|
42
|
+
if noob_mode_choice(repo_root) is None:
|
|
43
|
+
source = (
|
|
44
|
+
f"project default (mode: {get_project_mode(repo_root)}), "
|
|
45
|
+
"no personal setting"
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
source = "your own setting"
|
|
49
|
+
print(f"Source: {source}", file=sys.stderr)
|
|
38
50
|
|
|
39
51
|
|
|
40
52
|
if __name__ == "__main__":
|
|
@@ -9,6 +9,7 @@ Usage:
|
|
|
9
9
|
python3 task.py validate <dir> # Validate jsonl files
|
|
10
10
|
python3 task.py list-context <dir> # List jsonl entries
|
|
11
11
|
python3 task.py start <dir> # Set active task, record current branch
|
|
12
|
+
python3 task.py resume <dir> # Set active task, leave its status as it is
|
|
12
13
|
python3 task.py current [--source] [--json] # Show active task
|
|
13
14
|
python3 task.py finish # Clear active task
|
|
14
15
|
python3 task.py set-branch <dir> <branch> # Set git branch
|
|
@@ -48,6 +49,7 @@ from common.active_task import (
|
|
|
48
49
|
resolve_context_key,
|
|
49
50
|
set_active_task,
|
|
50
51
|
)
|
|
52
|
+
from common.config import get_project_mode
|
|
51
53
|
from common.git import current_branch_name
|
|
52
54
|
from common.io import (
|
|
53
55
|
describe_json_read_failure,
|
|
@@ -186,14 +188,13 @@ def _record_start_state(
|
|
|
186
188
|
)
|
|
187
189
|
|
|
188
190
|
|
|
189
|
-
def
|
|
190
|
-
"""
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
def _resolve_session_task(task_input: str, repo_root: Path) -> tuple[Path, str] | None:
|
|
192
|
+
"""Resolve a task ref to its directory and the repo-relative path stored
|
|
193
|
+
in the session pointer. Prints the reason and returns None when it cannot.
|
|
194
|
+
"""
|
|
194
195
|
if not task_input:
|
|
195
196
|
print(colored("Error: task directory or name required", Colors.RED))
|
|
196
|
-
return
|
|
197
|
+
return None
|
|
197
198
|
|
|
198
199
|
# Resolve task directory (supports task name, relative path, or absolute path)
|
|
199
200
|
full_path = resolve_task_dir(task_input, repo_root)
|
|
@@ -202,19 +203,70 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
202
203
|
# resolve_task_dir already named the exact reason on stderr. A second,
|
|
203
204
|
# generic line on stdout would split one diagnosis across two streams
|
|
204
205
|
# and bury the specific message.
|
|
205
|
-
return
|
|
206
|
+
return None
|
|
206
207
|
|
|
207
208
|
if not full_path.is_dir():
|
|
208
209
|
print(colored(f"Error: Task not found: {task_input}", Colors.RED))
|
|
209
210
|
print("Hint: Use task name (e.g., 'my-task') or full path (e.g., '.atw/tasks/01-31-my-task')")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
# Convert to relative path for storage. repo_root is resolved because
|
|
214
|
+
# full_path already is (resolve_task_dir only returns paths inside the
|
|
215
|
+
# resolved root), so an unresolved repo_root would mismatch under a
|
|
216
|
+
# symlink (e.g. /tmp on macOS) and reject a perfectly normal task.
|
|
217
|
+
try:
|
|
218
|
+
task_dir = full_path.relative_to(repo_root.resolve()).as_posix()
|
|
219
|
+
except ValueError:
|
|
220
|
+
# resolve_task_dir already refused everything outside the repo, so
|
|
221
|
+
# this is unreachable in practice. Refuse rather than fall back to
|
|
222
|
+
# str(full_path) — that fallback (a lexical relative_to() paired with
|
|
223
|
+
# an absolute-path fallback) is exactly the pattern that let a `..`
|
|
224
|
+
# ref escape into storage before this fix.
|
|
225
|
+
print(colored(f"Error: Task not found: {task_input}", Colors.RED))
|
|
226
|
+
print("Hint: Use task name (e.g., 'my-task') or full path (e.g., '.atw/tasks/01-31-my-task')")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
return full_path, task_dir
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _print_no_session_identity(command: str) -> None:
|
|
233
|
+
# Degraded mode: no session identity available.
|
|
234
|
+
# Hook didn't inject ATW_CONTEXT_ID (common on Windows + Claude Code,
|
|
235
|
+
# --continue resume path, fork distribution, hooks disabled, etc.). Skip
|
|
236
|
+
# per-session pointer write; AI continues based on conversation context.
|
|
237
|
+
print(colored(
|
|
238
|
+
"ℹ Session identity not available; active-task pointer not persisted "
|
|
239
|
+
"this session (degraded mode). AI continues based on conversation context.",
|
|
240
|
+
Colors.YELLOW,
|
|
241
|
+
))
|
|
242
|
+
print(colored(
|
|
243
|
+
"Hint: run inside an AI IDE/session that exposes session identity, "
|
|
244
|
+
f"or set ATW_CONTEXT_ID before running task.py {command}.",
|
|
245
|
+
Colors.YELLOW,
|
|
246
|
+
))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def cmd_start(args: argparse.Namespace) -> int:
|
|
250
|
+
"""Set active task."""
|
|
251
|
+
repo_root = get_repo_root()
|
|
252
|
+
task_input = args.dir
|
|
253
|
+
|
|
254
|
+
resolved = _resolve_session_task(task_input, repo_root)
|
|
255
|
+
if resolved is None:
|
|
210
256
|
return 1
|
|
257
|
+
full_path, task_dir = resolved
|
|
211
258
|
|
|
212
259
|
# Context-manifest gate: a seeded-but-uncurated implement/check
|
|
213
260
|
# manifest means every sub-agent dispatched for this task runs with zero
|
|
214
261
|
# spec context, and nothing downstream surfaces that to the main session.
|
|
215
262
|
# An absent manifest is not gated — create seeds the files only on
|
|
216
263
|
# sub-agent-capable platforms, so absence means no sub-agent reads them.
|
|
217
|
-
|
|
264
|
+
# Nor is a general project: it installs no sub-agents, and a task brought
|
|
265
|
+
# over from dev may still carry an empty manifest.
|
|
266
|
+
if (
|
|
267
|
+
not getattr(args, "allow_empty_context", False)
|
|
268
|
+
and get_project_mode(repo_root) != "general"
|
|
269
|
+
):
|
|
218
270
|
empty_manifests = [
|
|
219
271
|
name
|
|
220
272
|
for name in ("implement.jsonl", "check.jsonl")
|
|
@@ -232,39 +284,10 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
232
284
|
print(" Intentionally empty? Re-run start with --allow-empty-context")
|
|
233
285
|
return 1
|
|
234
286
|
|
|
235
|
-
# Convert to relative path for storage. repo_root is resolved because
|
|
236
|
-
# full_path already is (resolve_task_dir only returns paths inside the
|
|
237
|
-
# resolved root), so an unresolved repo_root would mismatch under a
|
|
238
|
-
# symlink (e.g. /tmp on macOS) and reject a perfectly normal task.
|
|
239
|
-
try:
|
|
240
|
-
task_dir = full_path.relative_to(repo_root.resolve()).as_posix()
|
|
241
|
-
except ValueError:
|
|
242
|
-
# resolve_task_dir already refused everything outside the repo, so
|
|
243
|
-
# this is unreachable in practice. Refuse rather than fall back to
|
|
244
|
-
# str(full_path) — that fallback (a lexical relative_to() paired with
|
|
245
|
-
# an absolute-path fallback) is exactly the pattern that let a `..`
|
|
246
|
-
# ref escape into storage before this fix.
|
|
247
|
-
print(colored(f"Error: Task not found: {task_input}", Colors.RED))
|
|
248
|
-
print("Hint: Use task name (e.g., 'my-task') or full path (e.g., '.atw/tasks/01-31-my-task')")
|
|
249
|
-
return 1
|
|
250
|
-
|
|
251
287
|
task_json_path = full_path / FILE_TASK_JSON
|
|
252
288
|
|
|
253
289
|
if not resolve_context_key():
|
|
254
|
-
|
|
255
|
-
# Hook didn't inject ATW_CONTEXT_ID (common on Windows + Claude Code,
|
|
256
|
-
# --continue resume path, fork distribution, hooks disabled, etc.). Skip
|
|
257
|
-
# per-session pointer write; AI continues based on conversation context.
|
|
258
|
-
print(colored(
|
|
259
|
-
"ℹ Session identity not available; active-task pointer not persisted "
|
|
260
|
-
"this session (degraded mode). AI continues based on conversation context.",
|
|
261
|
-
Colors.YELLOW,
|
|
262
|
-
))
|
|
263
|
-
print(colored(
|
|
264
|
-
"Hint: run inside an AI IDE/session that exposes session identity, "
|
|
265
|
-
"or set ATW_CONTEXT_ID before running task.py start.",
|
|
266
|
-
Colors.YELLOW,
|
|
267
|
-
))
|
|
290
|
+
_print_no_session_identity("start")
|
|
268
291
|
|
|
269
292
|
# Still flip task.json status: discover → implement so downstream phases proceed.
|
|
270
293
|
if task_json_path.is_file():
|
|
@@ -290,6 +313,36 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
290
313
|
return 1
|
|
291
314
|
|
|
292
315
|
|
|
316
|
+
def cmd_resume(args: argparse.Namespace) -> int:
|
|
317
|
+
"""Set active task without touching its record.
|
|
318
|
+
|
|
319
|
+
`start` is also "begin the work": it moves a planning-stage task to
|
|
320
|
+
`implement`. Picking a task up in a later session must not do that — a
|
|
321
|
+
task still being discussed would read as started before it was agreed.
|
|
322
|
+
So this writes the session pointer and nothing else: no status, no branch,
|
|
323
|
+
no manifest gate, no `after_start` hook.
|
|
324
|
+
"""
|
|
325
|
+
repo_root = get_repo_root()
|
|
326
|
+
|
|
327
|
+
resolved = _resolve_session_task(args.dir, repo_root)
|
|
328
|
+
if resolved is None:
|
|
329
|
+
return 1
|
|
330
|
+
_, task_dir = resolved
|
|
331
|
+
|
|
332
|
+
if not resolve_context_key():
|
|
333
|
+
_print_no_session_identity("resume")
|
|
334
|
+
return 0
|
|
335
|
+
|
|
336
|
+
active = set_active_task(task_dir, repo_root)
|
|
337
|
+
if not active:
|
|
338
|
+
print(colored("Error: Failed to set current task", Colors.RED))
|
|
339
|
+
return 1
|
|
340
|
+
|
|
341
|
+
print(colored(f"✓ Current task set to: {task_dir}", Colors.GREEN))
|
|
342
|
+
print(f"Source: {active.source}")
|
|
343
|
+
return 0
|
|
344
|
+
|
|
345
|
+
|
|
293
346
|
def cmd_finish(args: argparse.Namespace) -> int:
|
|
294
347
|
"""Clear active task."""
|
|
295
348
|
repo_root = get_repo_root()
|
|
@@ -556,6 +609,7 @@ Usage:
|
|
|
556
609
|
python3 task.py validate <dir> Validate jsonl files
|
|
557
610
|
python3 task.py list-context <dir> List jsonl entries
|
|
558
611
|
python3 task.py start <dir> Set active task; records the checked-out branch when unset
|
|
612
|
+
python3 task.py resume <dir> Set active task; its status and record stay as they are
|
|
559
613
|
python3 task.py current [--source] Show active task
|
|
560
614
|
python3 task.py finish Clear active task
|
|
561
615
|
python3 task.py set-branch <dir> <branch> Set git branch
|
|
@@ -713,6 +767,12 @@ def main() -> int:
|
|
|
713
767
|
help="Start even when implement.jsonl / check.jsonl have no curated entries",
|
|
714
768
|
)
|
|
715
769
|
|
|
770
|
+
# resume
|
|
771
|
+
p_resume = subparsers.add_parser(
|
|
772
|
+
"resume", help="Set active task, leave its status as it is"
|
|
773
|
+
)
|
|
774
|
+
p_resume.add_argument("dir", help="Task directory")
|
|
775
|
+
|
|
716
776
|
# current
|
|
717
777
|
p_current = subparsers.add_parser("current", help="Show active task")
|
|
718
778
|
p_current.add_argument("--source", action="store_true",
|
|
@@ -807,6 +867,7 @@ def main() -> int:
|
|
|
807
867
|
"validate": cmd_validate,
|
|
808
868
|
"list-context": cmd_list_context,
|
|
809
869
|
"start": cmd_start,
|
|
870
|
+
"resume": cmd_resume,
|
|
810
871
|
"current": cmd_current,
|
|
811
872
|
"finish": cmd_finish,
|
|
812
873
|
"set-branch": cmd_set_branch,
|