@mindfoldhq/trellis 0.6.0-beta.4 → 0.6.0-beta.5
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/dist/migrations/manifests/0.5.10.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.5.json +9 -0
- package/dist/templates/common/commands/start.md +2 -0
- package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +39 -0
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +289 -43
- package/dist/templates/pi/extensions/trellis/index.ts.txt +179 -4
- package/dist/templates/pi/settings.json +9 -0
- package/dist/templates/trellis/index.d.ts +1 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +2 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +44 -24
- package/dist/templates/trellis/scripts/common/safe_commit.py +229 -0
- package/dist/templates/trellis/scripts/common/session_context.py +170 -0
- package/dist/templates/trellis/scripts/common/task_store.py +34 -5
- package/dist/utils/uninstall-scrubbers.d.ts +1 -0
- package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
- package/dist/utils/uninstall-scrubbers.js +21 -0
- package/dist/utils/uninstall-scrubbers.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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);
|
|
@@ -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;
|
|
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":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AAED,gCAAgC;AAChC,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC,qBAAqB,CAAC,CAAC;AAE/D,0BAA0B;AAC1B,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAC,4BAA4B,CAAC,CAAC;AACrE,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC,yBAAyB,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC,6BAA6B,CAAC,CAAC;AAC3E,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC,+BAA+B,CAAC,CAAC;AAC9E,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC,8BAA8B,CAAC,CAAC;AAC5E,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC,8BAA8B,CAAC,CAAC;AAC5E,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC,+BAA+B,CAAC,CAAC;AAC9E,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC,+BAA+B,CAAC,CAAC;AAC9E,MAAM,CAAC,MAAM,YAAY,GAAG,YAAY,CAAC,0BAA0B,CAAC,CAAC;AACrE,MAAM,CAAC,MAAM,QAAQ,GAAG,YAAY,CAAC,sBAAsB,CAAC,CAAC;AAC7D,MAAM,CAAC,MAAM,SAAS,GAAG,YAAY,CAAC,uBAAuB,CAAC,CAAC;AAC/D,MAAM,CAAC,MAAM,SAAS,GAAG,YAAY,CAAC,uBAAuB,CAAC,CAAC;AAC/D,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC,yBAAyB,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC,yBAAyB,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,YAAY,CAAC,gCAAgC,CAAC,CAAC;AAChF,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC,8BAA8B,CAAC,CAAC;AAC5E,MAAM,CAAC,MAAM,oBAAoB,GAAG,YAAY,CAC9C,mCAAmC,CACpC,CAAC;AACF,MAAM,CAAC,MAAM,qBAAqB,GAAG,YAAY,CAC/C,oCAAoC,CACrC,CAAC;AACF,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAC7C,kCAAkC,CACnC,CAAC;AACF,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAC7C,kCAAkC,CACnC,CAAC;
|
|
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,6 +36,11 @@ 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,
|
|
@@ -314,36 +318,52 @@ def update_index(
|
|
|
314
318
|
# =============================================================================
|
|
315
319
|
|
|
316
320
|
def _auto_commit_workspace(repo_root: Path) -> None:
|
|
317
|
-
"""Stage
|
|
321
|
+
"""Stage Trellis-owned workspace + task paths and commit.
|
|
322
|
+
|
|
323
|
+
Path scope is restricted to specific products (journal files, index.md,
|
|
324
|
+
active task dirs, the archive subtree). We never `git add` the whole
|
|
325
|
+
`.trellis/` tree, and if `.gitignore` blocks the specific paths we retry
|
|
326
|
+
with `git add -f <those-specific-paths>` — never `-f .trellis/`.
|
|
327
|
+
"""
|
|
318
328
|
commit_msg = get_session_commit_message(repo_root)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
)
|
|
325
|
-
if
|
|
326
|
-
|
|
327
|
-
|
|
329
|
+
paths = safe_trellis_paths_to_add(repo_root)
|
|
330
|
+
if not paths:
|
|
331
|
+
print("[OK] No workspace changes to commit.", file=sys.stderr)
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
success, used_force, err = safe_git_add(paths, repo_root)
|
|
335
|
+
if not success:
|
|
336
|
+
if err and "ignored by" in err.lower():
|
|
337
|
+
print_gitignore_warning(paths)
|
|
338
|
+
else:
|
|
339
|
+
print(
|
|
340
|
+
f"[WARN] git add failed: {err.strip() if err else 'unknown error'}",
|
|
341
|
+
file=sys.stderr,
|
|
342
|
+
)
|
|
328
343
|
return
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
344
|
+
|
|
345
|
+
if used_force:
|
|
346
|
+
print(
|
|
347
|
+
"[OK] Staged Trellis-owned paths with -f (specific paths, not .trellis/).",
|
|
348
|
+
file=sys.stderr,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Check if there are staged changes for the paths we just staged.
|
|
352
|
+
rc, _, _ = run_git(
|
|
353
|
+
["diff", "--cached", "--quiet", "--", *paths], cwd=repo_root
|
|
333
354
|
)
|
|
334
|
-
if
|
|
355
|
+
if rc == 0:
|
|
335
356
|
print("[OK] No workspace changes to commit.", file=sys.stderr)
|
|
336
357
|
return
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
capture_output=True,
|
|
341
|
-
text=True,
|
|
342
|
-
)
|
|
343
|
-
if commit_result.returncode == 0:
|
|
358
|
+
|
|
359
|
+
rc, _, commit_err = run_git(["commit", "-m", commit_msg], cwd=repo_root)
|
|
360
|
+
if rc == 0:
|
|
344
361
|
print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
|
|
345
362
|
else:
|
|
346
|
-
print(
|
|
363
|
+
print(
|
|
364
|
+
f"[WARN] Auto-commit failed: {commit_err.strip()}",
|
|
365
|
+
file=sys.stderr,
|
|
366
|
+
)
|
|
347
367
|
|
|
348
368
|
|
|
349
369
|
def add_session(
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Safe git-add helpers for Trellis-owned paths.
|
|
3
|
+
|
|
4
|
+
Why this module exists
|
|
5
|
+
----------------------
|
|
6
|
+
A real user incident: a project's `.gitignore` listed `.trellis/` (company-wide
|
|
7
|
+
template / personal habit). When `add_session.py` and `task.py archive` ran
|
|
8
|
+
their auto-commit and `git add` failed with `ignored by .gitignore`, the AI
|
|
9
|
+
agent driving the workflow "fixed" it by retrying with
|
|
10
|
+
`git add -f .trellis/` — which fan-out-included every ignored subtree
|
|
11
|
+
(`.trellis/.backup-*/`, `.trellis/worktrees/`, `.trellis/.template-hashes.json`,
|
|
12
|
+
`.trellis/.runtime/`), committing 548 files / 83474 lines of caches/backups.
|
|
13
|
+
|
|
14
|
+
Design
|
|
15
|
+
------
|
|
16
|
+
- Scripts only stage SPECIFIC product paths (journal files, index.md, the
|
|
17
|
+
current task dir, the archive dir). Never the whole `.trellis/` tree.
|
|
18
|
+
- If plain `git add <specific>` fails with "ignored by", retry with
|
|
19
|
+
`git add -f <specific>` — forcing only the paths the script knows it owns.
|
|
20
|
+
This is safe because the paths are narrow; it is NOT equivalent to
|
|
21
|
+
`git add -f .trellis/` (which would fan out to backups/worktrees/runtime).
|
|
22
|
+
- If the -f retry also fails, print an explicit warning that includes a
|
|
23
|
+
negative example: ``Do NOT use `git add -f .trellis/` ...``
|
|
24
|
+
|
|
25
|
+
The wider-grain forbidden command stays forbidden.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import sys
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from .git import run_git
|
|
34
|
+
from .paths import (
|
|
35
|
+
DIR_ARCHIVE,
|
|
36
|
+
DIR_TASKS,
|
|
37
|
+
DIR_WORKFLOW,
|
|
38
|
+
DIR_WORKSPACE,
|
|
39
|
+
FILE_JOURNAL_PREFIX,
|
|
40
|
+
get_developer,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Paths under .trellis/ that must NEVER be auto-staged. Listed here so the
|
|
45
|
+
# warning to the user can show concrete subpaths to ignore individually
|
|
46
|
+
# instead of ignoring the whole `.trellis/` tree.
|
|
47
|
+
TRELLIS_IGNORED_SUBPATHS = (
|
|
48
|
+
".trellis/.backup-*",
|
|
49
|
+
".trellis/worktrees/",
|
|
50
|
+
".trellis/.template-hashes.json",
|
|
51
|
+
".trellis/.runtime/",
|
|
52
|
+
".trellis/.cache/",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def safe_trellis_paths_to_add(repo_root: Path) -> list[str]:
|
|
57
|
+
"""Return the list of repo-relative paths the auto-commit should stage.
|
|
58
|
+
|
|
59
|
+
Only includes paths that exist on disk so callers don't pass non-existent
|
|
60
|
+
arguments to git. The caller is responsible for `git diff --cached`
|
|
61
|
+
checking afterwards.
|
|
62
|
+
|
|
63
|
+
Included:
|
|
64
|
+
- .trellis/workspace/<developer>/journal-*.md
|
|
65
|
+
- .trellis/workspace/<developer>/index.md
|
|
66
|
+
- .trellis/tasks/<task-dir>/ (every active task directory)
|
|
67
|
+
- .trellis/tasks/archive/ (whole archive subtree, if present)
|
|
68
|
+
|
|
69
|
+
Excluded (intentionally — these must not be staged):
|
|
70
|
+
- .trellis/.backup-*, .trellis/worktrees/,
|
|
71
|
+
.trellis/.template-hashes.json, .trellis/.runtime/, .trellis/.cache/
|
|
72
|
+
"""
|
|
73
|
+
paths: list[str] = []
|
|
74
|
+
|
|
75
|
+
# Workspace journal files + index.md
|
|
76
|
+
developer = get_developer(repo_root)
|
|
77
|
+
if developer:
|
|
78
|
+
ws = repo_root / DIR_WORKFLOW / DIR_WORKSPACE / developer
|
|
79
|
+
if ws.is_dir():
|
|
80
|
+
for f in sorted(ws.glob(f"{FILE_JOURNAL_PREFIX}*.md")):
|
|
81
|
+
if f.is_file():
|
|
82
|
+
paths.append(
|
|
83
|
+
f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{f.name}"
|
|
84
|
+
)
|
|
85
|
+
index_md = ws / "index.md"
|
|
86
|
+
if index_md.is_file():
|
|
87
|
+
paths.append(
|
|
88
|
+
f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/index.md"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Active tasks: each direct child of tasks/ that is a directory and not
|
|
92
|
+
# the archive root. The archive subtree is added as a single path below.
|
|
93
|
+
tasks_dir = repo_root / DIR_WORKFLOW / DIR_TASKS
|
|
94
|
+
if tasks_dir.is_dir():
|
|
95
|
+
for child in sorted(tasks_dir.iterdir()):
|
|
96
|
+
if not child.is_dir():
|
|
97
|
+
continue
|
|
98
|
+
if child.name == DIR_ARCHIVE:
|
|
99
|
+
continue
|
|
100
|
+
paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child.name}")
|
|
101
|
+
|
|
102
|
+
archive_dir = tasks_dir / DIR_ARCHIVE
|
|
103
|
+
if archive_dir.is_dir():
|
|
104
|
+
paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}")
|
|
105
|
+
|
|
106
|
+
return paths
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def safe_archive_paths_to_add(repo_root: Path) -> list[str]:
|
|
110
|
+
"""Return paths to stage after `task.py archive`.
|
|
111
|
+
|
|
112
|
+
Limited to the archive subtree (where the freshly-moved task lives) plus
|
|
113
|
+
the source task directory's parent area to capture the deletion in the
|
|
114
|
+
same commit. We pass the whole `.trellis/tasks/` path so deletions of the
|
|
115
|
+
pre-move path are tracked, but only as a SPECIFIC subpath — not the whole
|
|
116
|
+
`.trellis/` tree.
|
|
117
|
+
"""
|
|
118
|
+
paths: list[str] = []
|
|
119
|
+
tasks_dir = repo_root / DIR_WORKFLOW / DIR_TASKS
|
|
120
|
+
if tasks_dir.is_dir():
|
|
121
|
+
# The archive copy.
|
|
122
|
+
archive_dir = tasks_dir / DIR_ARCHIVE
|
|
123
|
+
if archive_dir.is_dir():
|
|
124
|
+
paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}")
|
|
125
|
+
# Active tasks (some may have been re-touched, e.g. parent's
|
|
126
|
+
# children list). This captures the source-path deletion too because
|
|
127
|
+
# `git add` on a directory records removals.
|
|
128
|
+
for child in sorted(tasks_dir.iterdir()):
|
|
129
|
+
if not child.is_dir():
|
|
130
|
+
continue
|
|
131
|
+
if child.name == DIR_ARCHIVE:
|
|
132
|
+
continue
|
|
133
|
+
paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child.name}")
|
|
134
|
+
return paths
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _stderr_indicates_ignored(stderr: str) -> bool:
|
|
138
|
+
"""git add error indicates the path is excluded by .gitignore."""
|
|
139
|
+
if not stderr:
|
|
140
|
+
return False
|
|
141
|
+
lowered = stderr.lower()
|
|
142
|
+
return "ignored by" in lowered
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def safe_git_add(
|
|
146
|
+
paths: list[str], repo_root: Path
|
|
147
|
+
) -> tuple[bool, bool, str]:
|
|
148
|
+
"""Run `git add` on specific paths, retrying with -f if .gitignore blocks.
|
|
149
|
+
|
|
150
|
+
Returns (success, used_force, stderr). On success, callers should still
|
|
151
|
+
`git diff --cached` to detect whether anything was actually staged.
|
|
152
|
+
|
|
153
|
+
Behavior:
|
|
154
|
+
- No paths passed → success, no force, empty stderr.
|
|
155
|
+
- Plain `git add <paths>` succeeds → return.
|
|
156
|
+
- Plain fails with "ignored by" → retry with `git add -f <paths>`.
|
|
157
|
+
- Retry succeeds → return success with used_force=True.
|
|
158
|
+
- Retry fails → return failure; caller should print the gitignore
|
|
159
|
+
warning (see :func:`print_gitignore_warning`).
|
|
160
|
+
- Plain fails with a non-ignored error → return failure; do NOT retry
|
|
161
|
+
with -f (we only force when ignore is the cause).
|
|
162
|
+
"""
|
|
163
|
+
if not paths:
|
|
164
|
+
return True, False, ""
|
|
165
|
+
|
|
166
|
+
rc, _, err = run_git(["add", "--", *paths], cwd=repo_root)
|
|
167
|
+
if rc == 0:
|
|
168
|
+
return True, False, ""
|
|
169
|
+
|
|
170
|
+
if not _stderr_indicates_ignored(err):
|
|
171
|
+
return False, False, err
|
|
172
|
+
|
|
173
|
+
rc2, _, err2 = run_git(["add", "-f", "--", *paths], cwd=repo_root)
|
|
174
|
+
if rc2 == 0:
|
|
175
|
+
return True, True, err2 or err
|
|
176
|
+
return False, True, err2 or err
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def print_gitignore_warning(paths: list[str]) -> None:
|
|
180
|
+
"""Explain to the user (and any AI reading the log) what to do.
|
|
181
|
+
|
|
182
|
+
CRITICAL: includes the negative example
|
|
183
|
+
``Do NOT use `git add -f .trellis/``` — agents reading the warning are
|
|
184
|
+
known to invent that command, which fans out to ignored caches/backups.
|
|
185
|
+
"""
|
|
186
|
+
print(
|
|
187
|
+
"[WARN] git add failed because .trellis/ paths are ignored by your .gitignore.",
|
|
188
|
+
file=sys.stderr,
|
|
189
|
+
)
|
|
190
|
+
print(
|
|
191
|
+
"[WARN] Trellis manages these specific paths and they should be tracked:",
|
|
192
|
+
file=sys.stderr,
|
|
193
|
+
)
|
|
194
|
+
if paths:
|
|
195
|
+
for p in paths:
|
|
196
|
+
print(f"[WARN] {p}", file=sys.stderr)
|
|
197
|
+
else:
|
|
198
|
+
print(
|
|
199
|
+
"[WARN] .trellis/workspace/<developer>/{journal-*.md,index.md}",
|
|
200
|
+
file=sys.stderr,
|
|
201
|
+
)
|
|
202
|
+
print(
|
|
203
|
+
"[WARN] .trellis/tasks/<task-dir>/",
|
|
204
|
+
file=sys.stderr,
|
|
205
|
+
)
|
|
206
|
+
print(
|
|
207
|
+
"[WARN] .trellis/tasks/archive/",
|
|
208
|
+
file=sys.stderr,
|
|
209
|
+
)
|
|
210
|
+
print("[WARN]", file=sys.stderr)
|
|
211
|
+
print(
|
|
212
|
+
"[WARN] Recommended: change your .gitignore from `.trellis/` to specific",
|
|
213
|
+
file=sys.stderr,
|
|
214
|
+
)
|
|
215
|
+
print(
|
|
216
|
+
"[WARN] subpaths that should remain ignored, e.g.:",
|
|
217
|
+
file=sys.stderr,
|
|
218
|
+
)
|
|
219
|
+
for sub in TRELLIS_IGNORED_SUBPATHS:
|
|
220
|
+
print(f"[WARN] {sub}", file=sys.stderr)
|
|
221
|
+
print("[WARN]", file=sys.stderr)
|
|
222
|
+
print(
|
|
223
|
+
"[WARN] Do NOT use `git add -f .trellis/` — it pulls in backups, worktrees,",
|
|
224
|
+
file=sys.stderr,
|
|
225
|
+
)
|
|
226
|
+
print(
|
|
227
|
+
"[WARN] and runtime caches that should never be committed.",
|
|
228
|
+
file=sys.stderr,
|
|
229
|
+
)
|