@aipper/aiws 0.0.28 → 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.
@@ -77,6 +77,14 @@ export async function initCommand(options) {
77
77
  await copyTemplateFileToWorkspace({ templateDir: tpl.templateDir, workspaceRoot, relPosix: r });
78
78
  }
79
79
 
80
+ // Ensure replace_file-only entries (not listed in required/optional) are also copied
81
+ for (const r of replaceFiles) {
82
+ if (!r || r === ".aiws/manifest.json") continue;
83
+ const dest = joinRel(workspaceRoot, r);
84
+ if (await pathExists(dest)) continue;
85
+ await copyTemplateFileToWorkspace({ templateDir: tpl.templateDir, workspaceRoot, relPosix: r });
86
+ }
87
+
80
88
  const now = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
81
89
  await writeWorkspaceManifest({
82
90
  workspaceRoot,
@@ -0,0 +1,167 @@
1
+ import path from "node:path";
2
+ import { UserError } from "../errors.js";
3
+ import { pathExists, readText } from "../fs.js";
4
+ import { loadAiwsPackage } from "../aiws-package.js";
5
+ import { loadTemplate } from "../spec.js";
6
+ import { resolveWorkspaceRoot } from "../workspace.js";
7
+ import { updateCommand } from "./update.js";
8
+ import { detectOpenCodeEnvironment } from "../opencode-env.js";
9
+ import { ensureOpenCodeWatchdog, tmuxInstalled } from "./opencode-supervise.js";
10
+
11
+ const REQUIRED_TRUTH_FILES = ["AI_PROJECT.md", "REQUIREMENTS.md", "AI_WORKSPACE.md"];
12
+ const REQUIRED_AUTONOMY_FILES = [
13
+ ".opencode/oh-my-opencode.json.example",
14
+ ".claude/settings.json.example",
15
+ ".opencode/helpers/approval-whitelist-check.sh",
16
+ ".opencode/helpers/approval-whitelist-run.sh",
17
+ ".opencode/helpers/approval-whitelist-watchdog.sh",
18
+ ".opencode/helpers/tmux-swarm-scan.sh",
19
+ ".opencode/helpers/tmux-swarm-rescue.sh",
20
+ ".opencode/skills/ws-autonomy/SKILL.md",
21
+ ".opencode/commands/ws-autonomy.md",
22
+ ".opencode/command/ws-autonomy.md",
23
+ ".opencode/skills/ws-auto/SKILL.md",
24
+ ".opencode/commands/ws-auto.md",
25
+ ".opencode/command/ws-auto.md",
26
+ ];
27
+
28
+ async function collectMissingPaths(workspaceRoot, relPaths) {
29
+ /** @type {string[]} */
30
+ const missing = [];
31
+ for (const rel of relPaths) {
32
+ if (!(await pathExists(path.join(workspaceRoot, rel)))) {
33
+ missing.push(rel);
34
+ }
35
+ }
36
+ return missing;
37
+ }
38
+
39
+ async function readWorkspaceManifest(workspaceRoot) {
40
+ const manifestPath = path.join(workspaceRoot, ".aiws", "manifest.json");
41
+ if (!(await pathExists(manifestPath))) {
42
+ throw new UserError("Missing .aiws/manifest.json. Run `aiws init .` first.");
43
+ }
44
+ return JSON.parse(await readText(manifestPath));
45
+ }
46
+
47
+ /**
48
+ * @param {string} workspaceRoot
49
+ */
50
+ async function detectAutoBootstrapState(workspaceRoot) {
51
+ const env = await detectOpenCodeEnvironment(workspaceRoot);
52
+ const manifest = await readWorkspaceManifest(workspaceRoot);
53
+ const templateId = String(manifest.template_id || "workspace");
54
+ const tpl = await loadTemplate(templateId);
55
+ const aiws = await loadAiwsPackage();
56
+ const truthMissing = await collectMissingPaths(workspaceRoot, REQUIRED_TRUTH_FILES);
57
+ const autonomyMissing = await collectMissingPaths(workspaceRoot, REQUIRED_AUTONOMY_FILES);
58
+
59
+ /** @type {string[]} */
60
+ const updateReasons = [];
61
+ if (autonomyMissing.length > 0) {
62
+ updateReasons.push(`autonomy_files_missing=${autonomyMissing.join(",")}`);
63
+ }
64
+ if (String(manifest.spec_version || "") !== String(tpl.specVersion || "")) {
65
+ updateReasons.push(`spec_version=${String(manifest.spec_version || "(missing)")}->${String(tpl.specVersion || "(missing)")}`);
66
+ }
67
+ if (String(manifest.aiws_version || "") !== String(aiws.version || "")) {
68
+ updateReasons.push(`aiws_version=${String(manifest.aiws_version || "(missing)")}->${String(aiws.version || "(missing)")}`);
69
+ }
70
+
71
+ return {
72
+ env,
73
+ manifest,
74
+ truthMissing,
75
+ autonomyMissing,
76
+ updateRequired: updateReasons.length > 0,
77
+ updateReasons,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * @param {{
83
+ * targetPath: string,
84
+ * sessionName?: string,
85
+ * windowName?: string,
86
+ * once?: boolean,
87
+ * pollMs?: string,
88
+ * noUpdate?: boolean,
89
+ * }} options
90
+ */
91
+ export async function opencodeAutoCommand(options) {
92
+ const workspaceRoot = await resolveWorkspaceRoot(options.targetPath, { create: false });
93
+ let state = await detectAutoBootstrapState(workspaceRoot);
94
+ let updateApplied = false;
95
+
96
+ console.log(`✓ aiws opencode auto: ${workspaceRoot}`);
97
+ console.log(`found_truth: ${state.truthMissing.length === 0 ? REQUIRED_TRUTH_FILES.join(", ") : "(incomplete)"}`);
98
+
99
+ if (state.truthMissing.length > 0) {
100
+ console.log(`update: skipped (missing truth files: ${state.truthMissing.join(", ")})`);
101
+ console.log(`mode: ${state.env.mode}`);
102
+ console.log(`watchdog: skipped (missing truth files: ${state.truthMissing.join(", ")})`);
103
+ console.log("next: 先补齐真值文件,再重新运行 `aiws opencode auto .` 或 `/ws-preflight`。");
104
+ return;
105
+ }
106
+
107
+ if (state.updateRequired && options.noUpdate !== true) {
108
+ console.log(`update: required (${state.updateReasons.join("; ")})`);
109
+ await updateCommand({ targetPath: workspaceRoot });
110
+ updateApplied = true;
111
+ state = await detectAutoBootstrapState(workspaceRoot);
112
+ } else if (state.updateRequired) {
113
+ console.log(`update: required but skipped (--no-update)`);
114
+ console.log(`update_reasons: ${state.updateReasons.join("; ")}`);
115
+ } else {
116
+ console.log("update: not needed");
117
+ }
118
+
119
+ console.log(`mode: ${state.env.mode}`);
120
+ if (updateApplied) {
121
+ console.log("update_applied: true");
122
+ }
123
+
124
+ if (state.env.configStatus === "invalid") {
125
+ console.log(`watchdog: skipped (invalid config: ${state.env.rel.configPath})`);
126
+ console.log(`next: 修复 ${state.env.rel.configPath} 的 JSON,然后重新运行 \`aiws opencode auto .\`.`);
127
+ return;
128
+ }
129
+ if (!state.env.configExists) {
130
+ console.log("watchdog: skipped (oMo config missing)");
131
+ console.log(`next: 如需启用 autonomous bootstrap,可复制 ${state.env.rel.examplePath} -> ${state.env.rel.configPath},然后重新运行 \`aiws opencode auto .\`.`);
132
+ return;
133
+ }
134
+ if (!state.env.watchdogSuperviseReady) {
135
+ console.log("watchdog: skipped (approval/watchdog chain incomplete)");
136
+ console.log("next: 先运行 `aiws opencode status .` 查看缺哪一项;如是托管文件缺失,可运行 `aiws update .`。");
137
+ return;
138
+ }
139
+ if (!tmuxInstalled()) {
140
+ console.log("watchdog: skipped (tmux missing)");
141
+ console.log("next: 安装 tmux 后重新运行 `aiws opencode auto .`,或手工执行 approval-whitelist-watchdog helper。");
142
+ return;
143
+ }
144
+
145
+ const ensureResult = ensureOpenCodeWatchdog({
146
+ workspaceRoot,
147
+ env: state.env,
148
+ sessionName: options.sessionName,
149
+ windowName: options.windowName,
150
+ once: options.once === true,
151
+ pollMs: options.pollMs ?? null,
152
+ });
153
+ console.log(`watchdog: ${ensureResult.state}`);
154
+ console.log(`tmux session: ${ensureResult.sessionName}${ensureResult.currentSession ? " (current)" : ""}`);
155
+ console.log(`watchdog window: ${ensureResult.windowName}`);
156
+ console.log(`watchdog helper: ${ensureResult.helperRelPath}`);
157
+ if (ensureResult.once) {
158
+ console.log("watchdog_mode: once");
159
+ } else {
160
+ console.log(`watchdog_mode: loop${ensureResult.pollMs !== null ? ` (poll_ms=${ensureResult.pollMs})` : ""}`);
161
+ }
162
+ if (state.env.autonomyReady) {
163
+ console.log("next: watchdog 已就绪,可继续 `/ws-auto` -> `/using-aiws` / `/ws-plan` / `/ws-autonomy`。");
164
+ return;
165
+ }
166
+ console.log("next: watchdog 已启动,但 autonomy contract 尚未完整;建议先运行 `aiws opencode status .` 或 `/ws-autonomy` 查看缺口。");
167
+ }
@@ -39,22 +39,64 @@ export async function opencodeStatusCommand(options) {
39
39
 
40
40
  if (planner.present) {
41
41
  console.log(`planner-sisyphus.replace_plan: ${planner.replacePlan === null ? "(unset)" : String(planner.replacePlan)}`);
42
+ console.log(`planner-sisyphus.prompt_append: ${planner.promptAppendConfigured ? "configured" : "missing"}`);
42
43
  }
44
+ console.log(`background_tasks: ${env.backgroundTasksConfigured ? "configured" : "missing"}`);
45
+ console.log(`auto_resume: ${env.autoResumeEnabled ? "true" : "false"}`);
46
+ console.log(
47
+ `approval_whitelist: ${
48
+ env.approvalWhitelist.present
49
+ ? env.approvalWhitelist.configured
50
+ ? `configured (${env.approvalWhitelist.mode || "unset"})`
51
+ : "incomplete"
52
+ : "missing"
53
+ }`,
54
+ );
55
+ console.log(`host_permission_mode: ${env.approvalWhitelist.hostPermissionMode || "missing"}`);
56
+ console.log(
57
+ `approval_helper: ${env.approvalWhitelistHelperExists ? `${env.rel.approvalWhitelistHelperPath} (present)` : "missing"}`,
58
+ );
59
+ console.log(
60
+ `approval_runner: ${env.approvalWhitelistRunnerExists ? `${env.rel.approvalWhitelistRunnerPath} (present)` : "missing"}`,
61
+ );
62
+ console.log(
63
+ `approval_watchdog: ${env.approvalWhitelistWatchdogExists ? `${env.rel.approvalWhitelistWatchdogPath} (present)` : "missing"}`,
64
+ );
65
+ console.log(`watchdog_supervisor: ${env.watchdogSuperviseReady ? "configured" : "incomplete"}`);
66
+ console.log(
67
+ `claude_hooks_example: ${env.claudeSettingsExampleExists ? `${env.rel.claudeSettingsExamplePath} (present)` : "missing"}`,
68
+ );
69
+ console.log(
70
+ `tmux_helpers: scan=${env.tmuxScanHelperExists ? env.rel.tmuxScanHelperPath : "missing"}, rescue=${
71
+ env.tmuxRescueHelperExists ? env.rel.tmuxRescueHelperPath : "missing"
72
+ }`,
73
+ );
74
+ console.log(`autonomy_ready: ${env.autonomyReady}`);
43
75
 
44
76
  if (env.configStatus === "invalid") {
45
77
  console.log(`Next: 修复 ${env.rel.configPath} 的 JSON,然后重新运行 aiws opencode status .`);
46
78
  return;
47
79
  }
48
80
  if (!env.configExists && env.exampleExists) {
49
- console.log(`Next: 如需启用 oMo 优先模式,可复制 ${env.rel.examplePath} -> ${env.rel.configPath} 后按项目调整 agents。`);
81
+ console.log(`Next: 如需启用 oMo 优先模式,可复制 ${env.rel.examplePath} -> ${env.rel.configPath} 后按项目调整 agents;随后运行 \`aiws opencode auto .\` 或 \`/ws-auto\`.`);
50
82
  return;
51
83
  }
52
84
  if (env.mode === "oMo-enabled" && !env.recommendedAgentsReady) {
53
85
  console.log("Next: 补齐 planner-sisyphus / librarian / explore / oracle,或接受 fallback 到 standard-opencode。");
54
86
  return;
55
87
  }
88
+ if (env.mode === "oMo-enabled" && !env.autonomyReady) {
89
+ console.log(
90
+ "Next: 如需 autonomous 模式,补齐 planner-sisyphus.prompt_append / backgroundTasks / experimental.auto_resume / approval whitelist,并确认 .claude/settings.json.example / approval helper / approval runner / approval watchdog / tmux helpers 存在。",
91
+ );
92
+ return;
93
+ }
56
94
  if (env.mode === "oMo-enabled") {
57
- console.log("Next: 运行 /using-aiws 或 /ws-preflight,确认技能路由输出 OpenCode mode: oMo-enabled。");
95
+ if (env.watchdogSuperviseReady) {
96
+ console.log("Next: 如需一键做 bootstrap + watchdog ensure,运行 `aiws opencode auto .` 或 `/ws-auto`;若只想单独拉起 watchdog,再用 `aiws opencode supervise .`。");
97
+ return;
98
+ }
99
+ console.log("Next: 运行 /ws-autonomy 或 /using-aiws,确认当前仓库的 completion/retry/rescue 合同。");
58
100
  return;
59
101
  }
60
102
  console.log("Next: 当前按 standard-opencode 运行;如需多 agent 优先委托,可启用 oh-my-opencode 项目配置。");
@@ -0,0 +1,200 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { UserError } from "../errors.js";
5
+ import { resolveWorkspaceRoot } from "../workspace.js";
6
+ import { detectOpenCodeEnvironment } from "../opencode-env.js";
7
+
8
+ function shellQuote(value) {
9
+ return `'${String(value).replaceAll("'", `'\\''`)}'`;
10
+ }
11
+
12
+ function sanitizeName(value) {
13
+ return String(value || "")
14
+ .replaceAll(/[^A-Za-z0-9._-]+/g, "-")
15
+ .replaceAll(/^-+|-+$/g, "") || "workspace";
16
+ }
17
+
18
+ function defaultSessionName(workspaceRoot) {
19
+ const baseName = sanitizeName(path.basename(workspaceRoot));
20
+ const hash = createHash("sha1").update(workspaceRoot).digest("hex").slice(0, 6);
21
+ return `${baseName}-${hash}`;
22
+ }
23
+
24
+ function runTmux(args, options = {}) {
25
+ const result = spawnSync("tmux", args, {
26
+ encoding: "utf8",
27
+ env: process.env,
28
+ });
29
+ if (options.allowFailure === true) {
30
+ return result;
31
+ }
32
+ if (result.error) {
33
+ throw new UserError(`Failed to run tmux ${args[0] ?? ""}`.trim(), {
34
+ details: result.error instanceof Error ? result.error.message : String(result.error),
35
+ });
36
+ }
37
+ if (result.status !== 0) {
38
+ const stderr = (result.stderr || "").trim();
39
+ throw new UserError(`tmux ${args[0] ?? ""} failed`, { details: stderr || `exit=${result.status}` });
40
+ }
41
+ return result;
42
+ }
43
+
44
+ export function tmuxInstalled() {
45
+ const result = spawnSync("tmux", ["-V"], {
46
+ encoding: "utf8",
47
+ env: process.env,
48
+ });
49
+ return !result.error && result.status === 0;
50
+ }
51
+
52
+ function parsePositiveInt(value, optionName) {
53
+ const parsed = Number.parseInt(String(value), 10);
54
+ if (!Number.isFinite(parsed) || parsed <= 0) {
55
+ throw new UserError(`Invalid ${optionName}: ${value}`, { details: `${optionName} must be a positive integer.` });
56
+ }
57
+ return parsed;
58
+ }
59
+
60
+ function buildWatchdogCommand(env, workspaceRoot, options) {
61
+ const autonomyDir = path.join(workspaceRoot, ".agentdocs", "tmp", "opencode-autonomy");
62
+ const args = [shellQuote(env.approvalWhitelistWatchdogPath), shellQuote(workspaceRoot)];
63
+ if (options.once === true) {
64
+ args.push("--once");
65
+ }
66
+ if (options.pollMs !== null) {
67
+ args.push("--poll-ms", String(options.pollMs));
68
+ }
69
+ return `mkdir -p ${shellQuote(autonomyDir)} && export AIWS_OPENCODE_AUTONOMY_DIR=${shellQuote(autonomyDir)} && exec bash ${args.join(" ")}`;
70
+ }
71
+
72
+ function currentTmuxSessionName() {
73
+ if (!process.env.TMUX) {
74
+ return "";
75
+ }
76
+ const result = runTmux(["display-message", "-p", "#S"]);
77
+ return (result.stdout || "").trim();
78
+ }
79
+
80
+ function sessionExists(sessionName) {
81
+ const result = runTmux(["has-session", "-t", sessionName], { allowFailure: true });
82
+ return result.status === 0;
83
+ }
84
+
85
+ function windowExists(sessionName, windowName) {
86
+ const result = runTmux(["list-windows", "-t", sessionName, "-F", "#W"], { allowFailure: true });
87
+ if (result.status !== 0) {
88
+ return false;
89
+ }
90
+ return String(result.stdout || "")
91
+ .split(/\r?\n/)
92
+ .map((line) => line.trim())
93
+ .filter(Boolean)
94
+ .includes(windowName);
95
+ }
96
+
97
+ /**
98
+ * @param {{
99
+ * workspaceRoot: string,
100
+ * env: Awaited<ReturnType<typeof detectOpenCodeEnvironment>>,
101
+ * sessionName?: string,
102
+ * windowName?: string,
103
+ * once?: boolean,
104
+ * pollMs?: string | null,
105
+ * }} options
106
+ */
107
+ export function ensureOpenCodeWatchdog(options) {
108
+ const workspaceRoot = options.workspaceRoot;
109
+ const env = options.env;
110
+ if (!tmuxInstalled()) {
111
+ throw new UserError("tmux is required for watchdog supervision.", {
112
+ details: "Install tmux first, or run the watchdog helper manually.",
113
+ });
114
+ }
115
+ if (env.configStatus !== "loaded") {
116
+ throw new UserError("oMo config is not ready for supervision.", {
117
+ details: `Expected ${env.rel.configPath} to exist and be valid JSON before starting watchdog supervision.`,
118
+ });
119
+ }
120
+ if (!env.approvalWhitelist.configured) {
121
+ throw new UserError("approval whitelist policy is not ready.", {
122
+ details: "Enable aiws.autonomy.approval_whitelist with assist-only + manual-only before starting watchdog supervision.",
123
+ });
124
+ }
125
+ if (!env.approvalWhitelistHelperExists || !env.approvalWhitelistRunnerExists || !env.approvalWhitelistWatchdogExists) {
126
+ throw new UserError("watchdog helper chain is incomplete.", {
127
+ details: "Expected approval-whitelist-check.sh, approval-whitelist-run.sh, and approval-whitelist-watchdog.sh to exist under .opencode/helpers/.",
128
+ });
129
+ }
130
+
131
+ const pollMs = options.pollMs ? parsePositiveInt(options.pollMs, "--poll-ms") : null;
132
+ const currentSession = currentTmuxSessionName();
133
+ const sessionName = options.sessionName || currentSession || defaultSessionName(workspaceRoot);
134
+ const windowName = options.windowName || "watchdog";
135
+ const command = buildWatchdogCommand(env, workspaceRoot, {
136
+ once: options.once === true,
137
+ pollMs,
138
+ });
139
+ const existed = sessionExists(sessionName);
140
+
141
+ if (existed && windowExists(sessionName, windowName)) {
142
+ return {
143
+ sessionName,
144
+ windowName,
145
+ helperRelPath: env.rel.approvalWhitelistWatchdogPath,
146
+ state: "already-running",
147
+ currentSession: Boolean(currentSession && sessionName === currentSession),
148
+ once: options.once === true,
149
+ pollMs,
150
+ };
151
+ }
152
+
153
+ if (existed) {
154
+ runTmux(["new-window", "-d", "-t", sessionName, "-n", windowName, "-c", workspaceRoot, command]);
155
+ } else {
156
+ runTmux(["new-session", "-d", "-s", sessionName, "-n", windowName, "-c", workspaceRoot, command]);
157
+ }
158
+
159
+ return {
160
+ sessionName,
161
+ windowName,
162
+ helperRelPath: env.rel.approvalWhitelistWatchdogPath,
163
+ state: existed ? "created" : "started-with-new-session",
164
+ currentSession: Boolean(currentSession && sessionName === currentSession),
165
+ once: options.once === true,
166
+ pollMs,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * @param {{ targetPath: string, sessionName?: string, windowName?: string, once?: boolean, pollMs?: string }} options
172
+ */
173
+ export async function opencodeSuperviseCommand(options) {
174
+ const workspaceRoot = await resolveWorkspaceRoot(options.targetPath, { create: false });
175
+ const env = await detectOpenCodeEnvironment(workspaceRoot);
176
+ const result = ensureOpenCodeWatchdog({
177
+ workspaceRoot,
178
+ env,
179
+ sessionName: options.sessionName,
180
+ windowName: options.windowName,
181
+ once: options.once,
182
+ pollMs: options.pollMs ?? null,
183
+ });
184
+
185
+ console.log(`✓ aiws opencode supervise: ${workspaceRoot}`);
186
+ console.log(`tmux session: ${result.sessionName}${result.currentSession ? " (current)" : ""}`);
187
+ if (result.state === "already-running") {
188
+ console.log(`watchdog window: ${result.windowName} (already running)`);
189
+ } else if (result.state === "created") {
190
+ console.log(`watchdog window: ${result.windowName} (created)`);
191
+ } else {
192
+ console.log(`watchdog window: ${result.windowName} (started with new session)`);
193
+ }
194
+ console.log(`watchdog helper: ${result.helperRelPath}`);
195
+ if (result.once) {
196
+ console.log("mode: once");
197
+ } else {
198
+ console.log(`mode: loop${result.pollMs !== null ? ` (poll_ms=${result.pollMs})` : ""}`);
199
+ }
200
+ }
@@ -10,7 +10,7 @@ import { findManagedBlock } from "../managed-blocks.js";
10
10
  import { BackupSession } from "../backup.js";
11
11
  import { normalizeRel, joinRel } from "../path-utils.js";
12
12
  import { copyTemplateFileToWorkspace, applyManagedBlocksFromTemplate } from "../template.js";
13
- import { writeWorkspaceManifest } from "../manifest.js";
13
+ import { detectLegacyWorkspaceTemplateId, writeWorkspaceManifest } from "../manifest.js";
14
14
 
15
15
  /**
16
16
  * @param {{ targetPath: string }} options
@@ -20,11 +20,17 @@ export async function updateCommand(options) {
20
20
  const aiws = await loadAiwsPackage();
21
21
 
22
22
  const manifestPath = path.join(workspaceRoot, ".aiws", "manifest.json");
23
- if (!(await pathExists(manifestPath))) {
23
+ const hasManifest = await pathExists(manifestPath);
24
+ const legacyTemplateId = !hasManifest ? await detectLegacyWorkspaceTemplateId(workspaceRoot) : null;
25
+ const legacyMigration = !hasManifest && Boolean(legacyTemplateId);
26
+ if (!hasManifest && !legacyTemplateId) {
24
27
  throw new UserError("Missing .aiws/manifest.json. Run `aiws init` first.");
25
28
  }
26
- const stored = JSON.parse(await readText(manifestPath));
27
- const templateId = String(stored.template_id || "workspace");
29
+
30
+ const stored = hasManifest
31
+ ? JSON.parse(await readText(manifestPath))
32
+ : { template_id: legacyTemplateId, installed_at: "", tools: [], managed: [] };
33
+ const templateId = String(stored.template_id || legacyTemplateId || "workspace");
28
34
  const tpl = await loadTemplate(templateId);
29
35
  const storedManagedByPath = new Map(
30
36
  (Array.isArray(stored.managed) ? stored.managed : [])
@@ -56,6 +62,9 @@ export async function updateCommand(options) {
56
62
  const ids = Array.isArray(blockIdsRaw) ? blockIdsRaw.map(String) : [];
57
63
  for (const id of ids) {
58
64
  if (!findManagedBlock(text, id)) {
65
+ if (legacyMigration) {
66
+ continue;
67
+ }
59
68
  throw new UserError("Managed block markers are missing or broken; refusing to update.", {
60
69
  details: `File: ${fileRel}\nBlock: ${id}\nHint: re-run \`aiws init\` or repair markers manually.`,
61
70
  });
@@ -109,7 +118,7 @@ export async function updateCommand(options) {
109
118
  workspaceRoot,
110
119
  fileRel,
111
120
  blockIds: ids,
112
- insertIfMissing: false,
121
+ insertIfMissing: legacyMigration,
113
122
  });
114
123
  }
115
124
 
@@ -129,5 +138,8 @@ export async function updateCommand(options) {
129
138
  templateManifest: tpl.manifest,
130
139
  });
131
140
 
141
+ if (legacyMigration) {
142
+ console.log(` migrated legacy AIWS workspace: generated .aiws/manifest.json for template=${legacyTemplateId}`);
143
+ }
132
144
  console.log(`✓ aiws update: ${workspaceRoot}`);
133
145
  }
