@imdeadpool/guardex 7.0.15 → 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 +182 -51
- package/bin/multiagent-safety.js +993 -172
- package/package.json +3 -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-branch-merge.sh +421 -0
- package/templates/scripts/agent-branch-start.sh +43 -3
- package/templates/scripts/agent-session-state.js +110 -0
- package/templates/scripts/codex-agent.sh +124 -2
- 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 +592 -48
- 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
|
@@ -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}"
|
|
@@ -14,6 +15,7 @@ OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-true}"
|
|
|
14
15
|
OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
|
|
15
16
|
OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
|
|
16
17
|
OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
|
|
18
|
+
OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}"
|
|
17
19
|
|
|
18
20
|
normalize_bool() {
|
|
19
21
|
local raw="${1:-}"
|
|
@@ -34,6 +36,19 @@ AUTO_CLEANUP="$(normalize_bool "$AUTO_CLEANUP_RAW" "1")"
|
|
|
34
36
|
AUTO_WAIT_FOR_MERGE="$(normalize_bool "$AUTO_WAIT_FOR_MERGE_RAW" "1")"
|
|
35
37
|
OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
|
|
36
38
|
|
|
39
|
+
resolve_openspec_masterplan_label() {
|
|
40
|
+
local raw="${OPENSPEC_MASTERPLAN_LABEL_RAW:-}"
|
|
41
|
+
local label
|
|
42
|
+
|
|
43
|
+
if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]] || [[ -z "$raw" ]]; then
|
|
44
|
+
printf ''
|
|
45
|
+
return 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
label="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
|
|
49
|
+
printf '%s' "$label"
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
38
53
|
BASE_BRANCH_EXPLICIT=1
|
|
39
54
|
fi
|
|
@@ -129,6 +144,7 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
|
129
144
|
exit 1
|
|
130
145
|
fi
|
|
131
146
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
147
|
+
active_session_state_script="${repo_root}/scripts/agent-session-state.js"
|
|
132
148
|
|
|
133
149
|
guardex_env_helper="${repo_root}/scripts/guardex-env.sh"
|
|
134
150
|
if [[ -f "$guardex_env_helper" ]]; then
|
|
@@ -161,11 +177,21 @@ sanitize_slug() {
|
|
|
161
177
|
resolve_openspec_plan_slug() {
|
|
162
178
|
local branch_name="$1"
|
|
163
179
|
local task_slug
|
|
180
|
+
local masterplan_label=""
|
|
181
|
+
local branch_role=""
|
|
182
|
+
local branch_leaf=""
|
|
164
183
|
task_slug="$(sanitize_slug "$TASK_NAME" "task")"
|
|
165
184
|
if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then
|
|
166
185
|
sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug"
|
|
167
186
|
return 0
|
|
168
187
|
fi
|
|
188
|
+
masterplan_label="$(resolve_openspec_masterplan_label)"
|
|
189
|
+
if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then
|
|
190
|
+
branch_role="${BASH_REMATCH[1]}"
|
|
191
|
+
branch_leaf="${BASH_REMATCH[2]}"
|
|
192
|
+
sanitize_slug "agent-${branch_role}-${masterplan_label}-${branch_leaf}" "$task_slug"
|
|
193
|
+
return 0
|
|
194
|
+
fi
|
|
169
195
|
sanitize_slug "${branch_name//\//-}" "$task_slug"
|
|
170
196
|
}
|
|
171
197
|
|
|
@@ -190,6 +216,23 @@ resolve_openspec_capability_slug() {
|
|
|
190
216
|
sanitize_slug "$task_slug" "general-behavior"
|
|
191
217
|
}
|
|
192
218
|
|
|
219
|
+
resolve_worktree_leaf() {
|
|
220
|
+
local branch_name="$1"
|
|
221
|
+
local masterplan_label=""
|
|
222
|
+
local branch_role=""
|
|
223
|
+
local branch_leaf=""
|
|
224
|
+
|
|
225
|
+
masterplan_label="$(resolve_openspec_masterplan_label)"
|
|
226
|
+
if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then
|
|
227
|
+
branch_role="${BASH_REMATCH[1]}"
|
|
228
|
+
branch_leaf="${BASH_REMATCH[2]}"
|
|
229
|
+
printf 'agent__%s__%s__%s' "$branch_role" "$masterplan_label" "$branch_leaf"
|
|
230
|
+
return 0
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
printf '%s' "${branch_name//\//__}"
|
|
234
|
+
}
|
|
235
|
+
|
|
193
236
|
hydrate_local_helper_in_worktree() {
|
|
194
237
|
local worktree="$1"
|
|
195
238
|
local relative_path="$2"
|
|
@@ -314,7 +357,7 @@ start_sandbox_fallback() {
|
|
|
314
357
|
|
|
315
358
|
worktree_root="${repo_root}/.omx/agent-worktrees"
|
|
316
359
|
mkdir -p "$worktree_root"
|
|
317
|
-
worktree_path="${worktree_root}/$
|
|
360
|
+
worktree_path="${worktree_root}/$(resolve_worktree_leaf "$branch_name")"
|
|
318
361
|
if [[ -e "$worktree_path" ]]; then
|
|
319
362
|
echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2
|
|
320
363
|
return 1
|
|
@@ -346,7 +389,11 @@ initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/nu
|
|
|
346
389
|
start_output=""
|
|
347
390
|
start_status=0
|
|
348
391
|
set +e
|
|
349
|
-
start_output="$(
|
|
392
|
+
start_output="$(
|
|
393
|
+
GUARDEX_OPENSPEC_AUTO_INIT="$OPENSPEC_AUTO_INIT" \
|
|
394
|
+
GUARDEX_OPENSPEC_MASTERPLAN_LABEL="$OPENSPEC_MASTERPLAN_LABEL_RAW" \
|
|
395
|
+
bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1
|
|
396
|
+
)"
|
|
350
397
|
start_status=$?
|
|
351
398
|
set -e
|
|
352
399
|
|
|
@@ -401,6 +448,40 @@ has_origin_remote() {
|
|
|
401
448
|
git -C "$repo_root" remote get-url origin >/dev/null 2>&1
|
|
402
449
|
}
|
|
403
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
|
+
|
|
404
485
|
origin_remote_supports_pr_finish() {
|
|
405
486
|
local origin_url
|
|
406
487
|
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
|
|
@@ -429,6 +510,31 @@ resolve_worktree_base_branch() {
|
|
|
429
510
|
printf 'dev'
|
|
430
511
|
}
|
|
431
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
|
+
|
|
432
538
|
sync_worktree_with_base() {
|
|
433
539
|
local wt="$1"
|
|
434
540
|
if ! has_origin_remote; then
|
|
@@ -788,6 +894,19 @@ if ! ensure_openspec_plan_workspace "$worktree_path" "$worktree_branch"; then
|
|
|
788
894
|
exit 1
|
|
789
895
|
fi
|
|
790
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
|
+
|
|
791
910
|
echo "[codex-agent] Launching ${CODEX_BIN} in sandbox: $worktree_path"
|
|
792
911
|
cd "$worktree_path"
|
|
793
912
|
set +e
|
|
@@ -796,6 +915,8 @@ codex_exit="$?"
|
|
|
796
915
|
set -e
|
|
797
916
|
|
|
798
917
|
cd "$repo_root"
|
|
918
|
+
cleanup_active_session_state_on_exit
|
|
919
|
+
trap - EXIT INT TERM
|
|
799
920
|
final_exit="$codex_exit"
|
|
800
921
|
auto_finish_completed=0
|
|
801
922
|
|
|
@@ -856,6 +977,7 @@ else
|
|
|
856
977
|
if [[ "$auto_finish_completed" -eq 1 ]]; then
|
|
857
978
|
echo "[codex-agent] Branch kept intentionally. Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
|
|
858
979
|
else
|
|
980
|
+
print_takeover_prompt "$worktree_path" "$worktree_branch"
|
|
859
981
|
echo "[codex-agent] If finished, merge with: bash scripts/agent-branch-finish.sh --branch \"${worktree_branch}\" --base dev --via-pr --wait-for-merge"
|
|
860
982
|
echo "[codex-agent] Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
|
|
861
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
|
|