@aipper/aiws 0.0.27 → 0.0.29

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.
@@ -0,0 +1,83 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { UserError } from "../errors.js";
4
+
5
+ /**
6
+ * aiws finish — ff-merge + push + archive
7
+ * @param {{ cwd?: string, changeId?: string, into?: string, push?: boolean, remote?: string }} opts
8
+ */
9
+ export async function wsFinishCommand({ cwd = ".", changeId, into, push = false, remote = "origin" } = {}) {
10
+ checkClean(cwd);
11
+
12
+ if (!changeId) {
13
+ changeId = detectChangeBranch(cwd);
14
+ }
15
+
16
+ if (!changeId) {
17
+ throw new UserError("Could not detect change ID. Provide as argument or be on a change branch.", { exitCode: 2 });
18
+ }
19
+
20
+ const targetBranch = into || "main";
21
+
22
+ // Validate before complete
23
+ try {
24
+ execSync(`npx -y @aipper/aiws change validate ${changeId} --strict 2>/dev/null`, { cwd, stdio: "inherit" });
25
+ } catch {
26
+ throw new UserError(`Validation failed for ${changeId}. Fix issues first.`, { exitCode: 1 });
27
+ }
28
+
29
+ // FF merge
30
+ const changeBranch = `change/${changeId}`;
31
+ const branchExists = execSync(`git branch --list '${changeBranch}'`, { cwd, encoding: "utf8" }).trim();
32
+ if (branchExists) {
33
+ console.log(`info: ff-merging ${changeBranch} → ${targetBranch}...`);
34
+ execSync(`git checkout ${targetBranch}`, { cwd, stdio: "inherit" });
35
+ execSync(`git merge --ff-only ${changeBranch}`, { cwd, stdio: "inherit" });
36
+ } else {
37
+ console.log("info: no change branch found, using current branch");
38
+ }
39
+
40
+ // Push
41
+ if (push) {
42
+ const currentBranch = execSync("git branch --show-current", { cwd, encoding: "utf8" }).trim();
43
+
44
+ // Push submodules first
45
+ if (existsSync(`${cwd}/.gitmodules`)) {
46
+ const targetsFile = `changes/${changeId}/submodules.targets`;
47
+ if (existsSync(targetsFile)) {
48
+ const content = execSync(`cat "${targetsFile}"`, { cwd, encoding: "utf8" });
49
+ for (const line of content.split("\n")) {
50
+ const t = line.trim();
51
+ if (!t || t.startsWith("#")) continue;
52
+ const [subPath, targetBranch] = t.split(/\s+/);
53
+ if (subPath && targetBranch) {
54
+ console.log(`info: pushing submodule ${subPath}...`);
55
+ execSync(`git push ${remote} ${targetBranch}`, { cwd: `${cwd}/${subPath}`, stdio: "inherit" });
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ execSync(`git push ${remote} ${currentBranch}`, { cwd, stdio: "inherit" });
62
+ }
63
+
64
+ // Archive
65
+ try {
66
+ execSync(`npx -y @aipper/aiws change archive ${changeId} --force 2>/dev/null || aiws change archive ${changeId} --force 2>/dev/null || true`, { cwd, stdio: "inherit" });
67
+ } catch { /* ignore archive failure — main flow done */ }
68
+
69
+ console.log(`ok: finish complete. ${push ? "Pushed and " : ""}Archived.`);
70
+ }
71
+
72
+ function checkClean(cwd) {
73
+ const out = execSync("git status --porcelain", { cwd, encoding: "utf8" }).trim();
74
+ if (out) {
75
+ throw new UserError("Working tree is not clean. Commit or stash first.", { exitCode: 2 });
76
+ }
77
+ }
78
+
79
+ function detectChangeBranch(cwd) {
80
+ const branch = execSync("git branch --show-current", { cwd, encoding: "utf8" }).trim();
81
+ const match = branch.match(/^(?:change|changes|ws|ws-change)\/(.+)$/);
82
+ return match ? match[1] : null;
83
+ }
@@ -0,0 +1,40 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+
3
+ /**
4
+ * aiws handoff — read/display handoff documents
5
+ * @param {{ cwd?: string }} opts
6
+ */
7
+ export async function wsHandoffCommand({ cwd = "." } = {}) {
8
+ const changesDir = `${cwd}/changes`;
9
+ if (!existsSync(changesDir)) {
10
+ console.log("info: no changes/ directory found");
11
+ return;
12
+ }
13
+
14
+ // Check archive handoffs first
15
+ const archiveDir = `${changesDir}/archive`;
16
+ if (existsSync(archiveDir)) {
17
+ const archives = readdirSync(archiveDir).filter(f => f !== ".DS_Store");
18
+ for (const a of archives) {
19
+ const hf = `${archiveDir}/${a}/handoff.md`;
20
+ if (existsSync(hf)) {
21
+ console.log(`=== ${a} ===`);
22
+ console.log(readFileSync(hf, "utf8"));
23
+ }
24
+ }
25
+ }
26
+
27
+ // Check active change handoffs
28
+ const changes = readdirSync(changesDir).filter(f =>
29
+ !f.startsWith(".") && f !== "templates" && f !== "archive" && f !== "README.md"
30
+ );
31
+ for (const c of changes) {
32
+ const hf = `${changesDir}/${c}/handoff.md`;
33
+ if (existsSync(hf)) {
34
+ console.log(`=== ${c} (active) ===`);
35
+ console.log(readFileSync(hf, "utf8"));
36
+ }
37
+ }
38
+
39
+ console.log("info: handoffs are generated by aiws change finish --push");
40
+ }
@@ -0,0 +1,41 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+
4
+ /**
5
+ * aiws migrate — workspace migration (init or update)
6
+ * @param {{ cwd?: string }} opts
7
+ */
8
+ export async function wsMigrateCommand({ cwd = "." } = {}) {
9
+ const localAiws = `${cwd}/node_modules/.bin/aiws`;
10
+
11
+ if (existsSync(`${cwd}/.aiws/manifest.json`)) {
12
+ console.log("info: workspace detected, running update...");
13
+ runAiws(localAiws, `update ${cwd}`);
14
+ } else {
15
+ console.log("info: no workspace detected, running init...");
16
+ runAiws(localAiws, `init ${cwd}`);
17
+ }
18
+
19
+ // Enable hooks
20
+ try {
21
+ execSync("git config core.hooksPath .githooks", { cwd, stdio: "inherit" });
22
+ console.log("info: hooks enabled (core.hooksPath=.githooks)");
23
+ } catch {
24
+ console.log("warn: could not set hooks (not a git repo?)");
25
+ }
26
+
27
+ console.log("ok: migrate complete");
28
+ }
29
+
30
+ function runAiws(localAiws, args) {
31
+ try {
32
+ if (existsSync(localAiws)) {
33
+ execSync(`${localAiws} ${args}`, { stdio: "inherit" });
34
+ } else {
35
+ execSync(`npx -y @aipper/aiws ${args}`, { stdio: "inherit" });
36
+ }
37
+ } catch (e) {
38
+ console.error(`error: aiws ${args} failed`);
39
+ throw e;
40
+ }
41
+ }
@@ -0,0 +1,44 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { UserError } from "../errors.js";
4
+
5
+ /**
6
+ * aiws plan-verify — check plan completeness
7
+ * @param {{ cwd?: string }} opts
8
+ */
9
+ export async function wsPlanVerifyCommand({ cwd = "." } = {}) {
10
+ const planDir = `${cwd}/plan`;
11
+ if (!existsSync(planDir)) {
12
+ throw new UserError("No plan/ directory found", { exitCode: 2 });
13
+ }
14
+ const plans = readdirSync(planDir).filter(f => f.endsWith(".md"));
15
+ if (plans.length === 0) {
16
+ throw new UserError("No plan files found in plan/", { exitCode: 2 });
17
+ }
18
+
19
+ const failures = [];
20
+ for (const planFile of plans) {
21
+ const content = readFileSync(`${planDir}/${planFile}`, "utf8");
22
+ const checks = [];
23
+
24
+ checks.push({ name: "Goal", pass: /^##?\s*Goal/im.test(content) });
25
+ checks.push({ name: "Non-goals", pass: /^##?\s*Non-goals/im.test(content) });
26
+ checks.push({ name: "Scope", pass: /^##?\s*(Scope|In Scope)/im.test(content) });
27
+ checks.push({ name: "Verify", pass: /^##?\s*Verify/im.test(content) });
28
+ checks.push({ name: "Risks", pass: /^##?\s*Risks/im.test(content) });
29
+
30
+ const failed = checks.filter(c => !c.pass);
31
+ if (failed.length > 0) {
32
+ failures.push({ file: planFile, missing: failed.map(c => c.name) });
33
+ }
34
+ }
35
+
36
+ if (failures.length > 0) {
37
+ for (const f of failures) {
38
+ console.error(`error: ${f.file} missing sections: ${f.missing.join(", ")}`);
39
+ }
40
+ throw new UserError("Plan verification failed. Fix missing sections.", { exitCode: 1 });
41
+ }
42
+
43
+ console.log("ok: plan verification passed");
44
+ }
@@ -0,0 +1,30 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { UserError } from "../errors.js";
4
+
5
+ /**
6
+ * aiws pull — git pull --ff-only + submodule sync
7
+ * @param {{ cwd?: string }} opts
8
+ */
9
+ export async function wsPullCommand({ cwd = "." } = {}) {
10
+ checkClean(cwd);
11
+
12
+ // git pull --ff-only
13
+ execSync("git pull --ff-only", { cwd, stdio: "inherit" });
14
+
15
+ // submodule sync if .gitmodules exists
16
+ if (existsSync(`${cwd}/.gitmodules`)) {
17
+ console.log("info: submodules detected, syncing...");
18
+ execSync("git submodule sync --recursive", { cwd, stdio: "inherit" });
19
+ execSync("git submodule update --init --recursive", { cwd, stdio: "inherit" });
20
+ }
21
+
22
+ console.log("ok: pull complete");
23
+ }
24
+
25
+ function checkClean(cwd) {
26
+ const out = execSync("git status --porcelain", { cwd, encoding: "utf8" }).trim();
27
+ if (out) {
28
+ throw new UserError("Working tree is not clean. Commit or stash first.", { exitCode: 2 });
29
+ }
30
+ }
@@ -0,0 +1,71 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { UserError } from "../errors.js";
4
+
5
+ /**
6
+ * aiws push — submodule-aware push (submodules first, then superproject)
7
+ * @param {{ cwd?: string, remote?: string }} opts
8
+ */
9
+ export async function wsPushCommand({ cwd = ".", remote = "origin" } = {}) {
10
+ checkClean(cwd);
11
+
12
+ if (existsSync(`${cwd}/.gitmodules`)) {
13
+ pushSubmodules(cwd, remote);
14
+ }
15
+
16
+ const branch = execSync("git branch --show-current", { cwd, encoding: "utf8" }).trim();
17
+ console.log(`info: pushing ${branch} to ${remote}...`);
18
+
19
+ try {
20
+ execSync(`git push ${remote} ${branch}`, { cwd, stdio: "inherit" });
21
+ } catch {
22
+ throw new UserError("Push failed. Check for diverged branch.", { exitCode: 1 });
23
+ }
24
+
25
+ console.log("ok: push complete");
26
+ }
27
+
28
+ function pushSubmodules(cwd, remote) {
29
+ const targetFile = findSubmodulesTargets(cwd);
30
+ if (!targetFile) {
31
+ console.log("info: no submodules.targets found, skipping submodule push");
32
+ return;
33
+ }
34
+
35
+ const content = readFileSync(targetFile, "utf8");
36
+ for (const line of content.split("\n")) {
37
+ const trimmed = line.trim();
38
+ if (!trimmed || trimmed.startsWith("#")) continue;
39
+ const [subPath, targetBranch, rmt = remote] = trimmed.split(/\s+/);
40
+ if (!subPath || !targetBranch) continue;
41
+
42
+ console.log(`info: pushing submodule ${subPath} (${targetBranch})...`);
43
+ try {
44
+ execSync(`git push ${rmt} ${targetBranch}`, { cwd: `${cwd}/${subPath}`, stdio: "inherit" });
45
+ } catch {
46
+ throw new UserError(`Failed to push submodule ${subPath}`, { exitCode: 1 });
47
+ }
48
+ }
49
+ }
50
+
51
+ function findSubmodulesTargets(cwd) {
52
+ const paths = [
53
+ `${cwd}/changes/*/submodules.targets`,
54
+ ];
55
+ // Find first match using shell expansion
56
+ try {
57
+ const out = execSync("ls changes/*/submodules.targets 2>/dev/null || true", { cwd, encoding: "utf8" }).trim();
58
+ if (out) {
59
+ const first = out.split("\n")[0];
60
+ if (first) return `${cwd}/${first}`;
61
+ }
62
+ } catch { /* ignore */ }
63
+ return null;
64
+ }
65
+
66
+ function checkClean(cwd) {
67
+ const out = execSync("git status --porcelain", { cwd, encoding: "utf8" }).trim();
68
+ if (out) {
69
+ throw new UserError("Working tree is not clean. Commit or stash first.", { exitCode: 2 });
70
+ }
71
+ }
@@ -0,0 +1,65 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { UserError } from "../errors.js";
4
+
5
+ /**
6
+ * aiws submodule-setup — set .gitmodules branch for each submodule
7
+ * @param {{ cwd?: string, branch?: string }} opts
8
+ */
9
+ export async function wsSubmoduleSetupCommand({ cwd = ".", branch = "main" } = {}) {
10
+ checkClean(cwd);
11
+
12
+ if (!existsSync(`${cwd}/.gitmodules`)) {
13
+ console.log("info: no .gitmodules found, nothing to configure");
14
+ return;
15
+ }
16
+
17
+ // List submodules
18
+ const out = execSync("git config --file .gitmodules --get-regexp '^submodule\\..*\\.path$'", {
19
+ cwd, encoding: "utf8",
20
+ }).trim();
21
+
22
+ if (!out) {
23
+ console.log("info: no submodules in .gitmodules");
24
+ return;
25
+ }
26
+
27
+ const lines = out.split("\n");
28
+ for (const line of lines) {
29
+ const parts = line.trim().split(/\s+/);
30
+ if (parts.length < 2) continue;
31
+ const key = parts[0]; // submodule.<name>.path
32
+ const subPath = parts[1];
33
+ const name = key.replace(/^submodule\./, "").replace(/\.path$/, "");
34
+
35
+ // Check if branch already set
36
+ const existing = execSync(
37
+ `git config --file .gitmodules --get submodule.${name}.branch 2>/dev/null || true`,
38
+ { cwd, encoding: "utf8" }
39
+ ).trim();
40
+
41
+ if (existing) {
42
+ console.log(`info: ${subPath} branch already set: ${existing}`);
43
+ continue;
44
+ }
45
+
46
+ // Detect current branch
47
+ let detected = branch;
48
+ try {
49
+ const current = execSync(`git -C "${subPath}" branch --show-current 2>/dev/null || true`, { cwd, encoding: "utf8" }).trim();
50
+ if (current) detected = current;
51
+ } catch { /* use default */ }
52
+
53
+ console.log(`info: setting ${subPath} branch → ${detected}`);
54
+ execSync(`git submodule set-branch --branch ${detected} ${name}`, { cwd, stdio: "inherit" });
55
+ }
56
+
57
+ console.log("ok: submodule branches configured. Review .gitmodules changes before committing.");
58
+ }
59
+
60
+ function checkClean(cwd) {
61
+ const out = execSync("git status --porcelain", { cwd, encoding: "utf8" }).trim();
62
+ if (out) {
63
+ throw new UserError("Working tree is not clean. Commit or stash first.", { exitCode: 2 });
64
+ }
65
+ }
@@ -0,0 +1,39 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { UserError } from "../errors.js";
3
+
4
+ /**
5
+ * aiws verify-bc — verify before complete (check review + validate)
6
+ * @param {{ cwd?: string }} opts
7
+ */
8
+ export async function wsVerifyBeforeCompleteCommand({ cwd = "." } = {}) {
9
+ const missing = [];
10
+
11
+ // Check spec-review
12
+ const changesDir = `${cwd}/changes`;
13
+ if (existsSync(changesDir)) {
14
+ const changes = readdirSync(changesDir).filter(f => !f.startsWith(".") && f !== "templates" && f !== "archive");
15
+ const hasSpecReview = changes.some(c =>
16
+ existsSync(`${changesDir}/${c}/review/spec-review.md`)
17
+ );
18
+ const hasQualityReview = changes.some(c =>
19
+ existsSync(`${changesDir}/${c}/review/quality-review.md`)
20
+ );
21
+ if (!hasSpecReview) missing.push("spec-review.md");
22
+ if (!hasQualityReview) missing.push("quality-review.md");
23
+ } else {
24
+ missing.push("changes/ directory not found");
25
+ }
26
+
27
+ // Check validate stamp
28
+ const stampDir = `${cwd}/.agentdocs/tmp/aiws-validate`;
29
+ const hasStamp = existsSync(stampDir) && readdirSync(stampDir).some(f => f.endsWith(".json"));
30
+ if (!hasStamp) {
31
+ console.log("warn: no validate stamp found. Run aiws validate . --stamp");
32
+ }
33
+
34
+ if (missing.length > 0) {
35
+ throw new UserError(`Missing gates: ${missing.join(", ")}`, { exitCode: 1 });
36
+ }
37
+
38
+ console.log("ok: all gates passed");
39
+ }
package/src/manifest.js CHANGED
@@ -5,6 +5,31 @@ import { pathExists, readText, writeText } from "./fs.js";
5
5
  import { findManagedBlock } from "./managed-blocks.js";
6
6
  import { UserError } from "./errors.js";
7
7
 
8
+ /**
9
+ * Detect an older AIWS workspace that predates `.aiws/manifest.json`.
10
+ *
11
+ * Current migration only recognizes the built-in `workspace` template.
12
+ *
13
+ * @param {string} workspaceRoot
14
+ * @returns {Promise<string | null>}
15
+ */
16
+ export async function detectLegacyWorkspaceTemplateId(workspaceRoot) {
17
+ const requiredSignals = [
18
+ "AI_PROJECT.md",
19
+ "REQUIREMENTS.md",
20
+ "AI_WORKSPACE.md",
21
+ "tools/ws_change_check.py",
22
+ "tools/requirements_contract.py",
23
+ ];
24
+
25
+ for (const rel of requiredSignals) {
26
+ const abs = joinRel(workspaceRoot, rel);
27
+ if (!(await pathExists(abs))) return null;
28
+ }
29
+
30
+ return "workspace";
31
+ }
32
+
8
33
  /**
9
34
  * @param {any} manifest
10
35
  */
@@ -150,4 +175,3 @@ export async function validateDrift(options) {
150
175
  throw new UserError("Workspace drift detected.", { details: problems.join("\n") });
151
176
  }
152
177
  }
153
-
@@ -2,6 +2,10 @@ import path from "node:path";
2
2
  import { pathExists, readText } from "./fs.js";
3
3
 
4
4
  const OPENCODE_AGENT_NAMES = ["planner-sisyphus", "librarian", "explore", "oracle"];
5
+ const SAFE_READ_ONLY_COMMANDS = new Set(["rg", "cat", "sed", "ls", "find", "git status", "git diff"]);
6
+ const REQUIRED_WRITE_ALLOW_PATHS = [".agentdocs/tmp/", "changes/*/analysis/", "changes/*/review/", "changes/*/evidence/"];
7
+ const REQUIRED_DENY_PATHS = ["secrets/", ".env*"];
8
+ const REQUIRED_DENY_COMMANDS = ["rm", "mv", "git commit", "git push", "npm publish"];
5
9
 
6
10
  /**
7
11
  * @param {string} value
@@ -21,6 +25,72 @@ function readAgentState(raw, agentName) {
21
25
  present: !!entry && typeof entry === "object",
22
26
  enabled: !!(entry && typeof entry === "object" && entry.enabled === true),
23
27
  replacePlan: entry && typeof entry === "object" && typeof entry.replace_plan === "boolean" ? entry.replace_plan : null,
28
+ promptAppendConfigured: !!(
29
+ entry &&
30
+ typeof entry === "object" &&
31
+ typeof entry.prompt_append === "string" &&
32
+ entry.prompt_append.trim().length > 0
33
+ ),
34
+ };
35
+ }
36
+
37
+ /**
38
+ * @param {any} raw
39
+ */
40
+ function readApprovalWhitelistState(raw) {
41
+ const policy =
42
+ raw &&
43
+ typeof raw === "object" &&
44
+ raw.aiws &&
45
+ typeof raw.aiws === "object" &&
46
+ raw.aiws.autonomy &&
47
+ typeof raw.aiws.autonomy === "object" &&
48
+ raw.aiws.autonomy.approval_whitelist &&
49
+ typeof raw.aiws.autonomy.approval_whitelist === "object"
50
+ ? raw.aiws.autonomy.approval_whitelist
51
+ : null;
52
+ const readOnlyCommands = Array.isArray(policy?.read_only_commands)
53
+ ? policy.read_only_commands.filter((value) => typeof value === "string" && value.trim().length > 0)
54
+ : [];
55
+ const writeAllowPaths = Array.isArray(policy?.write_allow_paths)
56
+ ? policy.write_allow_paths.filter((value) => typeof value === "string" && value.trim().length > 0)
57
+ : [];
58
+ const denyPaths = Array.isArray(policy?.deny_paths)
59
+ ? policy.deny_paths.filter((value) => typeof value === "string" && value.trim().length > 0)
60
+ : [];
61
+ const denyCommands = Array.isArray(policy?.deny_commands)
62
+ ? policy.deny_commands.filter((value) => typeof value === "string" && value.trim().length > 0)
63
+ : [];
64
+ const mode = typeof policy?.mode === "string" && policy.mode.trim().length > 0 ? policy.mode : "";
65
+ const hostPermissionMode =
66
+ typeof policy?.host_permission_mode === "string" && policy.host_permission_mode.trim().length > 0
67
+ ? policy.host_permission_mode
68
+ : "";
69
+ const enabled = policy?.enabled === true;
70
+ const safeReadOnlyCommands = readOnlyCommands.every((value) => SAFE_READ_ONLY_COMMANDS.has(value));
71
+ const requiredWritePathsPresent = REQUIRED_WRITE_ALLOW_PATHS.every((value) => writeAllowPaths.includes(value));
72
+ const requiredDenyPathsPresent = REQUIRED_DENY_PATHS.every((value) => denyPaths.includes(value));
73
+ const requiredDenyCommandsPresent = REQUIRED_DENY_COMMANDS.every((value) => denyCommands.includes(value));
74
+ const modeValid = mode === "assist-only";
75
+
76
+ return {
77
+ present: !!policy,
78
+ enabled,
79
+ mode,
80
+ hostPermissionMode,
81
+ readOnlyCommands,
82
+ writeAllowPaths,
83
+ denyPaths,
84
+ denyCommands,
85
+ configured:
86
+ enabled &&
87
+ modeValid &&
88
+ safeReadOnlyCommands &&
89
+ readOnlyCommands.length > 0 &&
90
+ requiredWritePathsPresent &&
91
+ requiredDenyPathsPresent &&
92
+ requiredDenyCommandsPresent &&
93
+ hostPermissionMode === "manual-only",
24
94
  };
25
95
  }
26
96
 
@@ -52,6 +122,45 @@ export async function detectOpenCodeEnvironment(workspaceRoot) {
52
122
  const agents = Object.fromEntries(OPENCODE_AGENT_NAMES.map((name) => [name, readAgentState(config, name)]));
53
123
  const enabledAgents = OPENCODE_AGENT_NAMES.filter((name) => agents[name]?.enabled === true);
54
124
  const recommendedAgentsReady = ["planner-sisyphus", "librarian", "explore", "oracle"].every((name) => agents[name]?.enabled === true);
125
+ const backgroundTasksConfigured = !!(config && typeof config === "object" && config.backgroundTasks && typeof config.backgroundTasks === "object");
126
+ const autoResumeEnabled = !!(
127
+ config &&
128
+ typeof config === "object" &&
129
+ config.experimental &&
130
+ typeof config.experimental === "object" &&
131
+ config.experimental.auto_resume === true
132
+ );
133
+ const approvalWhitelist = readApprovalWhitelistState(config);
134
+ const claudeSettingsExamplePath = path.join(workspaceRoot, ".claude", "settings.json.example");
135
+ const approvalWhitelistHelperPath = path.join(opencodeDir, "helpers", "approval-whitelist-check.sh");
136
+ const approvalWhitelistRunnerPath = path.join(opencodeDir, "helpers", "approval-whitelist-run.sh");
137
+ const approvalWhitelistWatchdogPath = path.join(opencodeDir, "helpers", "approval-whitelist-watchdog.sh");
138
+ const tmuxScanHelperPath = path.join(opencodeDir, "helpers", "tmux-swarm-scan.sh");
139
+ const tmuxRescueHelperPath = path.join(opencodeDir, "helpers", "tmux-swarm-rescue.sh");
140
+ const claudeSettingsExampleExists = await pathExists(claudeSettingsExamplePath);
141
+ const approvalWhitelistHelperExists = await pathExists(approvalWhitelistHelperPath);
142
+ const approvalWhitelistRunnerExists = await pathExists(approvalWhitelistRunnerPath);
143
+ const approvalWhitelistWatchdogExists = await pathExists(approvalWhitelistWatchdogPath);
144
+ const tmuxScanHelperExists = await pathExists(tmuxScanHelperPath);
145
+ const tmuxRescueHelperExists = await pathExists(tmuxRescueHelperPath);
146
+ const watchdogSuperviseReady =
147
+ configStatus === "loaded" &&
148
+ approvalWhitelist.configured &&
149
+ approvalWhitelistHelperExists &&
150
+ approvalWhitelistRunnerExists &&
151
+ approvalWhitelistWatchdogExists;
152
+ const autonomyReady =
153
+ configStatus === "loaded" &&
154
+ agents["planner-sisyphus"]?.promptAppendConfigured === true &&
155
+ backgroundTasksConfigured &&
156
+ autoResumeEnabled &&
157
+ approvalWhitelist.configured &&
158
+ claudeSettingsExampleExists &&
159
+ approvalWhitelistHelperExists &&
160
+ approvalWhitelistRunnerExists &&
161
+ approvalWhitelistWatchdogExists &&
162
+ tmuxScanHelperExists &&
163
+ tmuxRescueHelperExists;
55
164
 
56
165
  return {
57
166
  workspaceRoot,
@@ -67,10 +176,33 @@ export async function detectOpenCodeEnvironment(workspaceRoot) {
67
176
  recommendedAgentsReady,
68
177
  enabledAgents,
69
178
  agents,
179
+ backgroundTasksConfigured,
180
+ autoResumeEnabled,
181
+ approvalWhitelist,
182
+ watchdogSuperviseReady,
183
+ autonomyReady,
184
+ claudeSettingsExamplePath,
185
+ claudeSettingsExampleExists,
186
+ approvalWhitelistHelperPath,
187
+ approvalWhitelistHelperExists,
188
+ approvalWhitelistRunnerPath,
189
+ approvalWhitelistRunnerExists,
190
+ approvalWhitelistWatchdogPath,
191
+ approvalWhitelistWatchdogExists,
192
+ tmuxScanHelperPath,
193
+ tmuxScanHelperExists,
194
+ tmuxRescueHelperPath,
195
+ tmuxRescueHelperExists,
70
196
  rel: {
71
197
  opencodeDir: relativeOrDot(path.relative(workspaceRoot, opencodeDir)),
72
198
  configPath: relativeOrDot(path.relative(workspaceRoot, configPath)),
73
199
  examplePath: relativeOrDot(path.relative(workspaceRoot, examplePath)),
200
+ claudeSettingsExamplePath: relativeOrDot(path.relative(workspaceRoot, claudeSettingsExamplePath)),
201
+ approvalWhitelistHelperPath: relativeOrDot(path.relative(workspaceRoot, approvalWhitelistHelperPath)),
202
+ approvalWhitelistRunnerPath: relativeOrDot(path.relative(workspaceRoot, approvalWhitelistRunnerPath)),
203
+ approvalWhitelistWatchdogPath: relativeOrDot(path.relative(workspaceRoot, approvalWhitelistWatchdogPath)),
204
+ tmuxScanHelperPath: relativeOrDot(path.relative(workspaceRoot, tmuxScanHelperPath)),
205
+ tmuxRescueHelperPath: relativeOrDot(path.relative(workspaceRoot, tmuxRescueHelperPath)),
74
206
  },
75
207
  };
76
208
  }