@@ -3,7 +3,7 @@ import { loadTemplate } from "../spec.js";
3
3
  import { resolveWorkspaceRoot } from "../workspace.js";
4
4
  import { ensureDir, pathExists, readText, writeText } from "../fs.js";
5
5
  import { UserError } from "../errors.js";
6
- import { validateDrift } from "../manifest.js";
6
+ import { detectLegacyWorkspaceTemplateId, validateDrift } from "../manifest.js";
7
7
  import { runCommand } from "../exec.js";
8
8
  import { expandManifestEntries } from "../template.js";
9
9
  import { loadAiwsPackage } from "../aiws-package.js";
@@ -142,6 +142,12 @@ export async function validateCommand(options) {
142
142
 
143
143
  const manifestPath = path.join(workspaceRoot, ".aiws", "manifest.json");
144
144
  if (!(await pathExists(manifestPath))) {
145
+ const legacyTemplateId = await detectLegacyWorkspaceTemplateId(workspaceRoot);
146
+ if (legacyTemplateId) {
147
+ throw new UserError("Missing .aiws/manifest.json in a legacy AIWS workspace.", {
148
+ details: "This workspace looks like an older AIWS install. Run `aiws update .` once to generate the manifest, then rerun `aiws validate`.",
149
+ });
150
+ }
145
151
  throw new UserError("Missing .aiws/manifest.json. Run `aiws init` first.");
146
152
  }
147
153
  const stored = JSON.parse(await readText(manifestPath));
@@ -0,0 +1,44 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { UserError } from "../errors.js";
4
+
5
+ /**
6
+ * aiws commit — validate + commit
7
+ * @param {{ cwd?: string, message?: string }} opts
8
+ */
9
+ export async function wsCommitCommand({ cwd = ".", message } = {}) {
10
+ checkClean(cwd);
11
+
12
+ // Check review artifacts
13
+ const reviewsExist = existsSync(`${cwd}/changes/*/review/spec-review.md`) ||
14
+ existsSync(`${cwd}/changes/*/review/quality-review.md`);
15
+ if (!reviewsExist) {
16
+ console.log("warn: no spec-review or quality-review found");
17
+ }
18
+
19
+ // Run validate
20
+ try {
21
+ execSync("npx -y @aipper/aiws validate . 2>/dev/null || aiws validate . 2>/dev/null || true", { cwd, stdio: "inherit" });
22
+ } catch { /* validate is advisory, not blocking */ }
23
+
24
+ // Get staged changes
25
+ const staged = execSync("git diff --cached --name-only", { cwd, encoding: "utf8" }).trim();
26
+ if (!staged) {
27
+ console.log("info: nothing staged. Use git add first.");
28
+ }
29
+
30
+ // Commit
31
+ if (!message) {
32
+ throw new UserError("commit requires --message (use -m or --message)", { exitCode: 2 });
33
+ }
34
+
35
+ execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd, stdio: "inherit" });
36
+ console.log("ok: commit created");
37
+ }
38
+
39
+ function checkClean(cwd) {
40
+ const out = execSync("git status --porcelain", { cwd, encoding: "utf8" }).trim();
41
+ if (!out) {
42
+ throw new UserError("Nothing to commit. Working tree clean.", { exitCode: 2 });
43
+ }
44
+ }
@@ -0,0 +1,67 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { UserError } from "../errors.js";
4
+
5
+ /**
6
+ * aiws deliver — submodule + superproject commit, then finish
7
+ * @param {{ cwd?: string, message?: string, changeId?: string }} opts
8
+ */
9
+ export async function wsDeliverCommand({ cwd = ".", message, changeId } = {}) {
10
+ if (!changeId) {
11
+ changeId = detectChangeBranch(cwd);
12
+ }
13
+ if (!changeId) {
14
+ throw new UserError("Could not detect change ID.", { exitCode: 2 });
15
+ }
16
+
17
+ if (!message) {
18
+ throw new UserError("deliver requires --message for the superproject commit", { exitCode: 2 });
19
+ }
20
+
21
+ console.log("info: starting delivery...");
22
+
23
+ // Commit submodules first
24
+ if (existsSync(`${cwd}/.gitmodules`)) {
25
+ const out = execSync("git config --file .gitmodules --get-regexp '^submodule\\..*\\.path$'", {
26
+ cwd, encoding: "utf8",
27
+ }).trim();
28
+
29
+ for (const line of out.split("\n")) {
30
+ const parts = line.trim().split(/\s+/);
31
+ if (parts.length < 2) continue;
32
+ const subPath = parts[1];
33
+
34
+ const status = execSync(`git -C "${subPath}" status --porcelain`, { cwd, encoding: "utf8" }).trim();
35
+ if (status) {
36
+ console.log(`info: submodule ${subPath} has changes. Committing...`);
37
+ execSync(`git -C "${subPath}" add -A`, { cwd, stdio: "inherit" });
38
+ execSync(`git -C "${subPath}" commit -m "${message}"`, { cwd, stdio: "inherit" });
39
+ } else {
40
+ console.log(`info: submodule ${subPath} has no changes, skipping`);
41
+ }
42
+ }
43
+ }
44
+
45
+ // Commit superproject
46
+ const superStatus = execSync("git status --porcelain", { cwd, encoding: "utf8" }).trim();
47
+ if (superStatus) {
48
+ console.log("info: committing superproject...");
49
+ execSync("git add -A", { cwd, stdio: "inherit" });
50
+ execSync(`git commit -m "${message}"`, { cwd, stdio: "inherit" });
51
+ } else {
52
+ console.log("info: superproject has no changes");
53
+ }
54
+
55
+ // Finish
56
+ await import("./ws-finish.js").then(m =>
57
+ m.wsFinishCommand({ cwd, changeId, push: true })
58
+ );
59
+
60
+ console.log("ok: deliver complete");
61
+ }
62
+
63
+ function detectChangeBranch(cwd) {
64
+ const branch = execSync("git branch --show-current", { cwd, encoding: "utf8" }).trim();
65
+ const match = branch.match(/^(?:change|changes|ws|ws-change)\/(.+)$/);
66
+ return match ? match[1] : null;
67
+ }