@mindfoldhq/trellis 0.6.0-beta.4 → 0.6.0-beta.6

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.
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import { delimiter, dirname, join, resolve } from "node:path";
4
- import { spawn } from "node:child_process";
4
+ import { spawn, spawnSync } from "node:child_process";
5
5
 
6
6
  type JsonObject = Record<string, unknown>;
7
7
  type TextContent = { type: "text"; text: string };
@@ -632,6 +632,166 @@ function buildTrellisContext(
632
632
  ].join("\n");
633
633
  }
634
634
 
635
+ // ---------------------------------------------------------------------------
636
+ // Workflow-state breadcrumb (TypeScript port of the shared workflow-state
637
+ // hook used by class-1 platforms).
638
+ //
639
+ // Pi is extension-backed and MUST NOT receive Python hook scripts under .pi/.
640
+ // We therefore parse `.trellis/workflow.md` `[workflow-state:STATUS]...
641
+ // [/workflow-state:STATUS]` blocks directly in TypeScript and emit the
642
+ // per-turn `<workflow-state>` breadcrumb in `before_agent_start` and `input`.
643
+ // Tag regex mirrors the shared parser so the breadcrumb body stays
644
+ // byte-identical with hook-driven platforms.
645
+ // ---------------------------------------------------------------------------
646
+
647
+ const WORKFLOW_STATE_TAG_RE =
648
+ /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
649
+
650
+ function loadWorkflowBreadcrumbs(projectRoot: string): Record<string, string> {
651
+ const workflow = readText(join(projectRoot, ".trellis", "workflow.md"));
652
+ if (!workflow) return {};
653
+ const result: Record<string, string> = {};
654
+ for (const match of workflow.matchAll(WORKFLOW_STATE_TAG_RE)) {
655
+ const status = match[1] ?? "";
656
+ const body = (match[2] ?? "").trim();
657
+ if (status && body) result[status] = body;
658
+ }
659
+ return result;
660
+ }
661
+
662
+ function readActiveTaskStatus(
663
+ projectRoot: string,
664
+ taskDir: string,
665
+ ): { taskId: string; status: string } | null {
666
+ try {
667
+ const data = JSON.parse(
668
+ readText(join(taskDir, "task.json")),
669
+ ) as JsonObject;
670
+ const status = stringValue(data.status);
671
+ if (!status) return null;
672
+ const id = stringValue(data.id) ?? taskDir.split(/[\\/]/).pop() ?? "";
673
+ return { taskId: id, status };
674
+ } catch {
675
+ return null;
676
+ }
677
+ }
678
+
679
+ function buildWorkflowStateBreadcrumb(
680
+ projectRoot: string,
681
+ contextKey: string | null,
682
+ ): string {
683
+ const templates = loadWorkflowBreadcrumbs(projectRoot);
684
+ const taskDir = readCurrentTask(
685
+ projectRoot,
686
+ undefined,
687
+ undefined,
688
+ contextKey,
689
+ );
690
+ let header: string;
691
+ let lookupKey: string;
692
+ if (!taskDir) {
693
+ header = "Status: no_task\nSource: session";
694
+ lookupKey = "no_task";
695
+ } else {
696
+ const info = readActiveTaskStatus(projectRoot, taskDir);
697
+ if (!info) {
698
+ header = "Status: no_task\nSource: session";
699
+ lookupKey = "no_task";
700
+ } else {
701
+ header = `Task: ${info.taskId} (${info.status})\nSource: session`;
702
+ lookupKey = info.status;
703
+ }
704
+ }
705
+ const body = templates[lookupKey] ?? "Refer to workflow.md for current step.";
706
+ return `<workflow-state>\n${header}\n${body}\n</workflow-state>`;
707
+ }
708
+
709
+ // ---------------------------------------------------------------------------
710
+ // Session overview (developer / git branch / active tasks)
711
+ //
712
+ // Spawns `python3 .trellis/scripts/get_context.py` (the same script other
713
+ // platform session-start hooks invoke) to keep developer/git/active-task
714
+ // summary byte-identical with class-1 platforms. Failure is non-fatal — we
715
+ // emit an empty overview rather than block the conversation.
716
+ // ---------------------------------------------------------------------------
717
+
718
+ const SESSION_OVERVIEW_TIMEOUT_MS = 5000;
719
+
720
+ function pythonExecutable(): string {
721
+ const override = stringValue(process.env.TRELLIS_PYTHON);
722
+ if (override) return override;
723
+ return process.platform === "win32" ? "python" : "python3";
724
+ }
725
+
726
+ function buildSessionOverview(
727
+ projectRoot: string,
728
+ contextKey: string | null,
729
+ ): string {
730
+ const script = join(projectRoot, ".trellis", "scripts", "get_context.py");
731
+ if (!isExistingFile(script)) return "";
732
+ try {
733
+ const result = spawnSync(pythonExecutable(), [script], {
734
+ cwd: projectRoot,
735
+ env: contextKey
736
+ ? { ...process.env, TRELLIS_CONTEXT_ID: contextKey }
737
+ : process.env,
738
+ encoding: "utf-8",
739
+ timeout: SESSION_OVERVIEW_TIMEOUT_MS,
740
+ windowsHide: true,
741
+ });
742
+ if (result.status !== 0) return "";
743
+ const stdout = (result.stdout ?? "").trim();
744
+ if (!stdout) return "";
745
+ return `<session-overview>\n${stdout}\n</session-overview>`;
746
+ } catch {
747
+ return "";
748
+ }
749
+ }
750
+
751
+ // Per-turn cache so input + before_agent_start in the same turn don't double-spawn.
752
+ class TurnContextCache {
753
+ private key: string | null = null;
754
+ private timestamp = 0;
755
+ private workflowState = "";
756
+ private sessionOverview = "";
757
+ // Refresh window: per-turn injections that fire close together share a
758
+ // single python3 spawn; anything older than this re-runs the resolver.
759
+ private static readonly TTL_MS = 1500;
760
+
761
+ get(
762
+ projectRoot: string,
763
+ contextKey: string | null,
764
+ ): { workflowState: string; sessionOverview: string } {
765
+ const now = Date.now();
766
+ if (this.key === contextKey && now - this.timestamp < TurnContextCache.TTL_MS) {
767
+ return {
768
+ workflowState: this.workflowState,
769
+ sessionOverview: this.sessionOverview,
770
+ };
771
+ }
772
+ this.workflowState = buildWorkflowStateBreadcrumb(projectRoot, contextKey);
773
+ this.sessionOverview = buildSessionOverview(projectRoot, contextKey);
774
+ this.key = contextKey;
775
+ this.timestamp = now;
776
+ return {
777
+ workflowState: this.workflowState,
778
+ sessionOverview: this.sessionOverview,
779
+ };
780
+ }
781
+ }
782
+
783
+ // ---------------------------------------------------------------------------
784
+ // Sub-agent dispatch protocol snippet (registered with the `subagent` tool).
785
+ // Mirrors the [workflow-state:in_progress] dispatch protocol text in
786
+ // trellis/workflow.md so the AI sees the same `Active task: <path>` rule
787
+ // whether it reads workflow.md, the per-turn breadcrumb, or the tool prompt.
788
+ // ---------------------------------------------------------------------------
789
+
790
+ const SUBAGENT_DISPATCH_PROTOCOL = `Sub-agent dispatch protocol (Trellis): your dispatch prompt MUST start with one line "Active task: <task path from \`task.py current\`>" before any other instructions. No exceptions. On class-2 platforms (codex / copilot / gemini / qoder) the sub-agent depends on this line because there is no hook to inject task context. On class-1 platforms (claude / cursor / opencode / kiro / codebuddy / droid) and on Pi, the line is the canonical fallback when hook/extension injection misses. trellis-research uses the line to know which {task_dir}/research/ to write into.
791
+
792
+ Wrong: prompt: "implement the new feature"
793
+ Correct: prompt: "Active task: .trellis/tasks/05-09-pi-workflow-state-injection\\n\\nImplement the new feature ..."`;
794
+
635
795
  function normalizeAgentName(agent: string): string {
636
796
  return agent.startsWith("trellis-") ? agent : `trellis-${agent}`;
637
797
  }
