@pennixrv/trellis 0.7.0-beta.5 → 0.7.0-beta.50

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 (219) hide show
  1. package/dist/cli/index.d.ts.map +1 -1
  2. package/dist/cli/index.js +47 -3
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/backup.d.ts +25 -0
  5. package/dist/commands/backup.d.ts.map +1 -0
  6. package/dist/commands/backup.js +152 -0
  7. package/dist/commands/backup.js.map +1 -0
  8. package/dist/commands/channel/adapters/claude.d.ts.map +1 -1
  9. package/dist/commands/channel/adapters/claude.js +2 -0
  10. package/dist/commands/channel/adapters/claude.js.map +1 -1
  11. package/dist/commands/channel/adapters/codex.d.ts +5 -1
  12. package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
  13. package/dist/commands/channel/adapters/codex.js +19 -9
  14. package/dist/commands/channel/adapters/codex.js.map +1 -1
  15. package/dist/commands/channel/adapters/index.d.ts +3 -0
  16. package/dist/commands/channel/adapters/index.d.ts.map +1 -1
  17. package/dist/commands/channel/adapters/index.js +3 -3
  18. package/dist/commands/channel/adapters/index.js.map +1 -1
  19. package/dist/commands/channel/adapters/types.d.ts +2 -0
  20. package/dist/commands/channel/adapters/types.d.ts.map +1 -1
  21. package/dist/commands/channel/guard.d.ts.map +1 -1
  22. package/dist/commands/channel/guard.js +38 -29
  23. package/dist/commands/channel/guard.js.map +1 -1
  24. package/dist/commands/channel/index.d.ts.map +1 -1
  25. package/dist/commands/channel/index.js +52 -16
  26. package/dist/commands/channel/index.js.map +1 -1
  27. package/dist/commands/channel/kill.d.ts.map +1 -1
  28. package/dist/commands/channel/kill.js +37 -15
  29. package/dist/commands/channel/kill.js.map +1 -1
  30. package/dist/commands/channel/messages.d.ts.map +1 -1
  31. package/dist/commands/channel/messages.js +3 -2
  32. package/dist/commands/channel/messages.js.map +1 -1
  33. package/dist/commands/channel/process-identity.d.ts +3 -0
  34. package/dist/commands/channel/process-identity.d.ts.map +1 -0
  35. package/dist/commands/channel/process-identity.js +24 -0
  36. package/dist/commands/channel/process-identity.js.map +1 -0
  37. package/dist/commands/channel/profiles.d.ts +26 -0
  38. package/dist/commands/channel/profiles.d.ts.map +1 -0
  39. package/dist/commands/channel/profiles.js +186 -0
  40. package/dist/commands/channel/profiles.js.map +1 -0
  41. package/dist/commands/channel/rm.d.ts +0 -1
  42. package/dist/commands/channel/rm.d.ts.map +1 -1
  43. package/dist/commands/channel/rm.js +203 -96
  44. package/dist/commands/channel/rm.js.map +1 -1
  45. package/dist/commands/channel/spawn.d.ts +3 -0
  46. package/dist/commands/channel/spawn.d.ts.map +1 -1
  47. package/dist/commands/channel/spawn.js +43 -15
  48. package/dist/commands/channel/spawn.js.map +1 -1
  49. package/dist/commands/channel/supervisor/idle.d.ts +2 -0
  50. package/dist/commands/channel/supervisor/idle.d.ts.map +1 -1
  51. package/dist/commands/channel/supervisor/idle.js +15 -7
  52. package/dist/commands/channel/supervisor/idle.js.map +1 -1
  53. package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -1
  54. package/dist/commands/channel/supervisor/inbox.js +48 -33
  55. package/dist/commands/channel/supervisor/inbox.js.map +1 -1
  56. package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -1
  57. package/dist/commands/channel/supervisor/stdout.js +6 -3
  58. package/dist/commands/channel/supervisor/stdout.js.map +1 -1
  59. package/dist/commands/channel/supervisor/warning.d.ts.map +1 -1
  60. package/dist/commands/channel/supervisor/warning.js +2 -3
  61. package/dist/commands/channel/supervisor/warning.js.map +1 -1
  62. package/dist/commands/channel/supervisor.d.ts +12 -0
  63. package/dist/commands/channel/supervisor.d.ts.map +1 -1
  64. package/dist/commands/channel/supervisor.js +88 -16
  65. package/dist/commands/channel/supervisor.js.map +1 -1
  66. package/dist/commands/channel/wait.d.ts +3 -1
  67. package/dist/commands/channel/wait.d.ts.map +1 -1
  68. package/dist/commands/channel/wait.js +59 -14
  69. package/dist/commands/channel/wait.js.map +1 -1
  70. package/dist/commands/init.d.ts.map +1 -1
  71. package/dist/commands/init.js +23 -10
  72. package/dist/commands/init.js.map +1 -1
  73. package/dist/commands/update.d.ts.map +1 -1
  74. package/dist/commands/update.js +446 -333
  75. package/dist/commands/update.js.map +1 -1
  76. package/dist/commands/workflow.d.ts +1 -0
  77. package/dist/commands/workflow.d.ts.map +1 -1
  78. package/dist/commands/workflow.js +56 -1
  79. package/dist/commands/workflow.js.map +1 -1
  80. package/dist/configurators/codex.d.ts +6 -0
  81. package/dist/configurators/codex.d.ts.map +1 -1
  82. package/dist/configurators/codex.js +93 -0
  83. package/dist/configurators/codex.js.map +1 -1
  84. package/dist/configurators/shared.js +2 -2
  85. package/dist/configurators/shared.js.map +1 -1
  86. package/dist/constants/paths.d.ts +2 -0
  87. package/dist/constants/paths.d.ts.map +1 -1
  88. package/dist/constants/paths.js +2 -0
  89. package/dist/constants/paths.js.map +1 -1
  90. package/dist/migrations/manifests/0.7.0-beta.10.json +9 -0
  91. package/dist/migrations/manifests/0.7.0-beta.11.json +9 -0
  92. package/dist/migrations/manifests/0.7.0-beta.12.json +9 -0
  93. package/dist/migrations/manifests/0.7.0-beta.13.json +9 -0
  94. package/dist/migrations/manifests/0.7.0-beta.14.json +9 -0
  95. package/dist/migrations/manifests/0.7.0-beta.15.json +9 -0
  96. package/dist/migrations/manifests/0.7.0-beta.16.json +9 -0
  97. package/dist/migrations/manifests/0.7.0-beta.17.json +9 -0
  98. package/dist/migrations/manifests/0.7.0-beta.18.json +9 -0
  99. package/dist/migrations/manifests/0.7.0-beta.19.json +9 -0
  100. package/dist/migrations/manifests/0.7.0-beta.20.json +9 -0
  101. package/dist/migrations/manifests/0.7.0-beta.21.json +17 -0
  102. package/dist/migrations/manifests/0.7.0-beta.22.json +9 -0
  103. package/dist/migrations/manifests/0.7.0-beta.23.json +9 -0
  104. package/dist/migrations/manifests/0.7.0-beta.24.json +9 -0
  105. package/dist/migrations/manifests/0.7.0-beta.25.json +9 -0
  106. package/dist/migrations/manifests/0.7.0-beta.26.json +9 -0
  107. package/dist/migrations/manifests/0.7.0-beta.27.json +9 -0
  108. package/dist/migrations/manifests/0.7.0-beta.28.json +9 -0
  109. package/dist/migrations/manifests/0.7.0-beta.29.json +9 -0
  110. package/dist/migrations/manifests/0.7.0-beta.30.json +9 -0
  111. package/dist/migrations/manifests/0.7.0-beta.31.json +9 -0
  112. package/dist/migrations/manifests/0.7.0-beta.32.json +9 -0
  113. package/dist/migrations/manifests/0.7.0-beta.33.json +9 -0
  114. package/dist/migrations/manifests/0.7.0-beta.34.json +9 -0
  115. package/dist/migrations/manifests/0.7.0-beta.35.json +9 -0
  116. package/dist/migrations/manifests/0.7.0-beta.36.json +9 -0
  117. package/dist/migrations/manifests/0.7.0-beta.37.json +9 -0
  118. package/dist/migrations/manifests/0.7.0-beta.38.json +9 -0
  119. package/dist/migrations/manifests/0.7.0-beta.39.json +9 -0
  120. package/dist/migrations/manifests/0.7.0-beta.40.json +9 -0
  121. package/dist/migrations/manifests/0.7.0-beta.41.json +9 -0
  122. package/dist/migrations/manifests/0.7.0-beta.42.json +9 -0
  123. package/dist/migrations/manifests/0.7.0-beta.43.json +9 -0
  124. package/dist/migrations/manifests/0.7.0-beta.44.json +9 -0
  125. package/dist/migrations/manifests/0.7.0-beta.45.json +9 -0
  126. package/dist/migrations/manifests/0.7.0-beta.46.json +9 -0
  127. package/dist/migrations/manifests/0.7.0-beta.47.json +9 -0
  128. package/dist/migrations/manifests/0.7.0-beta.48.json +9 -0
  129. package/dist/migrations/manifests/0.7.0-beta.49.json +13 -0
  130. package/dist/migrations/manifests/0.7.0-beta.50.json +12 -0
  131. package/dist/migrations/manifests/0.7.0-beta.6.json +9 -0
  132. package/dist/migrations/manifests/0.7.0-beta.7.json +9 -0
  133. package/dist/migrations/manifests/0.7.0-beta.8.json +9 -0
  134. package/dist/migrations/manifests/0.7.0-beta.9.json +9 -0
  135. package/dist/templates/codex/hooks/session-start.py +21 -17
  136. package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +3 -1
  137. package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +24 -16
  138. package/dist/templates/common/bundled-skills/trellis-channel/references/forum.md +4 -12
  139. package/dist/templates/common/bundled-skills/trellis-channel/references/multi-target-dispatch.md +139 -0
  140. package/dist/templates/common/bundled-skills/trellis-channel/references/progress-debugging.md +9 -5
  141. package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +84 -14
  142. package/dist/templates/common/bundled-skills/trellis-channel/references/workers.md +9 -11
  143. package/dist/templates/common/bundled-skills/trellis-channel/references/workflows.md +25 -14
  144. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +13 -11
  145. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +7 -2
  146. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +6 -1
  147. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +6 -4
  148. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +42 -3
  149. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +1 -1
  150. package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +10 -2
  151. package/dist/templates/common/commands/continue.md +36 -8
  152. package/dist/templates/common/commands/finish-work.md +17 -2
  153. package/dist/templates/common/commands/start.md +8 -1
  154. package/dist/templates/common/skills/brainstorm.md +49 -22
  155. package/dist/templates/common/skills/break-loop.md +6 -10
  156. package/dist/templates/common/skills/check.md +34 -5
  157. package/dist/templates/copilot/hooks/session-start.py +19 -15
  158. package/dist/templates/copilot/prompts/brainstorm.prompt.md +27 -21
  159. package/dist/templates/markdown/spec/backend/database-guidelines.md.txt +22 -22
  160. package/dist/templates/markdown/spec/backend/directory-structure.md.txt +20 -20
  161. package/dist/templates/markdown/spec/backend/error-handling.md.txt +22 -22
  162. package/dist/templates/markdown/spec/backend/index.md.txt +19 -19
  163. package/dist/templates/markdown/spec/backend/logging-guidelines.md.txt +22 -22
  164. package/dist/templates/markdown/spec/backend/quality-guidelines.md.txt +22 -22
  165. package/dist/templates/markdown/spec/frontend/component-guidelines.md.txt +25 -25
  166. package/dist/templates/markdown/spec/frontend/directory-structure.md.txt +20 -20
  167. package/dist/templates/markdown/spec/frontend/hook-guidelines.md.txt +22 -22
  168. package/dist/templates/markdown/spec/frontend/index.md.txt +20 -20
  169. package/dist/templates/markdown/spec/frontend/quality-guidelines.md.txt +22 -22
  170. package/dist/templates/markdown/spec/frontend/state-management.md.txt +22 -22
  171. package/dist/templates/markdown/spec/frontend/type-safety.md.txt +22 -22
  172. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +80 -85
  173. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +164 -209
  174. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +161 -185
  175. package/dist/templates/markdown/spec/guides/index.md.txt +51 -51
  176. package/dist/templates/markdown/workspace-index.md +44 -44
  177. package/dist/templates/opencode/lib/session-utils.js +2 -2
  178. package/dist/templates/shared-hooks/inject-shell-session-context.py +3 -6
  179. package/dist/templates/shared-hooks/inject-workflow-state.py +3 -2
  180. package/dist/templates/shared-hooks/session-start.py +23 -18
  181. package/dist/templates/trellis/agents/subnode-profiles.json +13 -0
  182. package/dist/templates/trellis/agents/subnode.md +11 -5
  183. package/dist/templates/trellis/config.yaml +2 -0
  184. package/dist/templates/trellis/index.d.ts +2 -1
  185. package/dist/templates/trellis/index.d.ts.map +1 -1
  186. package/dist/templates/trellis/index.js +4 -2
  187. package/dist/templates/trellis/index.js.map +1 -1
  188. package/dist/templates/trellis/scripts/add_session.py +35 -32
  189. package/dist/templates/trellis/scripts/common/active_task.py +8 -8
  190. package/dist/templates/trellis/scripts/common/developer.py +18 -18
  191. package/dist/templates/trellis/scripts/common/ownership_record.py +13 -8
  192. package/dist/templates/trellis/scripts/common/safe_commit.py +38 -9
  193. package/dist/templates/trellis/scripts/common/task_planning.py +137 -0
  194. package/dist/templates/trellis/scripts/common/task_store.py +46 -14
  195. package/dist/templates/trellis/scripts/common/workflow_phase.py +4 -3
  196. package/dist/templates/trellis/scripts/subnode_artifact.py +470 -21
  197. package/dist/templates/trellis/scripts/task.py +175 -2
  198. package/dist/templates/trellis/workflow.md +407 -388
  199. package/dist/utils/managed-paths.d.ts +2 -0
  200. package/dist/utils/managed-paths.d.ts.map +1 -1
  201. package/dist/utils/managed-paths.js +24 -2
  202. package/dist/utils/managed-paths.js.map +1 -1
  203. package/dist/utils/template-fetcher.d.ts +1 -0
  204. package/dist/utils/template-fetcher.d.ts.map +1 -1
  205. package/dist/utils/template-fetcher.js +2 -1
  206. package/dist/utils/template-fetcher.js.map +1 -1
  207. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
  208. package/dist/utils/uninstall-scrubbers.js +5 -0
  209. package/dist/utils/uninstall-scrubbers.js.map +1 -1
  210. package/dist/utils/workflow-provenance.d.ts +24 -0
  211. package/dist/utils/workflow-provenance.d.ts.map +1 -0
  212. package/dist/utils/workflow-provenance.js +71 -0
  213. package/dist/utils/workflow-provenance.js.map +1 -0
  214. package/dist/utils/workflow-resolver.d.ts +9 -2
  215. package/dist/utils/workflow-resolver.d.ts.map +1 -1
  216. package/dist/utils/workflow-resolver.js +33 -7
  217. package/dist/utils/workflow-resolver.js.map +1 -1
  218. package/package.json +2 -2
  219. package/dist/templates/trellis/agents/subnode.env +0 -4
