forge-workflow 0.0.6 → 0.0.7
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/.cursorrules +149 -0
- package/bin/forge.js +36 -3
- package/lib/agents/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +127 -79
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_registry.js +40 -1
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/dev.js +26 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +17 -1
- package/lib/commands/validate.js +13 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/frontmatter.js +79 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +15 -5
- package/scripts/beads-context.sh +12 -4
- package/scripts/check-agents.js +103 -0
- package/scripts/pr-coordinator.sh +71 -21
- package/scripts/smart-status.sh +21 -11
- package/scripts/sync-commands.js +49 -20
- package/scripts/test.js +16 -1
package/opencode.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"permission": {
|
|
4
|
+
"*": "ask",
|
|
5
|
+
"bash": {
|
|
6
|
+
"*": "ask",
|
|
7
|
+
"git status *": "allow",
|
|
8
|
+
"git log *": "allow",
|
|
9
|
+
"git diff *": "allow",
|
|
10
|
+
"git branch *": "allow",
|
|
11
|
+
"git show *": "allow",
|
|
12
|
+
"git remote *": "allow",
|
|
13
|
+
"git stash *": "allow",
|
|
14
|
+
"git tag *": "allow",
|
|
15
|
+
"git rev-parse *": "allow",
|
|
16
|
+
"git describe *": "allow",
|
|
17
|
+
"git ls-files *": "allow",
|
|
18
|
+
"git blame *": "allow",
|
|
19
|
+
"git shortlog *": "allow",
|
|
20
|
+
"git add *": "allow",
|
|
21
|
+
"git commit *": "allow",
|
|
22
|
+
"git push *": "allow",
|
|
23
|
+
"git checkout *": "allow",
|
|
24
|
+
"git switch *": "allow",
|
|
25
|
+
"git merge *": "allow",
|
|
26
|
+
"git rebase *": "ask",
|
|
27
|
+
"git cherry-pick *": "allow",
|
|
28
|
+
"bd *": "allow",
|
|
29
|
+
"gh *": "allow",
|
|
30
|
+
"ls *": "allow",
|
|
31
|
+
"cat *": "allow",
|
|
32
|
+
"grep *": "allow",
|
|
33
|
+
"find *": "allow",
|
|
34
|
+
"pwd *": "allow",
|
|
35
|
+
"which *": "allow",
|
|
36
|
+
"echo *": "allow",
|
|
37
|
+
"head *": "allow",
|
|
38
|
+
"tail *": "allow",
|
|
39
|
+
"sort *": "allow",
|
|
40
|
+
"wc *": "allow",
|
|
41
|
+
"awk *": "allow",
|
|
42
|
+
"sed *": "allow",
|
|
43
|
+
"stat *": "allow",
|
|
44
|
+
"du *": "allow",
|
|
45
|
+
"df *": "allow",
|
|
46
|
+
"file *": "allow",
|
|
47
|
+
"mkdir *": "allow",
|
|
48
|
+
"touch *": "allow",
|
|
49
|
+
"cp *": "allow",
|
|
50
|
+
"mv *": "allow",
|
|
51
|
+
"bun *": "allow",
|
|
52
|
+
"bunx *": "allow",
|
|
53
|
+
"npm *": "allow",
|
|
54
|
+
"npx *": "allow",
|
|
55
|
+
"node *": "allow",
|
|
56
|
+
|
|
57
|
+
"git worktree *": "allow",
|
|
58
|
+
"git reset --hard *": "ask",
|
|
59
|
+
"rm -rf *": "deny",
|
|
60
|
+
"git push --force *": "deny",
|
|
61
|
+
"git push -f *": "deny"
|
|
62
|
+
},
|
|
63
|
+
"edit": {
|
|
64
|
+
"*": "allow"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-workflow",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "7-stage TDD workflow for ALL AI coding agents (Claude, Cursor, Cline, OpenCode, Copilot, Kilo Code, Roo Code, Codex)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forge": "bin/forge.js",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"eslint": "^10.0.2",
|
|
44
44
|
"globals": "^17.3.0",
|
|
45
45
|
"js-yaml": "^4.1.1",
|
|
46
|
-
"lefthook": "^1.
|
|
47
|
-
"yaml": "^2.8.
|
|
46
|
+
"lefthook": "^2.1.4",
|
|
47
|
+
"yaml": "^2.8.3"
|
|
48
48
|
},
|
|
49
49
|
"keywords": [
|
|
50
50
|
"ai",
|
|
@@ -81,11 +81,17 @@
|
|
|
81
81
|
".claude/scripts/",
|
|
82
82
|
".claude/skills/",
|
|
83
83
|
".cursor/",
|
|
84
|
+
"!.cursor/node_modules/",
|
|
84
85
|
".cline/",
|
|
86
|
+
"!.cline/node_modules/",
|
|
85
87
|
".roo/",
|
|
88
|
+
"!.roo/node_modules/",
|
|
86
89
|
".codex/",
|
|
90
|
+
"!.codex/node_modules/",
|
|
87
91
|
".kilocode/",
|
|
92
|
+
"!.kilocode/node_modules/",
|
|
88
93
|
".opencode/",
|
|
94
|
+
"!.opencode/node_modules/",
|
|
89
95
|
"scripts/",
|
|
90
96
|
".github/prompts/",
|
|
91
97
|
".github/PLUGIN_TEMPLATE.json",
|
|
@@ -97,7 +103,9 @@
|
|
|
97
103
|
"lefthook.yml",
|
|
98
104
|
"AGENTS.md",
|
|
99
105
|
"CLAUDE.md",
|
|
100
|
-
".mcp.json.example"
|
|
106
|
+
".mcp.json.example",
|
|
107
|
+
".cursorrules",
|
|
108
|
+
"opencode.json"
|
|
101
109
|
],
|
|
102
110
|
"dependencies": {
|
|
103
111
|
"@babel/parser": "^7.29.2",
|
|
@@ -106,7 +114,9 @@
|
|
|
106
114
|
"overrides": {
|
|
107
115
|
"eslint": "^10.0.2",
|
|
108
116
|
"flatted": "3.4.2",
|
|
109
|
-
"minimatch": "10.2.4"
|
|
117
|
+
"minimatch": "10.2.4",
|
|
118
|
+
"brace-expansion": "5.0.5",
|
|
119
|
+
"lodash": "4.18.1"
|
|
110
120
|
},
|
|
111
121
|
"c8": {
|
|
112
122
|
"lines": 80,
|
package/scripts/beads-context.sh
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# set-acceptance <issue-id> "<criteria-text>"
|
|
8
8
|
# update-progress <issue-id> <task-num> <total> "<title>" <commit-sha> <test-count> <gate-count>
|
|
9
9
|
# parse-progress <issue-id>
|
|
10
|
-
# stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."]
|
|
10
|
+
# stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."] [--workflow-state "{...}"]
|
|
11
11
|
# validate <issue-id>
|
|
12
12
|
#
|
|
13
13
|
# Cross-platform: works on Windows (Git Bash), macOS, and Linux.
|
|
@@ -30,7 +30,7 @@ Subcommands:
|
|
|
30
30
|
set-acceptance <issue-id> "<criteria-text>"
|
|
31
31
|
update-progress <issue-id> <task-num> <total> "<title>" <commit-sha> <test-count> <gate-count>
|
|
32
32
|
parse-progress <issue-id>
|
|
33
|
-
stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."]
|
|
33
|
+
stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."] [--workflow-state "{...}"]
|
|
34
34
|
validate <issue-id>
|
|
35
35
|
EOF
|
|
36
36
|
exit 1
|
|
@@ -238,7 +238,7 @@ cmd_parse_progress() {
|
|
|
238
238
|
|
|
239
239
|
cmd_stage_transition() {
|
|
240
240
|
if [[ $# -lt 3 ]]; then
|
|
241
|
-
echo "Usage: beads-context.sh stage-transition <issue-id> <completed-stage> <next-stage> [--summary \"...\"] [--decisions \"...\"] [--artifacts \"...\"] [--next \"...\"]" >&2
|
|
241
|
+
echo "Usage: beads-context.sh stage-transition <issue-id> <completed-stage> <next-stage> [--summary \"...\"] [--decisions \"...\"] [--artifacts \"...\"] [--next \"...\"] [--workflow-state \"{...}\"]" >&2
|
|
242
242
|
exit 1
|
|
243
243
|
fi
|
|
244
244
|
|
|
@@ -250,7 +250,7 @@ cmd_stage_transition() {
|
|
|
250
250
|
shift 3
|
|
251
251
|
|
|
252
252
|
# Parse optional flags
|
|
253
|
-
local flag_summary="" flag_decisions="" flag_artifacts="" flag_next=""
|
|
253
|
+
local flag_summary="" flag_decisions="" flag_artifacts="" flag_next="" flag_workflow_state=""
|
|
254
254
|
while [[ $# -gt 0 ]]; do
|
|
255
255
|
case "$1" in
|
|
256
256
|
--summary)
|
|
@@ -269,6 +269,10 @@ cmd_stage_transition() {
|
|
|
269
269
|
shift
|
|
270
270
|
flag_next="$(sanitize "${1:-}")"
|
|
271
271
|
;;
|
|
272
|
+
--workflow-state)
|
|
273
|
+
shift
|
|
274
|
+
flag_workflow_state="$(sanitize_config_value "${1:-}")"
|
|
275
|
+
;;
|
|
272
276
|
*)
|
|
273
277
|
# Ignore unknown flags for forward compatibility
|
|
274
278
|
;;
|
|
@@ -296,6 +300,10 @@ Artifacts: ${flag_artifacts}"
|
|
|
296
300
|
comment="${comment}
|
|
297
301
|
Next: ${flag_next}"
|
|
298
302
|
fi
|
|
303
|
+
if [[ -n "$flag_workflow_state" ]]; then
|
|
304
|
+
comment="${comment}
|
|
305
|
+
WorkflowState: ${flag_workflow_state}"
|
|
306
|
+
fi
|
|
299
307
|
|
|
300
308
|
if ! bd_comment "$issue_id" "$comment" > /dev/null; then
|
|
301
309
|
die "Failed to record stage transition on issue ${issue_id}"
|
package/scripts/check-agents.js
CHANGED
|
@@ -19,8 +19,110 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
const { syncCommands } = require('./sync-commands');
|
|
22
|
+
const { AGENT_ADAPTERS } = require('./sync-commands');
|
|
22
23
|
const path = require('path');
|
|
23
24
|
const fs = require('fs');
|
|
25
|
+
const { validatePluginSchema } = require('../lib/plugin-manager');
|
|
26
|
+
|
|
27
|
+
const SYNC_ADAPTER_BY_PLUGIN_ID = Object.freeze({
|
|
28
|
+
claude: 'claude-code',
|
|
29
|
+
cline: 'cline',
|
|
30
|
+
codex: 'codex',
|
|
31
|
+
copilot: 'github-copilot',
|
|
32
|
+
cursor: 'cursor',
|
|
33
|
+
kilocode: 'kilo-code',
|
|
34
|
+
opencode: 'opencode',
|
|
35
|
+
roo: 'roo-code',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function normalizeRelativeDir(dir) {
|
|
39
|
+
if (!dir) return null;
|
|
40
|
+
return String(dir).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getDeclaredCommandDir(plugin) {
|
|
44
|
+
const dirs = plugin.directories || {};
|
|
45
|
+
return dirs.commands || dirs.workflows || dirs.prompts || dirs.skills || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getExpectedCommandDir(plugin) {
|
|
49
|
+
const adapterId = SYNC_ADAPTER_BY_PLUGIN_ID[plugin.id];
|
|
50
|
+
if (!adapterId) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const adapter = AGENT_ADAPTERS[adapterId];
|
|
55
|
+
if (!adapter) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (plugin.id === 'codex') {
|
|
60
|
+
return normalizeRelativeDir(plugin.directories?.skills || adapter.baseDir);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return normalizeRelativeDir(adapter.baseDir);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validatePluginParity(plugin, errors) {
|
|
67
|
+
const validation = validatePluginSchema(plugin);
|
|
68
|
+
if (!validation.valid) {
|
|
69
|
+
const parityErrors = validation.errors.filter((error) =>
|
|
70
|
+
error.startsWith('"support') || error.startsWith('"capabilities')
|
|
71
|
+
);
|
|
72
|
+
parityErrors.forEach((error) => {
|
|
73
|
+
errors.push(`Plugin "${plugin.name || plugin.id}" (${plugin.id || 'unknown'}): ${error}`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const capabilities = plugin.capabilities || {};
|
|
78
|
+
const dirs = plugin.directories || {};
|
|
79
|
+
const supportStatus = plugin.support?.status;
|
|
80
|
+
|
|
81
|
+
if (capabilities.commands) {
|
|
82
|
+
const expectedDir = getExpectedCommandDir(plugin);
|
|
83
|
+
const declaredDir = normalizeRelativeDir(getDeclaredCommandDir(plugin));
|
|
84
|
+
|
|
85
|
+
if (!expectedDir) {
|
|
86
|
+
errors.push(
|
|
87
|
+
`Plugin "${plugin.name}" (${plugin.id}) declares commands support but has no Forge sync adapter.`
|
|
88
|
+
);
|
|
89
|
+
} else if (!declaredDir) {
|
|
90
|
+
errors.push(
|
|
91
|
+
`Plugin "${plugin.name}" (${plugin.id}) declares commands support but has no command directory configured.`
|
|
92
|
+
);
|
|
93
|
+
} else if (declaredDir !== expectedDir) {
|
|
94
|
+
errors.push(
|
|
95
|
+
`Plugin "${plugin.name}" (${plugin.id}) command directory "${declaredDir}" does not match sync output "${expectedDir}".`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (capabilities.rules && !dirs.rules) {
|
|
101
|
+
errors.push(
|
|
102
|
+
`Plugin "${plugin.name}" (${plugin.id}) declares rules capability but has no scaffold path configured.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (capabilities.skills && !dirs.skills) {
|
|
107
|
+
errors.push(
|
|
108
|
+
`Plugin "${plugin.name}" (${plugin.id}) declares skills capability but has no scaffold path configured.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (supportStatus === 'deprecated' || supportStatus === 'unsupported') {
|
|
113
|
+
if (capabilities.skills) {
|
|
114
|
+
errors.push(
|
|
115
|
+
`Plugin "${plugin.name}" (${plugin.id}) is ${supportStatus} but still declares skills parity.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (plugin.setup?.createSkill || dirs.skills) {
|
|
120
|
+
errors.push(
|
|
121
|
+
`Plugin "${plugin.name}" (${plugin.id}) is ${supportStatus} but still scaffolds Forge skills.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
24
126
|
|
|
25
127
|
/**
|
|
26
128
|
* Validate all agent configs are complete and in sync.
|
|
@@ -89,6 +191,7 @@ function checkAgents(repoRoot) {
|
|
|
89
191
|
try {
|
|
90
192
|
const plugin = JSON.parse(fs.readFileSync(path.join(pluginDir, file), 'utf8'));
|
|
91
193
|
plugins.push(plugin);
|
|
194
|
+
validatePluginParity(plugin, errors);
|
|
92
195
|
} catch (_err) {
|
|
93
196
|
errors.push(`Failed to parse plugin file: lib/agents/${file}`);
|
|
94
197
|
}
|
|
@@ -563,9 +563,54 @@ cmd_auto_label() {
|
|
|
563
563
|
echo "No label changes needed for PR #$pr_number"
|
|
564
564
|
fi
|
|
565
565
|
}
|
|
566
|
+
collect_worktree_entries() {
|
|
567
|
+
local git_cmd porcelain current_path current_branch entries line separator
|
|
568
|
+
entries=""
|
|
569
|
+
current_path=""
|
|
570
|
+
current_branch=""
|
|
571
|
+
separator=$'\t'
|
|
572
|
+
git_cmd="${GIT_CMD:-git}"
|
|
573
|
+
porcelain="$("$git_cmd" worktree list --porcelain 2>/dev/null || true)"
|
|
574
|
+
|
|
575
|
+
while IFS= read -r line; do
|
|
576
|
+
case "$line" in
|
|
577
|
+
worktree\ *)
|
|
578
|
+
current_path="${line#worktree }"
|
|
579
|
+
;;
|
|
580
|
+
branch\ refs/heads/*)
|
|
581
|
+
current_branch="${line#branch refs/heads/}"
|
|
582
|
+
;;
|
|
583
|
+
"")
|
|
584
|
+
if [[ -n "$current_path" ]]; then
|
|
585
|
+
if [[ -n "$entries" ]]; then
|
|
586
|
+
entries+=$'\n'
|
|
587
|
+
fi
|
|
588
|
+
entries+="${current_path}${separator}${current_branch}"
|
|
589
|
+
fi
|
|
590
|
+
current_path=""
|
|
591
|
+
current_branch=""
|
|
592
|
+
;;
|
|
593
|
+
esac
|
|
594
|
+
done <<EOF
|
|
595
|
+
$porcelain
|
|
596
|
+
|
|
597
|
+
EOF
|
|
598
|
+
|
|
599
|
+
if [[ -n "$current_path" ]]; then
|
|
600
|
+
if [[ -n "$entries" ]]; then
|
|
601
|
+
entries+=$'\n'
|
|
602
|
+
fi
|
|
603
|
+
entries+="${current_path}${separator}${current_branch}"
|
|
604
|
+
fi
|
|
605
|
+
|
|
606
|
+
printf '%s' "$entries"
|
|
607
|
+
}
|
|
608
|
+
|
|
566
609
|
cmd_stale_worktrees() {
|
|
567
610
|
local threshold_hours=48
|
|
568
|
-
local
|
|
611
|
+
local worktree_filter=""
|
|
612
|
+
local git_cmd
|
|
613
|
+
git_cmd="${GIT_CMD:-git}"
|
|
569
614
|
|
|
570
615
|
while [[ $# -gt 0 ]]; do
|
|
571
616
|
case "$1" in
|
|
@@ -578,17 +623,19 @@ cmd_stale_worktrees() {
|
|
|
578
623
|
return 1
|
|
579
624
|
fi
|
|
580
625
|
shift ;;
|
|
581
|
-
--dir=*)
|
|
626
|
+
--dir=*) worktree_filter="${1#--dir=}"; shift ;;
|
|
582
627
|
*) echo "Error: unknown flag '$1'" >&2; return 1 ;;
|
|
583
628
|
esac
|
|
584
629
|
done
|
|
585
630
|
|
|
586
631
|
local repo_root
|
|
587
|
-
repo_root="$(
|
|
588
|
-
|
|
632
|
+
repo_root="$("$git_cmd" rev-parse --show-toplevel 2>/dev/null)" || repo_root="."
|
|
633
|
+
|
|
634
|
+
local worktree_entries
|
|
635
|
+
worktree_entries="$(collect_worktree_entries)"
|
|
589
636
|
|
|
590
|
-
if [[
|
|
591
|
-
echo "No worktrees found
|
|
637
|
+
if [[ -z "$worktree_entries" ]]; then
|
|
638
|
+
echo "No worktrees found"
|
|
592
639
|
return 0
|
|
593
640
|
fi
|
|
594
641
|
|
|
@@ -596,28 +643,29 @@ cmd_stale_worktrees() {
|
|
|
596
643
|
local found_any=0
|
|
597
644
|
local stale_count=0
|
|
598
645
|
|
|
599
|
-
|
|
600
|
-
[[ -
|
|
601
|
-
found_any=1
|
|
646
|
+
while IFS=$'\t' read -r entry_path entry_branch; do
|
|
647
|
+
[[ -n "$entry_path" ]] || continue
|
|
602
648
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
local real_root
|
|
607
|
-
real_root="$(realpath "$repo_root" 2>/dev/null)" || continue
|
|
649
|
+
if [[ "$entry_path" == "$repo_root" ]]; then
|
|
650
|
+
continue
|
|
651
|
+
fi
|
|
608
652
|
|
|
609
|
-
if [[ "$
|
|
610
|
-
echo "WARNING: skipping symlink outside repo: $entry" >&2
|
|
653
|
+
if [[ -n "$worktree_filter" ]] && [[ "$entry_path" != *"$worktree_filter"* ]]; then
|
|
611
654
|
continue
|
|
612
655
|
fi
|
|
613
656
|
|
|
657
|
+
found_any=1
|
|
658
|
+
|
|
614
659
|
# Get last commit date
|
|
615
660
|
local last_commit_date
|
|
616
|
-
last_commit_date="$(
|
|
661
|
+
last_commit_date="$("$git_cmd" -C "$entry_path" log -1 --format=%ci 2>/dev/null)" || continue
|
|
617
662
|
|
|
618
663
|
# Get branch name
|
|
619
|
-
local branch_name
|
|
620
|
-
|
|
664
|
+
local branch_name="${entry_branch:-}"
|
|
665
|
+
if [[ -z "$branch_name" ]]; then
|
|
666
|
+
branch_name="$("$git_cmd" -C "$entry_path" branch --show-current 2>/dev/null)" || branch_name="unknown"
|
|
667
|
+
fi
|
|
668
|
+
branch_name="${branch_name:-unknown}"
|
|
621
669
|
|
|
622
670
|
# Calculate age in hours
|
|
623
671
|
local last_epoch now_epoch
|
|
@@ -628,10 +676,12 @@ cmd_stale_worktrees() {
|
|
|
628
676
|
if [[ "$age_hours" -ge "$threshold_hours" ]]; then
|
|
629
677
|
stale_count=$((stale_count + 1))
|
|
630
678
|
local dir_name
|
|
631
|
-
dir_name="$(basename "$
|
|
679
|
+
dir_name="$(basename "$entry_path")"
|
|
632
680
|
echo "STALE: $dir_name (branch: $branch_name, last commit: ${age_hours}h ago)"
|
|
633
681
|
fi
|
|
634
|
-
done
|
|
682
|
+
done <<EOF
|
|
683
|
+
$worktree_entries
|
|
684
|
+
EOF
|
|
635
685
|
|
|
636
686
|
if [[ "$found_any" -eq 0 ]]; then
|
|
637
687
|
echo "No worktrees found"
|
package/scripts/smart-status.sh
CHANGED
|
@@ -20,8 +20,24 @@
|
|
|
20
20
|
|
|
21
21
|
set -euo pipefail
|
|
22
22
|
|
|
23
|
+
# ── Source cross-dev awareness scripts ────────────────────────────────
|
|
24
|
+
# file-index.sh: file_index_read for reading file index entries
|
|
25
|
+
# sync-utils.sh: get_session_identity for current developer identity
|
|
26
|
+
|
|
27
|
+
_SMART_STATUS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
if [ -f "$_SMART_STATUS_DIR/lib/sanitize.sh" ]; then
|
|
29
|
+
source "$_SMART_STATUS_DIR/lib/sanitize.sh"
|
|
30
|
+
fi
|
|
31
|
+
if [ -f "$_SMART_STATUS_DIR/file-index.sh" ]; then
|
|
32
|
+
source "$_SMART_STATUS_DIR/file-index.sh"
|
|
33
|
+
fi
|
|
34
|
+
if [ -f "$_SMART_STATUS_DIR/sync-utils.sh" ]; then
|
|
35
|
+
source "$_SMART_STATUS_DIR/sync-utils.sh"
|
|
36
|
+
fi
|
|
37
|
+
|
|
23
38
|
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
24
39
|
|
|
40
|
+
if ! declare -F sanitize >/dev/null; then
|
|
25
41
|
# Sanitize a string: strip shell-injection patterns (OWASP A03)
|
|
26
42
|
# Removes: double quotes, $(...), backticks, semicolons, and newlines
|
|
27
43
|
sanitize() {
|
|
@@ -38,17 +54,6 @@ sanitize() {
|
|
|
38
54
|
val="$(printf '%s' "$val" | tr '\n' ' ')"
|
|
39
55
|
printf '%s' "$val"
|
|
40
56
|
}
|
|
41
|
-
|
|
42
|
-
# ── Source cross-dev awareness scripts ────────────────────────────────
|
|
43
|
-
# file-index.sh: file_index_read for reading file index entries
|
|
44
|
-
# sync-utils.sh: get_session_identity for current developer identity
|
|
45
|
-
|
|
46
|
-
_SMART_STATUS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
47
|
-
if [ -f "$_SMART_STATUS_DIR/file-index.sh" ]; then
|
|
48
|
-
source "$_SMART_STATUS_DIR/file-index.sh"
|
|
49
|
-
fi
|
|
50
|
-
if [ -f "$_SMART_STATUS_DIR/sync-utils.sh" ]; then
|
|
51
|
-
source "$_SMART_STATUS_DIR/sync-utils.sh"
|
|
52
57
|
fi
|
|
53
58
|
|
|
54
59
|
# ── Dependency check ────────────────────────────────────────────────────
|
|
@@ -117,6 +122,11 @@ done
|
|
|
117
122
|
# not found" error, runs bd init --force + bd backup restore, then retries.
|
|
118
123
|
# Capture stderr into variable (2>&1 redirects stderr to stdout for capture,
|
|
119
124
|
# then >/dev/null discards original stdout so only errors remain).
|
|
125
|
+
if ! command -v "$BD" >/dev/null 2>&1; then
|
|
126
|
+
echo "Error: bd is required but not found." >&2
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
|
|
120
130
|
_bd_list_err="$("$BD" list --json --limit 0 2>&1 >/dev/null || true)"
|
|
121
131
|
if printf '%s' "$_bd_list_err" | grep -qi "database.*not found"; then
|
|
122
132
|
# Derive prefix from repo root directory name (not pwd, which could be a subdirectory)
|
package/scripts/sync-commands.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const YAML = require('yaml');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const { listCodexSkillEntries } = require('../lib/codex-skills');
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Parse YAML frontmatter from a markdown string.
|
|
@@ -293,9 +297,14 @@ function adaptForAgent(agentName, frontmatter, body, commandName) {
|
|
|
293
297
|
|
|
294
298
|
// ---- Sync logic -----------------------------------------------------------------
|
|
295
299
|
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
300
|
+
function resolveCanonicalCommandsDir(repoRoot) {
|
|
301
|
+
const candidates = [
|
|
302
|
+
path.join(repoRoot, 'commands'),
|
|
303
|
+
path.join(repoRoot, '.claude', 'commands'),
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
return candidates.find(candidate => fs.existsSync(candidate)) || null;
|
|
307
|
+
}
|
|
299
308
|
|
|
300
309
|
/**
|
|
301
310
|
* Compute a content hash for change detection.
|
|
@@ -310,6 +319,25 @@ function contentHash(content) {
|
|
|
310
319
|
return crypto.createHash('sha256').update(normalized).digest('hex');
|
|
311
320
|
}
|
|
312
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Write the sync manifest for generated entries without rewriting command files.
|
|
324
|
+
*
|
|
325
|
+
* @param {string} repoRoot
|
|
326
|
+
* @param {SyncEntry[]} entries
|
|
327
|
+
*/
|
|
328
|
+
function writeSyncManifest(repoRoot, entries) {
|
|
329
|
+
const manifestDir = path.join(repoRoot, '.forge');
|
|
330
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
331
|
+
const manifestData = {
|
|
332
|
+
generatedAt: new Date().toISOString(),
|
|
333
|
+
files: entries.map((e) => path.relative(repoRoot, e.filePath).replace(/\\/g, '/')),
|
|
334
|
+
};
|
|
335
|
+
fs.writeFileSync(
|
|
336
|
+
path.join(manifestDir, 'sync-manifest.json'),
|
|
337
|
+
JSON.stringify(manifestData, null, 2) + '\n'
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
313
341
|
/**
|
|
314
342
|
* @typedef {Object} SyncEntry
|
|
315
343
|
* @property {string} agent - Agent slug
|
|
@@ -339,11 +367,11 @@ function contentHash(content) {
|
|
|
339
367
|
* - `check: true` — compares generated content with existing files, reports mismatches
|
|
340
368
|
* - default (both false) — writes files, creating directories as needed
|
|
341
369
|
*
|
|
342
|
-
* @param {{ dryRun: boolean, check: boolean, repoRoot: string }} options
|
|
370
|
+
* @param {{ dryRun: boolean, check: boolean, repoRoot: string, canonicalDir?: string | null }} options
|
|
343
371
|
* @returns {SyncResult}
|
|
344
372
|
*/
|
|
345
|
-
function syncCommands({ dryRun, check, repoRoot }) {
|
|
346
|
-
const commandsDir = path.join(repoRoot, '.claude', 'commands');
|
|
373
|
+
function syncCommands({ dryRun, check, repoRoot, canonicalDir = null }) {
|
|
374
|
+
const commandsDir = canonicalDir || resolveCanonicalCommandsDir(repoRoot) || path.join(repoRoot, '.claude', 'commands');
|
|
347
375
|
|
|
348
376
|
// Read all .md files from the commands directory
|
|
349
377
|
/** @type {string[]} */
|
|
@@ -474,16 +502,7 @@ function syncCommands({ dryRun, check, repoRoot }) {
|
|
|
474
502
|
|
|
475
503
|
// Write sync manifest — records every file generated so stale detection
|
|
476
504
|
// can identify orphaned files without false-flagging custom files.
|
|
477
|
-
|
|
478
|
-
fs.mkdirSync(manifestDir, { recursive: true });
|
|
479
|
-
const manifestData = {
|
|
480
|
-
generatedAt: new Date().toISOString(),
|
|
481
|
-
files: written.map((e) => path.relative(repoRoot, e.filePath).replace(/\\/g, '/')),
|
|
482
|
-
};
|
|
483
|
-
fs.writeFileSync(
|
|
484
|
-
path.join(manifestDir, 'sync-manifest.json'),
|
|
485
|
-
JSON.stringify(manifestData, null, 2) + '\n'
|
|
486
|
-
);
|
|
505
|
+
writeSyncManifest(repoRoot, written);
|
|
487
506
|
|
|
488
507
|
return { written, overwritten };
|
|
489
508
|
}
|
|
@@ -506,7 +525,7 @@ if (require.main === module) {
|
|
|
506
525
|
|
|
507
526
|
if (dryRun) {
|
|
508
527
|
if (result.planned.length === 0) {
|
|
509
|
-
console.log('No command files found in .claude/commands/');
|
|
528
|
+
console.log('No command files found in commands/ or .claude/commands/');
|
|
510
529
|
} else {
|
|
511
530
|
console.log('Dry run — files that would be generated:\n');
|
|
512
531
|
for (const entry of result.planned) {
|
|
@@ -516,7 +535,7 @@ if (require.main === module) {
|
|
|
516
535
|
}
|
|
517
536
|
} else if (check) {
|
|
518
537
|
if (result.empty) {
|
|
519
|
-
console.error('Error: no command files found in .claude/commands/ — cannot verify sync.');
|
|
538
|
+
console.error('Error: no command files found in commands/ or .claude/commands/ — cannot verify sync.');
|
|
520
539
|
process.exit(1);
|
|
521
540
|
}
|
|
522
541
|
if (result.manifestMissing) {
|
|
@@ -561,11 +580,21 @@ if (require.main === module) {
|
|
|
561
580
|
}
|
|
562
581
|
|
|
563
582
|
if (result.written.length === 0) {
|
|
564
|
-
console.log('No command files found in .claude/commands/');
|
|
583
|
+
console.log('No command files found in commands/ or .claude/commands/');
|
|
565
584
|
} else {
|
|
566
585
|
console.log(`Synced ${result.written.length} file(s) across agents.`);
|
|
567
586
|
}
|
|
568
587
|
}
|
|
569
588
|
}
|
|
570
589
|
|
|
571
|
-
module.exports = {
|
|
590
|
+
module.exports = {
|
|
591
|
+
parseFrontmatter,
|
|
592
|
+
buildFile,
|
|
593
|
+
AGENT_ADAPTERS,
|
|
594
|
+
adaptForAgent,
|
|
595
|
+
listCodexSkillEntries,
|
|
596
|
+
syncCommands,
|
|
597
|
+
contentHash,
|
|
598
|
+
resolveCanonicalCommandsDir,
|
|
599
|
+
writeSyncManifest,
|
|
600
|
+
};
|
package/scripts/test.js
CHANGED
|
@@ -22,9 +22,24 @@ function detectPackageManager() {
|
|
|
22
22
|
const pkgManager = detectPackageManager();
|
|
23
23
|
console.log(`🧪 Running test suite (${pkgManager} test)...`);
|
|
24
24
|
|
|
25
|
+
// Strip git hook environment variables so child processes (especially tests
|
|
26
|
+
// that create temp git repos) never accidentally operate on the real worktree.
|
|
27
|
+
// During pre-push hooks, git sets GIT_DIR pointing to the repo — any test that
|
|
28
|
+
// runs `git init` / `git commit` in a temp dir inherits this and silently
|
|
29
|
+
// commits into the worktree instead, creating rogue "initial commit" that
|
|
30
|
+
// deletes the entire codebase.
|
|
31
|
+
const env = { ...process.env };
|
|
32
|
+
for (const key of Object.keys(env)) {
|
|
33
|
+
if (key === 'GIT_DIR' || key === 'GIT_WORK_TREE' || key === 'GIT_INDEX_FILE'
|
|
34
|
+
|| key === 'GIT_OBJECT_DIRECTORY' || key === 'GIT_ALTERNATE_OBJECT_DIRECTORIES'
|
|
35
|
+
|| key === 'GIT_QUARANTINE_PATH') {
|
|
36
|
+
delete env[key];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
// Use 'run test' to invoke the package.json script (which may include --timeout flags)
|
|
26
41
|
// 'bun test' is a built-in that ignores package.json scripts
|
|
27
|
-
const result = spawnSync(pkgManager, ['run', 'test'], { stdio: 'inherit', shell: isWindows });
|
|
42
|
+
const result = spawnSync(pkgManager, ['run', 'test'], { stdio: 'inherit', shell: isWindows, env });
|
|
28
43
|
|
|
29
44
|
if (result.error) {
|
|
30
45
|
console.error('');
|