@@ -886,6 +1046,15 @@ export default function trellisExtension(pi: {
886
1046
  const projectRoot = findProjectRoot(pi.cwd ?? process.cwd());
887
1047
  const processContextKey = createProcessContextKey(projectRoot);
888
1048
  let currentContextKey: string | null = null;
1049
+ const turnContextCache = new TurnContextCache();
1050
+
1051
+ const buildPerTurnInjection = (contextKey: string | null): string => {
1052
+ const { workflowState, sessionOverview } = turnContextCache.get(
1053
+ projectRoot,
1054
+ contextKey,
1055
+ );
1056
+ return [workflowState, sessionOverview].filter(Boolean).join("\n\n");
1057
+ };
889
1058
 
890
1059
  const getContextKey = (input?: unknown, ctx?: PiExtensionContext): string => {
891
1060
  const resolvedContextKey = resolveContextKey(
@@ -904,6 +1073,8 @@ export default function trellisExtension(pi: {
904
1073
  name: "subagent",
905
1074
  label: "Subagent",
906
1075
  description: "Run a Trellis project sub-agent with active task context.",
1076
+ promptSnippet: SUBAGENT_DISPATCH_PROTOCOL,
1077
+ promptGuidelines: SUBAGENT_DISPATCH_PROTOCOL,
907
1078
  parameters: {
908
1079
  type: "object",
909
1080
  properties: {
@@ -976,8 +1147,9 @@ export default function trellisExtension(pi: {
976
1147
  ctx,
977
1148
  contextKey,
978
1149
  );
1150
+ const perTurn = buildPerTurnInjection(contextKey);
979
1151
  return {
980
- systemPrompt: [current, context].filter(Boolean).join("\n\n"),
1152
+ systemPrompt: [current, context, perTurn].filter(Boolean).join("\n\n"),
981
1153
  };
982
1154
  });
983
1155
  pi.on?.("context", (event, ctx) => {
@@ -986,8 +1158,11 @@ export default function trellisExtension(pi: {
986
1158
  return Array.isArray(messages) ? { messages } : undefined;
987
1159
  });
988
1160
  pi.on?.("input", (event, ctx) => {
989
- getContextKey(event, ctx);
990
- return { action: "continue" };
1161
+ const contextKey = getContextKey(event, ctx);
1162
+ const additionalContext = buildPerTurnInjection(contextKey);
1163
+ return additionalContext
1164
+ ? { action: "continue", additionalContext, systemPrompt: additionalContext }
1165
+ : { action: "continue" };
991
1166
  });
992
1167
  pi.on?.("tool_call", (event, ctx) => {
993
1168
  const contextKey = getContextKey(event, ctx);
@@ -8,5 +8,14 @@
8
8
  ],
9
9
  "prompts": [
10
10
  "./prompts"
11
+ ],
12
+ "packages": [
13
+ {
14
+ "source": "npm:pi-subagents",
15
+ "extensions": [],
16
+ "skills": [],
17
+ "prompts": [],
18
+ "themes": []
19
+ }
11
20
  ]
12
21
  }
@@ -14,6 +14,24 @@ session_commit_message: "chore: record journal"
14
14
  # Maximum lines per journal file before rotating to a new one
15
15
  max_journal_lines: 2000
16
16
 
17
+ #-------------------------------------------------------------------------------
18
+ # Session Auto-Commit
19
+ #-------------------------------------------------------------------------------
20
+
21
+ # Auto-commit behavior for session journal + task archive operations.
22
+ # - true (default): scripts auto-stage and auto-commit journal / task changes
23
+ # after add_session.py / task.py archive runs.
24
+ # - false: scripts do not touch git. Files (journal-*.md, task archive moves)
25
+ # are still written to disk; you decide whether to git add / commit.
26
+ #
27
+ # Use `false` if your project's .gitignore intentionally excludes `.trellis/`
28
+ # and you want session data kept local-only, or if you prefer to review
29
+ # staged changes manually before each commit.
30
+ #
31
+ # Accepts: true / false / yes / no / 1 / 0 / on / off (case-insensitive).
32
+ #
33
+ # session_auto_commit: true
34
+
17
35
  #-------------------------------------------------------------------------------
18
36
  # Task Lifecycle Hooks
19
37
  #-------------------------------------------------------------------------------
@@ -36,6 +36,7 @@ export declare const commonSessionContext: string;
36
36
  export declare const commonPackagesContext: string;
37
37
  export declare const commonWorkflowPhase: string;
38
38
  export declare const commonTrellisConfig: string;
39
+ export declare const commonSafeCommit: string;
39
40
  export declare const getDeveloperScript: string;
40
41
  export declare const initDeveloperScript: string;
41
42
  export declare const taskScript: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AACrE,eAAO,MAAM,QAAQ,QAAuC,CAAC;AAC7D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AAGF,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AAGvE,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAgC,CAAC;AAE/D;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAoCnD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AACrE,eAAO,MAAM,QAAQ,QAAuC,CAAC;AAC7D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAG9E,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AAGvE,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAgC,CAAC;AAE/D;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAqCnD"}
@@ -46,6 +46,7 @@ export const commonSessionContext = readTemplate("scripts/common/session_context
46
46
  export const commonPackagesContext = readTemplate("scripts/common/packages_context.py");
47
47
  export const commonWorkflowPhase = readTemplate("scripts/common/workflow_phase.py");
48
48
  export const commonTrellisConfig = readTemplate("scripts/common/trellis_config.py");
49
+ export const commonSafeCommit = readTemplate("scripts/common/safe_commit.py");
49
50
  // Python scripts - main
50
51
  export const getDeveloperScript = readTemplate("scripts/get_developer.py");
51
52
  export const initDeveloperScript = readTemplate("scripts/init_developer.py");
@@ -84,6 +85,7 @@ export function getAllScripts() {
84
85
  scripts.set("common/packages_context.py", commonPackagesContext);
85
86
  scripts.set("common/workflow_phase.py", commonWorkflowPhase);
86
87
  scripts.set("common/trellis_config.py", commonTrellisConfig);
88
+ scripts.set("common/safe_commit.py", commonSafeCommit);
87
89
  // Main
88
90
  scripts.set("get_developer.py", getDeveloperScript);
89
91
  scripts.set("init_developer.py", initDeveloperScript);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"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"}
@@ -23,7 +23,6 @@ from __future__ import annotations
23
23
 
24
24
  import argparse
25
25
  import re
26
- import subprocess
27
26
  import sys
28
27
  from datetime import datetime
29
28
  from pathlib import Path
@@ -37,9 +36,15 @@ from common.paths import (
37
36
  )
38
37
  from common.developer import ensure_developer
39
38
  from common.git import run_git
39
+ from common.safe_commit import (
40
+ print_gitignore_warning,
41
+ safe_git_add,
42
+ safe_trellis_paths_to_add,
43
+ )
40
44
  from common.tasks import load_task
41
45
  from common.config import (
42
46
  get_packages,
47
+ get_session_auto_commit,
43
48
  get_session_commit_message,
44
49
  get_max_journal_lines,
45
50
  is_monorepo,
@@ -314,36 +319,57 @@ def update_index(
314
319
  # =============================================================================
315
320
 
316
321
  def _auto_commit_workspace(repo_root: Path) -> None:
317
- """Stage .trellis/workspace and .trellis/tasks, then commit with a configured message."""
322
+ """Stage Trellis-owned workspace + task paths and commit.
323
+
324
+ Path scope is restricted to specific products (journal files, index.md,
325
+ active task dirs, the archive subtree). We never `git add` the whole
326
+ `.trellis/` tree, and if `.gitignore` blocks the specific paths we
327
+ warn + skip — never retry with ``-f``.
328
+
329
+ Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when set to
330
+ ``false``, this function returns immediately without touching git
331
+ (journal/index files are still written to disk by the caller).
332
+ """
333
+ if not get_session_auto_commit(repo_root):
334
+ print(
335
+ "[OK] session_auto_commit: false — skipping git stage/commit.",
336
+ file=sys.stderr,
337
+ )
338
+ return
339
+
318
340
  commit_msg = get_session_commit_message(repo_root)
319
- add_result = subprocess.run(
320
- ["git", "add", "-A", ".trellis/workspace", ".trellis/tasks"],
321
- cwd=repo_root,
322
- capture_output=True,
323
- text=True,
324
- )
325
- if add_result.returncode != 0:
326
- print(f"[WARN] git add failed (exit {add_result.returncode}): {add_result.stderr.strip()}", file=sys.stderr)
327
- print("[WARN] Please commit .trellis/ changes manually: git add .trellis && git commit", file=sys.stderr)
341
+ paths = safe_trellis_paths_to_add(repo_root)
342
+ if not paths:
343
+ print("[OK] No workspace changes to commit.", file=sys.stderr)
344
+ return
345
+
346
+ success, _, err = safe_git_add(paths, repo_root)
347
+ if not success:
348
+ if err and "ignored by" in err.lower():
349
+ print_gitignore_warning(paths)
350
+ else:
351
+ print(
352
+ f"[WARN] git add failed: {err.strip() if err else 'unknown error'}",
353
+ file=sys.stderr,
354
+ )
328
355
  return
329
- # Check if there are staged changes
330
- result = subprocess.run(
331
- ["git", "diff", "--cached", "--quiet", "--", ".trellis/workspace", ".trellis/tasks"],
332
- cwd=repo_root,
356
+
357
+ # Check if there are staged changes for the paths we just staged.
358
+ rc, _, _ = run_git(
359
+ ["diff", "--cached", "--quiet", "--", *paths], cwd=repo_root
333
360
  )
334
- if result.returncode == 0:
361
+ if rc == 0:
335
362
  print("[OK] No workspace changes to commit.", file=sys.stderr)
336
363
  return
337
- commit_result = subprocess.run(
338
- ["git", "commit", "-m", commit_msg],
339
- cwd=repo_root,
340
- capture_output=True,
341
- text=True,
342
- )
343
- if commit_result.returncode == 0:
364
+
365
+ rc, _, commit_err = run_git(["commit", "-m", commit_msg], cwd=repo_root)
366
+ if rc == 0:
344
367
  print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
345
368
  else:
346
- print(f"[WARN] Auto-commit failed: {commit_result.stderr.strip()}", file=sys.stderr)
369
+ print(
370
+ f"[WARN] Auto-commit failed: {commit_err.strip()}",
371
+ file=sys.stderr,
372
+ )
347
373
 
348
374
 
349
375
  def add_session(
@@ -36,6 +36,29 @@ def _unquote(s: str) -> str:
36
36
  return s
37
37
 
38
38
 
39
+ def _strip_inline_comment(value: str) -> str:
40
+ """Strip ` # …` inline comments while preserving `#` inside quoted strings.
41
+
42
+ YAML treats ` #` (space-hash) as a comment opener; bare `#` inside a token
43
+ is part of the value. Quoted strings are immune.
44
+
45
+ Mirrors :func:`common.trellis_config._strip_inline_comment` so both
46
+ parsers handle ``key: value # comment`` identically.
47
+ """
48
+ in_quote: str | None = None
49
+ for idx, ch in enumerate(value):
50
+ if in_quote:
51
+ if ch == in_quote:
52
+ in_quote = None
53
+ continue
54
+ if ch in ('"', "'"):
55
+ in_quote = ch
56
+ continue
57
+ if ch == "#" and (idx == 0 or value[idx - 1].isspace()):
58
+ return value[:idx]
59
+ return value
60
+
61
+
39
62
  def parse_simple_yaml(content: str) -> dict:
40
63
  """Parse simple YAML with nested dict support (no dependencies).
41
64
 
@@ -93,7 +116,8 @@ def _parse_yaml_block(
93
116
  elif ":" in stripped:
94
117
  key, _, value = stripped.partition(":")
95
118
  key = key.strip()
96
- value = _unquote(value.strip())
119
+ value = _strip_inline_comment(value).strip()
120
+ value = _unquote(value)
97
121
  current_list = None
98
122
 
99
123
  if value:
@@ -142,6 +166,7 @@ def _next_content_line(lines: list[str], start: int) -> tuple[int, str]:
142
166
  # Defaults
143
167
  DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal"
144
168
  DEFAULT_MAX_JOURNAL_LINES = 2000
169
+ DEFAULT_SESSION_AUTO_COMMIT = True
145
170
 
146
171
  CONFIG_FILE = "config.yaml"
147
172
 
@@ -187,6 +212,37 @@ def get_max_journal_lines(repo_root: Path | None = None) -> int:
187
212
  return DEFAULT_MAX_JOURNAL_LINES
188
213
 
189
214
 
215
+ def get_session_auto_commit(repo_root: Path | None = None) -> bool:
216
+ """Whether scripts should auto-stage + auto-commit session/task changes.
217
+
218
+ Governs both ``add_session.py:_auto_commit_workspace`` and
219
+ ``task_store.py:_auto_commit_archive``.
220
+
221
+ Default: ``True`` (existing behavior — auto-stage + auto-commit).
222
+ Set ``session_auto_commit: false`` in ``.trellis/config.yaml`` to skip
223
+ auto-staging entirely; the journal/archive files are still written to
224
+ disk, but the user manages ``git add`` / ``git commit`` themselves.
225
+
226
+ Accepts native YAML booleans (``true`` / ``false``) and the string
227
+ aliases ``true / false / yes / no / 1 / 0 / on / off`` (case-insensitive).
228
+ Invalid values fall back to ``True`` with a stderr warning.
229
+ """
230
+ config = _load_config(repo_root)
231
+ raw = config.get("session_auto_commit", DEFAULT_SESSION_AUTO_COMMIT)
232
+ if isinstance(raw, bool):
233
+ return raw
234
+ s = str(raw).strip().lower()
235
+ if s in ("true", "yes", "1", "on"):
236
+ return True
237
+ if s in ("false", "no", "0", "off"):
238
+ return False
239
+ print(
240
+ f"[WARN] invalid session_auto_commit value: {raw!r}; using true (default)",
241
+ file=sys.stderr,
242
+ )
243
+ return DEFAULT_SESSION_AUTO_COMMIT
244
+
245
+
190
246
  def get_hooks(event: str, repo_root: Path | None = None) -> list[str]:
191
247
  """Get hook commands for a lifecycle event.
192
248