@mindfoldhq/trellis 0.5.0-beta.8 → 0.5.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -95
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +474 -210
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +295 -54
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/antigravity.d.ts.map +1 -1
- package/dist/configurators/antigravity.js +2 -8
- package/dist/configurators/antigravity.js.map +1 -1
- package/dist/configurators/claude.d.ts.map +1 -1
- package/dist/configurators/claude.js +4 -10
- package/dist/configurators/claude.js.map +1 -1
- package/dist/configurators/codebuddy.d.ts.map +1 -1
- package/dist/configurators/codebuddy.js +3 -3
- package/dist/configurators/codebuddy.js.map +1 -1
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +5 -13
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/copilot.d.ts.map +1 -1
- package/dist/configurators/copilot.js +5 -19
- package/dist/configurators/copilot.js.map +1 -1
- package/dist/configurators/cursor.d.ts.map +1 -1
- package/dist/configurators/cursor.js +3 -3
- package/dist/configurators/cursor.js.map +1 -1
- package/dist/configurators/droid.d.ts.map +1 -1
- package/dist/configurators/droid.js +3 -3
- package/dist/configurators/droid.js.map +1 -1
- package/dist/configurators/gemini.d.ts.map +1 -1
- package/dist/configurators/gemini.js +3 -5
- package/dist/configurators/gemini.js.map +1 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +44 -55
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/kilo.d.ts.map +1 -1
- package/dist/configurators/kilo.js +2 -8
- package/dist/configurators/kilo.js.map +1 -1
- package/dist/configurators/kiro.d.ts.map +1 -1
- package/dist/configurators/kiro.js +3 -3
- package/dist/configurators/kiro.js.map +1 -1
- package/dist/configurators/opencode.d.ts.map +1 -1
- package/dist/configurators/opencode.js +7 -4
- package/dist/configurators/opencode.js.map +1 -1
- package/dist/configurators/pi.d.ts +3 -0
- package/dist/configurators/pi.d.ts.map +1 -0
- package/dist/configurators/pi.js +44 -0
- package/dist/configurators/pi.js.map +1 -0
- package/dist/configurators/qoder.d.ts +7 -6
- package/dist/configurators/qoder.d.ts.map +1 -1
- package/dist/configurators/qoder.js +18 -12
- package/dist/configurators/qoder.js.map +1 -1
- package/dist/configurators/shared.d.ts +30 -6
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +65 -15
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/windsurf.d.ts.map +1 -1
- package/dist/configurators/windsurf.js +2 -8
- package/dist/configurators/windsurf.js.map +1 -1
- package/dist/constants/paths.d.ts +2 -0
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +2 -0
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.5.0-beta.0.json +2 -0
- package/dist/migrations/manifests/0.5.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.11.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.12.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.13.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.14.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.15.json +116 -0
- package/dist/migrations/manifests/0.5.0-beta.16.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.17.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.18.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.19.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.5.json +2 -0
- package/dist/migrations/manifests/0.5.0-beta.9.json +48 -0
- package/dist/migrations/manifests/0.5.0-rc.0.json +9 -0
- package/dist/templates/claude/agents/trellis-research.md +1 -1
- package/dist/templates/claude/settings.json +0 -4
- package/dist/templates/codebuddy/agents/trellis-research.md +1 -1
- package/dist/templates/codex/agents/trellis-research.toml +3 -2
- package/dist/templates/codex/hooks/session-start.py +126 -26
- package/dist/templates/codex/skills/finish-work/SKILL.md +41 -109
- package/dist/templates/codex/skills/start/SKILL.md +12 -9
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +73 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +83 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +54 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +81 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-hooks.md +57 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +78 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +83 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +90 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +64 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +55 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +68 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +80 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/overview.md +51 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +102 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +101 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +75 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workspace-memory.md +71 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +79 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +69 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +59 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +74 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +83 -0
- package/dist/templates/common/commands/continue.md +9 -5
- package/dist/templates/common/commands/finish-work.md +34 -10
- package/dist/templates/common/index.d.ts +22 -2
- package/dist/templates/common/index.d.ts.map +1 -1
- package/dist/templates/common/index.js +53 -4
- package/dist/templates/common/index.js.map +1 -1
- package/dist/templates/common/skills/brainstorm.md +50 -4
- package/dist/templates/copilot/hooks/session-start.py +127 -30
- package/dist/templates/copilot/prompts/finish-work.prompt.md +44 -112
- package/dist/templates/copilot/prompts/start.prompt.md +12 -9
- package/dist/templates/cursor/agents/trellis-check.md +1 -1
- package/dist/templates/cursor/agents/trellis-implement.md +1 -1
- package/dist/templates/cursor/agents/trellis-research.md +2 -2
- package/dist/templates/cursor/hooks.json +7 -1
- package/dist/templates/droid/droids/trellis-research.md +1 -1
- package/dist/templates/extract.d.ts +6 -0
- package/dist/templates/extract.d.ts.map +1 -1
- package/dist/templates/extract.js +14 -0
- package/dist/templates/extract.js.map +1 -1
- package/dist/templates/gemini/agents/trellis-research.md +1 -1
- package/dist/templates/kiro/agents/trellis-research.json +1 -1
- package/dist/templates/markdown/agents.md +19 -12
- package/dist/templates/markdown/gitignore.txt +3 -0
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +24 -0
- package/dist/templates/opencode/agents/trellis-check.md +1 -1
- package/dist/templates/opencode/agents/trellis-implement.md +7 -4
- package/dist/templates/opencode/agents/trellis-research.md +2 -2
- package/dist/templates/opencode/lib/trellis-context.js +100 -13
- package/dist/templates/opencode/plugins/inject-subagent-context.js +70 -5
- package/dist/templates/opencode/plugins/inject-workflow-state.js +38 -44
- package/dist/templates/opencode/plugins/session-start.js +76 -31
- package/dist/templates/pi/agents/trellis-check.md +28 -0
- package/dist/templates/pi/agents/trellis-implement.md +33 -0
- package/dist/templates/pi/agents/trellis-research.md +25 -0
- package/dist/templates/pi/extensions/trellis/index.ts.txt +997 -0
- package/dist/templates/pi/index.d.ts +5 -0
- package/dist/templates/pi/index.d.ts.map +1 -0
- package/dist/templates/pi/index.js +12 -0
- package/dist/templates/pi/index.js.map +1 -0
- package/dist/templates/pi/settings.json +12 -0
- package/dist/templates/qoder/agents/trellis-research.md +1 -1
- package/dist/templates/shared-hooks/index.d.ts +31 -0
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +59 -0
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-shell-session-context.py +180 -0
- package/dist/templates/shared-hooks/inject-subagent-context.py +156 -27
- package/dist/templates/shared-hooks/inject-workflow-state.py +85 -92
- package/dist/templates/shared-hooks/session-start.py +232 -36
- package/dist/templates/trellis/config.yaml +6 -0
- package/dist/templates/trellis/gitignore.txt +3 -0
- package/dist/templates/trellis/index.d.ts +1 -1
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +2 -2
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/__init__.py +8 -0
- package/dist/templates/trellis/scripts/common/active_task.py +593 -0
- package/dist/templates/trellis/scripts/common/cli_adapter.py +72 -14
- package/dist/templates/trellis/scripts/common/paths.py +61 -58
- package/dist/templates/trellis/scripts/common/session_context.py +12 -0
- package/dist/templates/trellis/scripts/common/task_context.py +27 -194
- package/dist/templates/trellis/scripts/common/task_store.py +102 -26
- package/dist/templates/trellis/scripts/common/tasks.py +4 -1
- package/dist/templates/trellis/scripts/common/types.py +0 -2
- package/dist/templates/trellis/scripts/common/workflow_phase.py +15 -3
- package/dist/templates/trellis/scripts/task.py +99 -34
- package/dist/templates/trellis/workflow.md +332 -64
- package/dist/types/ai-tools.d.ts +12 -3
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +29 -0
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/utils/file-writer.d.ts.map +1 -1
- package/dist/utils/file-writer.js +7 -2
- package/dist/utils/file-writer.js.map +1 -1
- package/dist/utils/posix.d.ts +13 -0
- package/dist/utils/posix.d.ts.map +1 -0
- package/dist/utils/posix.js +15 -0
- package/dist/utils/posix.js.map +1 -0
- package/dist/utils/project-detector.d.ts +2 -0
- package/dist/utils/project-detector.d.ts.map +1 -1
- package/dist/utils/project-detector.js +120 -11
- package/dist/utils/project-detector.js.map +1 -1
- package/dist/utils/task-json.d.ts +46 -0
- package/dist/utils/task-json.d.ts.map +1 -0
- package/dist/utils/task-json.js +49 -0
- package/dist/utils/task-json.js.map +1 -0
- package/dist/utils/template-fetcher.d.ts +22 -6
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +405 -27
- package/dist/utils/template-fetcher.js.map +1 -1
- package/dist/utils/template-hash.d.ts +22 -3
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +99 -19
- package/dist/utils/template-hash.js.map +1 -1
- package/package.json +7 -7
- package/dist/templates/markdown/spec/backend/directory-structure.md +0 -292
- package/dist/templates/markdown/spec/backend/index.md +0 -40
- package/dist/templates/markdown/spec/backend/script-conventions.md +0 -742
- package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md +0 -118
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md +0 -394
- package/dist/templates/shared-hooks/statusline.py +0 -218
- package/dist/templates/trellis/scripts/create_bootstrap.py +0 -298
package/dist/commands/update.js
CHANGED
|
@@ -2,17 +2,30 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import inquirer from "inquirer";
|
|
5
|
-
import {
|
|
5
|
+
import { DIR_NAMES, FILE_NAMES, PATHS } from "../constants/paths.js";
|
|
6
6
|
import { VERSION, PACKAGE_NAME } from "../constants/version.js";
|
|
7
7
|
import { getMigrationsForVersion, getAllMigrations, getMigrationMetadata, } from "../migrations/index.js";
|
|
8
8
|
import { loadHashes, saveHashes, updateHashes, isTemplateModified, removeHash, renameHash, computeHash, } from "../utils/template-hash.js";
|
|
9
9
|
import { compareVersions } from "../utils/compare-versions.js";
|
|
10
|
+
import { toPosix } from "../utils/posix.js";
|
|
10
11
|
import { setupProxy } from "../utils/proxy.js";
|
|
12
|
+
import { emptyTaskJson } from "../utils/task-json.js";
|
|
11
13
|
// Import templates for comparison
|
|
12
14
|
import { getAllScripts,
|
|
13
15
|
// Configuration
|
|
14
16
|
configYamlTemplate, gitignoreTemplate, workflowMdTemplate, } from "../templates/trellis/index.js";
|
|
17
|
+
import { agentsMdContent } from "../templates/markdown/index.js";
|
|
15
18
|
import { ALL_MANAGED_DIRS, getConfiguredPlatforms, collectPlatformTemplates, isManagedPath, isManagedRootDir, } from "../configurators/index.js";
|
|
19
|
+
const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
|
|
20
|
+
const TRELLIS_BLOCK_START = "<!-- TRELLIS:START -->";
|
|
21
|
+
const TRELLIS_BLOCK_END = "<!-- TRELLIS:END -->";
|
|
22
|
+
const LEGACY_UNTRACKED_AGENTS_MD_BLOCK_HASHES = new Set([
|
|
23
|
+
// v0.5.0-beta.17 and earlier wrote AGENTS.md but did not hash-track it.
|
|
24
|
+
// This hash is the pristine Trellis-managed block before the Subagents
|
|
25
|
+
// section was added, so old untouched projects can be updated without a
|
|
26
|
+
// false "modified by you" conflict.
|
|
27
|
+
"c1f511b1cfc1902f2147da159f09cc51f380b0c9e341cdb3ac5dea5233f3e307",
|
|
28
|
+
]);
|
|
16
29
|
// Paths that should never be touched (true user data)
|
|
17
30
|
// spec/ is user-customized content created during init; update should never modify it
|
|
18
31
|
const PROTECTED_PATHS = [
|
|
@@ -22,6 +35,160 @@ const PROTECTED_PATHS = [
|
|
|
22
35
|
`${DIR_NAMES.WORKFLOW}/.developer`,
|
|
23
36
|
`${DIR_NAMES.WORKFLOW}/.current-task`,
|
|
24
37
|
];
|
|
38
|
+
function getTrellisManagedBlock(content) {
|
|
39
|
+
const start = content.indexOf(TRELLIS_BLOCK_START);
|
|
40
|
+
if (start === -1) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const end = content.indexOf(TRELLIS_BLOCK_END, start);
|
|
44
|
+
if (end === -1) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return content.slice(start, end + TRELLIS_BLOCK_END.length);
|
|
48
|
+
}
|
|
49
|
+
function replaceTrellisManagedBlock(existingContent, templateContent) {
|
|
50
|
+
const existingStart = existingContent.indexOf(TRELLIS_BLOCK_START);
|
|
51
|
+
if (existingStart === -1) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const existingEnd = existingContent.indexOf(TRELLIS_BLOCK_END, existingStart);
|
|
55
|
+
if (existingEnd === -1) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const templateBlock = getTrellisManagedBlock(templateContent);
|
|
59
|
+
if (!templateBlock) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return (existingContent.slice(0, existingStart) +
|
|
63
|
+
templateBlock +
|
|
64
|
+
existingContent.slice(existingEnd + TRELLIS_BLOCK_END.length));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Workflow-state breadcrumb tag-block replacement (used by buildWorkflowMdTemplate).
|
|
68
|
+
*
|
|
69
|
+
* Each `[workflow-state:STATUS]...[/workflow-state:STATUS]` block in workflow.md
|
|
70
|
+
* is the runtime source of truth for the per-turn breadcrumb that
|
|
71
|
+
* inject-workflow-state.py / .js read on every UserPromptSubmit. The blocks are
|
|
72
|
+
* managed by Trellis: when the CLI ships an updated breadcrumb body, every
|
|
73
|
+
* downstream user project must pick it up (otherwise the per-turn nudge stays
|
|
74
|
+
* silent on new mandatory steps — see Phase 3.4 commit drift / Phase 1.3 jsonl
|
|
75
|
+
* curation drift, the bugs that motivated workflow-state-contract.md).
|
|
76
|
+
*
|
|
77
|
+
* Replacement contract:
|
|
78
|
+
* - For every status block present in the *template* workflow.md, replace
|
|
79
|
+
* the user's same-named block with the template version.
|
|
80
|
+
* - If a block exists in the template but not in the user's file (either the
|
|
81
|
+
* user removed it, or they're upgrading from a pre-tag version), append
|
|
82
|
+
* the template block at the end of the file so the runtime hook can find it.
|
|
83
|
+
* - Everything outside the named blocks (the user's narrative customizations
|
|
84
|
+
* to the Phase Index, Skill Routing tables, etc.) is preserved verbatim.
|
|
85
|
+
*/
|
|
86
|
+
const WORKFLOW_STATE_TAG_RE = /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
|
|
87
|
+
function extractWorkflowStateBlocks(content) {
|
|
88
|
+
const blocks = new Map();
|
|
89
|
+
for (const match of content.matchAll(WORKFLOW_STATE_TAG_RE)) {
|
|
90
|
+
blocks.set(match[1], match[0]);
|
|
91
|
+
}
|
|
92
|
+
return blocks;
|
|
93
|
+
}
|
|
94
|
+
function replaceWorkflowStateBlock(content, status, newBlock) {
|
|
95
|
+
const re = new RegExp(`\\[workflow-state:${status}\\]\\s*\\n[\\s\\S]*?\\n\\s*\\[/workflow-state:${status}\\]`);
|
|
96
|
+
const match = content.match(re);
|
|
97
|
+
if (match?.index === undefined) {
|
|
98
|
+
return { content, replaced: false };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
content: content.slice(0, match.index) +
|
|
102
|
+
newBlock +
|
|
103
|
+
content.slice(match.index + match[0].length),
|
|
104
|
+
replaced: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function buildWorkflowMdTemplate(cwd) {
|
|
108
|
+
const fullPath = path.join(cwd, DIR_NAMES.WORKFLOW, "workflow.md");
|
|
109
|
+
if (!fs.existsSync(fullPath)) {
|
|
110
|
+
return workflowMdTemplate;
|
|
111
|
+
}
|
|
112
|
+
let existingContent;
|
|
113
|
+
try {
|
|
114
|
+
existingContent = fs.readFileSync(fullPath, "utf-8");
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return workflowMdTemplate;
|
|
118
|
+
}
|
|
119
|
+
const templateBlocks = extractWorkflowStateBlocks(workflowMdTemplate);
|
|
120
|
+
if (templateBlocks.size === 0) {
|
|
121
|
+
// Template has no breadcrumb tags — fall back to full overwrite contract.
|
|
122
|
+
return workflowMdTemplate;
|
|
123
|
+
}
|
|
124
|
+
const existingBlocks = extractWorkflowStateBlocks(existingContent);
|
|
125
|
+
let merged = existingContent;
|
|
126
|
+
const appendQueue = [];
|
|
127
|
+
const customizedOverwritten = [];
|
|
128
|
+
for (const [status, block] of templateBlocks) {
|
|
129
|
+
const existing = existingBlocks.get(status);
|
|
130
|
+
if (existing && existing !== block) {
|
|
131
|
+
customizedOverwritten.push(status);
|
|
132
|
+
}
|
|
133
|
+
const result = replaceWorkflowStateBlock(merged, status, block);
|
|
134
|
+
if (result.replaced) {
|
|
135
|
+
merged = result.content;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// User's file lacks this tag (older format, or user pruned it). Queue
|
|
139
|
+
// for append so the per-turn hook can find it; runtime degrades to
|
|
140
|
+
// generic "Refer to workflow.md" otherwise.
|
|
141
|
+
appendQueue.push(block);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (appendQueue.length > 0) {
|
|
145
|
+
const trimmed = merged.replace(/\s+$/, "");
|
|
146
|
+
merged = `${trimmed}\n\n${appendQueue.join("\n\n")}\n`;
|
|
147
|
+
}
|
|
148
|
+
if (customizedOverwritten.length > 0) {
|
|
149
|
+
// Surface a one-line note so users who customized breadcrumb bodies know
|
|
150
|
+
// their edits were replaced. Same trade-off as the AGENTS.md
|
|
151
|
+
// TRELLIS:START/END managed-block: customizations inside the marked
|
|
152
|
+
// region are owned by the CLI; everything outside is preserved verbatim.
|
|
153
|
+
console.log(chalk.yellow(` Note: workflow.md [workflow-state:${customizedOverwritten.join(", ")}] block(s) ` +
|
|
154
|
+
`were updated to the latest CLI version. If you had customized breadcrumb wording, ` +
|
|
155
|
+
`re-apply your edits to the new bodies (content outside [workflow-state:*] tags is preserved).`));
|
|
156
|
+
}
|
|
157
|
+
return merged;
|
|
158
|
+
}
|
|
159
|
+
function buildAgentsMdTemplate(cwd) {
|
|
160
|
+
const fullPath = path.join(cwd, FILE_NAMES.AGENTS);
|
|
161
|
+
if (!fs.existsSync(fullPath)) {
|
|
162
|
+
return agentsMdContent;
|
|
163
|
+
}
|
|
164
|
+
const existingContent = fs.readFileSync(fullPath, "utf-8");
|
|
165
|
+
// Existing file already has TRELLIS:START/END markers — replace just the
|
|
166
|
+
// managed block, preserving everything outside it.
|
|
167
|
+
const replaced = replaceTrellisManagedBlock(existingContent, agentsMdContent);
|
|
168
|
+
if (replaced !== null) {
|
|
169
|
+
return replaced;
|
|
170
|
+
}
|
|
171
|
+
// Existing file has no managed-block markers (pre-0.5.0-beta.18 project, or
|
|
172
|
+
// user hand-wrote AGENTS.md without ever running through Trellis). Append
|
|
173
|
+
// the template's managed block at the end so user content is preserved
|
|
174
|
+
// instead of clobbered.
|
|
175
|
+
const templateBlock = getTrellisManagedBlock(agentsMdContent);
|
|
176
|
+
if (!templateBlock) {
|
|
177
|
+
return agentsMdContent;
|
|
178
|
+
}
|
|
179
|
+
const trimmed = existingContent.replace(/\s+$/, "");
|
|
180
|
+
return `${trimmed}\n\n${templateBlock}\n`;
|
|
181
|
+
}
|
|
182
|
+
function isKnownUntrackedTemplate(relativePath, existingContent) {
|
|
183
|
+
if (relativePath !== FILE_NAMES.AGENTS) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const managedBlock = getTrellisManagedBlock(existingContent);
|
|
187
|
+
if (!managedBlock) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return LEGACY_UNTRACKED_AGENTS_MD_BLOCK_HASHES.has(computeHash(managedBlock));
|
|
191
|
+
}
|
|
25
192
|
/**
|
|
26
193
|
* Check if a path is blocked by PROTECTED_PATHS
|
|
27
194
|
*/
|
|
@@ -227,6 +394,29 @@ function needsCodexUpgrade(cwd) {
|
|
|
227
394
|
const hashes = loadHashes(cwd);
|
|
228
395
|
return Object.keys(hashes).some((key) => key.startsWith(".agents/skills/"));
|
|
229
396
|
}
|
|
397
|
+
function preserveExistingClaudeStatusLine(cwd, templates) {
|
|
398
|
+
const newSettingsContent = templates.get(CLAUDE_SETTINGS_PATH);
|
|
399
|
+
if (!newSettingsContent)
|
|
400
|
+
return;
|
|
401
|
+
const settingsPath = path.join(cwd, CLAUDE_SETTINGS_PATH);
|
|
402
|
+
if (!fs.existsSync(settingsPath))
|
|
403
|
+
return;
|
|
404
|
+
try {
|
|
405
|
+
const existingSettings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
406
|
+
if (!Object.prototype.hasOwnProperty.call(existingSettings, "statusLine")) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const newSettings = JSON.parse(newSettingsContent);
|
|
410
|
+
if (Object.prototype.hasOwnProperty.call(newSettings, "statusLine")) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
newSettings.statusLine = existingSettings.statusLine;
|
|
414
|
+
templates.set(CLAUDE_SETTINGS_PATH, `${JSON.stringify(newSettings, null, 2)}\n`);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// Invalid local JSON is handled by the normal conflict path.
|
|
418
|
+
}
|
|
419
|
+
}
|
|
230
420
|
function collectTemplateFiles(cwd, extraPlatforms,
|
|
231
421
|
/**
|
|
232
422
|
* Bypass `update.skip` when collecting templates. Enable this for breaking
|
|
@@ -255,11 +445,20 @@ bypassUpdateSkip = false) {
|
|
|
255
445
|
// just user-facing documentation — `## Phase Index`, `## Phase 1/2/3` headings,
|
|
256
446
|
// and `[workflow-state:STATUS]` tag blocks are parsed by get_context.py /
|
|
257
447
|
// shared hooks, so scripts break silently when workflow.md drifts from the
|
|
258
|
-
// CLI version.
|
|
259
|
-
//
|
|
260
|
-
|
|
448
|
+
// CLI version.
|
|
449
|
+
//
|
|
450
|
+
// Starting v0.5.0-rc.0, the breadcrumb tag blocks
|
|
451
|
+
// `[workflow-state:STATUS]...[/workflow-state:STATUS]` are managed via
|
|
452
|
+
// per-block replacement (similar to AGENTS.md's TRELLIS:START/END managed
|
|
453
|
+
// block) so user customizations to the rest of the file (Phase Index,
|
|
454
|
+
// Skill Routing tables, narrative customizations) are preserved while the
|
|
455
|
+
// runtime-critical breadcrumb bodies still pick up CLI updates. Outside
|
|
456
|
+
// the tag blocks the file falls through to the normal "Modified by you"
|
|
457
|
+
// confirm prompt at write time when the user has edited it.
|
|
458
|
+
files.set(`${DIR_NAMES.WORKFLOW}/workflow.md`, buildWorkflowMdTemplate(cwd));
|
|
261
459
|
// workspace/index.md stays excluded — it's runtime-appended by add_session.py
|
|
262
460
|
// (journal index) and has no script-parsed structure.
|
|
461
|
+
files.set(FILE_NAMES.AGENTS, buildAgentsMdTemplate(cwd));
|
|
263
462
|
// Platform-specific templates (only for configured platforms)
|
|
264
463
|
for (const platformId of platforms) {
|
|
265
464
|
const platformFiles = collectPlatformTemplates(platformId);
|
|
@@ -269,6 +468,7 @@ bypassUpdateSkip = false) {
|
|
|
269
468
|
}
|
|
270
469
|
}
|
|
271
470
|
}
|
|
471
|
+
preserveExistingClaudeStatusLine(cwd, files);
|
|
272
472
|
// Apply update.skip from config.yaml (unless bypassed for breaking release)
|
|
273
473
|
if (!bypassUpdateSkip) {
|
|
274
474
|
const skipPaths = loadUpdateSkipPaths(cwd);
|
|
@@ -331,9 +531,11 @@ function analyzeChanges(cwd, hashes, templates) {
|
|
|
331
531
|
// Content differs - check if user modified or template updated
|
|
332
532
|
const storedHash = hashes[relativePath];
|
|
333
533
|
const currentHash = computeHash(existingContent);
|
|
334
|
-
if (storedHash && storedHash === currentHash)
|
|
335
|
-
|
|
336
|
-
|
|
534
|
+
if ((storedHash && storedHash === currentHash) ||
|
|
535
|
+
(!storedHash &&
|
|
536
|
+
isKnownUntrackedTemplate(relativePath, existingContent))) {
|
|
537
|
+
// Either the tracked hash matches, or this is a known pristine template
|
|
538
|
+
// from before the path was hash-tracked. Safe to auto-update.
|
|
337
539
|
change.status = "changed";
|
|
338
540
|
result.autoUpdateFiles.push(change);
|
|
339
541
|
}
|
|
@@ -348,6 +550,15 @@ function analyzeChanges(cwd, hashes, templates) {
|
|
|
348
550
|
}
|
|
349
551
|
return result;
|
|
350
552
|
}
|
|
553
|
+
function collectMissingAgentsMdHash(changes, hashes) {
|
|
554
|
+
const files = new Map();
|
|
555
|
+
for (const file of changes.unchangedFiles) {
|
|
556
|
+
if (file.relativePath === FILE_NAMES.AGENTS && !hashes[file.relativePath]) {
|
|
557
|
+
files.set(file.relativePath, file.newContent);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return files;
|
|
561
|
+
}
|
|
351
562
|
/**
|
|
352
563
|
* Print change summary
|
|
353
564
|
*/
|
|
@@ -481,11 +692,14 @@ function backupFile(cwd, backupDir, relativePath) {
|
|
|
481
692
|
* Directories to backup as complete snapshot (derived from platform registry)
|
|
482
693
|
*/
|
|
483
694
|
const BACKUP_DIRS = ALL_MANAGED_DIRS;
|
|
695
|
+
/** Root-level managed files to include in update backups. */
|
|
696
|
+
const BACKUP_FILES = [FILE_NAMES.AGENTS];
|
|
484
697
|
/**
|
|
485
698
|
* Patterns to exclude from backup (user data that shouldn't be backed up)
|
|
486
699
|
*/
|
|
487
700
|
const BACKUP_EXCLUDE_PATTERNS = [
|
|
488
701
|
".backup-", // Previous backups
|
|
702
|
+
"/node_modules", // Installed dependencies; restore via package manager
|
|
489
703
|
"/workspace/", // Developer workspace (user data)
|
|
490
704
|
"/tasks/", // Task data (user data)
|
|
491
705
|
"/spec/", // Spec files (user-customized content)
|
|
@@ -506,8 +720,16 @@ const BACKUP_EXCLUDE_PATTERNS = [
|
|
|
506
720
|
* @internal Exported for testing only
|
|
507
721
|
*/
|
|
508
722
|
export function shouldExcludeFromBackup(relativePath) {
|
|
723
|
+
// Normalize Windows backslashes to forward slashes so patterns like
|
|
724
|
+
// "/worktrees/" / "/tasks/" match regardless of host OS. Without this,
|
|
725
|
+
// Windows `path.relative` returns `.claude\worktrees\...` and none of
|
|
726
|
+
// the slash-prefixed exclude patterns trigger — which causes
|
|
727
|
+
// `collectAllFiles` to descend into platform worktrees (full nested
|
|
728
|
+
// project copies) and explode the scan. Same normalization pattern
|
|
729
|
+
// used by `isManagedPath` in configurators/index.ts.
|
|
730
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
509
731
|
for (const pattern of BACKUP_EXCLUDE_PATTERNS) {
|
|
510
|
-
if (
|
|
732
|
+
if (normalized.includes(pattern)) {
|
|
511
733
|
return true;
|
|
512
734
|
}
|
|
513
735
|
}
|
|
@@ -525,7 +747,7 @@ function createFullBackup(cwd) {
|
|
|
525
747
|
const dirPath = path.join(cwd, dir);
|
|
526
748
|
if (!fs.existsSync(dirPath))
|
|
527
749
|
continue;
|
|
528
|
-
const files = collectAllFiles(dirPath);
|
|
750
|
+
const files = collectAllFiles(dirPath, cwd);
|
|
529
751
|
for (const fullPath of files) {
|
|
530
752
|
const relativePath = path.relative(cwd, fullPath);
|
|
531
753
|
// Skip excluded paths
|
|
@@ -539,6 +761,18 @@ function createFullBackup(cwd) {
|
|
|
539
761
|
backupFile(cwd, backupDir, relativePath);
|
|
540
762
|
}
|
|
541
763
|
}
|
|
764
|
+
for (const relativePath of BACKUP_FILES) {
|
|
765
|
+
const fullPath = path.join(cwd, relativePath);
|
|
766
|
+
if (!fs.existsSync(fullPath))
|
|
767
|
+
continue;
|
|
768
|
+
if (shouldExcludeFromBackup(relativePath))
|
|
769
|
+
continue;
|
|
770
|
+
if (!hasFiles) {
|
|
771
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
772
|
+
hasFiles = true;
|
|
773
|
+
}
|
|
774
|
+
backupFile(cwd, backupDir, relativePath);
|
|
775
|
+
}
|
|
542
776
|
return hasFiles ? backupDir : null;
|
|
543
777
|
}
|
|
544
778
|
/**
|
|
@@ -577,18 +811,32 @@ async function getLatestNpmVersion() {
|
|
|
577
811
|
/**
|
|
578
812
|
* Recursively collect all files in a directory
|
|
579
813
|
*/
|
|
580
|
-
function collectAllFiles(dirPath) {
|
|
814
|
+
function collectAllFiles(dirPath, cwd = process.cwd()) {
|
|
581
815
|
if (!fs.existsSync(dirPath))
|
|
582
816
|
return [];
|
|
583
817
|
const files = [];
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
|
|
818
|
+
const stack = [dirPath];
|
|
819
|
+
while (stack.length > 0) {
|
|
820
|
+
const currentDir = stack.pop();
|
|
821
|
+
if (!currentDir)
|
|
822
|
+
continue;
|
|
823
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
824
|
+
for (const entry of entries) {
|
|
825
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
826
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
827
|
+
// Never follow symlinks / Windows directory junctions — a junction
|
|
828
|
+
// pointing at an ancestor would loop the scan forever. Node's
|
|
829
|
+
// `isSymbolicLink()` returns true for NTFS junctions since v12.
|
|
830
|
+
if (entry.isSymbolicLink())
|
|
831
|
+
continue;
|
|
832
|
+
if (entry.isDirectory()) {
|
|
833
|
+
if (!shouldExcludeFromBackup(relativePath)) {
|
|
834
|
+
stack.push(fullPath);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
else if (entry.isFile()) {
|
|
838
|
+
files.push(fullPath);
|
|
839
|
+
}
|
|
592
840
|
}
|
|
593
841
|
}
|
|
594
842
|
return files;
|
|
@@ -603,11 +851,13 @@ function isDirectorySafeToReplace(cwd, dirRelativePath, hashes, templates) {
|
|
|
603
851
|
const dirFullPath = path.join(cwd, dirRelativePath);
|
|
604
852
|
if (!fs.existsSync(dirFullPath))
|
|
605
853
|
return true;
|
|
606
|
-
const files = collectAllFiles(dirFullPath);
|
|
854
|
+
const files = collectAllFiles(dirFullPath, cwd);
|
|
607
855
|
if (files.length === 0)
|
|
608
856
|
return true; // Empty directory is safe
|
|
609
857
|
for (const fullPath of files) {
|
|
610
|
-
|
|
858
|
+
// POSIX-normalize: hashes/templates keys are persisted as POSIX, but
|
|
859
|
+
// `path.relative` returns OS-native separators (backslash on Windows).
|
|
860
|
+
const relativePath = toPosix(path.relative(cwd, fullPath));
|
|
611
861
|
const storedHash = hashes[relativePath];
|
|
612
862
|
const templateContent = templates.get(relativePath);
|
|
613
863
|
// Check if file matches template content (handles untracked files)
|
|
@@ -1256,6 +1506,7 @@ export async function update(options) {
|
|
|
1256
1506
|
}
|
|
1257
1507
|
// Analyze changes (pass hashes for modification detection)
|
|
1258
1508
|
const changes = analyzeChanges(cwd, hashes, templates);
|
|
1509
|
+
const missingAgentsMdHash = collectMissingAgentsMdHash(changes, hashes);
|
|
1259
1510
|
// Print summary
|
|
1260
1511
|
printChangeSummary(changes);
|
|
1261
1512
|
// First-time hash tracking hint
|
|
@@ -1278,6 +1529,9 @@ export async function update(options) {
|
|
|
1278
1529
|
changes.changedFiles.length === 0 &&
|
|
1279
1530
|
!hasPendingMigrations &&
|
|
1280
1531
|
!hasSafeDeletes) {
|
|
1532
|
+
if (!options.dryRun && missingAgentsMdHash.size > 0) {
|
|
1533
|
+
updateHashes(cwd, missingAgentsMdHash);
|
|
1534
|
+
}
|
|
1281
1535
|
if (isSameVersion) {
|
|
1282
1536
|
console.log(chalk.green("✓ Already up to date!"));
|
|
1283
1537
|
}
|
|
@@ -1338,18 +1592,21 @@ export async function update(options) {
|
|
|
1338
1592
|
console.log(chalk.gray("[Dry run] No changes made."));
|
|
1339
1593
|
return;
|
|
1340
1594
|
}
|
|
1341
|
-
//
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1595
|
+
// Batch-resolution flags are explicit consent for non-interactive runs.
|
|
1596
|
+
// Prompting here breaks CI and `node ... update --force --migrate` smoke tests.
|
|
1597
|
+
if (!options.force && !options.skipAll && !options.createNew) {
|
|
1598
|
+
const { proceed } = await inquirer.prompt([
|
|
1599
|
+
{
|
|
1600
|
+
type: "confirm",
|
|
1601
|
+
name: "proceed",
|
|
1602
|
+
message: "Proceed?",
|
|
1603
|
+
default: true,
|
|
1604
|
+
},
|
|
1605
|
+
]);
|
|
1606
|
+
if (!proceed) {
|
|
1607
|
+
console.log(chalk.yellow("Update cancelled."));
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1353
1610
|
}
|
|
1354
1611
|
// Create complete backup of all managed platform/workflow directories
|
|
1355
1612
|
const backupDir = createFullBackup(cwd);
|
|
@@ -1466,7 +1723,7 @@ export async function update(options) {
|
|
|
1466
1723
|
// Update version file
|
|
1467
1724
|
updateVersionFile(cwd);
|
|
1468
1725
|
// Update template hashes for new, auto-updated, and overwritten files
|
|
1469
|
-
const filesToHash = new Map();
|
|
1726
|
+
const filesToHash = new Map(missingAgentsMdHash);
|
|
1470
1727
|
for (const file of changes.newFiles) {
|
|
1471
1728
|
filesToHash.set(file.relativePath, file.newContent);
|
|
1472
1729
|
}
|
|
@@ -1547,37 +1804,21 @@ export async function update(options) {
|
|
|
1547
1804
|
currentDeveloper = nameMatch[1];
|
|
1548
1805
|
}
|
|
1549
1806
|
}
|
|
1550
|
-
// Build task.json
|
|
1807
|
+
// Build task.json — canonical 24-field shape via shared factory.
|
|
1551
1808
|
const taskTitle = `Migrate to v${cliVersion}`;
|
|
1552
1809
|
const todayStr = today.toISOString().split("T")[0];
|
|
1553
|
-
const taskJson = {
|
|
1810
|
+
const taskJson = emptyTaskJson({
|
|
1811
|
+
id: taskSlug,
|
|
1812
|
+
name: taskSlug,
|
|
1554
1813
|
title: taskTitle,
|
|
1555
1814
|
description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
|
|
1556
1815
|
status: "planning",
|
|
1557
|
-
dev_type: null,
|
|
1558
1816
|
scope: "migration",
|
|
1559
1817
|
priority: "P1",
|
|
1560
1818
|
creator: "trellis-update",
|
|
1561
1819
|
assignee: currentDeveloper,
|
|
1562
1820
|
createdAt: todayStr,
|
|
1563
|
-
|
|
1564
|
-
branch: null,
|
|
1565
|
-
base_branch: null,
|
|
1566
|
-
worktree_path: null,
|
|
1567
|
-
current_phase: 0,
|
|
1568
|
-
next_action: [
|
|
1569
|
-
{ phase: 1, action: "review-guide" },
|
|
1570
|
-
{ phase: 2, action: "update-files" },
|
|
1571
|
-
{ phase: 3, action: "run-migrate" },
|
|
1572
|
-
{ phase: 4, action: "test" },
|
|
1573
|
-
],
|
|
1574
|
-
commit: null,
|
|
1575
|
-
pr_url: null,
|
|
1576
|
-
subtasks: [],
|
|
1577
|
-
children: [],
|
|
1578
|
-
parent: null,
|
|
1579
|
-
meta: {},
|
|
1580
|
-
};
|
|
1821
|
+
});
|
|
1581
1822
|
// Write task.json
|
|
1582
1823
|
const taskJsonPath = path.join(taskDir, "task.json");
|
|
1583
1824
|
fs.writeFileSync(taskJsonPath, JSON.stringify(taskJson, null, 2));
|