@@ -97,8 +97,8 @@ MARKER_PREFIX = "<!-- trellis-session:"
97
97
  # fingerprint mixed in the calendar date; see compute_record_fingerprint.
98
98
  MARKER_VERSION = 2
99
99
  LEGACY_MARKER_RE = re.compile(r"^<!-- trellis-session: fp=([0-9a-f]{16}) -->$")
100
- ENTRY_DATE_RE = re.compile(r"^\*\*Date\*\*: (\d{4}-\d{2}-\d{2})\s*$")
101
- SESSION_HEADING_RE = re.compile(r"^## Session (\d+):", re.MULTILINE)
100
+ ENTRY_DATE_RE = re.compile(r"^\*\*(?:Date|日期(Date))\*\*: (\d{4}-\d{2}-\d{2})\s*$")
101
+ SESSION_HEADING_RE = re.compile(r"^## (?:Session|会话(Session)) (\d+)(?::|:)", re.MULTILINE)
102
102
 
103
103
  # Recording states, in the order the operation walks them.
104
104
  STATE_ABSENT = "absent"
@@ -170,7 +170,7 @@ def _max_session_in_index(content: str) -> int:
170
170
 
171
171
 
172
172
  def _max_session_in_journal(content: str) -> int:
173
- """Highest `## Session N:` heading in a journal body (0 when none)."""
173
+ """Highest old or new session heading in a journal body (0 when none)."""
174
174
  numbers = [int(m.group(1)) for m in SESSION_HEADING_RE.finditer(content)]
