@imdeadpool/guardex 7.0.16 → 7.0.18
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/CONTRIBUTING.md +1 -1
- package/README.md +178 -53
- package/bin/multiagent-safety.js +691 -125
- package/package.json +2 -2
- package/templates/AGENTS.multiagent-safety.md +4 -4
- package/templates/githooks/post-checkout +1 -1
- package/templates/githooks/post-merge +19 -6
- package/templates/githooks/pre-commit +8 -8
- package/templates/scripts/agent-session-state.js +110 -0
- package/templates/scripts/codex-agent.sh +77 -0
- package/templates/scripts/install-vscode-active-agents-extension.js +92 -0
- package/templates/scripts/openspec/init-change-workspace.sh +77 -9
- package/templates/scripts/openspec/init-plan-workspace.sh +576 -74
- package/templates/vscode/guardex-active-agents/README.md +21 -0
- package/templates/vscode/guardex-active-agents/extension.js +317 -0
- package/templates/vscode/guardex-active-agents/package.json +57 -0
- package/templates/vscode/guardex-active-agents/session-schema.js +407 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "7.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "7.0.18",
|
|
4
|
+
"description": "Guardian T-Rex for your multi-agent repo. Isolated worktrees, file locks, and PR-only merges stop parallel Codex & Claude agents from overwriting each other's work. Auto-wires Oh My Codex, Oh My Claude, OpenSpec, and Caveman.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"preferGlobal": true,
|
|
7
7
|
"bin": {
|
|
@@ -7,22 +7,22 @@
|
|
|
7
7
|
`GUARDEX_ON=0` disables Guardex for that repo.
|
|
8
8
|
`GUARDEX_ON=1` explicitly enables Guardex for that repo again.
|
|
9
9
|
|
|
10
|
-
**Isolation.** Every task runs on a dedicated `agent/*` branch + worktree. Start with `
|
|
10
|
+
**Isolation.** Every task runs on a dedicated `agent/*` branch + worktree. Start with `gx branch start "<task>" "<agent-name>"`. Treat the base branch (`main`/`dev`) as read-only while an agent branch is active. Never `git checkout <branch>` on a primary working tree (including nested repos); use `git worktree add` instead. The `.githooks/post-checkout` hook auto-reverts primary-branch switches during agent sessions - bypass only with `GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH=1`.
|
|
11
11
|
For every new task, including follow-up work in the same chat/session, if an assigned agent sub-branch/worktree is already open, continue in that sub-branch instead of creating a fresh lane unless the user explicitly redirects scope.
|
|
12
12
|
Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
|
|
13
13
|
|
|
14
|
-
**Ownership.** Before editing, claim files: `
|
|
14
|
+
**Ownership.** Before editing, claim files: `gx locks claim --branch "<agent-branch>" <file...>`. Before deleting, confirm the path is in your claim. Don't edit outside your scope unless reassigned.
|
|
15
15
|
|
|
16
16
|
**Handoff gate.** Post a one-line handoff note (plan/change, owned scope, intended action) before editing. Re-read the latest handoffs before replacing others' code.
|
|
17
17
|
|
|
18
|
-
**Completion.** Finish with `
|
|
18
|
+
**Completion.** Finish with `gx branch finish --branch "<agent-branch>" --via-pr --wait-for-merge --cleanup` (or `gx finish --all`). Task is only complete when: commit pushed, PR URL recorded, state = `MERGED`, sandbox worktree pruned. If anything blocks, append a `BLOCKED:` note and stop - don't half-finish.
|
|
19
19
|
OMX completion policy: when a task is done, the agent must commit the task changes, push the agent branch, and create/update a PR before considering the branch complete.
|
|
20
20
|
|
|
21
21
|
**Parallel safety.** Assume other agents edit nearby. Never revert unrelated changes. Report conflicts in the handoff.
|
|
22
22
|
|
|
23
23
|
**Reporting.** Every completion handoff includes: files changed, behavior touched, verification commands + results, risks/follow-ups.
|
|
24
24
|
|
|
25
|
-
**OpenSpec (when change-driven).** Keep `openspec/changes/<slug>/tasks.md` checkboxes current during work, not batched at the end. Task scaffolds and manual task edits must include an explicit final completion/cleanup section that ends with PR merge + sandbox cleanup (`gx finish --via-pr --wait-for-merge --cleanup` or `
|
|
25
|
+
**OpenSpec (when change-driven).** Keep `openspec/changes/<slug>/tasks.md` checkboxes current during work, not batched at the end. Task scaffolds and manual task edits must include an explicit final completion/cleanup section that ends with PR merge + sandbox cleanup (`gx finish --via-pr --wait-for-merge --cleanup` or `gx branch finish ... --cleanup`) and records PR URL + final `MERGED` evidence. Verify specs with `openspec validate --specs` before archive. Don't archive unverified.
|
|
26
26
|
|
|
27
27
|
**Version bumps.** If a change bumps a published version, the same PR updates release notes/changelog.
|
|
28
28
|
<!-- multiagent-safety:END -->
|
|
@@ -64,7 +64,7 @@ echo "[agent-primary-branch-guard] Primary checkout switched branches." >&2
|
|
|
64
64
|
echo "[agent-primary-branch-guard] from: $prev_branch (protected)" >&2
|
|
65
65
|
echo "[agent-primary-branch-guard] to: $new_branch" >&2
|
|
66
66
|
echo "[agent-primary-branch-guard] The primary working tree must stay on its base/protected branch." >&2
|
|
67
|
-
echo "[agent-primary-branch-guard] Use 'git worktree add' (or
|
|
67
|
+
echo "[agent-primary-branch-guard] Use 'git worktree add' (or gx branch start) for feature work." >&2
|
|
68
68
|
|
|
69
69
|
if [[ "$is_agent" == "1" ]]; then
|
|
70
70
|
echo "[agent-primary-branch-guard] Agent session detected — reverting to '$prev_branch'." >&2
|
|
@@ -32,17 +32,30 @@ if [[ "$branch" != "$base_branch" ]]; then
|
|
|
32
32
|
exit 0
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
if [[ -n "${GUARDEX_CLI_ENTRY:-}" ]]; then
|
|
36
|
+
node_bin="${GUARDEX_NODE_BIN:-node}"
|
|
37
|
+
if command -v "$node_bin" >/dev/null 2>&1; then
|
|
38
|
+
"$node_bin" "$GUARDEX_CLI_ENTRY" cleanup \
|
|
39
|
+
--target "$repo_root" \
|
|
40
|
+
--base "$base_branch" \
|
|
41
|
+
--include-pr-merged \
|
|
42
|
+
--keep-clean-worktrees >/dev/null 2>&1 || true
|
|
43
|
+
fi
|
|
37
44
|
exit 0
|
|
38
45
|
fi
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
if
|
|
42
|
-
|
|
47
|
+
cli_bin="${GUARDEX_CLI_BIN:-}"
|
|
48
|
+
if [[ -z "$cli_bin" ]]; then
|
|
49
|
+
if command -v gx >/dev/null 2>&1; then
|
|
50
|
+
cli_bin="gx"
|
|
51
|
+
elif command -v gitguardex >/dev/null 2>&1; then
|
|
52
|
+
cli_bin="gitguardex"
|
|
53
|
+
else
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
43
56
|
fi
|
|
44
57
|
|
|
45
|
-
"$
|
|
58
|
+
"$cli_bin" cleanup \
|
|
46
59
|
--target "$repo_root" \
|
|
47
60
|
--base "$base_branch" \
|
|
48
61
|
--include-pr-merged \
|
|
@@ -116,11 +116,11 @@ if [[ "$should_require_codex_agent_branch" == "1" && "${GUARDEX_ALLOW_CODEX_ON_N
|
|
|
116
116
|
|
|
117
117
|
cat >&2 <<'MSG'
|
|
118
118
|
[guardex-preedit-guard] Codex edit/commit detected on a protected branch.
|
|
119
|
-
|
|
119
|
+
GitGuardex requires Codex work to run from an isolated agent/* branch.
|
|
120
120
|
Start the sub-branch/worktree with:
|
|
121
|
-
|
|
121
|
+
gx branch start "<task-or-plan>" "<agent-name>"
|
|
122
122
|
Or manually:
|
|
123
|
-
|
|
123
|
+
gx branch start "<task-or-plan>" "<agent-name>"
|
|
124
124
|
Then commit from the created agent/* branch.
|
|
125
125
|
|
|
126
126
|
Temporary bypass (not recommended):
|
|
@@ -132,7 +132,7 @@ MSG
|
|
|
132
132
|
cat >&2 <<'MSG'
|
|
133
133
|
[codex-branch-guard] Codex agent commit blocked on non-agent branch.
|
|
134
134
|
Use isolated branch/worktree first:
|
|
135
|
-
|
|
135
|
+
gx branch start "<task-or-plan>" "<agent-name>"
|
|
136
136
|
Then commit from the created agent/* branch.
|
|
137
137
|
|
|
138
138
|
Temporary bypass (not recommended):
|
|
@@ -163,9 +163,9 @@ if [[ "$is_protected_branch" == "1" ]]; then
|
|
|
163
163
|
cat >&2 <<'MSG'
|
|
164
164
|
[agent-branch-guard] Direct commits on protected branches are blocked.
|
|
165
165
|
Use an agent branch first:
|
|
166
|
-
|
|
166
|
+
gx branch start "<task-or-plan>" "<agent-name>"
|
|
167
167
|
After finishing work:
|
|
168
|
-
|
|
168
|
+
gx branch finish
|
|
169
169
|
|
|
170
170
|
Temporary bypass (not recommended):
|
|
171
171
|
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
|
|
@@ -177,7 +177,7 @@ if [[ "$is_agent_session" == "1" && "$branch" != agent/* ]]; then
|
|
|
177
177
|
cat >&2 <<'MSG'
|
|
178
178
|
[agent-branch-guard] Agent commits must run on dedicated agent/* branches.
|
|
179
179
|
Start an agent branch first:
|
|
180
|
-
|
|
180
|
+
gx branch start "<task-or-plan>" "<agent-name>"
|
|
181
181
|
Then commit on that branch.
|
|
182
182
|
|
|
183
183
|
Temporary bypass (not recommended):
|
|
@@ -199,7 +199,7 @@ if [[ "$branch" == agent/* ]]; then
|
|
|
199
199
|
cat >&2 <<'MSG'
|
|
200
200
|
[agent-branch-guard] Agent branch commits require file ownership locks.
|
|
201
201
|
Claim files first:
|
|
202
|
-
|
|
202
|
+
gx locks claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
|
|
203
203
|
MSG
|
|
204
204
|
exit 1
|
|
205
205
|
fi
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
function resolveSessionSchemaModule() {
|
|
7
|
+
const candidates = [
|
|
8
|
+
path.resolve(__dirname, '..', 'vscode', 'guardex-active-agents', 'session-schema.js'),
|
|
9
|
+
path.resolve(__dirname, '..', 'templates', 'vscode', 'guardex-active-agents', 'session-schema.js'),
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
for (const candidate of candidates) {
|
|
13
|
+
if (fs.existsSync(candidate)) {
|
|
14
|
+
return require(candidate);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw new Error('Could not resolve Guardex active-agent session schema module.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sessionSchema = resolveSessionSchemaModule();
|
|
22
|
+
|
|
23
|
+
function usage() {
|
|
24
|
+
return (
|
|
25
|
+
'Usage:\n' +
|
|
26
|
+
' node scripts/agent-session-state.js start --repo <path> --branch <name> --task <task> --agent <agent> --worktree <path> --pid <pid> --cli <name>\n' +
|
|
27
|
+
' node scripts/agent-session-state.js stop --repo <path> --branch <name>\n'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseOptions(argv) {
|
|
32
|
+
const options = {};
|
|
33
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
34
|
+
const token = argv[index];
|
|
35
|
+
if (!token.startsWith('--')) {
|
|
36
|
+
throw new Error(`Unexpected argument: ${token}`);
|
|
37
|
+
}
|
|
38
|
+
const key = token.slice(2);
|
|
39
|
+
const value = argv[index + 1];
|
|
40
|
+
if (!value || value.startsWith('--')) {
|
|
41
|
+
throw new Error(`Missing value for --${key}`);
|
|
42
|
+
}
|
|
43
|
+
options[key] = value;
|
|
44
|
+
index += 1;
|
|
45
|
+
}
|
|
46
|
+
return options;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function requireOption(options, key) {
|
|
50
|
+
const value = options[key];
|
|
51
|
+
if (!value) {
|
|
52
|
+
throw new Error(`Missing required option --${key}`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeSessionRecord(options) {
|
|
58
|
+
const repoRoot = requireOption(options, 'repo');
|
|
59
|
+
const branch = requireOption(options, 'branch');
|
|
60
|
+
const record = sessionSchema.buildSessionRecord({
|
|
61
|
+
repoRoot,
|
|
62
|
+
branch,
|
|
63
|
+
taskName: requireOption(options, 'task'),
|
|
64
|
+
agentName: requireOption(options, 'agent'),
|
|
65
|
+
worktreePath: requireOption(options, 'worktree'),
|
|
66
|
+
pid: requireOption(options, 'pid'),
|
|
67
|
+
cliName: requireOption(options, 'cli'),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const targetPath = sessionSchema.sessionFilePathForBranch(repoRoot, branch);
|
|
71
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
72
|
+
fs.writeFileSync(targetPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function removeSessionRecord(options) {
|
|
76
|
+
const repoRoot = requireOption(options, 'repo');
|
|
77
|
+
const branch = requireOption(options, 'branch');
|
|
78
|
+
const targetPath = sessionSchema.sessionFilePathForBranch(repoRoot, branch);
|
|
79
|
+
if (fs.existsSync(targetPath)) {
|
|
80
|
+
fs.unlinkSync(targetPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function main() {
|
|
85
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
86
|
+
if (!command || ['-h', '--help', 'help'].includes(command)) {
|
|
87
|
+
process.stdout.write(usage());
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const options = parseOptions(rest);
|
|
92
|
+
if (command === 'start') {
|
|
93
|
+
writeSessionRecord(options);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (command === 'stop') {
|
|
97
|
+
removeSessionRecord(options);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new Error(`Unknown subcommand: ${command}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
main();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
process.stderr.write(`[guardex-active-session] ${error.message}\n`);
|
|
108
|
+
process.stderr.write(usage());
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
}
|
|
@@ -6,6 +6,7 @@ AGENT_NAME="${GUARDEX_AGENT_NAME:-agent}"
|
|
|
6
6
|
BASE_BRANCH="${GUARDEX_BASE_BRANCH:-}"
|
|
7
7
|
BASE_BRANCH_EXPLICIT=0
|
|
8
8
|
CODEX_BIN="${GUARDEX_CODEX_BIN:-codex}"
|
|
9
|
+
NODE_BIN="${GUARDEX_NODE_BIN:-node}"
|
|
9
10
|
AUTO_FINISH_RAW="${GUARDEX_CODEX_AUTO_FINISH:-true}"
|
|
10
11
|
AUTO_REVIEW_ON_CONFLICT_RAW="${GUARDEX_CODEX_AUTO_REVIEW_ON_CONFLICT:-true}"
|
|
11
12
|
AUTO_CLEANUP_RAW="${GUARDEX_CODEX_AUTO_CLEANUP:-true}"
|
|
@@ -143,6 +144,7 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
|
143
144
|
exit 1
|
|
144
145
|
fi
|
|
145
146
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
147
|
+
active_session_state_script="${repo_root}/scripts/agent-session-state.js"
|
|
146
148
|
|
|
147
149
|
guardex_env_helper="${repo_root}/scripts/guardex-env.sh"
|
|
148
150
|
if [[ -f "$guardex_env_helper" ]]; then
|
|
@@ -446,6 +448,40 @@ has_origin_remote() {
|
|
|
446
448
|
git -C "$repo_root" remote get-url origin >/dev/null 2>&1
|
|
447
449
|
}
|
|
448
450
|
|
|
451
|
+
run_active_session_state() {
|
|
452
|
+
local action="$1"
|
|
453
|
+
shift
|
|
454
|
+
|
|
455
|
+
if [[ ! -f "$active_session_state_script" ]]; then
|
|
456
|
+
return 0
|
|
457
|
+
fi
|
|
458
|
+
if ! command -v "$NODE_BIN" >/dev/null 2>&1; then
|
|
459
|
+
return 0
|
|
460
|
+
fi
|
|
461
|
+
|
|
462
|
+
"$NODE_BIN" "$active_session_state_script" "$action" "$@" >/dev/null 2>&1 || true
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
record_active_session_state() {
|
|
466
|
+
local wt="$1"
|
|
467
|
+
local branch="$2"
|
|
468
|
+
|
|
469
|
+
run_active_session_state \
|
|
470
|
+
start \
|
|
471
|
+
--repo "$repo_root" \
|
|
472
|
+
--branch "$branch" \
|
|
473
|
+
--task "$TASK_NAME" \
|
|
474
|
+
--agent "$AGENT_NAME" \
|
|
475
|
+
--worktree "$wt" \
|
|
476
|
+
--pid "$$" \
|
|
477
|
+
--cli "$CODEX_BIN"
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
clear_active_session_state() {
|
|
481
|
+
local branch="$1"
|
|
482
|
+
run_active_session_state stop --repo "$repo_root" --branch "$branch"
|
|
483
|
+
}
|
|
484
|
+
|
|
449
485
|
origin_remote_supports_pr_finish() {
|
|
450
486
|
local origin_url
|
|
451
487
|
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
|
|
@@ -474,6 +510,31 @@ resolve_worktree_base_branch() {
|
|
|
474
510
|
printf 'dev'
|
|
475
511
|
}
|
|
476
512
|
|
|
513
|
+
print_takeover_prompt() {
|
|
514
|
+
local wt="$1"
|
|
515
|
+
local branch="$2"
|
|
516
|
+
local base_branch change_slug change_artifact finish_cmd
|
|
517
|
+
|
|
518
|
+
base_branch="$(resolve_worktree_base_branch "$wt")"
|
|
519
|
+
if [[ -z "$base_branch" ]]; then
|
|
520
|
+
base_branch="dev"
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
change_slug="$(resolve_openspec_change_slug "$branch")"
|
|
524
|
+
change_artifact="openspec/changes/${change_slug}/tasks.md"
|
|
525
|
+
if [[ ! -f "${wt}/${change_artifact}" ]]; then
|
|
526
|
+
change_artifact="openspec/changes/${change_slug}/notes.md"
|
|
527
|
+
fi
|
|
528
|
+
if [[ ! -f "${wt}/${change_artifact}" ]]; then
|
|
529
|
+
change_artifact="openspec/changes/${change_slug}/"
|
|
530
|
+
fi
|
|
531
|
+
|
|
532
|
+
finish_cmd="bash scripts/agent-branch-finish.sh --branch \"${branch}\" --base ${base_branch} --via-pr --wait-for-merge --cleanup"
|
|
533
|
+
|
|
534
|
+
echo "[codex-agent] Takeover sandbox: ${wt}"
|
|
535
|
+
echo "[codex-agent] Takeover prompt: Continue \`${change_slug}\` on branch \`${branch}\`. Work inside \`${wt}\`, review \`${change_artifact}\`, continue from the current state instead of creating a new sandbox, and when the work is done run \`${finish_cmd}\`."
|
|
536
|
+
}
|
|
537
|
+
|
|
477
538
|
sync_worktree_with_base() {
|
|
478
539
|
local wt="$1"
|
|
479
540
|
if ! has_origin_remote; then
|
|
@@ -833,6 +894,19 @@ if ! ensure_openspec_plan_workspace "$worktree_path" "$worktree_branch"; then
|
|
|
833
894
|
exit 1
|
|
834
895
|
fi
|
|
835
896
|
|
|
897
|
+
active_session_recorded=0
|
|
898
|
+
cleanup_active_session_state_on_exit() {
|
|
899
|
+
set +e
|
|
900
|
+
if [[ "${active_session_recorded:-0}" -eq 1 && -n "${worktree_branch:-}" && "${worktree_branch:-}" != "HEAD" ]]; then
|
|
901
|
+
clear_active_session_state "$worktree_branch"
|
|
902
|
+
active_session_recorded=0
|
|
903
|
+
fi
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
record_active_session_state "$worktree_path" "$worktree_branch"
|
|
907
|
+
active_session_recorded=1
|
|
908
|
+
trap cleanup_active_session_state_on_exit EXIT INT TERM
|
|
909
|
+
|
|
836
910
|
echo "[codex-agent] Launching ${CODEX_BIN} in sandbox: $worktree_path"
|
|
837
911
|
cd "$worktree_path"
|
|
838
912
|
set +e
|
|
@@ -841,6 +915,8 @@ codex_exit="$?"
|
|
|
841
915
|
set -e
|
|
842
916
|
|
|
843
917
|
cd "$repo_root"
|
|
918
|
+
cleanup_active_session_state_on_exit
|
|
919
|
+
trap - EXIT INT TERM
|
|
844
920
|
final_exit="$codex_exit"
|
|
845
921
|
auto_finish_completed=0
|
|
846
922
|
|
|
@@ -901,6 +977,7 @@ else
|
|
|
901
977
|
if [[ "$auto_finish_completed" -eq 1 ]]; then
|
|
902
978
|
echo "[codex-agent] Branch kept intentionally. Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
|
|
903
979
|
else
|
|
980
|
+
print_takeover_prompt "$worktree_path" "$worktree_branch"
|
|
904
981
|
echo "[codex-agent] If finished, merge with: bash scripts/agent-branch-finish.sh --branch \"${worktree_branch}\" --base dev --via-pr --wait-for-merge"
|
|
905
982
|
echo "[codex-agent] Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
|
|
906
983
|
fi
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
function parseOptions(argv) {
|
|
8
|
+
const options = {};
|
|
9
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
10
|
+
const token = argv[index];
|
|
11
|
+
if (!token.startsWith('--')) {
|
|
12
|
+
throw new Error(`Unexpected argument: ${token}`);
|
|
13
|
+
}
|
|
14
|
+
const key = token.slice(2);
|
|
15
|
+
const value = argv[index + 1];
|
|
16
|
+
if (!value || value.startsWith('--')) {
|
|
17
|
+
throw new Error(`Missing value for --${key}`);
|
|
18
|
+
}
|
|
19
|
+
options[key] = value;
|
|
20
|
+
index += 1;
|
|
21
|
+
}
|
|
22
|
+
return options;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveExtensionSource(repoRoot) {
|
|
26
|
+
const candidates = [
|
|
27
|
+
path.join(repoRoot, 'vscode', 'guardex-active-agents'),
|
|
28
|
+
path.join(repoRoot, 'templates', 'vscode', 'guardex-active-agents'),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
for (const candidate of candidates) {
|
|
32
|
+
if (fs.existsSync(path.join(candidate, 'package.json'))) {
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
throw new Error('Could not find the Guardex VS Code companion sources.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function removeIfExists(targetPath) {
|
|
41
|
+
if (fs.existsSync(targetPath)) {
|
|
42
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function main() {
|
|
47
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
48
|
+
const options = parseOptions(process.argv.slice(2));
|
|
49
|
+
const sourceDir = resolveExtensionSource(repoRoot);
|
|
50
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(sourceDir, 'package.json'), 'utf8'));
|
|
51
|
+
const extensionId = `${manifest.publisher}.${manifest.name}`;
|
|
52
|
+
const extensionsDir = path.resolve(
|
|
53
|
+
options['extensions-dir'] ||
|
|
54
|
+
process.env.GUARDEX_VSCODE_EXTENSIONS_DIR ||
|
|
55
|
+
process.env.VSCODE_EXTENSIONS_DIR ||
|
|
56
|
+
path.join(os.homedir(), '.vscode', 'extensions'),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
60
|
+
const targetDir = path.join(extensionsDir, `${extensionId}-${manifest.version}`);
|
|
61
|
+
|
|
62
|
+
for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
63
|
+
if (!entry.isDirectory()) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (entry.name === path.basename(targetDir)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (entry.name.startsWith(`${extensionId}-`)) {
|
|
70
|
+
removeIfExists(path.join(extensionsDir, entry.name));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
removeIfExists(targetDir);
|
|
75
|
+
fs.cpSync(sourceDir, targetDir, {
|
|
76
|
+
recursive: true,
|
|
77
|
+
force: true,
|
|
78
|
+
preserveTimestamps: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
process.stdout.write(
|
|
82
|
+
`[guardex-active-agents] Installed ${extensionId}@${manifest.version} to ${targetDir}\n` +
|
|
83
|
+
'[guardex-active-agents] Reload the VS Code window to activate the Source Control companion.\n',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
main();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
process.stderr.write(`[guardex-active-agents] ${error.message}\n`);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
if [[ $# -lt 1 || $# -gt
|
|
5
|
-
echo "Usage: $0 <change-slug> [capability-slug]"
|
|
6
|
-
echo "Example: $0 add-dashboard-live-usage runtime-migration"
|
|
4
|
+
if [[ $# -lt 1 || $# -gt 3 ]]; then
|
|
5
|
+
echo "Usage: $0 <change-slug> [capability-slug] [agent-branch]"
|
|
6
|
+
echo "Example: $0 add-dashboard-live-usage runtime-migration agent/claude-odin/add-dashboard-live-usage-123456"
|
|
7
7
|
exit 1
|
|
8
8
|
fi
|
|
9
9
|
|
|
10
10
|
CHANGE_SLUG="$1"
|
|
11
11
|
CAPABILITY_SLUG="${2:-$CHANGE_SLUG}"
|
|
12
|
+
AGENT_BRANCH="${3:-agent/<your-name>/<branch-slug>}"
|
|
12
13
|
|
|
13
14
|
if [[ "$CHANGE_SLUG" =~ [^a-z0-9-] ]]; then
|
|
14
15
|
echo "Error: change slug must be kebab-case (lowercase letters, numbers, hyphens)."
|
|
@@ -20,11 +21,39 @@ if [[ "$CAPABILITY_SLUG" =~ [^a-z0-9-] ]]; then
|
|
|
20
21
|
exit 1
|
|
21
22
|
fi
|
|
22
23
|
|
|
24
|
+
resolve_base_branch() {
|
|
25
|
+
local branch="$1"
|
|
26
|
+
local base_branch=""
|
|
27
|
+
|
|
28
|
+
if [[ -n "$branch" ]] && [[ "$branch" != "agent/<your-name>/<branch-slug>" ]]; then
|
|
29
|
+
base_branch="$(git config --get "branch.${branch}.guardexBase" || true)"
|
|
30
|
+
fi
|
|
31
|
+
if [[ -z "$base_branch" ]]; then
|
|
32
|
+
base_branch="$(git config --get multiagent.baseBranch || true)"
|
|
33
|
+
fi
|
|
34
|
+
if [[ -z "$base_branch" ]]; then
|
|
35
|
+
base_branch="dev"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
printf '%s' "$base_branch"
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
CHANGE_DIR="openspec/changes/${CHANGE_SLUG}"
|
|
24
42
|
SPEC_DIR="${CHANGE_DIR}/specs/${CAPABILITY_SLUG}"
|
|
25
43
|
TODAY="$(date -u +%Y-%m-%d)"
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
BASE_BRANCH="$(resolve_base_branch "$AGENT_BRANCH")"
|
|
45
|
+
|
|
46
|
+
MINIMAL_RAW="${GUARDEX_OPENSPEC_MINIMAL:-0}"
|
|
47
|
+
case "$(printf '%s' "$MINIMAL_RAW" | tr '[:upper:]' '[:lower:]')" in
|
|
48
|
+
1|true|yes|on) MINIMAL=1 ;;
|
|
49
|
+
*) MINIMAL=0 ;;
|
|
50
|
+
esac
|
|
51
|
+
|
|
52
|
+
if [[ "$MINIMAL" -eq 1 ]]; then
|
|
53
|
+
mkdir -p "$CHANGE_DIR"
|
|
54
|
+
else
|
|
55
|
+
mkdir -p "$SPEC_DIR"
|
|
56
|
+
fi
|
|
28
57
|
|
|
29
58
|
if [[ ! -f "${CHANGE_DIR}/.openspec.yaml" ]]; then
|
|
30
59
|
cat > "${CHANGE_DIR}/.openspec.yaml" <<YAMLEOF
|
|
@@ -33,6 +62,32 @@ created: ${TODAY}
|
|
|
33
62
|
YAMLEOF
|
|
34
63
|
fi
|
|
35
64
|
|
|
65
|
+
if [[ "$MINIMAL" -eq 1 ]]; then
|
|
66
|
+
if [[ ! -f "${CHANGE_DIR}/notes.md" ]]; then
|
|
67
|
+
cat > "${CHANGE_DIR}/notes.md" <<NOTESEOF
|
|
68
|
+
# ${CHANGE_SLUG} (minimal / T1)
|
|
69
|
+
|
|
70
|
+
Branch: \`${AGENT_BRANCH}\`
|
|
71
|
+
|
|
72
|
+
Describe the change in a sentence or two. Commit message is the spec of record.
|
|
73
|
+
|
|
74
|
+
## Handoff
|
|
75
|
+
|
|
76
|
+
- Handoff: change=\`${CHANGE_SLUG}\`; branch=\`${AGENT_BRANCH}\`; scope=\`TODO\`; action=\`continue this sandbox or finish cleanup after a usage-limit/manual takeover\`.
|
|
77
|
+
- Copy prompt: Continue \`${CHANGE_SLUG}\` on branch \`${AGENT_BRANCH}\`. Work inside the existing sandbox, review \`openspec/changes/${CHANGE_SLUG}/notes.md\`, continue from the current state instead of creating a new sandbox, and when the work is done run \`gx branch finish --branch ${AGENT_BRANCH} --base ${BASE_BRANCH} --via-pr --wait-for-merge --cleanup\`.
|
|
78
|
+
|
|
79
|
+
## Cleanup
|
|
80
|
+
|
|
81
|
+
- [ ] Run: \`gx branch finish --branch ${AGENT_BRANCH} --base ${BASE_BRANCH} --via-pr --wait-for-merge --cleanup\`
|
|
82
|
+
- [ ] Record PR URL + \`MERGED\` state in the completion handoff.
|
|
83
|
+
- [ ] Confirm sandbox worktree is gone (\`git worktree list\`, \`git branch -a\`).
|
|
84
|
+
NOTESEOF
|
|
85
|
+
fi
|
|
86
|
+
echo "[gitguardex] OpenSpec change workspace (minimal) ready: ${CHANGE_DIR}"
|
|
87
|
+
echo "[gitguardex] Notes-only scaffold: ${CHANGE_DIR}/notes.md"
|
|
88
|
+
exit 0
|
|
89
|
+
fi
|
|
90
|
+
|
|
36
91
|
if [[ ! -f "${CHANGE_DIR}/proposal.md" ]]; then
|
|
37
92
|
cat > "${CHANGE_DIR}/proposal.md" <<PROPOSALEOF
|
|
38
93
|
## Why
|
|
@@ -51,6 +106,19 @@ fi
|
|
|
51
106
|
|
|
52
107
|
if [[ ! -f "${CHANGE_DIR}/tasks.md" ]]; then
|
|
53
108
|
cat > "${CHANGE_DIR}/tasks.md" <<TASKSEOF
|
|
109
|
+
## Definition of Done
|
|
110
|
+
|
|
111
|
+
This change is complete only when **all** of the following are true:
|
|
112
|
+
|
|
113
|
+
- Every checkbox below is checked.
|
|
114
|
+
- The agent branch reaches \`MERGED\` state on \`origin\` and the PR URL + state are recorded in the completion handoff.
|
|
115
|
+
- If any step blocks (test failure, conflict, ambiguous result), append a \`BLOCKED:\` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline.
|
|
116
|
+
|
|
117
|
+
## Handoff
|
|
118
|
+
|
|
119
|
+
- Handoff: change=\`${CHANGE_SLUG}\`; branch=\`${AGENT_BRANCH}\`; scope=\`TODO\`; action=\`continue this sandbox or finish cleanup after a usage-limit/manual takeover\`.
|
|
120
|
+
- Copy prompt: Continue \`${CHANGE_SLUG}\` on branch \`${AGENT_BRANCH}\`. Work inside the existing sandbox, review \`openspec/changes/${CHANGE_SLUG}/tasks.md\`, continue from the current state instead of creating a new sandbox, and when the work is done run \`gx branch finish --branch ${AGENT_BRANCH} --base ${BASE_BRANCH} --via-pr --wait-for-merge --cleanup\`.
|
|
121
|
+
|
|
54
122
|
## 1. Specification
|
|
55
123
|
|
|
56
124
|
- [ ] 1.1 Finalize proposal scope and acceptance criteria for \`${CHANGE_SLUG}\`.
|
|
@@ -67,11 +135,11 @@ if [[ ! -f "${CHANGE_DIR}/tasks.md" ]]; then
|
|
|
67
135
|
- [ ] 3.2 Run \`openspec validate ${CHANGE_SLUG} --type change --strict\`.
|
|
68
136
|
- [ ] 3.3 Run \`openspec validate --specs\`.
|
|
69
137
|
|
|
70
|
-
## 4.
|
|
138
|
+
## 4. Cleanup (mandatory; run before claiming completion)
|
|
71
139
|
|
|
72
|
-
- [ ] 4.1
|
|
73
|
-
- [ ] 4.2 Record PR URL
|
|
74
|
-
- [ ] 4.3 Confirm sandbox
|
|
140
|
+
- [ ] 4.1 Run the cleanup pipeline: \`gx branch finish --branch ${AGENT_BRANCH} --base ${BASE_BRANCH} --via-pr --wait-for-merge --cleanup\`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation.
|
|
141
|
+
- [ ] 4.2 Record the PR URL and final merge state (\`MERGED\`) in the completion handoff.
|
|
142
|
+
- [ ] 4.3 Confirm the sandbox worktree is gone (\`git worktree list\` no longer shows the agent path; \`git branch -a\` shows no surviving local/remote refs for the branch).
|
|
75
143
|
TASKSEOF
|
|
76
144
|
fi
|
|
77
145
|
|