175
175
  return max(numbers) if numbers else 0
176
176
 
@@ -195,7 +195,7 @@ def count_journal_files(dev_dir: Path, active_num: int) -> str:
195
195
  for f in files:
196
196
  filename = f.name
197
197
  lines = len(f.read_text(encoding="utf-8").splitlines())
198
- status = "Active" if filename == active_file else "Archived"
198
+ status = "活动(Active)" if filename == active_file else "归档(Archived)"
199
199
  result_lines.append(f"| `{filename}` | ~{lines} | {status} |")
200
200
 
201
201
  return "\n".join(result_lines)
@@ -301,10 +301,10 @@ def create_new_journal_file(
301
301
  prev_num = num - 1
302
302
  new_file = dev_dir / f"{FILE_JOURNAL_PREFIX}{num}.md"
303
303
 
304
- content = f"""# Journal - {developer} (Part {num})
304
+ content = f"""# 日志(Journal)- {developer}(第 {num} 部分)
305
305
 
306
- > Continuation from `{FILE_JOURNAL_PREFIX}{prev_num}.md` (archived at ~{max_lines} lines)
307
- > Started: {today}
306
+ > 续接自 `{FILE_JOURNAL_PREFIX}{prev_num}.md`(约 {max_lines} 行时归档)
307
+ > 开始日期(Started): {today}
308
308
 
309
309
  ---
310
310
 
@@ -615,7 +615,7 @@ def _max_session_across_refs(repo_root: Path, refs: list[str], dev_rel: str) ->
615
615
  "-h",
616
616
  "-E",
617
617
  "-e",
618
- r"^## Session [0-9]+:",
618
+ r"^## (Session [0-9]+:|会话(Session) [0-9]+:)",
619
619
  "-e",
620
620
  r"Total Sessions",
621
621
  ]
@@ -629,7 +629,7 @@ def _max_session_across_refs(repo_root: Path, refs: list[str], dev_rel: str) ->
629
629
 
630
630
  # `git grep` prefixes each hit with `<rev>:<path>:`, so the numbers are
631
631
  # matched anywhere in the line rather than anchored.
632
- numbers = [int(m.group(1)) for m in re.finditer(r"## Session (\d+):", out)]
632
+ numbers = [int(m.group(1)) for m in re.finditer(r"## (?:Session|会话(Session)) (\d+)(?::|:)", out)]
633
633
  numbers += [
634
634
  int(m.group(1)) for m in re.finditer(r"Total Sessions[^\d]{0,8}(\d+)", out)
635
635
  ]
@@ -843,7 +843,7 @@ def classify_record(
843
843
  if session_num is None:
844
844
  return "", None, None, (
845
845
  f"{journal_file.name} carries this record's marker without a "
846
- "'## Session N:' heading above it. The pending record is malformed; "
846
+ "session heading above it. The pending record is malformed; "
847
847
  "repair it by hand before retrying."
848
848
  )
849
849
 
@@ -884,9 +884,9 @@ def _render_bullet_section(header: str, items: list[str], bullet_prefix: str = "
884
884
  def _render_main_changes(changes: list[str], extra_content: str | None) -> str:
885
885
  """Render the Main Changes section from --change bullets or freeform content."""
886
886
  if changes:
887
- return _render_bullet_section("Main Changes", changes)
887
+ return _render_bullet_section("主要变更(Main Changes)", changes)
888
888
  if extra_content:
889
- return f"\n\n### Main Changes\n\n{extra_content}"
889
+ return f"\n\n### 主要变更(Main Changes)\n\n{extra_content}"
890
890
  return ""
891
891
 
892
892
 
@@ -906,39 +906,39 @@ def generate_session_content(
906
906
  ) -> str:
907
907
  """Generate session content."""
908
908
  if evidence:
909
- commit_table = """| Hash | Message |
909
+ commit_table = """| 哈希(Hash) | 消息(Message) |
910
910
  |------|---------|"""
911
911
  for oid, subject in evidence:
912
912
  commit_table += f"\n| `{oid}` | {escape_markdown_cell(subject)} |"
913
913
  else:
914
- commit_table = "(No commits - planning session)"
914
+ commit_table = "(无提交——规划会话)"
915
915
 
916
- package_line = f"\n**Package**: {package}" if package else ""
917
- branch_line = f"\n**Branch**: `{branch}`" if branch else ""
916
+ package_line = f"\n**包(Package)**: {package}" if package else ""
917
+ branch_line = f"\n**分支(Branch)**: `{branch}`" if branch else ""
918
918
 
919
919
  main_changes_section = _render_main_changes(changes or [], extra_content)
920
- testing_section = _render_bullet_section("Testing", tests or [], bullet_prefix="- [OK] ")
921
- next_steps_section = _render_bullet_section("Next Steps", next_steps or [])
920
+ testing_section = _render_bullet_section("测试(Testing)", tests or [], bullet_prefix="- [OK] ")
921
+ next_steps_section = _render_bullet_section("下一步(Next Steps)", next_steps or [])
922
922
 
923
923
  return f"""
924
924
 
925
- ## Session {session_num}: {title}
925
+ ## 会话(Session) {session_num}:{title}
926
926
  {marker}
927
927
 
928
- **Date**: {today}
929
- **Task**: {title}{package_line}{branch_line}
928
+ **日期(Date)**: {today}
929
+ **任务(Task)**: {title}{package_line}{branch_line}
930
930
 
931
- ### Summary
931
+ ### 摘要(Summary)
932
932
 
933
933
  {summary}{main_changes_section}
934
934
 
935
- ### Git Commits
935
+ ### Git 提交(Git Commits)
936
936
 
937
937
  {commit_table}{testing_section}
938
938
 
939
- ### Status
939
+ ### 状态(Status)
940
940
 
941
- [OK] **Completed**{next_steps_section}
941
+ [OK] **已完成(Completed)**{next_steps_section}
942
942
  """
943
943
 
944
944
 
@@ -992,9 +992,9 @@ def update_index(
992
992
  if "@@@auto:current-status" in line:
993
993
  new_lines.append(line)
994
994
  in_current_status = True
995
- new_lines.append(f"- **Active File**: `{active_file}`")
996
- new_lines.append(f"- **Total Sessions**: {new_session}")
997
- new_lines.append(f"- **Last Active**: {today}")
995
+ new_lines.append(f"- **活动文件(Active File)**: `{active_file}`")
996
+ new_lines.append(f"- **会话总数(Total Sessions)**: {new_session}")
997
+ new_lines.append(f"- **最近活动(Last Active)**: {today}")
998
998
  continue
999
999
 
1000
1000
  if "@@@/auto:current-status" in line:
@@ -1005,7 +1005,7 @@ def update_index(
1005
1005
  if "@@@auto:active-documents" in line:
1006
1006
  new_lines.append(line)
1007
1007
  in_active_documents = True
1008
- new_lines.append("| File | Lines | Status |")
1008
+ new_lines.append("| 文件(File) | 行数(Lines) | 状态(Status) |")
1009
1009
  new_lines.append("|------|-------|--------|")
1010
1010
  new_lines.append(files_table)
1011
1011
  continue
@@ -1038,13 +1038,16 @@ def update_index(
1038
1038
  r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*Base Branch\s*\|\s*$",
1039
1039
  line,
1040
1040
  ):
1041
- new_lines.append("| # | Date | Title | Commits | Branch |")
1041
+ new_lines.append("| # | 日期(Date) | 标题(Title) | 提交(Commits) | 分支(Branch) |")
1042
1042
  continue
1043
1043
  if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*$", line):
1044
- new_lines.append("| # | Date | Title | Commits | Branch |")
1044
+ new_lines.append("| # | 日期(Date) | 标题(Title) | 提交(Commits) | 分支(Branch) |")
1045
+ continue
1046
+ if re.match(r"^\|\s*#\s*\|\s*日期(Date)\s*\|\s*标题(Title)\s*\|\s*提交(Commits)\s*\|\s*分支(Branch)\s*\|\s*$", line):
1047
+ new_lines.append("| # | 日期(Date) | 标题(Title) | 提交(Commits) | 分支(Branch) |")
1045
1048
  continue
1046
1049
  if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*$", line):
1047
- new_lines.append("| # | Date | Title | Commits | Branch |")
1050
+ new_lines.append("| # | 日期(Date) | 标题(Title) | 提交(Commits) | 分支(Branch) |")
1048
1051
  continue
1049
1052
  if re.match(r"^\|[-| ]+\|\s*$", line) and not header_written:
1050
1053
  new_lines.append("|---|------|-------|---------|--------|")
@@ -33,7 +33,7 @@ DIR_SHELL_TICKETS = "shell-tickets"
33
33
  # platform that works today.
34
34
  DIR_LEGACY_CURSOR_SHELL_TICKETS = "cursor-shell"
35
35
  SHELL_TICKET_TTL_SECONDS = 30
36
- TASK_SESSION_COMMANDS = {"start", "current", "finish"}
36
+ TASK_SESSION_COMMANDS = {"start", "replan", "current", "finish"}
37
37
 
38
38
  _SESSION_KEYS = ("session_id", "sessionId", "sessionID")
39
39
  _CONVERSATION_KEYS = ("conversation_id", "conversationId", "conversationID")
@@ -425,7 +425,7 @@ def _pending_ticket_matches_args(ticket: dict[str, Any], repo_root: Path) -> boo
425
425
  continue
426
426
  if _string_value(subcommand.get("name")) != command_name:
427
427
  continue
428
- if command_name != "start":
428
+ if command_name not in {"start", "replan"}:
429
429
  return True
430
430
  task_ref = args[1] if len(args) > 1 else None
431
431
  if _task_refs_match(_string_value(subcommand.get("task_ref")), task_ref, repo_root):
@@ -694,7 +694,7 @@ def resolve_active_task(
694
694
  active = _active_from_ref(task_ref, repo_root, "session", context_key)
695
695
  if active:
696
696
  return active
697
- unbound = _resolve_unbound_task(repo_root)
697
+ unbound = _resolve_unbound_task(repo_root, context_key)
698
698
  if unbound is not None:
699
699
  return unbound
700
700
  return ActiveTask(None, "none", context_key)
@@ -736,8 +736,8 @@ def _resolve_single_session_fallback(repo_root: Path) -> ActiveTask | None:
736
736
  return _active_from_ref(task_ref, repo_root, "session-fallback", fallback_key)
737
737
 
738
738
 
739
- def _resolve_unbound_task(repo_root: Path) -> ActiveTask | None:
740
- """Expose one developer-owned task when no session pointer exists."""
739
+ def _resolve_unbound_task(repo_root: Path, context_key: str | None = None) -> ActiveTask | None:
740
+ """Expose read-only candidates without discarding a resolved identity."""
741
741
  sessions_dir = _runtime_sessions_dir(repo_root)
742
742
  if sessions_dir.is_dir():
743
743
  session_files = sorted(sessions_dir.glob("*.json"))
@@ -764,8 +764,8 @@ def _resolve_unbound_task(repo_root: Path) -> ActiveTask | None:
764
764
  for task in candidates
765
765
  ))
766
766
  if len(task_paths) == 1:
767
- return ActiveTask(task_paths[0], "unbound", None)
768
- return ActiveTask(None, "unbound_ambiguous", None, candidate_paths=task_paths)
767
+ return ActiveTask(task_paths[0], "unbound", context_key)
768
+ return ActiveTask(None, "unbound_ambiguous", context_key, candidate_paths=task_paths)
769
769
 
770
770
 
771
771
  def _utc_now() -> str:
@@ -834,7 +834,7 @@ def clear_active_task(
834
834
  return ActiveTask(None, "none")
835
835
 
836
836
  previous = resolve_active_task(repo_root, platform_input, platform)
837
- if not previous.task_path or not previous.context_key:
837
+ if previous.source_type != "session" or not previous.task_path or not previous.context_key:
838
838
  return previous
839
839
 
840
840
  context_path = _context_path(repo_root, previous.context_key)
@@ -78,10 +78,10 @@ def init_developer(name: str, repo_root: Path | None = None) -> bool:
78
78
  journal_file = workspace_dir / f"{FILE_JOURNAL_PREFIX}1.md"
79
79
  if not journal_file.exists():
80
80
  today = datetime.now().strftime("%Y-%m-%d")
81
- journal_content = f"""# Journal - {name} (Part 1)
81
+ journal_content = f"""# 日志(Journal)- {name}(第 1 部分)
82
82
 
83
- > AI development session journal
84
- > Started: {today}
83
+ > AI 开发会话日志
84
+ > 开始日期(Started): {today}
85
85
 
86
86
  ---
87
87
 
@@ -95,46 +95,46 @@ def init_developer(name: str, repo_root: Path | None = None) -> bool:
95
95
  # Create index.md with markers for auto-update
96
96
  index_file = workspace_dir / "index.md"
97
97
  if not index_file.exists():
98
- index_content = f"""# Workspace Index - {name}
98
+ index_content = f"""# 工作区索引(Workspace Index)- {name}
99
99
 
100
- > Journal tracking for AI development sessions.
100
+ > AI 开发会话跟踪日志。
101
101
 
102
102
  ---
103
103
 
104
- ## Current Status
104
+ ## 当前状态(Current Status)
105
105
 
106
106
  <!-- @@@auto:current-status -->
107
- - **Active File**: `journal-1.md`
108
- - **Total Sessions**: 0
109
- - **Last Active**: -
107
+ - **活动文件(Active File)**: `journal-1.md`
108
+ - **会话总数(Total Sessions)**: 0
109
+ - **最近活动(Last Active)**: -
110
110
  <!-- @@@/auto:current-status -->
111
111
 
112
112
  ---
113
113
 
114
- ## Active Documents
114
+ ## 活动文档(Active Documents)
115
115
 
116
116
  <!-- @@@auto:active-documents -->
117
- | File | Lines | Status |
117
+ | 文件(File) | 行数(Lines) | 状态(Status) |
118
118
  |------|-------|--------|
119
- | `journal-1.md` | ~0 | Active |
119
+ | `journal-1.md` | ~0 | 活动(Active) |
120
120
  <!-- @@@/auto:active-documents -->
121
121
 
122
122
  ---
123
123
 
124
- ## Session History
124
+ ## 会话历史(Session History)
125
125
 
126
126
  <!-- @@@auto:session-history -->
127
- | # | Date | Title | Commits | Branch |
127
+ | # | 日期(Date) | 标题(Title) | 提交(Commits) | 分支(Branch) |
128
128
  |---|------|-------|---------|--------|
129
129
  <!-- @@@/auto:session-history -->
130
130
 
131
131
  ---
132
132
 
133
- ## Notes
133
+ ## 备注(Notes)
134
134
 
135
- - Sessions are appended to journal files
136
- - New journal file created when current exceeds 2000 lines
137
- - Use `add_session.py` to record sessions
135
+ - 会话会追加到日志文件
136
+ - 当前日志超过 2000 行时创建新的日志文件
137
+ - 使用 `add_session.py` 记录会话
138
138
  """
139
139
  try:
140
140
  index_file.write_text(index_content, encoding="utf-8")
@@ -10,7 +10,7 @@ from datetime import datetime, timezone
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
 
13
- from .active_task import clear_active_task, clear_active_task_for_context, resolve_active_task, resolve_context_key, set_active_task
13
+ from .active_task import ActiveTask, clear_active_task, clear_active_task_for_context, resolve_active_task, resolve_context_key, set_active_task
14
14
  from .continuation_record import (
15
15
  ContinuationError,
16
16
  SAFE_ID,
@@ -83,6 +83,9 @@ def _direct_context(root: Path, require_task: bool = False) -> tuple[str, Any]:
83
83
  raise OwnershipError("session_fallback_untrusted")
84
84
  if active.context_key != context_key:
85
85
  raise OwnershipError("direct_session_identity_unstable")
86
+ if active.source_type in {"unbound", "unbound_ambiguous"}:
87
+ # Candidate paths are recovery context, never an owned session pointer.
88
+ active = ActiveTask(None, "none", context_key)
86
89
  if require_task and not active.task_path:
87
90
  raise OwnershipError("no_direct_current_task")
88
91
  return context_key, active
@@ -273,14 +276,14 @@ def retire(root: Path, task_id: str, handoff_id: str, core_digest: str, expected
273
276
  elif record["archive_observation"] != archive_observation:
274
277
  raise OwnershipError("archive observation does not match retirement")
275
278
 
276
- active = resolve_active_task(root, allow_single_session_fallback=False)
279
+ _, active = _direct_context(root)
277
280
  if active.context_key != actor:
278
281
  raise OwnershipError("direct_session_identity_unstable")
279
282
  if active.task_path == record["task"]["path"]:
280
283
  clear_active_task(root)
281
284
  elif active.task_path:
282
285
  raise OwnershipError("source task pointer changed during retirement")
283
- active = resolve_active_task(root, allow_single_session_fallback=False)
286
+ _, active = _direct_context(root)
284
287
  if active.context_key != actor or active.task_path:
285
288
  raise OwnershipError("source task retirement could not be verified; recovery_required")
286
289
  record["previous_event_digest"] = record["integrity"]["record_digest"]
@@ -334,7 +337,7 @@ def claim(root: Path, task_id: str, task_path: str, handoff_id: str, core_digest
334
337
  bound = set_active_task(record["task"]["path"], root)
335
338
  if not bound:
336
339
  raise OwnershipError("target task binding failed; recovery_required")
337
- active = resolve_active_task(root, allow_single_session_fallback=False)
340
+ _, active = _direct_context(root)
338
341
  if active.context_key != actor or active.task_path != record["task"]["path"]:
339
342
  raise OwnershipError("target task binding could not be verified; recovery_required")
340
343
  record["previous_event_digest"] = record["integrity"]["record_digest"]
@@ -361,7 +364,7 @@ def claim(root: Path, task_id: str, task_path: str, handoff_id: str, core_digest
361
364
  bound = set_active_task(task["path"], root)
362
365
  if not bound:
363
366
  raise OwnershipError("target task binding failed; recovery_required")
364
- active = resolve_active_task(root, allow_single_session_fallback=False)
367
+ _, active = _direct_context(root)
365
368
  if active.context_key != actor or active.task_path != task["path"]:
366
369
  raise OwnershipError("target task binding could not be verified; recovery_required")
367
370
  record["previous_event_digest"] = record["integrity"]["record_digest"]
@@ -416,17 +419,19 @@ def status(root: Path | None, task_id: str, handoff_id: str, core_digest: str) -
416
419
  def assert_task_mutation_allowed(root: Path, task_path: Path) -> None:
417
420
  """Reject ordinary task pointer changes that would bypass ownership fencing."""
418
421
  actor = resolve_context_key()
419
- if not actor:
420
- return
421
422
  try:
422
423
  task_id = _safe_task_id(task_path.name)
423
424
  except ContinuationError:
424
425
  return
425
426
  relative = task_path.relative_to(root).as_posix()
426
427
  records_dir = root / ".trellis" / ".runtime" / "handoff-ownership" / task_id
427
- if not records_dir.is_dir() or records_dir.is_symlink():
428
+ if records_dir.is_symlink():
429
+ raise OwnershipError("ownership record directory is unsafe")
430
+ if not records_dir.is_dir():
428
431
  return
429
432
  for record_path in sorted(records_dir.glob("*.json")):
433
+ if not actor:
434
+ raise OwnershipError("no_direct_session_identity: task has handoff ownership fencing")
430
435
  if record_path.is_symlink():
431
436
  raise OwnershipError("ownership record is unsafe")
432
437
  record = _decode(record_path)
@@ -145,12 +145,13 @@ def safe_archive_paths_to_add(
145
145
  repo_root: Path,
146
146
  task_name: str | None = None,
147
147
  modified_children: list[str] | None = None,
148
+ archive_dest: Path | None = None,
148
149
  ) -> list[str]:
149
150
  """Return paths to stage after `task.py archive`.
150
151
 
151
152
  Scoped to ONLY the paths the archive operation actually touched:
152
153
 
153
- - the archive subtree (where the freshly-moved task lives)
154
+ - the exact archive task directory (where the freshly-moved task lives)
154
155
  - the source task directory (for source-side deletes; caller pairs
155
156
  this with `git rm --cached` since `git add` won't stage deletes
156
157
  for a path that no longer exists in the working tree)
@@ -174,14 +175,42 @@ def safe_archive_paths_to_add(
174
175
  archive_dir = tasks_dir / DIR_ARCHIVE
175
176
 
176
177
  if task_name is not None:
177
- # Narrow scope — only paths that still exist on disk (so
178
- # `git add` doesn't choke on the moved-away source). The caller
179
- # handles the source-side deletes via `git rm --cached`
180
- # explicitly.
181
- if archive_dir.is_dir():
182
- paths.append(
183
- f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}"
184
- )
178
+ # Narrow scope — only the exact destination that the archive operation
179
+ # moved. The destination is passed by task_store, which already owns
180
+ # the move result. Validate it again so a caller cannot widen the
181
+ # auto-commit to the archive root or a sibling month/task directory.
182
+ candidate: Path | None = None
183
+ if archive_dest is not None:
184
+ candidate = archive_dest
185
+ if not candidate.is_absolute():
186
+ candidate = repo_root / candidate
187
+ elif archive_dir.is_dir():
188
+ # Compatibility for callers that only have a task name. An
189
+ # ambiguous historical layout fails closed instead of staging the
190
+ # whole archive tree.
191
+ matches = [
192
+ month_dir / task_name
193
+ for month_dir in sorted(archive_dir.iterdir())
194
+ if month_dir.is_dir() and (month_dir / task_name).is_dir()
195
+ ]
196
+ if len(matches) == 1:
197
+ candidate = matches[0]
198
+
199
+ if candidate is not None and candidate.is_dir():
200
+ try:
201
+ candidate_resolved = candidate.resolve()
202
+ archive_resolved = archive_dir.resolve()
203
+ if (
204
+ candidate_resolved.name == task_name
205
+ and candidate_resolved.parent.parent == archive_resolved
206
+ ):
207
+ relative = candidate_resolved.relative_to(repo_root.resolve())
208
+ paths.append(relative.as_posix())
209
+ except (OSError, RuntimeError, ValueError):
210
+ # An invalid or ambiguous destination must not widen scope.
211
+ pass
212
+ if not paths:
213
+ return []
185
214
  for child_name in modified_children or []:
186
215
  paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child_name}")
187
216
  return paths
@@ -0,0 +1,137 @@
1
+ """Task-local material plan revisions; chat approval remains coordinator-owned."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from hashlib import sha256
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ class PlanningError(ValueError):
12
+ """A planning contract needs correction before a task can start."""
13
+
14
+
15
+ def validate_meta_value(key: str, value: str) -> None:
16
+ if key == "planning" or key.startswith("planning."):
17
+ raise PlanningError("meta.planning is reserved; use task.py plan seal/approve")
18
+ choices = {
19
+ "execution_class": {"direct", "planned"},
20
+ "delivery_mode": {"change_bearing", "analysis_only"},
21
+ }
22
+ if key in choices and value not in choices[key]:
23
+ raise PlanningError(f"{key} must be one of: {', '.join(sorted(choices[key]))}")
24
+
25
+
26
+ def classification(data: dict[str, Any]) -> tuple[str, str]:
27
+ meta = data.get("meta", {})
28
+ if not isinstance(meta, dict):
29
+ raise PlanningError("task meta must be an object")
30
+ execution = meta.get("execution_class")
31
+ delivery = meta.get("delivery_mode")
32
+ if execution not in ("direct", "planned") or delivery not in ("change_bearing", "analysis_only"):
33
+ raise PlanningError("classify this task with execution_class=direct|planned and delivery_mode=change_bearing|analysis_only before start")
34
+ return execution, delivery
35
+
36
+
37
+ def planning_record(data: dict[str, Any]) -> dict[str, Any]:
38
+ meta = data.get("meta", {})
39
+ if not isinstance(meta, dict):
40
+ raise PlanningError("task meta must be an object")
41
+ record = meta.get("planning", {})
42
+ if not isinstance(record, dict):
43
+ raise PlanningError("meta.planning must be an object")
44
+ revision = record.get("revision", 1)
45
+ if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1:
46
+ raise PlanningError("planning revision must be a positive integer")
47
+ return {**record, "revision": revision}
48
+
49
+
50
+ def invalidate_plan(data: dict[str, Any]) -> None:
51
+ record = planning_record(data)
52
+ data.setdefault("meta", {})["planning"] = {"revision": record["revision"] + 1}
53
+
54
+
55
+ def _plan_documents(task_dir: Path, execution: str, delivery: str) -> list[str]:
56
+ names = ["prd.md"]
57
+ if execution == "planned":
58
+ names.append("design.md")
59
+ if delivery == "change_bearing":
60
+ names.append("implement.md")
61
+ for name in names:
62
+ document = task_dir / name
63
+ if document.is_symlink() or not document.is_file():
64
+ raise PlanningError(f"plan requires a regular task document: {name}")
65
+ if document.stat().st_size > 256 * 1024 or not document.read_text(encoding="utf-8").strip():
66
+ raise PlanningError(f"plan document must be bounded, non-empty UTF-8: {name}")
67
+ return names
68
+
69
+
70
+ def seal_plan(data: dict[str, Any], task_dir: Path) -> int:
71
+ execution, delivery = classification(data)
72
+ documents = _plan_documents(task_dir, execution, delivery)
73
+ record = planning_record(data)
74
+ # Resealing an already sealed plan declares a material new version. A replan
75
+ # already advanced the revision; its first seal keeps that revision.
76
+ revision = record["revision"] + (1 if record.get("seal") else 0)
77
+ data.setdefault("meta", {})["planning"] = {
78
+ "revision": revision,
79
+ "seal": {
80
+ "revision": revision,
81
+ "task_id": data.get("id") or task_dir.name,
82
+ "task_name": task_dir.name,
83
+ "documents": documents,
84
+ "document_digests": {name: sha256((task_dir / name).read_bytes()).hexdigest() for name in documents},
85
+ "execution_class": execution,
86
+ "delivery_mode": delivery,
87
+ "sealed_at": datetime.now(timezone.utc).isoformat(),
88
+ },
89
+ }
90
+ return revision
91
+
92
+
93
+ def _require_seal(data: dict[str, Any], task_dir: Path) -> dict[str, Any]:
94
+ execution, delivery = classification(data)
95
+ record = planning_record(data)
96
+ seal = record.get("seal")
97
+ if not isinstance(seal, dict) or type(seal.get("revision")) is not int or seal.get("revision") != record["revision"]:
98
+ raise PlanningError("current material plan is not sealed; use task.py plan seal")
99
+ if seal.get("task_id") != (data.get("id") or task_dir.name) or seal.get("task_name") != task_dir.name:
100
+ raise PlanningError("sealed plan belongs to another task")
101
+ if seal.get("execution_class") != execution or seal.get("delivery_mode") != delivery:
102
+ raise PlanningError("plan classification changed; reseal the material plan")
103
+ if seal.get("documents") != _plan_documents(task_dir, execution, delivery):
104
+ raise PlanningError("sealed plan documents do not match this task")
105
+ if seal.get("document_digests") != {name: sha256((task_dir / name).read_bytes()).hexdigest() for name in seal["documents"]}:
106
+ raise PlanningError("sealed plan content changed or lacks content digests; reseal the material plan")
107
+ return record
108
+
109
+
110
+ def approve_plan(data: dict[str, Any], task_dir: Path, revision: int, basis: str) -> None:
111
+ record = _require_seal(data, task_dir)
112
+ if revision != record["revision"]:
113
+ raise PlanningError(f"approval revision must match current revision {record['revision']}")
114
+ if not basis.strip() or len(basis) > 1024 or "\x00" in basis:
115
+ raise PlanningError("approval basis must be non-empty bounded text without credentials")
116
+ record.update({
117
+ "approved_revision": revision,
118
+ "approval_basis": basis.strip(),
119
+ "approved_at": datetime.now(timezone.utc).isoformat(),
120
+ })
121
+ data["meta"]["planning"] = record
122
+
123
+
124
+ def require_start_approval(data: dict[str, Any], task_dir: Path) -> None:
125
+ if data.get("status") not in {"planning", "in_progress"}:
126
+ raise PlanningError("start requires an active planning or in_progress task")
127
+ # Existing running tasks retain their authorization; only crossing the
128
+ # planning boundary is gated. Selecting a task never crosses that boundary.
129
+ if data.get("status") == "in_progress":
130
+ return
131
+ execution, delivery = classification(data)
132
+ if delivery == "analysis_only":
133
+ raise PlanningError("analysis_only tasks remain in planning and cannot be started")
134
+ if execution == "planned" and delivery == "change_bearing":
135
+ record = _require_seal(data, task_dir)
136
+ if type(record.get("approved_revision")) is not int or record.get("approved_revision") != record["revision"] or not record.get("approval_basis"):
137
+ raise PlanningError("current sealed plan has no matching later implementation approval; use task.py plan approve only after the user's approval")