acstack 0.53.1 → 0.54.0

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.
Files changed (87) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +11 -11
  3. package/agents/codex/frontend-verifier.toml +8 -7
  4. package/agents/codex/task-checker.toml +1 -1
  5. package/agents/codex/task-executor.toml +1 -1
  6. package/agents/codex/test-writer.toml +1 -1
  7. package/agents/frontend-verifier.md +8 -7
  8. package/agents/task-checker.md +1 -1
  9. package/agents/task-executor.md +1 -1
  10. package/agents/test-writer.md +1 -1
  11. package/assets/playwriter-bridge/doctor.ps1 +1 -1
  12. package/assets/playwriter-bridge/register-tasks.ps1 +4 -4
  13. package/assets/terminal-workspace/config.example.yaml +2 -2
  14. package/assets/terminal-workspace/helpers/acstack-bridge +3 -1
  15. package/assets/terminal-workspace/helpers/acstack-db +1 -1
  16. package/assets/terminal-workspace/helpers/acstack-md +4 -2
  17. package/assets/terminal-workspace/helpers/acstack-md-lib +2 -2
  18. package/assets/terminal-workspace/helpers/acstack-open +1 -1
  19. package/assets/terminal-workspace/lib/apply-herdr-numbers.sh +1 -1
  20. package/assets/terminal-workspace/lib/apply-tools.sh +1 -1
  21. package/assets/terminal-workspace/lib/apply-tuicr.sh +2 -2
  22. package/assets/terminal-workspace/lib/commands.sh +1 -1
  23. package/assets/terminal-workspace/lib/helper-install.sh +1 -1
  24. package/assets/terminal-workspace/setup.sh +6 -1
  25. package/cli/dist/index.js +697 -509
  26. package/cli/dist/index.js.map +4 -4
  27. package/cli/package.json +1 -1
  28. package/docs/config-reference.md +129 -87
  29. package/docs/dev-setups.md +4 -4
  30. package/docs/terminal-workspace.md +7 -6
  31. package/docs/workflows.md +13 -12
  32. package/examples/design-doc/acstack/README.md +3 -3
  33. package/examples/design-doc/acstack/journey.md +1 -1
  34. package/examples/design-doc/acstack/publication.md +1 -1
  35. package/hooks/context-pressure.sh +6 -2
  36. package/hooks/lib/read-guard.mjs +25 -21
  37. package/hooks/project-data.sh +5 -34
  38. package/hooks/session-start-handoff.sh +11 -22
  39. package/hooks/session-start.sh +5 -6
  40. package/orchestrator/AGENTS.md +30 -25
  41. package/package.json +1 -1
  42. package/pi/agents/frontend-verifier.md +8 -7
  43. package/pi/agents/task-checker.md +1 -1
  44. package/pi/agents/task-executor.md +1 -1
  45. package/pi/agents/test-writer.md +1 -1
  46. package/pi/extension.ts +39 -57
  47. package/profiles/fullstack-python.yaml +1 -1
  48. package/profiles/node-typescript.yaml +1 -1
  49. package/profiles/python.yaml +1 -1
  50. package/skills/archive/SKILL.md +20 -18
  51. package/skills/bootstrap/SKILL.md +6 -6
  52. package/skills/bridge/SKILL-VM.md +5 -5
  53. package/skills/bridge/SKILL-WINDOWS.md +1 -1
  54. package/skills/browser-verify/SKILL.md +6 -6
  55. package/skills/commit/SKILL.md +8 -8
  56. package/skills/compact-and-continue/SKILL.md +11 -9
  57. package/skills/design-doc/SKILL.md +8 -5
  58. package/skills/explore/SKILL.md +1 -1
  59. package/skills/grilling/SKILL.md +4 -3
  60. package/skills/handoff/SKILL.md +4 -4
  61. package/skills/host/SKILL.md +2 -2
  62. package/skills/host/detect-host.sh +8 -4
  63. package/skills/host/herdr.md +1 -1
  64. package/skills/host/install-herdr-block.sh +1 -1
  65. package/skills/host/notes.md +5 -4
  66. package/skills/host/questions.md +1 -1
  67. package/skills/host/report.md +2 -2
  68. package/skills/implement/SKILL.md +6 -5
  69. package/skills/implement/decision-log.md +10 -15
  70. package/skills/implement/design-pass.md +2 -2
  71. package/skills/map/SKILL.md +5 -3
  72. package/skills/map/frontend.md +2 -2
  73. package/skills/map/materialising.md +5 -3
  74. package/skills/map/trackers/files.md +6 -4
  75. package/skills/pre-commit-review/SKILL.md +3 -3
  76. package/skills/quick-fix/SKILL.md +1 -1
  77. package/skills/record-context/SKILL.md +6 -6
  78. package/skills/setup/SKILL.md +20 -11
  79. package/skills/setup/browser.md +2 -2
  80. package/skills/setup/first-run.md +14 -16
  81. package/skills/setup/report.md +4 -3
  82. package/skills/setup/row-fixes.md +7 -8
  83. package/skills/sync/SKILL.md +14 -12
  84. package/skills/terminal-workspace/SKILL.md +4 -4
  85. package/skills/terminal-workspace/headless-clipboard.md +1 -1
  86. package/skills/terminal-workspace/turn-off-and-update.md +1 -1
  87. package/skills/test-conventions/SKILL.md +2 -2
package/cli/dist/index.js CHANGED
@@ -10790,328 +10790,6 @@ var require_dist = __commonJS({
10790
10790
  }
10791
10791
  });
10792
10792
 
10793
- // src/lib/atomic-file.ts
10794
- import { randomBytes } from "node:crypto";
10795
- import { renameSync, rmSync, writeFileSync } from "node:fs";
10796
- function tempPathFor(path) {
10797
- return `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
10798
- }
10799
- function replaceFileAtomically(path, text2, beforeRename) {
10800
- const temp = tempPathFor(path);
10801
- try {
10802
- writeFileSync(temp, text2, "utf-8");
10803
- beforeRename?.(temp);
10804
- renameSync(temp, path);
10805
- } catch (err) {
10806
- try {
10807
- rmSync(temp, { force: true });
10808
- } catch {
10809
- }
10810
- throw err;
10811
- }
10812
- }
10813
- var init_atomic_file = __esm({
10814
- "src/lib/atomic-file.ts"() {
10815
- "use strict";
10816
- }
10817
- });
10818
-
10819
- // src/lib/constants.ts
10820
- import { join as join2 } from "node:path";
10821
- import { homedir } from "node:os";
10822
- function acstackFile(name, root) {
10823
- const relative7 = join2(ACSTACK_DIR, name);
10824
- return root === void 0 ? relative7 : join2(root, relative7);
10825
- }
10826
- function projectFile(root) {
10827
- return acstackFile(PROJECT_FILE, root);
10828
- }
10829
- function domainMapFile(root) {
10830
- return acstackFile(DOMAIN_MAP_FILE, root);
10831
- }
10832
- function claudeConfigRoot() {
10833
- const stated = process.env.CLAUDE_CONFIG_DIR;
10834
- return stated ? stated : join2(homedir(), CLAUDE_DIR);
10835
- }
10836
- var ACSTACK_DIR, LEGACY_ACSTACK_DIR, DECISIONS_DIR, DECISION_LOGS_DIR, VERIFICATION_DIR, CHANGES_DIR, MAPS_DIR, SPECS_DIR, ARCHIVE_DIR, PROJECT_FILE, DOMAIN_MAP_FILE, PROFILES_DIR, AGENT_BROWSER_JSON_FILE, VERIFICATION_KNOWLEDGE_FILE, AGENT_BROWSER_BIN, CLAUDE_DIR, CLAUDE_SETTINGS_FILE, CLAUDE_LOCAL_SETTINGS_FILE, ACSTACK_OUTPUT_STYLE, VOCABULARY_PATH, GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_FILE, GLOBAL_DATA_DIR;
10837
- var init_constants = __esm({
10838
- "src/lib/constants.ts"() {
10839
- "use strict";
10840
- ACSTACK_DIR = ".acstack";
10841
- LEGACY_ACSTACK_DIR = "acstack";
10842
- DECISIONS_DIR = "decisions";
10843
- DECISION_LOGS_DIR = "decision-logs";
10844
- VERIFICATION_DIR = "verification";
10845
- CHANGES_DIR = "changes";
10846
- MAPS_DIR = "maps";
10847
- SPECS_DIR = "specs";
10848
- ARCHIVE_DIR = "archive";
10849
- PROJECT_FILE = "project.yaml";
10850
- DOMAIN_MAP_FILE = "domain-map.yaml";
10851
- PROFILES_DIR = "profiles";
10852
- AGENT_BROWSER_JSON_FILE = join2(ACSTACK_DIR, "agent-browser.json");
10853
- VERIFICATION_KNOWLEDGE_FILE = join2(ACSTACK_DIR, VERIFICATION_DIR, "VERIFICATION-KNOWLEDGE.md");
10854
- AGENT_BROWSER_BIN = "agent-browser";
10855
- CLAUDE_DIR = ".claude";
10856
- CLAUDE_SETTINGS_FILE = "settings.json";
10857
- CLAUDE_LOCAL_SETTINGS_FILE = "settings.local.json";
10858
- ACSTACK_OUTPUT_STYLE = "acstack:ac-writing-style";
10859
- VOCABULARY_PATH = join2("skills", "writing", "vocabulary.md");
10860
- GLOBAL_CONFIG_DIR = join2(
10861
- process.env.XDG_CONFIG_HOME ?? join2(homedir(), ".config"),
10862
- "acstack"
10863
- );
10864
- GLOBAL_CONFIG_FILE = join2(GLOBAL_CONFIG_DIR, "config.json");
10865
- GLOBAL_DATA_DIR = join2(
10866
- process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share"),
10867
- "acstack"
10868
- );
10869
- }
10870
- });
10871
-
10872
- // src/lib/project.ts
10873
- import { existsSync, lstatSync } from "node:fs";
10874
- import { dirname as dirname2, join as join3, resolve } from "node:path";
10875
- function findUp(startDir, marker) {
10876
- let dir = resolve(startDir);
10877
- while (true) {
10878
- if (existsSync(join3(dir, marker))) return dir;
10879
- const parent = dirname2(dir);
10880
- if (parent === dir) return null;
10881
- dir = parent;
10882
- }
10883
- }
10884
- function hasLegacyProjectData(root) {
10885
- const legacy = join3(root, LEGACY_ACSTACK_DIR);
10886
- if (!lstatSync(legacy, { throwIfNoEntry: false })?.isDirectory()) return false;
10887
- return [PROJECT_FILE, DOMAIN_MAP_FILE].some((name) => {
10888
- const entry = lstatSync(join3(legacy, name), { throwIfNoEntry: false });
10889
- return entry?.isFile() || entry?.isSymbolicLink();
10890
- }) || ["alignment-packs", "design-reviews", MAPS_DIR, SPECS_DIR, CHANGES_DIR, ARCHIVE_DIR, DECISION_LOGS_DIR].some((name) => {
10891
- const entry = lstatSync(join3(legacy, name), { throwIfNoEntry: false });
10892
- return entry?.isDirectory() || entry?.isSymbolicLink();
10893
- });
10894
- }
10895
- function findProjectRoot(from = process.cwd()) {
10896
- let dir = resolve(from);
10897
- while (true) {
10898
- if (existsSync(join3(dir, ACSTACK_DIR)) || hasLegacyProjectData(dir)) return dir;
10899
- const parent = dirname2(dir);
10900
- if (parent === dir) return null;
10901
- dir = parent;
10902
- }
10903
- }
10904
- function findPluginRoot(startDir = import.meta.dirname) {
10905
- const fromEnv = process.env.CLAUDE_PLUGIN_ROOT;
10906
- if (fromEnv && existsSync(join3(fromEnv, PLUGIN_MARKER))) return resolve(fromEnv);
10907
- return findUp(startDir, PLUGIN_MARKER);
10908
- }
10909
- var PLUGIN_MARKER;
10910
- var init_project = __esm({
10911
- "src/lib/project.ts"() {
10912
- "use strict";
10913
- init_constants();
10914
- PLUGIN_MARKER = join3(".claude-plugin", "plugin.json");
10915
- }
10916
- });
10917
-
10918
- // src/lib/project-data.ts
10919
- import { chmodSync, existsSync as existsSync2, lstatSync as lstatSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, readlinkSync, realpathSync, renameSync as renameSync2, rmdirSync, symlinkSync, unlinkSync } from "node:fs";
10920
- import { dirname as dirname3, isAbsolute, join as join4, relative, resolve as resolve2 } from "node:path";
10921
- function present(path) {
10922
- try {
10923
- lstatSync2(path);
10924
- return true;
10925
- } catch (error) {
10926
- if (error.code === "ENOENT") return false;
10927
- throw error;
10928
- }
10929
- }
10930
- function assertCanonicalProject(root) {
10931
- if (!existsSync2(join4(root, ACSTACK_DIR)) && hasLegacyProjectData(root)) {
10932
- throw new Error("Legacy ACStack project data found. Stop review servers and agents, back up the project, then run `acstack project-data migrate` from the project root.");
10933
- }
10934
- }
10935
- function gitignorePlan(root) {
10936
- const path = join4(root, ".gitignore");
10937
- const stat = present(path) ? lstatSync2(path) : void 0;
10938
- if (stat && (!stat.isFile() || stat.isSymbolicLink())) throw new Error(`${path}: .gitignore must be a regular file, not a symlink`);
10939
- let before = "";
10940
- if (stat) {
10941
- try {
10942
- before = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readFileSync2(path));
10943
- } catch (error) {
10944
- if (error instanceof TypeError) throw new Error(`${path}: invalid UTF-8; refusing to rewrite .gitignore`);
10945
- throw error;
10946
- }
10947
- }
10948
- const bom = before.startsWith("\uFEFF") ? "\uFEFF" : "";
10949
- const body = before.slice(bom.length);
10950
- const eol = body.includes("\r\n") ? "\r\n" : "\n";
10951
- const lines = body.match(/[^\n]*\n|[^\n]+$/g) ?? [];
10952
- const markers = lines.flatMap((line, index) => {
10953
- const text2 = line.replace(/\r?\n$/, "");
10954
- return text2.includes("acstack project-data") && (text2.includes(">>>") || text2.includes("<<<")) ? [{ text: text2, index }] : [];
10955
- });
10956
- if (markers.length !== 0 && (markers.length !== 2 || markers[0].text !== BEGIN || markers[1].text !== END)) {
10957
- throw new Error(`${path}: malformed ACStack project-data managed markers`);
10958
- }
10959
- const outside = markers.length === 0 ? body : lines.filter((_, i) => i < markers[0].index || i > markers[1].index).join("");
10960
- const baseline = [BEGIN, `!/${ACSTACK_DIR}/`, `/${ACSTACK_DIR}/**`, END, ""].join(eol);
10961
- return { path, before, after: bom + baseline + outside, mode: stat?.mode };
10962
- }
10963
- function ensureProjectGitignore(root) {
10964
- if (!present(join4(root, ACSTACK_DIR))) return;
10965
- const plan = gitignorePlan(root);
10966
- if (plan.before === plan.after) return;
10967
- replaceFileAtomically(plan.path, plan.after, plan.mode === void 0 ? void 0 : (temp) => chmodSync(temp, plan.mode));
10968
- }
10969
- function inside(root, path) {
10970
- const rel = relative(root, path);
10971
- return rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(rel);
10972
- }
10973
- function migrateProjectData(root) {
10974
- root = resolve2(root);
10975
- const legacy = join4(root, LEGACY_ACSTACK_DIR);
10976
- const canonical = join4(root, ACSTACK_DIR);
10977
- if (present(legacy) && (!lstatSync2(legacy).isDirectory() || lstatSync2(legacy).isSymbolicLink())) throw new Error(`${legacy}: legacy project data must be a real directory`);
10978
- const candidates = [
10979
- ...hasLegacyProjectData(root) ? [{ from: legacy, to: canonical }] : [],
10980
- { from: join4(root, "docs", "adr"), to: join4(canonical, DECISIONS_DIR) },
10981
- { from: join4(root, "CONTEXT.md"), to: join4(canonical, "CONTEXT.md") },
10982
- { from: join4(root, "agent-browser.json"), to: join4(canonical, "agent-browser.json") },
10983
- { from: join4(root, ".test-baseline"), to: join4(canonical, ".test-baseline") },
10984
- { from: join4(root, ".decisions"), to: join4(canonical, DECISION_LOGS_DIR) },
10985
- { from: join4(root, KNOWLEDGE), to: join4(canonical, VERIFICATION_DIR, KNOWLEDGE) }
10986
- ];
10987
- const config = join4(legacy, "project.yaml");
10988
- if (hasLegacyProjectData(root) && existsSync2(config)) {
10989
- const yaml = (0, import_yaml.parse)(readFileSync2(config, "utf8"));
10990
- const helpers = yaml?.frontend?.helpers_dir;
10991
- if (typeof helpers === "string") {
10992
- const path = resolve2(root, helpers, KNOWLEDGE);
10993
- if (inside(root, path) && present(path) && inside(realpathSync(root), realpathSync(dirname3(path)))) {
10994
- if (!candidates.some((move) => move.from === path)) candidates.push({ from: path, to: join4(canonical, VERIFICATION_DIR, KNOWLEDGE) });
10995
- }
10996
- }
10997
- }
10998
- const moves = candidates.filter((move) => present(move.from));
10999
- if (moves.length === 0) {
11000
- ensureProjectGitignore(root);
11001
- return { moved: [] };
11002
- }
11003
- if (present(canonical)) throw new Error(`${canonical}: migration destination already exists; refusing to merge or overwrite project data`);
11004
- const ignore = gitignorePlan(root);
11005
- const mapPath = (path) => {
11006
- const move = [...moves].sort((a, b) => b.from.length - a.from.length).find((candidate) => inside(candidate.from, path));
11007
- return move ? join4(move.to, relative(move.from, path)) : path;
11008
- };
11009
- for (const move of moves) {
11010
- for (let parent = dirname3(move.from); parent !== root; parent = dirname3(parent)) {
11011
- if (!inside(root, parent) || lstatSync2(parent).isSymbolicLink()) throw new Error(`${parent}: migration cannot traverse a symlinked source directory`);
11012
- }
11013
- for (const other of moves) {
11014
- if (move === other) continue;
11015
- if (move.to === other.to) throw new Error(`${move.to}: conflicting incoming project data`);
11016
- if (inside(other.to, move.to)) {
11017
- const incoming = join4(other.from, relative(other.to, move.to));
11018
- if (present(incoming)) throw new Error(`${move.to}: destination conflicts with incoming legacy data`);
11019
- for (let parent = dirname3(incoming); parent !== other.from; parent = dirname3(parent)) {
11020
- if (present(parent) && (!lstatSync2(parent).isDirectory() || lstatSync2(parent).isSymbolicLink())) throw new Error(`${parent}: unsafe incoming destination parent`);
11021
- }
11022
- }
11023
- }
11024
- }
11025
- const links = [];
11026
- const removals = [];
11027
- const ownedIgnores = /* @__PURE__ */ new Set([
11028
- join4(legacy, "alignment-packs", ".gitignore"),
11029
- join4(legacy, "design-reviews", ".gitignore"),
11030
- join4(root, ".decisions", ".gitignore"),
11031
- join4(legacy, DECISION_LOGS_DIR, ".gitignore")
11032
- ]);
11033
- const visited = /* @__PURE__ */ new Set();
11034
- const inspect = (path) => {
11035
- if (visited.has(path)) return;
11036
- visited.add(path);
11037
- const stat = lstatSync2(path);
11038
- if (stat.isSymbolicLink()) {
11039
- const before = readlinkSync(path);
11040
- const target = mapPath(resolve2(dirname3(path), before));
11041
- const after = isAbsolute(before) ? target : relative(dirname3(mapPath(path)), target) || ".";
11042
- if (before !== after) links.push({ path: mapPath(path), before, after });
11043
- } else if (stat.isDirectory()) {
11044
- for (const name of readdirSync(path)) inspect(join4(path, name));
11045
- } else if (ownedIgnores.has(path) && stat.isFile()) {
11046
- const text2 = readFileSync2(path, "utf8");
11047
- const normalized = text2.replace(/\r\n/g, "\n");
11048
- if (normalized === "*\n" || normalized === "*" || normalized === "# Written by acstack. Delete this file and set workflow.alignment_packs.commit_generated_packs: true to commit packs.\n*\n") {
11049
- removals.push({ path: mapPath(path), text: text2, mode: stat.mode });
11050
- }
11051
- }
11052
- };
11053
- for (const move of moves) inspect(move.from);
11054
- const undo = [];
11055
- const makeParents = (path) => {
11056
- if (present(path)) return;
11057
- makeParents(dirname3(path));
11058
- mkdirSync(path);
11059
- undo.push(() => rmdirSync(path));
11060
- };
11061
- try {
11062
- for (const move of moves) {
11063
- const from = present(move.from) ? move.from : join4(canonical, relative(legacy, move.from));
11064
- makeParents(dirname3(move.to));
11065
- if (present(move.to)) throw new Error(`${move.to}: migration destination appeared during migration`);
11066
- renameSync2(from, move.to);
11067
- undo.push(() => renameSync2(move.to, from));
11068
- }
11069
- for (const link of links) {
11070
- unlinkSync(link.path);
11071
- try {
11072
- symlinkSync(link.after, link.path);
11073
- } catch (error) {
11074
- symlinkSync(link.before, link.path);
11075
- throw error;
11076
- }
11077
- undo.push(() => {
11078
- unlinkSync(link.path);
11079
- symlinkSync(link.before, link.path);
11080
- });
11081
- }
11082
- for (const removal of removals) {
11083
- unlinkSync(removal.path);
11084
- undo.push(() => replaceFileAtomically(removal.path, removal.text, (temp) => chmodSync(temp, removal.mode)));
11085
- }
11086
- replaceFileAtomically(ignore.path, ignore.after, ignore.mode === void 0 ? void 0 : (temp) => chmodSync(temp, ignore.mode));
11087
- return { moved: moves.map((move) => relative(root, move.to)) };
11088
- } catch (error) {
11089
- const failures = [];
11090
- for (const rollback of undo.reverse()) {
11091
- try {
11092
- rollback();
11093
- } catch (failure) {
11094
- failures.push(String(failure));
11095
- }
11096
- }
11097
- if (failures.length) throw new Error(`${String(error)}; rollback incomplete: ${failures.join("; ")}`);
11098
- throw error;
11099
- }
11100
- }
11101
- var import_yaml, BEGIN, END, KNOWLEDGE;
11102
- var init_project_data = __esm({
11103
- "src/lib/project-data.ts"() {
11104
- "use strict";
11105
- import_yaml = __toESM(require_dist(), 1);
11106
- init_atomic_file();
11107
- init_project();
11108
- init_constants();
11109
- BEGIN = "# >>> acstack project-data >>>";
11110
- END = "# <<< acstack project-data <<<";
11111
- KNOWLEDGE = "VERIFICATION-KNOWLEDGE.md";
11112
- }
11113
- });
11114
-
11115
10793
  // node_modules/chalk/source/vendor/ansi-styles/index.js
11116
10794
  function assembleStyles() {
11117
10795
  const codes = /* @__PURE__ */ new Map();
@@ -11619,18 +11297,190 @@ var init_source = __esm({
11619
11297
  if (lfIndex !== -1) {
11620
11298
  string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
11621
11299
  }
11622
- return openAll + string + closeAll;
11300
+ return openAll + string + closeAll;
11301
+ };
11302
+ Object.defineProperties(createChalk.prototype, styles2);
11303
+ chalk = createChalk();
11304
+ chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
11305
+ source_default = chalk;
11306
+ }
11307
+ });
11308
+
11309
+ // src/lib/constants.ts
11310
+ import { join as join2 } from "node:path";
11311
+ import { homedir } from "node:os";
11312
+ function acstackFile(name, root) {
11313
+ return join2(root ?? PROJECT_FOLDER_LABEL, name);
11314
+ }
11315
+ function projectFile(root) {
11316
+ return acstackFile(PROJECT_FILE, root);
11317
+ }
11318
+ function domainMapFile(root) {
11319
+ return acstackFile(DOMAIN_MAP_FILE, root);
11320
+ }
11321
+ function claudeConfigRoot() {
11322
+ const stated = process.env.CLAUDE_CONFIG_DIR;
11323
+ return stated ? stated : join2(homedir(), CLAUDE_DIR);
11324
+ }
11325
+ var ACSTACK_DIR, LEGACY_ACSTACK_DIR, DECISIONS_DIR, DECISION_LOGS_DIR, VERIFICATION_DIR, CHANGES_DIR, MAPS_DIR, SPECS_DIR, ARCHIVE_DIR, PROJECT_FILE, DOMAIN_MAP_FILE, PROJECT_FOLDER_LABEL, PROFILES_DIR, AGENT_BROWSER_JSON_FILE, VERIFICATION_KNOWLEDGE_FILE, AGENT_BROWSER_BIN, CLAUDE_DIR, CLAUDE_SETTINGS_FILE, CLAUDE_LOCAL_SETTINGS_FILE, ACSTACK_OUTPUT_STYLE, VOCABULARY_PATH, ACSTACK_HOME, GLOBAL_CONFIG_FILE, GLOBAL_CACHE_DIR, PROJECTS_DIR, PROJECT_JSON_FILE;
11326
+ var init_constants = __esm({
11327
+ "src/lib/constants.ts"() {
11328
+ "use strict";
11329
+ ACSTACK_DIR = ".acstack";
11330
+ LEGACY_ACSTACK_DIR = "acstack";
11331
+ DECISIONS_DIR = "decisions";
11332
+ DECISION_LOGS_DIR = "decision-logs";
11333
+ VERIFICATION_DIR = "verification";
11334
+ CHANGES_DIR = "changes";
11335
+ MAPS_DIR = "maps";
11336
+ SPECS_DIR = "specs";
11337
+ ARCHIVE_DIR = "archive";
11338
+ PROJECT_FILE = "project.yaml";
11339
+ DOMAIN_MAP_FILE = "domain-map.yaml";
11340
+ PROJECT_FOLDER_LABEL = "<project folder>";
11341
+ PROFILES_DIR = "profiles";
11342
+ AGENT_BROWSER_JSON_FILE = "agent-browser.json";
11343
+ VERIFICATION_KNOWLEDGE_FILE = join2(VERIFICATION_DIR, "VERIFICATION-KNOWLEDGE.md");
11344
+ AGENT_BROWSER_BIN = "agent-browser";
11345
+ CLAUDE_DIR = ".claude";
11346
+ CLAUDE_SETTINGS_FILE = "settings.json";
11347
+ CLAUDE_LOCAL_SETTINGS_FILE = "settings.local.json";
11348
+ ACSTACK_OUTPUT_STYLE = "acstack:ac-writing-style";
11349
+ VOCABULARY_PATH = join2("skills", "writing", "vocabulary.md");
11350
+ ACSTACK_HOME = process.env.ACSTACK_HOME || join2(homedir(), ".acstack");
11351
+ GLOBAL_CONFIG_FILE = join2(ACSTACK_HOME, "config.json");
11352
+ GLOBAL_CACHE_DIR = join2(ACSTACK_HOME, "cache");
11353
+ PROJECTS_DIR = join2(ACSTACK_HOME, "projects");
11354
+ PROJECT_JSON_FILE = "project.json";
11355
+ }
11356
+ });
11357
+
11358
+ // src/lib/project.ts
11359
+ import { spawnSync } from "node:child_process";
11360
+ import { createHash } from "node:crypto";
11361
+ import { existsSync, lstatSync, mkdirSync, readFileSync as readFileSync2, readdirSync, realpathSync, symlinkSync, writeFileSync } from "node:fs";
11362
+ import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
11363
+ function findUp(startDir, marker) {
11364
+ let dir = resolve(startDir);
11365
+ while (true) {
11366
+ if (existsSync(join3(dir, marker))) return dir;
11367
+ const parent = dirname2(dir);
11368
+ if (parent === dir) return null;
11369
+ dir = parent;
11370
+ }
11371
+ }
11372
+ function hasLegacyProjectData(root) {
11373
+ const legacy = join3(root, LEGACY_ACSTACK_DIR);
11374
+ if (!lstatSync(legacy, { throwIfNoEntry: false })?.isDirectory()) return false;
11375
+ return [PROJECT_FILE, DOMAIN_MAP_FILE].some((name) => {
11376
+ const entry = lstatSync(join3(legacy, name), { throwIfNoEntry: false });
11377
+ return entry?.isFile() || entry?.isSymbolicLink();
11378
+ }) || ["alignment-packs", "design-reviews", MAPS_DIR, SPECS_DIR, CHANGES_DIR, ARCHIVE_DIR, DECISION_LOGS_DIR].some((name) => {
11379
+ const entry = lstatSync(join3(legacy, name), { throwIfNoEntry: false });
11380
+ return entry?.isDirectory() || entry?.isSymbolicLink();
11381
+ });
11382
+ }
11383
+ function git(cwd, args) {
11384
+ const result = spawnSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
11385
+ return result.status === 0 ? result.stdout.trim() : null;
11386
+ }
11387
+ function checkoutRoot(from = process.cwd()) {
11388
+ return git(from, ["rev-parse", "--show-toplevel"]) || null;
11389
+ }
11390
+ function locateProject(from = process.cwd(), projects = PROJECTS_DIR) {
11391
+ const common = git(from, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
11392
+ if (!common) return null;
11393
+ const commonDir = realpathSync.native(common);
11394
+ const { main, bare } = mainCheckout(commonDir, from);
11395
+ const key2 = `${basename(main)}-${createHash("sha256").update(commonDir).digest("hex").slice(0, 6)}`;
11396
+ return { key: key2, commonDir, main, bare, entry: join3(projects, key2) };
11397
+ }
11398
+ function mainCheckout(commonDir, from) {
11399
+ if (basename(commonDir) === ".git") return { main: dirname2(commonDir), bare: false };
11400
+ const worktree = git(commonDir, ["config", "core.worktree"]);
11401
+ if (worktree) return { main: realpathSync.native(resolve(commonDir, worktree)), bare: false };
11402
+ if (git(commonDir, ["rev-parse", "--is-bare-repository"]) === "true") return { main: commonDir, bare: true };
11403
+ return { main: realpathSync.native(checkoutRoot(from) ?? from), bare: false };
11404
+ }
11405
+ function hasTrackedProject(location) {
11406
+ return !location.bare && existsSync(join3(location.main, ACSTACK_DIR, PROJECT_FILE)) && !!git(location.main, ["ls-files", "--", `${ACSTACK_DIR}/${PROJECT_FILE}`]);
11407
+ }
11408
+ function findOrphans(location) {
11409
+ const projects = dirname2(location.entry);
11410
+ if (!existsSync(projects)) return [];
11411
+ const prefix = `${basename(location.main)}-`;
11412
+ return readdirSync(projects).filter((name) => name.startsWith(prefix) && /^[0-9a-f]{6}$/.test(name.slice(prefix.length)) && name !== location.key).map((name) => join3(projects, name)).filter((path) => {
11413
+ if (lstatSync(path).isSymbolicLink()) return !existsSync(path);
11414
+ const recorded = recordedCommonDir(path);
11415
+ return recorded !== null && !existsSync(recorded);
11416
+ });
11417
+ }
11418
+ function orphanWarnings(location) {
11419
+ if (lstatSync(location.entry, { throwIfNoEntry: false })) return [];
11420
+ return findOrphans(location).map((orphan) => `${orphan} belonged to a repository that is no longer there. If that was this repository before it moved, run \`acstack project-data relink ${basename(orphan)}\`.`);
11421
+ }
11422
+ function recordedCommonDir(folder) {
11423
+ try {
11424
+ return JSON.parse(readFileSync2(join3(folder, PROJECT_JSON_FILE), "utf8")).commonDir ?? null;
11425
+ } catch {
11426
+ return null;
11427
+ }
11428
+ }
11429
+ function recordProject(location) {
11430
+ writeFileSync(join3(location.entry, PROJECT_JSON_FILE), JSON.stringify({ commonDir: location.commonDir }, null, 2) + "\n");
11431
+ }
11432
+ function linkTracked(location) {
11433
+ mkdirSync(dirname2(location.entry), { recursive: true });
11434
+ symlinkSync(join3(location.main, ACSTACK_DIR), location.entry, process.platform === "win32" ? "junction" : "dir");
11435
+ }
11436
+ function findProjectRoot(from = process.cwd()) {
11437
+ const location = locateProject(from);
11438
+ if (!location) return null;
11439
+ const entry = lstatSync(location.entry, { throwIfNoEntry: false });
11440
+ if (entry?.isSymbolicLink()) {
11441
+ if (!existsSync(location.entry)) throw new ProjectDataError(`${location.entry} links to ${join3(location.main, ACSTACK_DIR)}, which does not exist. Restore it or run \`acstack project-data mode global\`.`);
11442
+ return realpathSync(location.entry);
11443
+ }
11444
+ if (entry) {
11445
+ if (hasTrackedProject(location)) throw new ProjectDataError(`This repository has both a project folder (${location.entry}) and a committed ${ACSTACK_DIR}/. Choose one with \`acstack project-data mode tracked\` or \`acstack project-data mode global\`.`);
11446
+ if (!existsSync(join3(location.entry, PROJECT_JSON_FILE))) recordProject(location);
11447
+ return realpathSync(location.entry);
11448
+ }
11449
+ if (hasTrackedProject(location)) {
11450
+ linkTracked(location);
11451
+ return realpathSync(location.entry);
11452
+ }
11453
+ if (hasLegacyData(location, from)) throw new ProjectDataError(LEGACY_MESSAGE);
11454
+ return null;
11455
+ }
11456
+ function hasLegacyData(location, from = process.cwd()) {
11457
+ const top = git(from, ["rev-parse", "--show-toplevel"]);
11458
+ return [location.main, ...top ? [top] : []].some((root) => existsSync(join3(root, ACSTACK_DIR)) || hasLegacyProjectData(root));
11459
+ }
11460
+ function findPluginRoot(startDir = import.meta.dirname) {
11461
+ const fromEnv = process.env.CLAUDE_PLUGIN_ROOT;
11462
+ if (fromEnv && existsSync(join3(fromEnv, PLUGIN_MARKER))) return resolve(fromEnv);
11463
+ return findUp(startDir, PLUGIN_MARKER);
11464
+ }
11465
+ var PLUGIN_MARKER, ProjectDataError, LEGACY_MESSAGE;
11466
+ var init_project = __esm({
11467
+ "src/lib/project.ts"() {
11468
+ "use strict";
11469
+ init_constants();
11470
+ PLUGIN_MARKER = join3(".claude-plugin", "plugin.json");
11471
+ ProjectDataError = class extends Error {
11472
+ constructor(message) {
11473
+ super(message);
11474
+ this.name = "ProjectDataError";
11475
+ }
11623
11476
  };
11624
- Object.defineProperties(createChalk.prototype, styles2);
11625
- chalk = createChalk();
11626
- chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
11627
- source_default = chalk;
11477
+ LEGACY_MESSAGE = "Legacy ACStack project data found. Stop review servers and agents, back up the project, then run `acstack project-data migrate` from the repository.";
11628
11478
  }
11629
11479
  });
11630
11480
 
11631
11481
  // src/lib/utils.ts
11632
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readSync, readdirSync as readdirSync2 } from "node:fs";
11633
- import { join as join5 } from "node:path";
11482
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readSync, readdirSync as readdirSync2 } from "node:fs";
11483
+ import { join as join4 } from "node:path";
11634
11484
  function fail(message, hint) {
11635
11485
  console.error(source_default.red(message));
11636
11486
  if (hint) console.error(source_default.dim(hint));
@@ -11640,37 +11490,37 @@ function errorHint(err) {
11640
11490
  return err.hint;
11641
11491
  }
11642
11492
  function ensureDir(path) {
11643
- if (!existsSync3(path)) mkdirSync2(path, { recursive: true });
11493
+ if (!existsSync2(path)) mkdirSync2(path, { recursive: true });
11644
11494
  }
11645
11495
  function readYaml(path) {
11646
- if (!existsSync3(path)) return null;
11647
- return (0, import_yaml2.parse)(readFileSync3(path, "utf-8"));
11496
+ if (!existsSync2(path)) return null;
11497
+ return (0, import_yaml.parse)(readFileSync3(path, "utf-8"));
11648
11498
  }
11649
11499
  function readMarkdown(path) {
11650
- if (!existsSync3(path)) return null;
11500
+ if (!existsSync2(path)) return null;
11651
11501
  return readFileSync3(path, "utf-8");
11652
11502
  }
11653
11503
  function listChanges(root) {
11654
- const dir = join5(root, ACSTACK_DIR, CHANGES_DIR);
11655
- if (!existsSync3(dir)) return [];
11504
+ const dir = join4(root, CHANGES_DIR);
11505
+ if (!existsSync2(dir)) return [];
11656
11506
  return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== ARCHIVE_DIR).map((e) => e.name);
11657
11507
  }
11658
11508
  function listSpecs(root) {
11659
- const dir = join5(root, ACSTACK_DIR, SPECS_DIR);
11660
- if (!existsSync3(dir)) return [];
11509
+ const dir = join4(root, SPECS_DIR);
11510
+ if (!existsSync2(dir)) return [];
11661
11511
  return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
11662
11512
  }
11663
11513
  function requireProject(root) {
11664
11514
  if (!root) fail("Not in an ACStack project. Run `/acstack:setup` first.");
11665
- try {
11666
- assertCanonicalProject(root);
11667
- } catch (error) {
11668
- fail(error.message);
11669
- }
11670
11515
  }
11671
11516
  function inProject(action) {
11672
11517
  return async (...args) => {
11673
- const root = findProjectRoot();
11518
+ let root = null;
11519
+ try {
11520
+ root = findProjectRoot();
11521
+ } catch (err) {
11522
+ fail(err.message);
11523
+ }
11674
11524
  requireProject(root);
11675
11525
  try {
11676
11526
  await action(root, ...args);
@@ -11722,14 +11572,13 @@ function stdinWentSilent(windowMs) {
11722
11572
  err.hint = `Set ${STDIN_SILENCE_ENV} to widen the window, in milliseconds.`;
11723
11573
  return err;
11724
11574
  }
11725
- var import_yaml2, NAP, NAP_MS, CHUNK_BYTES, DEFAULT_STDIN_SILENCE_MS, STDIN_SILENCE_ENV, TTY_REFUSAL;
11575
+ var import_yaml, NAP, NAP_MS, CHUNK_BYTES, DEFAULT_STDIN_SILENCE_MS, STDIN_SILENCE_ENV, TTY_REFUSAL;
11726
11576
  var init_utils = __esm({
11727
11577
  "src/lib/utils.ts"() {
11728
11578
  "use strict";
11729
- import_yaml2 = __toESM(require_dist(), 1);
11579
+ import_yaml = __toESM(require_dist(), 1);
11730
11580
  init_source();
11731
11581
  init_constants();
11732
- init_project_data();
11733
11582
  init_project();
11734
11583
  NAP = new Int32Array(new SharedArrayBuffer(4));
11735
11584
  NAP_MS = 5;
@@ -11740,24 +11589,380 @@ var init_utils = __esm({
11740
11589
  }
11741
11590
  });
11742
11591
 
11592
+ // src/lib/atomic-file.ts
11593
+ import { randomBytes } from "node:crypto";
11594
+ import { renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
11595
+ function tempPathFor(path) {
11596
+ return `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
11597
+ }
11598
+ function replaceFileAtomically(path, text2, beforeRename) {
11599
+ const temp = tempPathFor(path);
11600
+ try {
11601
+ writeFileSync2(temp, text2, "utf-8");
11602
+ beforeRename?.(temp);
11603
+ renameSync(temp, path);
11604
+ } catch (err) {
11605
+ try {
11606
+ rmSync(temp, { force: true });
11607
+ } catch {
11608
+ }
11609
+ throw err;
11610
+ }
11611
+ }
11612
+ var init_atomic_file = __esm({
11613
+ "src/lib/atomic-file.ts"() {
11614
+ "use strict";
11615
+ }
11616
+ });
11617
+
11618
+ // src/lib/project-data.ts
11619
+ import { spawnSync as spawnSync2 } from "node:child_process";
11620
+ import { chmodSync, cpSync, existsSync as existsSync3, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync3, readlinkSync, realpathSync as realpathSync2, renameSync as renameSync2, rmSync as rmSync2, rmdirSync, symlinkSync as symlinkSync2, unlinkSync } from "node:fs";
11621
+ import { homedir as homedir2 } from "node:os";
11622
+ import { basename as basename2, dirname as dirname3, isAbsolute, join as join5, relative, resolve as resolve2 } from "node:path";
11623
+ function present(path) {
11624
+ try {
11625
+ lstatSync2(path);
11626
+ return true;
11627
+ } catch (error) {
11628
+ if (error.code === "ENOENT") return false;
11629
+ throw error;
11630
+ }
11631
+ }
11632
+ function removeManagedGitignoreBlock(root) {
11633
+ const path = join5(root, ".gitignore");
11634
+ const stat = lstatSync2(path, { throwIfNoEntry: false });
11635
+ if (!stat?.isFile()) return;
11636
+ const before = readFileSync4(path, "utf8");
11637
+ const lines = before.split(/(?<=\n)/);
11638
+ const begin = lines.findIndex((line) => line.trimEnd() === BEGIN);
11639
+ const end = lines.findIndex((line) => line.trimEnd() === END);
11640
+ if (begin === -1 || end < begin) return;
11641
+ const after = [...lines.slice(0, begin), ...lines.slice(end + 1)].join("").replace(/^(\r?\n)+/, "");
11642
+ replaceFileAtomically(path, after, (temp) => chmodSync(temp, stat.mode));
11643
+ }
11644
+ function inside(root, path) {
11645
+ const rel = relative(root, path);
11646
+ return rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(rel);
11647
+ }
11648
+ function migrateOldLayout(root) {
11649
+ root = resolve2(root);
11650
+ const legacy = join5(root, LEGACY_ACSTACK_DIR);
11651
+ const canonical = join5(root, ACSTACK_DIR);
11652
+ if (present(legacy) && (!lstatSync2(legacy).isDirectory() || lstatSync2(legacy).isSymbolicLink())) throw new Error(`${legacy}: legacy project data must be a real directory`);
11653
+ const candidates = [
11654
+ ...hasLegacyProjectData(root) ? [{ from: legacy, to: canonical }] : [],
11655
+ { from: join5(root, "docs", "adr"), to: join5(canonical, DECISIONS_DIR) },
11656
+ { from: join5(root, "CONTEXT.md"), to: join5(canonical, "CONTEXT.md") },
11657
+ { from: join5(root, "agent-browser.json"), to: join5(canonical, "agent-browser.json") },
11658
+ { from: join5(root, ".test-baseline"), to: join5(canonical, ".test-baseline") },
11659
+ { from: join5(root, ".decisions"), to: join5(canonical, DECISION_LOGS_DIR) },
11660
+ { from: join5(root, KNOWLEDGE), to: join5(canonical, VERIFICATION_DIR, KNOWLEDGE) }
11661
+ ];
11662
+ const config = join5(legacy, "project.yaml");
11663
+ if (hasLegacyProjectData(root) && existsSync3(config)) {
11664
+ const yaml = (0, import_yaml2.parse)(readFileSync4(config, "utf8"));
11665
+ const helpers = yaml?.frontend?.helpers_dir;
11666
+ if (typeof helpers === "string") {
11667
+ const path = resolve2(root, helpers, KNOWLEDGE);
11668
+ if (inside(root, path) && present(path) && inside(realpathSync2(root), realpathSync2(dirname3(path)))) {
11669
+ if (!candidates.some((move) => move.from === path)) candidates.push({ from: path, to: join5(canonical, VERIFICATION_DIR, KNOWLEDGE) });
11670
+ }
11671
+ }
11672
+ }
11673
+ const moves = candidates.filter((move) => present(move.from));
11674
+ if (moves.length === 0) return { moved: [] };
11675
+ if (present(canonical)) throw new Error(`${canonical}: migration destination already exists; refusing to merge or overwrite project data`);
11676
+ const mapPath = (path) => {
11677
+ const move = [...moves].sort((a, b) => b.from.length - a.from.length).find((candidate) => inside(candidate.from, path));
11678
+ return move ? join5(move.to, relative(move.from, path)) : path;
11679
+ };
11680
+ for (const move of moves) {
11681
+ for (let parent = dirname3(move.from); parent !== root; parent = dirname3(parent)) {
11682
+ if (!inside(root, parent) || lstatSync2(parent).isSymbolicLink()) throw new Error(`${parent}: migration cannot traverse a symlinked source directory`);
11683
+ }
11684
+ for (const other of moves) {
11685
+ if (move === other) continue;
11686
+ if (move.to === other.to) throw new Error(`${move.to}: conflicting incoming project data`);
11687
+ if (inside(other.to, move.to)) {
11688
+ const incoming = join5(other.from, relative(other.to, move.to));
11689
+ if (present(incoming)) throw new Error(`${move.to}: destination conflicts with incoming legacy data`);
11690
+ for (let parent = dirname3(incoming); parent !== other.from; parent = dirname3(parent)) {
11691
+ if (present(parent) && (!lstatSync2(parent).isDirectory() || lstatSync2(parent).isSymbolicLink())) throw new Error(`${parent}: unsafe incoming destination parent`);
11692
+ }
11693
+ }
11694
+ }
11695
+ }
11696
+ const links = [];
11697
+ const removals = [];
11698
+ const ownedIgnores = /* @__PURE__ */ new Set([
11699
+ join5(legacy, "alignment-packs", ".gitignore"),
11700
+ join5(legacy, "design-reviews", ".gitignore"),
11701
+ join5(root, ".decisions", ".gitignore"),
11702
+ join5(legacy, DECISION_LOGS_DIR, ".gitignore")
11703
+ ]);
11704
+ const visited = /* @__PURE__ */ new Set();
11705
+ const inspect = (path) => {
11706
+ if (visited.has(path)) return;
11707
+ visited.add(path);
11708
+ const stat = lstatSync2(path);
11709
+ if (stat.isSymbolicLink()) {
11710
+ const before = readlinkSync(path);
11711
+ const target = mapPath(resolve2(dirname3(path), before));
11712
+ const after = isAbsolute(before) ? target : relative(dirname3(mapPath(path)), target) || ".";
11713
+ if (before !== after) links.push({ path: mapPath(path), before, after });
11714
+ } else if (stat.isDirectory()) {
11715
+ for (const name of readdirSync3(path)) inspect(join5(path, name));
11716
+ } else if (ownedIgnores.has(path) && stat.isFile()) {
11717
+ const text2 = readFileSync4(path, "utf8");
11718
+ const normalized = text2.replace(/\r\n/g, "\n");
11719
+ if (normalized === "*\n" || normalized === "*" || normalized === "# Written by acstack. Delete this file and set workflow.alignment_packs.commit_generated_packs: true to commit packs.\n*\n") {
11720
+ removals.push({ path: mapPath(path), text: text2, mode: stat.mode });
11721
+ }
11722
+ }
11723
+ };
11724
+ for (const move of moves) inspect(move.from);
11725
+ const undo = [];
11726
+ const makeParents = (path) => {
11727
+ if (present(path)) return;
11728
+ makeParents(dirname3(path));
11729
+ mkdirSync3(path);
11730
+ undo.push(() => rmdirSync(path));
11731
+ };
11732
+ try {
11733
+ for (const move of moves) {
11734
+ const from = present(move.from) ? move.from : join5(canonical, relative(legacy, move.from));
11735
+ makeParents(dirname3(move.to));
11736
+ if (present(move.to)) throw new Error(`${move.to}: migration destination appeared during migration`);
11737
+ renameSync2(from, move.to);
11738
+ undo.push(() => renameSync2(move.to, from));
11739
+ }
11740
+ for (const link of links) {
11741
+ unlinkSync(link.path);
11742
+ try {
11743
+ symlinkSync2(link.after, link.path);
11744
+ } catch (error) {
11745
+ symlinkSync2(link.before, link.path);
11746
+ throw error;
11747
+ }
11748
+ undo.push(() => {
11749
+ unlinkSync(link.path);
11750
+ symlinkSync2(link.before, link.path);
11751
+ });
11752
+ }
11753
+ for (const removal of removals) {
11754
+ unlinkSync(removal.path);
11755
+ undo.push(() => replaceFileAtomically(removal.path, removal.text, (temp) => chmodSync(temp, removal.mode)));
11756
+ }
11757
+ return { moved: moves.map((move) => relative(root, move.to)) };
11758
+ } catch (error) {
11759
+ const failures = [];
11760
+ for (const rollback of undo.reverse()) {
11761
+ try {
11762
+ rollback();
11763
+ } catch (failure) {
11764
+ failures.push(String(failure));
11765
+ }
11766
+ }
11767
+ if (failures.length) throw new Error(`${String(error)}; rollback incomplete: ${failures.join("; ")}`);
11768
+ throw error;
11769
+ }
11770
+ }
11771
+ function git2(cwd, args) {
11772
+ const result = spawnSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11773
+ if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim()}`);
11774
+ return result.stdout;
11775
+ }
11776
+ function requireLocation(from) {
11777
+ const location = locateProject(from);
11778
+ if (!location) throw new Error("Not in a Git repository; ACStack keys project data by repository.");
11779
+ return location;
11780
+ }
11781
+ function moveTree(from, to) {
11782
+ mkdirSync3(dirname3(to), { recursive: true });
11783
+ try {
11784
+ renameSync2(from, to);
11785
+ } catch (error) {
11786
+ if (error.code !== "EXDEV") throw error;
11787
+ const temp = `${to}.acstack-${process.pid}`;
11788
+ try {
11789
+ cpSync(from, temp, { recursive: true, verbatimSymlinks: true, errorOnExist: true, force: false });
11790
+ } catch (copyError) {
11791
+ rmSync2(temp, { recursive: true, force: true });
11792
+ throw copyError;
11793
+ }
11794
+ renameSync2(temp, to);
11795
+ rmSync2(from, { recursive: true });
11796
+ }
11797
+ }
11798
+ function safeStranded(location) {
11799
+ try {
11800
+ return strandedWorktrees(location);
11801
+ } catch (error) {
11802
+ return [`(could not list worktrees: ${error.message})`];
11803
+ }
11804
+ }
11805
+ function requireRealDirectory(path) {
11806
+ if (!lstatSync2(path).isDirectory()) throw new Error(`${path} is not a real folder; move its contents into a folder named ${ACSTACK_DIR} first.`);
11807
+ }
11808
+ function strandedWorktrees(location) {
11809
+ const paths = git2(location.main, ["worktree", "list", "--porcelain"]).split("\n").filter((line) => line.startsWith("worktree ")).map((line) => line.slice("worktree ".length));
11810
+ return paths.filter((path) => existsSync3(path) && realpathSync2(path) !== realpathSync2(location.main) && git2(path, ["status", "--porcelain", "--ignored", "--untracked-files=all", "--", ACSTACK_DIR]).trim() !== "");
11811
+ }
11812
+ function legacyGlobalMoves(home = ACSTACK_HOME) {
11813
+ const config = join5(process.env.XDG_CONFIG_HOME || join5(homedir2(), ".config"), "acstack");
11814
+ const data = join5(process.env.XDG_DATA_HOME || join5(homedir2(), ".local", "share"), "acstack");
11815
+ const children = (dir, to) => existsSync3(dir) ? readdirSync3(dir).map((name) => ({ from: join5(dir, name), to: join5(to, name) })) : [];
11816
+ return [...children(config, home), ...children(data, join5(home, "cache"))];
11817
+ }
11818
+ function legacyWarnings(from = process.cwd()) {
11819
+ const warnings = legacyGlobalMoves().length ? ["ACStack settings are still in ~/.config/acstack or ~/.local/share/acstack."] : [];
11820
+ const location = locateProject(from);
11821
+ if (location && !present(location.entry) && !hasTrackedProject(location) && (present(join5(location.main, ACSTACK_DIR)) || hasLegacyProjectData(location.main))) warnings.push(`${location.main} still holds its project data in the repository.`);
11822
+ return warnings.length ? [...warnings, "ACStack no longer reads these locations. Stop agents and review servers, back up, then run `acstack project-data migrate` from each repository."] : [];
11823
+ }
11824
+ function migrateProjectData(from) {
11825
+ const location = requireLocation(from);
11826
+ const global2 = legacyGlobalMoves();
11827
+ for (const move of global2) if (present(move.to)) throw new Error(`${move.to}: migration destination already exists; refusing to overwrite`);
11828
+ const top = checkoutRoot(from);
11829
+ const mainData = join5(location.main, ACSTACK_DIR);
11830
+ const repoData = !present(mainData) && top && top !== location.main && present(join5(top, ACSTACK_DIR)) ? join5(top, ACSTACK_DIR) : mainData;
11831
+ const source = dirname3(repoData);
11832
+ if (present(repoData)) requireRealDirectory(repoData);
11833
+ const oldLayout = !present(repoData) && (hasLegacyProjectData(location.main) || ["CONTEXT.md", join5("docs", "adr"), ".decisions"].some((name) => present(join5(location.main, name))));
11834
+ const entry = lstatSync2(location.entry, { throwIfNoEntry: false });
11835
+ if ((present(repoData) || oldLayout) && entry && !(entry.isSymbolicLink() && existsSync3(location.entry) && realpathSync2(location.entry) === realpathSync2(repoData))) {
11836
+ throw new Error(`${location.entry} already exists; refusing to merge ${repoData} into it. Choose with \`acstack project-data mode\`.`);
11837
+ }
11838
+ const moved = [];
11839
+ if (oldLayout) moved.push(...migrateOldLayout(location.main).moved.map((path) => join5(location.main, path)));
11840
+ if (present(repoData) && !entry) {
11841
+ const tracked = git2(source, ["ls-files", "--", ACSTACK_DIR]).trim() !== "";
11842
+ if (tracked && source !== location.main) throw new Error(`${repoData} is committed on this worktree's branch only. Merge it into the main checkout's branch, then run migrate there.`);
11843
+ if (tracked) {
11844
+ linkTracked(location);
11845
+ } else {
11846
+ moveTree(repoData, location.entry);
11847
+ recordProject(location);
11848
+ removeManagedGitignoreBlock(source);
11849
+ }
11850
+ moved.push(location.entry);
11851
+ }
11852
+ for (const move of global2) {
11853
+ moveTree(move.from, move.to);
11854
+ moved.push(move.to);
11855
+ }
11856
+ for (const dir of new Set(global2.map((move) => dirname3(move.from)))) if (readdirSync3(dir).length === 0) rmdirSync(dir);
11857
+ return { moved, stranded: safeStranded(location) };
11858
+ }
11859
+ function setProjectMode(from, mode) {
11860
+ const location = requireLocation(from);
11861
+ const repoData = join5(location.main, ACSTACK_DIR);
11862
+ const entry = lstatSync2(location.entry, { throwIfNoEntry: false });
11863
+ if (mode === "tracked") {
11864
+ if (entry?.isSymbolicLink()) throw new Error("This project is already in tracked mode.");
11865
+ if (!entry) throw new Error(`No project folder at ${location.entry}.`);
11866
+ if (location.bare) throw new Error("A bare repository has no main checkout to hold .acstack/.");
11867
+ if (present(repoData)) throw new Error(`${repoData} already exists; refusing to overwrite it.`);
11868
+ rmSync2(join5(location.entry, PROJECT_JSON_FILE), { force: true });
11869
+ moveTree(location.entry, repoData);
11870
+ try {
11871
+ linkTracked(location);
11872
+ } catch (error) {
11873
+ moveTree(repoData, location.entry);
11874
+ recordProject(location);
11875
+ throw error;
11876
+ }
11877
+ } else {
11878
+ if (entry && !entry.isSymbolicLink()) throw new Error("This project is already in global mode.");
11879
+ if (!lstatSync2(repoData, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`No ${ACSTACK_DIR}/ in ${location.main} to move.`);
11880
+ if (entry) unlinkSync(location.entry);
11881
+ try {
11882
+ moveTree(repoData, location.entry);
11883
+ } catch (error) {
11884
+ if (entry) linkTracked(location);
11885
+ throw error;
11886
+ }
11887
+ recordProject(location);
11888
+ }
11889
+ return { stranded: safeStranded(location) };
11890
+ }
11891
+ function relinkProject(from, name) {
11892
+ const location = requireLocation(from);
11893
+ if (present(location.entry)) throw new Error(`${location.entry} already exists; nothing to relink.`);
11894
+ const orphans = findOrphans(location);
11895
+ const orphan = orphans.find((path) => basename2(path) === name);
11896
+ if (!orphan) throw new Error(orphans.length ? `${name} is not an orphaned entry for this repository. Candidates: ${orphans.map((path) => basename2(path)).join(", ")}.` : "No orphaned project entry found for this repository.");
11897
+ if (lstatSync2(orphan).isSymbolicLink()) {
11898
+ if (!hasTrackedProject(location)) throw new Error(`${orphan} is a tracked-mode link, but ${location.main} commits no ${ACSTACK_DIR}/project.yaml.`);
11899
+ linkTracked(location);
11900
+ unlinkSync(orphan);
11901
+ } else {
11902
+ renameSync2(orphan, location.entry);
11903
+ recordProject(location);
11904
+ }
11905
+ return { from: orphan, to: location.entry };
11906
+ }
11907
+ var import_yaml2, BEGIN, END, KNOWLEDGE;
11908
+ var init_project_data = __esm({
11909
+ "src/lib/project-data.ts"() {
11910
+ "use strict";
11911
+ import_yaml2 = __toESM(require_dist(), 1);
11912
+ init_atomic_file();
11913
+ init_project();
11914
+ init_constants();
11915
+ BEGIN = "# >>> acstack project-data >>>";
11916
+ END = "# <<< acstack project-data <<<";
11917
+ KNOWLEDGE = "VERIFICATION-KNOWLEDGE.md";
11918
+ }
11919
+ });
11920
+
11743
11921
  // src/commands/project-data.ts
11744
11922
  var project_data_exports = {};
11745
11923
  __export(project_data_exports, {
11746
11924
  register: () => register
11747
11925
  });
11926
+ import { existsSync as existsSync4, realpathSync as realpathSync3 } from "node:fs";
11927
+ function reportStranded(stranded) {
11928
+ for (const path of stranded) console.error(`${path} still holds its own .acstack/ changes, which ACStack no longer reads. Move what you need, then delete it.`);
11929
+ }
11748
11930
  function register(program3) {
11749
- const command = program3.command("project-data").description("Keep project data local or explicitly migrate legacy data");
11750
- command.command("ensure").description("Reconcile the root Git ignore policy when .acstack exists, without requiring project.yaml").action(() => {
11931
+ const command = program3.command("project-data").description("Locate, migrate, and choose the mode of this repository's ACStack project data");
11932
+ command.command("path").description("Print the project folder for this repository, whether or not it exists yet; warn about legacy data").action(() => {
11751
11933
  try {
11752
- ensureProjectGitignore(findProjectRoot() ?? process.cwd());
11934
+ for (const line of legacyWarnings()) console.error(`acstack: ${line}`);
11935
+ const location = locateProject();
11936
+ if (!location) throw new Error("Not in a Git repository; ACStack keys project data by repository.");
11937
+ for (const line of orphanWarnings(location)) console.error(`acstack: ${line}`);
11938
+ findProjectRoot();
11939
+ console.log(existsSync4(location.entry) ? realpathSync3(location.entry) : location.entry);
11753
11940
  } catch (error) {
11754
11941
  fail(error.message);
11755
11942
  }
11756
11943
  });
11757
- command.command("migrate").description("Migrate legacy data in the current root; stop review servers and agents and back up first").action(() => {
11944
+ command.command("migrate").description("Move legacy global files and this repository's .acstack/ into ~/.acstack; stop review servers and agents and back up first").action(() => {
11758
11945
  try {
11759
11946
  const result = migrateProjectData(process.cwd());
11760
11947
  for (const path of result.moved) console.log(`Moved project data to ${path}`);
11948
+ reportStranded(result.stranded);
11949
+ } catch (error) {
11950
+ fail(error.message);
11951
+ }
11952
+ });
11953
+ command.command("mode <mode>").description("tracked: move the project folder into the main checkout's .acstack/ for committing; global: move it back").action((mode) => {
11954
+ try {
11955
+ if (mode !== "tracked" && mode !== "global") throw new Error("Mode must be tracked or global.");
11956
+ reportStranded(setProjectMode(process.cwd(), mode).stranded);
11957
+ console.log(mode === "tracked" ? "Project data is now in .acstack/ of the main checkout. Commit it to share it." : "Project data is now in the project folder. Commit the removal of .acstack/ to stop tracking it.");
11958
+ } catch (error) {
11959
+ fail(error.message);
11960
+ }
11961
+ });
11962
+ command.command("relink <entry>").description("Give the named project entry, left by this repository before it moved, this repository's key").action((entry) => {
11963
+ try {
11964
+ const result = relinkProject(process.cwd(), entry);
11965
+ console.log(`Relinked ${result.from} as ${result.to}`);
11761
11966
  } catch (error) {
11762
11967
  fail(error.message);
11763
11968
  }
@@ -11777,7 +11982,7 @@ var list_exports = {};
11777
11982
  __export(list_exports, {
11778
11983
  register: () => register2
11779
11984
  });
11780
- import { existsSync as existsSync4, readdirSync as readdirSync3, statSync } from "node:fs";
11985
+ import { existsSync as existsSync5, readdirSync as readdirSync4, statSync } from "node:fs";
11781
11986
  import { join as join6 } from "node:path";
11782
11987
  function register2(program3) {
11783
11988
  program3.command("list").description("List active changes or specs").option("--specs", "list specs instead of changes").option("--changes", "list changes (default)").option("--json", "output as JSON").option("--sort <order>", "sort order: name or recent", "recent").action(
@@ -11786,19 +11991,19 @@ function register2(program3) {
11786
11991
  const items = [];
11787
11992
  if (showSpecs) {
11788
11993
  for (const name of listSpecs(root)) {
11789
- const specDir = join6(root, ACSTACK_DIR, SPECS_DIR, name);
11994
+ const specDir = join6(root, SPECS_DIR, name);
11790
11995
  const specFile = join6(specDir, "spec.md");
11791
11996
  items.push({
11792
11997
  name,
11793
11998
  type: "spec",
11794
11999
  path: specDir,
11795
- modified: existsSync4(specFile) ? statSync(specFile).mtime.toISOString().split("T")[0] : void 0
12000
+ modified: existsSync5(specFile) ? statSync(specFile).mtime.toISOString().split("T")[0] : void 0
11796
12001
  });
11797
12002
  }
11798
12003
  } else {
11799
12004
  for (const name of listChanges(root)) {
11800
- const changeDir = join6(root, ACSTACK_DIR, CHANGES_DIR, name);
11801
- const artifacts = existsSync4(changeDir) ? readdirSync3(changeDir).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", "")) : [];
12005
+ const changeDir = join6(root, CHANGES_DIR, name);
12006
+ const artifacts = existsSync5(changeDir) ? readdirSync4(changeDir).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", "")) : [];
11802
12007
  items.push({
11803
12008
  name,
11804
12009
  type: "change",
@@ -11847,7 +12052,7 @@ var show_exports = {};
11847
12052
  __export(show_exports, {
11848
12053
  register: () => register3
11849
12054
  });
11850
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4 } from "node:fs";
12055
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5 } from "node:fs";
11851
12056
  import { join as join7 } from "node:path";
11852
12057
  function register3(program3) {
11853
12058
  program3.command("show").description("Display a change or spec").argument("[name]", "change or spec name").option("--json", "output as JSON").option("--type <type>", "force type: change or spec").action(
@@ -11856,17 +12061,17 @@ function register3(program3) {
11856
12061
  console.error(source_default.red("Provide a change or spec name."));
11857
12062
  process.exit(1);
11858
12063
  }
11859
- const changeDir = join7(root, ACSTACK_DIR, CHANGES_DIR, name);
11860
- const specDir = join7(root, ACSTACK_DIR, SPECS_DIR, name);
12064
+ const changeDir = join7(root, CHANGES_DIR, name);
12065
+ const specDir = join7(root, SPECS_DIR, name);
11861
12066
  let type;
11862
12067
  let targetDir;
11863
- if (opts.type === "spec" || !opts.type && !existsSync5(changeDir) && existsSync5(specDir)) {
12068
+ if (opts.type === "spec" || !opts.type && !existsSync6(changeDir) && existsSync6(specDir)) {
11864
12069
  type = "spec";
11865
12070
  targetDir = specDir;
11866
- } else if (existsSync5(changeDir)) {
12071
+ } else if (existsSync6(changeDir)) {
11867
12072
  type = "change";
11868
12073
  targetDir = changeDir;
11869
- } else if (existsSync5(specDir)) {
12074
+ } else if (existsSync6(specDir)) {
11870
12075
  type = "spec";
11871
12076
  targetDir = specDir;
11872
12077
  } else {
@@ -11875,11 +12080,11 @@ function register3(program3) {
11875
12080
  }
11876
12081
  if (type === "spec") {
11877
12082
  const specFile = join7(targetDir, "spec.md");
11878
- if (!existsSync5(specFile)) {
12083
+ if (!existsSync6(specFile)) {
11879
12084
  console.error(source_default.red(`Spec '${name}' has no spec.md file.`));
11880
12085
  process.exit(1);
11881
12086
  }
11882
- const content = readFileSync4(specFile, "utf-8");
12087
+ const content = readFileSync5(specFile, "utf-8");
11883
12088
  if (opts.json) {
11884
12089
  console.log(JSON.stringify({ name, type: "spec", content }, null, 2));
11885
12090
  } else {
@@ -11887,10 +12092,10 @@ function register3(program3) {
11887
12092
  }
11888
12093
  return;
11889
12094
  }
11890
- const files = readdirSync4(targetDir).filter((f) => f.endsWith(".md"));
12095
+ const files = readdirSync5(targetDir).filter((f) => f.endsWith(".md"));
11891
12096
  const artifacts = {};
11892
12097
  for (const file of files) {
11893
- artifacts[file.replace(".md", "")] = readFileSync4(join7(targetDir, file), "utf-8");
12098
+ artifacts[file.replace(".md", "")] = readFileSync5(join7(targetDir, file), "utf-8");
11894
12099
  }
11895
12100
  if (opts.json) {
11896
12101
  console.log(JSON.stringify({ name, type: "change", artifacts }, null, 2));
@@ -11918,7 +12123,7 @@ var archive_exports = {};
11918
12123
  __export(archive_exports, {
11919
12124
  register: () => register4
11920
12125
  });
11921
- import { existsSync as existsSync6, cpSync, rmSync as rmSync2 } from "node:fs";
12126
+ import { existsSync as existsSync7, cpSync as cpSync2, rmSync as rmSync3 } from "node:fs";
11922
12127
  import { join as join8 } from "node:path";
11923
12128
  function register4(program3) {
11924
12129
  program3.command("archive").description("Archive a completed change").argument("[name]", "change name").option("-y, --yes", "skip confirmation").option("--skip-specs", "skip living spec updates").action(
@@ -11936,19 +12141,19 @@ function register4(program3) {
11936
12141
  console.log(source_default.dim("\nRun: acstack archive <name>"));
11937
12142
  return;
11938
12143
  }
11939
- const changeDir = join8(root, ACSTACK_DIR, CHANGES_DIR, name);
11940
- if (!existsSync6(changeDir)) {
12144
+ const changeDir = join8(root, CHANGES_DIR, name);
12145
+ if (!existsSync7(changeDir)) {
11941
12146
  console.error(source_default.red(`Change '${name}' not found.`));
11942
12147
  process.exit(1);
11943
12148
  }
11944
12149
  const archiveName = `${formatDate()}-${name}`;
11945
- const archiveDir = join8(root, ACSTACK_DIR, CHANGES_DIR, ARCHIVE_DIR, archiveName);
11946
- ensureDir(join8(root, ACSTACK_DIR, CHANGES_DIR, ARCHIVE_DIR));
11947
- cpSync(changeDir, archiveDir, { recursive: true });
11948
- rmSync2(changeDir, { recursive: true });
12150
+ const archiveDir = join8(root, CHANGES_DIR, ARCHIVE_DIR, archiveName);
12151
+ ensureDir(join8(root, CHANGES_DIR, ARCHIVE_DIR));
12152
+ cpSync2(changeDir, archiveDir, { recursive: true });
12153
+ rmSync3(changeDir, { recursive: true });
11949
12154
  console.log(source_default.green(`Archived: ${name}`));
11950
- console.log(` From: ${ACSTACK_DIR}/${CHANGES_DIR}/${name}/`);
11951
- console.log(` To: ${ACSTACK_DIR}/${CHANGES_DIR}/${ARCHIVE_DIR}/${archiveName}/`);
12155
+ console.log(` From: ${CHANGES_DIR}/${name}/`);
12156
+ console.log(` To: ${CHANGES_DIR}/${ARCHIVE_DIR}/${archiveName}/`);
11952
12157
  if (!opts.skipSpecs) {
11953
12158
  console.log(source_default.dim("\nRemember to update living specs if needed."));
11954
12159
  }
@@ -11965,7 +12170,7 @@ var init_archive = __esm({
11965
12170
  });
11966
12171
 
11967
12172
  // src/lib/global-config.ts
11968
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
12173
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
11969
12174
  import { dirname as dirname4 } from "node:path";
11970
12175
  function parseBooleanWord(key2, raw) {
11971
12176
  const normalized = raw.toLowerCase();
@@ -12003,8 +12208,8 @@ function assertSettableKey(key2) {
12003
12208
  assertReadableKey(key2);
12004
12209
  }
12005
12210
  function readGlobalConfig(file) {
12006
- if (!existsSync7(file)) return {};
12007
- const raw = readFileSync5(file, "utf-8");
12211
+ if (!existsSync8(file)) return {};
12212
+ const raw = readFileSync6(file, "utf-8");
12008
12213
  try {
12009
12214
  return JSON.parse(raw);
12010
12215
  } catch (err) {
@@ -12012,8 +12217,8 @@ function readGlobalConfig(file) {
12012
12217
  }
12013
12218
  }
12014
12219
  function writeGlobalConfig(config, file) {
12015
- mkdirSync3(dirname4(file), { recursive: true });
12016
- writeFileSync2(file, JSON.stringify(config, null, 2) + "\n", "utf-8");
12220
+ mkdirSync4(dirname4(file), { recursive: true });
12221
+ writeFileSync3(file, JSON.stringify(config, null, 2) + "\n", "utf-8");
12017
12222
  }
12018
12223
  function parseValue(key2, raw) {
12019
12224
  if (key2 !== "host" && !key2.startsWith("host.")) {
@@ -12318,13 +12523,13 @@ import {
12318
12523
  accessSync,
12319
12524
  chmodSync as chmodSync2,
12320
12525
  constants as fsConstants,
12321
- existsSync as existsSync8,
12322
- readFileSync as readFileSync6,
12323
- readdirSync as readdirSync5,
12324
- realpathSync as realpathSync2,
12526
+ existsSync as existsSync9,
12527
+ readFileSync as readFileSync7,
12528
+ readdirSync as readdirSync6,
12529
+ realpathSync as realpathSync4,
12325
12530
  statSync as statSync2
12326
12531
  } from "node:fs";
12327
- import { basename, dirname as dirname5, join as join9 } from "node:path";
12532
+ import { basename as basename3, dirname as dirname5, join as join9 } from "node:path";
12328
12533
  import { isDeepStrictEqual } from "node:util";
12329
12534
  function projectKeySpec(key2) {
12330
12535
  return PROJECT_KEYS[key2];
@@ -12333,8 +12538,8 @@ function profileNames() {
12333
12538
  const pluginRoot = findPluginRoot();
12334
12539
  if (!pluginRoot) return [];
12335
12540
  const dir = join9(pluginRoot, PROFILES_DIR);
12336
- if (!existsSync8(dir)) return [];
12337
- return readdirSync5(dir).filter((name) => name.endsWith(".yaml")).map((name) => basename(name, ".yaml")).sort();
12541
+ if (!existsSync9(dir)) return [];
12542
+ return readdirSync6(dir).filter((name) => name.endsWith(".yaml")).map((name) => basename3(name, ".yaml")).sort();
12338
12543
  }
12339
12544
  function parseEnum(key2, raw, values) {
12340
12545
  if (values.includes(raw)) return raw;
@@ -12451,10 +12656,10 @@ function fsReason(err) {
12451
12656
  return err instanceof Error ? err.message.split("\n")[0] : String(err);
12452
12657
  }
12453
12658
  function readSource(file) {
12454
- if (!existsSync8(file)) return "";
12659
+ if (!existsSync9(file)) return "";
12455
12660
  let bytes;
12456
12661
  try {
12457
- bytes = readFileSync6(file);
12662
+ bytes = readFileSync7(file);
12458
12663
  } catch (err) {
12459
12664
  throw new ConfigFileError(`Could not read ${file}: ${fsReason(err)}`);
12460
12665
  }
@@ -12467,8 +12672,8 @@ function readSource(file) {
12467
12672
  }
12468
12673
  }
12469
12674
  function replaceFile(file, text2) {
12470
- const present2 = existsSync8(file);
12471
- const target = present2 ? realpathSync2(file) : file;
12675
+ const present2 = existsSync9(file);
12676
+ const target = present2 ? realpathSync4(file) : file;
12472
12677
  const existing = present2 ? statSync2(target) : null;
12473
12678
  if (existing) accessSync(target, fsConstants.W_OK);
12474
12679
  const directory = dirname5(target);
@@ -16864,7 +17069,7 @@ var init_zod = __esm({
16864
17069
  });
16865
17070
 
16866
17071
  // src/lib/nodes.ts
16867
- import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync6 } from "node:fs";
17072
+ import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync7 } from "node:fs";
16868
17073
  import { join as join10 } from "node:path";
16869
17074
  function oneOf(values) {
16870
17075
  return `one of: ${values.join(", ")}`;
@@ -16884,7 +17089,7 @@ function bareId(reference) {
16884
17089
  return reference.trim().replace(/^#/, "");
16885
17090
  }
16886
17091
  function mapDir(root, map) {
16887
- return join10(root, ACSTACK_DIR, MAPS_DIR, map);
17092
+ return join10(root, MAPS_DIR, map);
16888
17093
  }
16889
17094
  function slugify(title) {
16890
17095
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "node";
@@ -16960,7 +17165,7 @@ function frontmatterProblem(issue) {
16960
17165
  return `${issue.path.join(".")} ${issue.message}`;
16961
17166
  }
16962
17167
  function parseNodeFile(dir, file) {
16963
- const raw = readFileSync7(join10(dir, file), "utf-8").replace(/\r\n/g, "\n");
17168
+ const raw = readFileSync8(join10(dir, file), "utf-8").replace(/\r\n/g, "\n");
16964
17169
  const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
16965
17170
  if (!match) {
16966
17171
  throw new Error(`${file}: missing frontmatter`);
@@ -17004,8 +17209,8 @@ function parseNodeFile(dir, file) {
17004
17209
  }
17005
17210
  function loadNodes(root, map) {
17006
17211
  const dir = mapDir(root, map);
17007
- if (!existsSync9(dir)) return [];
17008
- const nodes = readdirSync6(dir).filter((f) => /^\d+.*\.md$/.test(f)).map((f) => parseNodeFile(dir, f)).sort((a, b) => compareIds(a.id, b.id));
17212
+ if (!existsSync10(dir)) return [];
17213
+ const nodes = readdirSync7(dir).filter((f) => /^\d+.*\.md$/.test(f)).map((f) => parseNodeFile(dir, f)).sort((a, b) => compareIds(a.id, b.id));
17009
17214
  const seen = /* @__PURE__ */ new Map();
17010
17215
  for (const node of nodes) {
17011
17216
  const other = seen.get(node.id);
@@ -17978,7 +18183,7 @@ var node_exports = {};
17978
18183
  __export(node_exports, {
17979
18184
  register: () => register5
17980
18185
  });
17981
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
18186
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
17982
18187
  import { join as join11 } from "node:path";
17983
18188
  function parseEnum2(flag, values) {
17984
18189
  return (value) => {
@@ -18004,7 +18209,7 @@ function readBody(body, bodyFile) {
18004
18209
  }
18005
18210
  return piped;
18006
18211
  }
18007
- return readFileSync8(bodyFile, "utf-8");
18212
+ return readFileSync9(bodyFile, "utf-8");
18008
18213
  }
18009
18214
  function refuseGithubMap(root, map, stdinRoute) {
18010
18215
  const tracked = getProjectKey(projectFile(root), TRACKER_KEY);
@@ -18014,7 +18219,7 @@ function refuseGithubMap(root, map, stdinRoute) {
18014
18219
  }
18015
18220
  function storedNodes(root, map) {
18016
18221
  const dir = mapDir(root, map);
18017
- if (!existsSync10(dir)) {
18222
+ if (!existsSync11(dir)) {
18018
18223
  fail(`Map '${map}' does not exist - there is no ${dir} directory.`);
18019
18224
  }
18020
18225
  return loadNodes(root, map);
@@ -18094,7 +18299,7 @@ function registerRead(nodeCmd) {
18094
18299
  if (opts.json) {
18095
18300
  console.log(JSON.stringify({ ...toJson(node), body: node.body }, null, 2));
18096
18301
  } else {
18097
- const text2 = readFileSync8(join11(mapDir(root, opts.map), node.file), "utf-8");
18302
+ const text2 = readFileSync9(join11(mapDir(root, opts.map), node.file), "utf-8");
18098
18303
  console.log(opts.visuals ? text2 : stripDiagrams(text2));
18099
18304
  }
18100
18305
  })
@@ -18284,12 +18489,12 @@ var init_node = __esm({
18284
18489
  });
18285
18490
 
18286
18491
  // src/lib/claude-plugins.ts
18287
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
18492
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
18288
18493
  import { join as join12 } from "node:path";
18289
18494
  function readJson(path) {
18290
- if (!existsSync11(path)) return void 0;
18495
+ if (!existsSync12(path)) return void 0;
18291
18496
  try {
18292
- return JSON.parse(readFileSync9(path, "utf-8"));
18497
+ return JSON.parse(readFileSync10(path, "utf-8"));
18293
18498
  } catch {
18294
18499
  return void 0;
18295
18500
  }
@@ -18371,7 +18576,7 @@ var init_impeccable = __esm({
18371
18576
  });
18372
18577
 
18373
18578
  // src/lib/host-probe.ts
18374
- import { spawnSync } from "node:child_process";
18579
+ import { spawnSync as spawnSync3 } from "node:child_process";
18375
18580
  import { accessSync as accessSync2, constants } from "node:fs";
18376
18581
  import { request } from "node:http";
18377
18582
  import { delimiter, join as join14 } from "node:path";
@@ -18390,7 +18595,7 @@ function firstBinaryOnPath(binaries) {
18390
18595
  return binaries.find(binaryOnPath);
18391
18596
  }
18392
18597
  function runCommand(binary, args) {
18393
- const result = spawnSync(binary, [...args], {
18598
+ const result = spawnSync3(binary, [...args], {
18394
18599
  // In order: no standard input, capture standard output, discard standard error.
18395
18600
  stdio: ["ignore", "pipe", "ignore"],
18396
18601
  encoding: "utf-8",
@@ -18432,8 +18637,8 @@ var init_host_probe = __esm({
18432
18637
  });
18433
18638
 
18434
18639
  // src/commands/config-check.ts
18435
- import { existsSync as existsSync12, readFileSync as readFileSync10, statSync as statSync3 } from "node:fs";
18436
- import { homedir as homedir2 } from "node:os";
18640
+ import { existsSync as existsSync13, readFileSync as readFileSync11, statSync as statSync3 } from "node:fs";
18641
+ import { homedir as homedir3 } from "node:os";
18437
18642
  import { join as join15 } from "node:path";
18438
18643
  function checkRequiredAxes(report, config, project) {
18439
18644
  report.heading("Required host axes are set");
@@ -18584,9 +18789,9 @@ function firstLineOf(err) {
18584
18789
  return (err instanceof Error ? err.message : String(err)).split("\n")[0];
18585
18790
  }
18586
18791
  function readJsonFile(path) {
18587
- if (!existsSync12(path)) return { status: "missing" };
18792
+ if (!existsSync13(path)) return { status: "missing" };
18588
18793
  try {
18589
- return { status: "read", value: JSON.parse(readFileSync10(path, "utf-8")) };
18794
+ return { status: "read", value: JSON.parse(readFileSync11(path, "utf-8")) };
18590
18795
  } catch (err) {
18591
18796
  return { status: "unreadable", detail: firstLineOf(err) };
18592
18797
  }
@@ -18602,7 +18807,7 @@ function checkDomainMap(report, root, specSync) {
18602
18807
  const id = "domain-map";
18603
18808
  const consequence = "spec sync then skips silently and the living specs stop being updated";
18604
18809
  const path = domainMapFile(root);
18605
- if (!existsSync12(path)) {
18810
+ if (!existsSync13(path)) {
18606
18811
  report.line(
18607
18812
  specSync ? "fail" : "info",
18608
18813
  id,
@@ -18674,8 +18879,8 @@ function checkAgentBrowserJson(report, root, expected) {
18674
18879
  }
18675
18880
  function checkVerificationKnowledge(report, root) {
18676
18881
  const id = "verification-knowledge";
18677
- const relative7 = VERIFICATION_KNOWLEDGE_FILE;
18678
- if (!isFile(join15(root, relative7))) {
18882
+ const relative7 = join15(root, VERIFICATION_KNOWLEDGE_FILE);
18883
+ if (!isFile(relative7)) {
18679
18884
  report.line(
18680
18885
  "fail",
18681
18886
  id,
@@ -18741,7 +18946,7 @@ function settingsFiles(root) {
18741
18946
  },
18742
18947
  {
18743
18948
  label: join15("~", CLAUDE_DIR, CLAUDE_SETTINGS_FILE),
18744
- path: join15(homedir2(), CLAUDE_DIR, CLAUDE_SETTINGS_FILE)
18949
+ path: join15(homedir3(), CLAUDE_DIR, CLAUDE_SETTINGS_FILE)
18745
18950
  }
18746
18951
  ];
18747
18952
  }
@@ -18930,7 +19135,7 @@ __export(config_exports, {
18930
19135
  register: () => register6
18931
19136
  });
18932
19137
  function reportUserError(err) {
18933
- if (err instanceof ConfigValidationError || err instanceof ConfigFileError) {
19138
+ if (err instanceof ConfigValidationError || err instanceof ConfigFileError || err instanceof ProjectDataError) {
18934
19139
  console.error(source_default.red(err.message));
18935
19140
  process.exit(1);
18936
19141
  }
@@ -18971,7 +19176,6 @@ function loadProject() {
18971
19176
  workflow: projectWorkflow(void 0)
18972
19177
  };
18973
19178
  }
18974
- assertCanonicalProject(root);
18975
19179
  const yaml = readYaml(projectFile(root));
18976
19180
  return {
18977
19181
  root,
@@ -19047,7 +19251,6 @@ function projectFileFor(key2, use) {
19047
19251
  );
19048
19252
  }
19049
19253
  }
19050
- assertCanonicalProject(root);
19051
19254
  return projectFile(root);
19052
19255
  }
19053
19256
  function setProjectFileKey(key2, value) {
@@ -19114,7 +19317,6 @@ function printProjectKeys(root) {
19114
19317
  function listConfig(asJson) {
19115
19318
  const config = readGlobalConfig(GLOBAL_CONFIG_FILE);
19116
19319
  const root = findProjectRoot();
19117
- if (root) assertCanonicalProject(root);
19118
19320
  if (asJson) {
19119
19321
  const project = root ? listProjectKeys(projectFile(root)) : null;
19120
19322
  console.log(JSON.stringify({ ...config, project }, null, 2));
@@ -19165,7 +19367,7 @@ async function checkConfig(asJson) {
19165
19367
  checkOptionalAxes(report, config);
19166
19368
  checkProjectFiles(report, loaded);
19167
19369
  checkImpeccable(report);
19168
- checkOutputStyle(report, loaded.root ?? process.cwd());
19370
+ checkOutputStyle(report, checkoutRoot() ?? process.cwd());
19169
19371
  report.finish(asJson);
19170
19372
  }
19171
19373
  function reportBrowserMode(asJson) {
@@ -19241,7 +19443,6 @@ var UNKNOWN_KEY_MARK;
19241
19443
  var init_config = __esm({
19242
19444
  "src/commands/config.ts"() {
19243
19445
  "use strict";
19244
- init_project_data();
19245
19446
  init_source();
19246
19447
  init_constants();
19247
19448
  init_global_config();
@@ -19380,25 +19581,25 @@ var init_alignment_packs = __esm({
19380
19581
  });
19381
19582
 
19382
19583
  // src/lib/design-doc.ts
19383
- import { readFileSync as readFileSync11, realpathSync as realpathSync3, statSync as statSync4 } from "node:fs";
19584
+ import { readFileSync as readFileSync12, realpathSync as realpathSync5, statSync as statSync4 } from "node:fs";
19384
19585
  import { dirname as dirname6, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "node:path";
19385
- import { createHash } from "node:crypto";
19586
+ import { createHash as createHash2 } from "node:crypto";
19386
19587
  function readBounded(file) {
19387
19588
  const stat = statSync4(file);
19388
19589
  if (!stat.isFile() || stat.size > 262144) throw new Error(`${file}: file type or size exceeds the 256 KiB limit`);
19389
- const bytes = readFileSync11(file);
19590
+ const bytes = readFileSync12(file);
19390
19591
  if (bytes.length > 262144) throw new Error(`${file}: file size exceeds the 256 KiB limit`);
19391
19592
  return bytes;
19392
19593
  }
19393
19594
  function childPath(root, name) {
19394
19595
  if (isAbsolute2(name) || name.split(/[\\/]/).includes("..")) throw new Error(`Invalid design path: ${name}`);
19395
- const path = realpathSync3(resolve3(root, name));
19596
+ const path = realpathSync5(resolve3(root, name));
19396
19597
  const rel = relative2(root, path);
19397
19598
  if (rel.split(/[\\/]/)[0] === ".." || isAbsolute2(rel)) throw new Error(`Design path escapes source directory: ${name}`);
19398
19599
  return path;
19399
19600
  }
19400
19601
  function loadDesignDoc(entryPath) {
19401
- const path = realpathSync3(entryPath);
19602
+ const path = realpathSync5(entryPath);
19402
19603
  const root = dirname6(path);
19403
19604
  const captured = /* @__PURE__ */ new Map();
19404
19605
  let totalBytes = 0;
@@ -19427,7 +19628,7 @@ function loadDesignDoc(entryPath) {
19427
19628
  return { ...element, file, detailText: readDetail(element.detail) };
19428
19629
  });
19429
19630
  const detailText = readDetail(entry.detail);
19430
- const sourceRevision = createHash("sha256").update(JSON.stringify([...captured].sort(([a], [b]) => a.localeCompare(b)).map(([name, bytes]) => [name, bytes.toString("base64")]))).digest("hex");
19631
+ const sourceRevision = createHash2("sha256").update(JSON.stringify([...captured].sort(([a], [b]) => a.localeCompare(b)).map(([name, bytes]) => [name, bytes.toString("base64")]))).digest("hex");
19431
19632
  const doc = { ...entry, items, detailText, sourceRevision };
19432
19633
  const ids = /* @__PURE__ */ new Set([entry.id]);
19433
19634
  for (const item of items) {
@@ -19499,7 +19700,7 @@ function designDocToPack(doc, orientation = "top-to-bottom") {
19499
19700
  const edges = [...new Map(doc.items.flatMap((item) => item.relationships).map((edge) => [edge.id, edge])).values()];
19500
19701
  const content = { ...doc };
19501
19702
  delete content.sourceRevision;
19502
- const version = createHash("sha256").update(JSON.stringify(content)).digest("hex").slice(0, 16);
19703
+ const version = createHash2("sha256").update(JSON.stringify(content)).digest("hex").slice(0, 16);
19503
19704
  return AlignmentPackSchema.parse({
19504
19705
  documentKind: "design",
19505
19706
  packVersion: `${doc.id}@${version}`,
@@ -19666,21 +19867,21 @@ var init_alignment_pack_renderer = __esm({
19666
19867
 
19667
19868
  // src/lib/design-map.ts
19668
19869
  import { execFileSync } from "node:child_process";
19669
- import { realpathSync as realpathSync4 } from "node:fs";
19870
+ import { realpathSync as realpathSync6 } from "node:fs";
19670
19871
  import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve4, win32 } from "node:path";
19671
- import { createHash as createHash2 } from "node:crypto";
19872
+ import { createHash as createHash3 } from "node:crypto";
19672
19873
  function designMapInputs(root, doc, entry) {
19673
19874
  let nodes = [];
19674
19875
  if (doc.planning) {
19675
19876
  const { tracker, map } = doc.planning;
19676
19877
  if (tracker === "files") {
19677
- const base = realpathSync4(mapDir(root, "."));
19678
- const path = realpathSync4(mapDir(root, map));
19679
- const rel2 = relative3(base, path);
19680
- if (!rel2 || isAbsolute3(rel2) || rel2.split(/[\\/]/).includes("..")) throw new Error("Invalid bound map path");
19878
+ const base = realpathSync6(mapDir(root, "."));
19879
+ const path = realpathSync6(mapDir(root, map));
19880
+ const rel = relative3(base, path);
19881
+ if (!rel || isAbsolute3(rel) || rel.split(/[\\/]/).includes("..")) throw new Error("Invalid bound map path");
19681
19882
  nodes = loadNodes(root, map);
19682
19883
  } else {
19683
- const raw = execFileSync("gh", ["issue", "list", "--state", "all", "--limit", "1000", "--json", "number,title,body,state,stateReason,labels,url"], { cwd: root, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
19884
+ const raw = execFileSync("gh", ["issue", "list", "--state", "all", "--limit", "1000", "--json", "number,title,body,state,stateReason,labels,url"], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
19684
19885
  try {
19685
19886
  const issues = JSON.parse(raw);
19686
19887
  if (Array.isArray(issues) && issues.length >= 1e3) throw new Error("GitHub issue list may be truncated at the 1000-issue limit; map completeness cannot be verified");
@@ -19697,10 +19898,18 @@ function designMapInputs(root, doc, entry) {
19697
19898
  if (references.length !== 1) throw new Error("Bound map root needs exactly one Design entry reference");
19698
19899
  const reference = references[0][1].trim();
19699
19900
  if (!reference || isAbsolute3(reference) || win32.isAbsolute(reference)) throw new Error("Invalid bound map Design entry path");
19700
- const project = realpathSync4(root);
19701
- const target = realpathSync4(resolve4(root, reference));
19702
- const rel = relative3(project, target);
19703
- if (!rel || isAbsolute3(rel) || rel.split(/[\\/]/).includes("..") || target !== realpathSync4(entry)) throw new Error("Bound map Design entry does not match the authored entry");
19901
+ const bases = [root, checkoutRoot()].filter((base) => base !== null).map((base) => realpathSync6(base));
19902
+ const within = bases.some((base) => {
19903
+ let target;
19904
+ try {
19905
+ target = realpathSync6(resolve4(base, reference));
19906
+ } catch {
19907
+ return false;
19908
+ }
19909
+ const rel = relative3(base, target);
19910
+ return Boolean(rel) && !isAbsolute3(rel) && !rel.split(/[\\/]/).includes("..") && target === realpathSync6(entry);
19911
+ });
19912
+ if (!within) throw new Error("Bound map Design entry does not match the authored entry");
19704
19913
  }
19705
19914
  const byId = new Map(nodes.map((node) => [node.id, node]));
19706
19915
  const targets = /* @__PURE__ */ new Set([doc.id, ...doc.items.map((item) => item.id), ...doc.items.flatMap((item) => item.relationships.map((edge) => edge.id)), ...doc.concerns.map((concern) => concern.id)]);
@@ -19738,7 +19947,8 @@ var init_design_map = __esm({
19738
19947
  init_zod();
19739
19948
  init_nodes();
19740
19949
  init_github_issues();
19741
- digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
19950
+ init_project();
19951
+ digest = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
19742
19952
  ContributionSchema = external_exports.object({
19743
19953
  id: external_exports.string().min(1),
19744
19954
  designId: external_exports.string().min(1),
@@ -19845,28 +20055,28 @@ var init_review_server = __esm({
19845
20055
 
19846
20056
  // src/lib/design-review.ts
19847
20057
  import { randomBytes as randomBytes3 } from "node:crypto";
19848
- import { existsSync as existsSync13, lstatSync as lstatSync3, mkdirSync as mkdirSync4, opendirSync, readFileSync as readFileSync12, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
19849
- import { join as join16, resolve as resolve5, dirname as dirname7, basename as basename2 } from "node:path";
20058
+ import { existsSync as existsSync14, lstatSync as lstatSync3, mkdirSync as mkdirSync5, opendirSync, readFileSync as readFileSync13, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
20059
+ import { join as join16, resolve as resolve5, dirname as dirname7, basename as basename4 } from "node:path";
19850
20060
  function readDesignReviewHistory(root, identity2, path) {
19851
20061
  const canonical = resolve5(path);
19852
- const base = join16(root, ACSTACK_DIR, "design-reviews", identity2);
19853
- if (basename2(canonical) !== "review.json" || !/^[a-f0-9]{16}$/.test(basename2(dirname7(canonical))) || dirname7(dirname7(canonical)) !== base) throw new Error("Not canonical history for this design");
19854
- for (const directory of [join16(root, ACSTACK_DIR), join16(root, ACSTACK_DIR, "design-reviews"), base, dirname7(canonical)]) {
20062
+ const base = join16(root, "design-reviews", identity2);
20063
+ if (basename4(canonical) !== "review.json" || !/^[a-f0-9]{16}$/.test(basename4(dirname7(canonical))) || dirname7(dirname7(canonical)) !== base) throw new Error("Not canonical history for this design");
20064
+ for (const directory of [root, join16(root, "design-reviews"), base, dirname7(canonical)]) {
19855
20065
  if (lstatSync3(directory).isSymbolicLink() || !lstatSync3(directory).isDirectory()) throw new Error("Invalid review history directory");
19856
20066
  }
19857
20067
  const stat = lstatSync3(canonical);
19858
20068
  if (!stat.isFile() || stat.size > 1e6) throw new Error("Invalid review history file");
19859
20069
  let snapshot;
19860
20070
  try {
19861
- snapshot = StoredReviewSchema.parse(JSON.parse(readFileSync12(canonical, "utf8")));
20071
+ snapshot = StoredReviewSchema.parse(JSON.parse(readFileSync13(canonical, "utf8")));
19862
20072
  } catch (error) {
19863
20073
  throw new Error(`Cannot read design review history ${canonical}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
19864
20074
  }
19865
- if (snapshot.packVersion !== `${identity2}@${basename2(dirname7(canonical))}` || snapshot.annotations.some((annotation) => annotation.packVersion !== snapshot.packVersion)) throw new Error("Review history identity mismatch");
20075
+ if (snapshot.packVersion !== `${identity2}@${basename4(dirname7(canonical))}` || snapshot.annotations.some((annotation) => annotation.packVersion !== snapshot.packVersion)) throw new Error("Review history identity mismatch");
19866
20076
  return snapshot;
19867
20077
  }
19868
20078
  function readDesignReviewHistories(root, identity2) {
19869
- const base = join16(root, ACSTACK_DIR, "design-reviews", identity2);
20079
+ const base = join16(root, "design-reviews", identity2);
19870
20080
  const histories = /* @__PURE__ */ new Map();
19871
20081
  const directory = opendirSync(base);
19872
20082
  let entries = 0;
@@ -19889,11 +20099,11 @@ function atomicWrite(path, text2) {
19889
20099
  const temporary = `${path}.${randomBytes3(16).toString("hex")}.tmp`;
19890
20100
  let created = false;
19891
20101
  try {
19892
- writeFileSync3(temporary, text2, { flag: "wx", mode: 384 });
20102
+ writeFileSync4(temporary, text2, { flag: "wx", mode: 384 });
19893
20103
  created = true;
19894
20104
  renameSync3(temporary, path);
19895
20105
  } finally {
19896
- if (created) rmSync3(temporary, { force: true });
20106
+ if (created) rmSync4(temporary, { force: true });
19897
20107
  }
19898
20108
  }
19899
20109
  function openDesignReview(root, pack, initialOperation) {
@@ -19901,12 +20111,11 @@ function openDesignReview(root, pack, initialOperation) {
19901
20111
  const version = pack.source.map.version;
19902
20112
  if (!identity2 || !/^[a-zA-Z][a-zA-Z0-9_-]{0,79}$/.test(identity2) || !/^[a-f0-9]{16}$/.test(version)) throw new Error("Invalid design review identity");
19903
20113
  let directory = root;
19904
- for (const part of [ACSTACK_DIR, "design-reviews", identity2, version]) {
20114
+ for (const part of ["design-reviews", identity2, version]) {
19905
20115
  directory = join16(directory, part);
19906
- if (!existsSync13(directory)) mkdirSync4(directory);
20116
+ if (!existsSync14(directory)) mkdirSync5(directory);
19907
20117
  if (lstatSync3(directory).isSymbolicLink() || !lstatSync3(directory).isDirectory()) throw new Error("Design review directory must be a real directory");
19908
20118
  }
19909
- if (!initialOperation) ensureProjectGitignore(root);
19910
20119
  const canonical = join16(directory, "review.json");
19911
20120
  const validate = (snapshot2) => {
19912
20121
  if (snapshot2.packVersion !== pack.packVersion) throw new Error("Design review pack version mismatch");
@@ -19919,20 +20128,20 @@ function openDesignReview(root, pack, initialOperation) {
19919
20128
  const lock = join16(directory, ".writer-lock");
19920
20129
  const acquire = () => {
19921
20130
  try {
19922
- mkdirSync4(lock);
20131
+ mkdirSync5(lock);
19923
20132
  } catch (error) {
19924
20133
  if (error.code === "EEXIST") throw new ReviewHttpError(409, `Another writer holds ${lock}. If a writer crashed, remove that directory before retrying.`);
19925
20134
  throw error;
19926
20135
  }
19927
20136
  };
19928
20137
  const release = () => {
19929
- rmSync3(lock, { recursive: true });
20138
+ rmSync4(lock, { recursive: true });
19930
20139
  };
19931
20140
  const readSnapshot = () => {
19932
20141
  try {
19933
20142
  const stat = lstatSync3(canonical);
19934
20143
  if (!stat.isFile() || stat.size > 1e6) throw new Error("Invalid review file");
19935
- const saved = StoredReviewSchema.parse(JSON.parse(readFileSync12(canonical, "utf8")));
20144
+ const saved = StoredReviewSchema.parse(JSON.parse(readFileSync13(canonical, "utf8")));
19936
20145
  validate(saved);
19937
20146
  return saved;
19938
20147
  } catch (error) {
@@ -20007,8 +20216,6 @@ var init_design_review = __esm({
20007
20216
  "use strict";
20008
20217
  init_zod();
20009
20218
  init_alignment_packs();
20010
- init_constants();
20011
- init_project_data();
20012
20219
  init_review_server();
20013
20220
  DesignReviewSnapshotSchema = external_exports.object({
20014
20221
  packVersion: external_exports.string().min(1),
@@ -20190,7 +20397,7 @@ var init_design_lifecycle = __esm({
20190
20397
 
20191
20398
  // src/lib/design-migration.ts
20192
20399
  import { constants as constants2, closeSync, fstatSync, lstatSync as lstatSync4, openSync, opendirSync as opendirSync2, readSync as readSync2 } from "node:fs";
20193
- import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute4, join as join17, relative as relative5, resolve as resolve7 } from "node:path";
20400
+ import { basename as basename5, dirname as dirname8, isAbsolute as isAbsolute4, join as join17, relative as relative5, resolve as resolve7 } from "node:path";
20194
20401
  import { TextDecoder as TextDecoder2 } from "node:util";
20195
20402
  function noLinks(path) {
20196
20403
  for (let cursor = resolve7(path); ; cursor = dirname8(cursor)) {
@@ -20249,25 +20456,12 @@ function legacyComments(annotations) {
20249
20456
  count: Math.max(1, group.ids.size + [...group.anonymousAnchors].filter((anchor) => !group.identifiedAnchors.has(anchor)).length)
20250
20457
  }]));
20251
20458
  }
20252
- function mainCheckout(root) {
20253
- let git = join17(root, ".git");
20254
- noLinks(git);
20255
- if (!lstatSync4(git).isDirectory()) {
20256
- const pointer = readBounded2(git, 4096).trim().match(/^gitdir: (.+)$/);
20257
- if (!pointer) throw new Error("Invalid legacy decision-log Git directory");
20258
- git = resolve7(root, pointer[1]);
20259
- noLinks(git);
20260
- }
20261
- if (lstatSync4(join17(git, "commondir"), { throwIfNoEntry: false })) git = resolve7(git, readBounded2(join17(git, "commondir"), 4096).trim());
20262
- noLinks(git);
20263
- return dirname8(git);
20264
- }
20265
20459
  function migrateDesignReview(root, entry, options) {
20266
20460
  const pack = designDocToPack(loadDesignDoc(entry));
20267
20461
  const directory = resolve7(options.legacyDir);
20268
- const base = join17(root, ".acstack", "alignment-packs");
20462
+ const base = join17(root, "alignment-packs");
20269
20463
  const rel = relative5(base, directory);
20270
- if (!rel || isAbsolute4(rel) || rel.split(/[\\/]/).includes("..")) throw new Error("Legacy directory must be under .acstack/alignment-packs");
20464
+ if (!rel || isAbsolute4(rel) || rel.split(/[\\/]/).includes("..")) throw new Error("Legacy directory must be under alignment-packs in the project folder");
20271
20465
  noLinks(directory);
20272
20466
  if (!lstatSync4(directory).isDirectory()) throw new Error("Invalid legacy directory");
20273
20467
  const sources = [];
@@ -20304,7 +20498,7 @@ function migrateDesignReview(root, entry, options) {
20304
20498
  const scripts = [...content.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi)].filter((match) => new RegExp(`\\bid=["']${PACK_DATA_ID}["']`).test(match[1]));
20305
20499
  if (scripts.length !== 1 || scripts[0][1].trim() !== `id="${PACK_DATA_ID}" type="application/json"`) throw new Error(`Invalid legacy pack JSON metadata: ${name}`);
20306
20500
  const saved = parseJson(`Legacy pack ${path}`, scripts[0][2], AlignmentPackSchema);
20307
- if (saved.source.map.id !== basename3(directory) || saved.packVersion !== `${saved.source.map.id}@${saved.source.map.version}`) throw new Error("Legacy pack directory identity mismatch");
20501
+ if (saved.source.map.id !== basename5(directory) || saved.packVersion !== `${saved.source.map.id}@${saved.source.map.version}`) throw new Error("Legacy pack directory identity mismatch");
20308
20502
  if (saved.reviewState.approval && saved.reviewState.approval.packVersion !== saved.packVersion) throw new Error("Legacy pack approval version mismatch");
20309
20503
  const previous = versions.get(saved.packVersion);
20310
20504
  if (previous && digest(previous) !== digest(saved)) throw new Error("Conflicting duplicate legacy pack version");
@@ -20336,13 +20530,11 @@ function migrateDesignReview(root, entry, options) {
20336
20530
  ids.set(identity2, fingerprint2);
20337
20531
  }
20338
20532
  }
20339
- const main = mainCheckout(root);
20340
- assertCanonicalProject(main);
20341
- const decisionPath = join17(main, ".acstack", "decision-logs", `${basename3(directory)}.tsv`);
20342
- for (const path of [join17(main, ".acstack"), dirname8(decisionPath), decisionPath]) {
20533
+ const decisionPath = join17(root, "decision-logs", `${basename5(directory)}.tsv`);
20534
+ for (const path of [dirname8(decisionPath), decisionPath]) {
20343
20535
  if (lstatSync4(path, { throwIfNoEntry: false })) noLinks(path);
20344
20536
  }
20345
- const decisionLog = lstatSync4(decisionPath, { throwIfNoEntry: false }) ? { path: `.acstack/decision-logs/${basename3(directory)}.tsv`, content: readBounded2(decisionPath, FILE_LIMIT) } : void 0;
20537
+ const decisionLog = lstatSync4(decisionPath, { throwIfNoEntry: false }) ? { path: `decision-logs/${basename5(directory)}.tsv`, content: readBounded2(decisionPath, FILE_LIMIT) } : void 0;
20346
20538
  const history = { directory: relative5(root, directory).split("\\").join("/"), sources: sources.sort((a, b) => a.path.localeCompare(b.path)), packs: [...versions.values()], annotations, mapping, feedback, decisionLog };
20347
20539
  const fingerprint = digest(history);
20348
20540
  const apply = (snapshot) => {
@@ -20373,7 +20565,6 @@ var init_design_migration = __esm({
20373
20565
  init_design_doc();
20374
20566
  init_design_map();
20375
20567
  init_design_review();
20376
- init_project_data();
20377
20568
  FILE_LIMIT = 1e6;
20378
20569
  HTML_LIMIT = 8e6;
20379
20570
  TOTAL_LIMIT = 16e6;
@@ -20387,13 +20578,13 @@ __export(design_doc_exports, {
20387
20578
  generateDesignDoc: () => generateDesignDoc,
20388
20579
  register: () => register7
20389
20580
  });
20390
- import { readFileSync as readFileSync13, statSync as statSync5, writeFileSync as writeFileSync4 } from "node:fs";
20581
+ import { readFileSync as readFileSync14, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
20391
20582
  import { resolve as resolve8 } from "node:path";
20392
20583
  function generateDesignDoc(entry, output, markdown, orientation = "top-to-bottom") {
20393
20584
  const doc = loadDesignDoc(entry);
20394
20585
  const html = renderAlignmentPackHtml(designDocToPack(doc, orientation));
20395
- writeFileSync4(resolve8(output), html, { flag: "wx" });
20396
- if (markdown) writeFileSync4(resolve8(markdown), renderDesignDocMarkdown(doc, orientation), { flag: "wx" });
20586
+ writeFileSync5(resolve8(output), html, { flag: "wx" });
20587
+ if (markdown) writeFileSync5(resolve8(markdown), renderDesignDocMarkdown(doc, orientation), { flag: "wx" });
20397
20588
  }
20398
20589
  function register7(program3) {
20399
20590
  const lifecycle = program3.command("design-review").description("Inspect and reconcile exact authored design revisions");
@@ -20407,7 +20598,7 @@ function register7(program3) {
20407
20598
  if (!statSync5(options.input).isFile() || statSync5(options.input).size > 1e6) throw new Error("Invalid reconciliation input");
20408
20599
  let input;
20409
20600
  try {
20410
- input = JSON.parse(readFileSync13(options.input, "utf8"));
20601
+ input = JSON.parse(readFileSync14(options.input, "utf8"));
20411
20602
  } catch (error) {
20412
20603
  throw new Error(`Invalid reconciliation input ${options.input}: ${error.message}`);
20413
20604
  }
@@ -20419,7 +20610,7 @@ function register7(program3) {
20419
20610
  const lifecycle2 = openDesignLifecycle(root, entry);
20420
20611
  const doc = lifecycle2.doc;
20421
20612
  const pack = designDocToPack(doc, options.orientation);
20422
- if (options.markdown) writeFileSync4(resolve8(options.markdown), renderDesignDocMarkdown(doc, options.orientation), { flag: "wx" });
20613
+ if (options.markdown) writeFileSync5(resolve8(options.markdown), renderDesignDocMarkdown(doc, options.orientation), { flag: "wx" });
20423
20614
  lifecycle2.status();
20424
20615
  const review = lifecycle2.review;
20425
20616
  const handoffPrompt = `Read the review JSON at ${JSON.stringify(review.reviewPath)}. Resolve feedback against the authored design entry ${JSON.stringify(resolve8(entry))} and update the authored source to address the comments. Explain unresolved decisions, run design-review status, explicitly reconcile the selected feedback and map contributions with design-review reconcile, and reopen the review with design-doc for approval of the resulting revision.`;
@@ -20472,7 +20663,12 @@ function impeccableJson(impeccable) {
20472
20663
  };
20473
20664
  }
20474
20665
  function runDesignGate(json) {
20475
- const root = findProjectRoot();
20666
+ let root = null;
20667
+ try {
20668
+ root = findProjectRoot();
20669
+ } catch (error) {
20670
+ fail(error.message);
20671
+ }
20476
20672
  const impeccable = findInstalledPlugin(IMPECCABLE_PLUGIN);
20477
20673
  const reasons = offReasons(root, impeccable);
20478
20674
  const on = reasons.length === 0;
@@ -20545,7 +20741,7 @@ var init_feedback = __esm({
20545
20741
  });
20546
20742
 
20547
20743
  // src/lib/ackfile.ts
20548
- import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
20744
+ import { existsSync as existsSync15, readFileSync as readFileSync15 } from "node:fs";
20549
20745
  function isAckDecision(value) {
20550
20746
  return typeof value === "string" && ACK_DECISIONS.includes(value);
20551
20747
  }
@@ -20597,7 +20793,7 @@ function writeAck(args) {
20597
20793
  `That is a sidecar path, not a handoff file. Pass ${file.slice(0, -ACK_SUFFIX.length)} instead.`
20598
20794
  );
20599
20795
  }
20600
- if (!existsSync14(file)) {
20796
+ if (!existsSync15(file)) {
20601
20797
  throw new Error(`No handoff file at ${file}. Check the path the handoff gave you.`);
20602
20798
  }
20603
20799
  const record2 = {
@@ -20613,7 +20809,7 @@ function writeAck(args) {
20613
20809
  function readAck(file) {
20614
20810
  let raw;
20615
20811
  try {
20616
- raw = readFileSync14(ackPath(file), "utf-8");
20812
+ raw = readFileSync15(ackPath(file), "utf-8");
20617
20813
  } catch (err) {
20618
20814
  if (err.code === "ENOENT") return null;
20619
20815
  throw err;
@@ -20645,11 +20841,11 @@ var init_ackfile = __esm({
20645
20841
  });
20646
20842
 
20647
20843
  // src/lib/ackwatch.ts
20648
- import { readFileSync as readFileSync15, statSync as statSync6 } from "node:fs";
20649
- import { homedir as homedir3 } from "node:os";
20650
- import { basename as basename4, join as join18 } from "node:path";
20844
+ import { readFileSync as readFileSync16, statSync as statSync6 } from "node:fs";
20845
+ import { homedir as homedir4 } from "node:os";
20846
+ import { basename as basename6, join as join18 } from "node:path";
20651
20847
  function transcriptPath(cwd, sessionId, projectsDir) {
20652
- const base = projectsDir ?? join18(homedir3(), ".claude", "projects");
20848
+ const base = projectsDir ?? join18(homedir4(), ".claude", "projects");
20653
20849
  return join18(base, cwd.replace(/[^a-zA-Z0-9]/g, "-"), `${sessionId}.jsonl`);
20654
20850
  }
20655
20851
  function parseAck(text2) {
@@ -20758,7 +20954,7 @@ function mentionsFile(input, file) {
20758
20954
  } catch {
20759
20955
  return false;
20760
20956
  }
20761
- const name = basename4(file);
20957
+ const name = basename6(file);
20762
20958
  return serialized.includes(file) || name.length > 0 && serialized.includes(name);
20763
20959
  }
20764
20960
  function isEngagement(record2, file) {
@@ -20831,7 +21027,7 @@ function analyze(lines, options) {
20831
21027
  }
20832
21028
  function readLines(path) {
20833
21029
  try {
20834
- return readFileSync15(path, "utf-8").split("\n");
21030
+ return readFileSync16(path, "utf-8").split("\n");
20835
21031
  } catch (err) {
20836
21032
  if (err.code === "ENOENT") return [];
20837
21033
  throw err;
@@ -24160,8 +24356,8 @@ var require_pattern = __commonJS({
24160
24356
  }
24161
24357
  exports.endsWithSlashGlobStar = endsWithSlashGlobStar;
24162
24358
  function isAffectDepthOfReadingPattern(pattern) {
24163
- const basename5 = path.basename(pattern);
24164
- return endsWithSlashGlobStar(pattern) || isStaticPattern(basename5);
24359
+ const basename7 = path.basename(pattern);
24360
+ return endsWithSlashGlobStar(pattern) || isStaticPattern(basename7);
24165
24361
  }
24166
24362
  exports.isAffectDepthOfReadingPattern = isAffectDepthOfReadingPattern;
24167
24363
  function expandPatternsWithBraceExpansion(patterns) {
@@ -27182,7 +27378,7 @@ __export(lint_prose_exports, {
27182
27378
  reportFile: () => reportFile,
27183
27379
  summarize: () => summarize
27184
27380
  });
27185
- import { existsSync as existsSync15, readFileSync as readFileSync16, statSync as statSync7 } from "node:fs";
27381
+ import { existsSync as existsSync16, readFileSync as readFileSync17, statSync as statSync7 } from "node:fs";
27186
27382
  import { join as join19, relative as relative6, resolve as resolve11 } from "node:path";
27187
27383
  function loadVocabulary() {
27188
27384
  const pluginRoot = findPluginRoot();
@@ -27220,7 +27416,7 @@ function collectFiles(paths, opts) {
27220
27416
  if (opts.all) files.push(...markdownIn(opts.root));
27221
27417
  for (const path of paths) {
27222
27418
  const full = resolve11(path);
27223
- if (!existsSync15(full)) {
27419
+ if (!existsSync16(full)) {
27224
27420
  missing.push(path);
27225
27421
  continue;
27226
27422
  }
@@ -27262,7 +27458,7 @@ function displayPath(root, file) {
27262
27458
  function reportFile(file, display, vocabulary) {
27263
27459
  let text2;
27264
27460
  try {
27265
- text2 = readFileSync16(file, "utf-8");
27461
+ text2 = readFileSync17(file, "utf-8");
27266
27462
  } catch {
27267
27463
  console.error(source_default.yellow(`Cannot read, skipping: ${display}`));
27268
27464
  return { path: display, findings: [], unreadable: true };
@@ -27304,7 +27500,7 @@ function printSummary(summary, missing) {
27304
27500
  }
27305
27501
  function register11(program3) {
27306
27502
  program3.command("lint-prose").description("Warn about prose that drifts from the writing standard").argument("[paths...]", "files or directories to lint").option("--all", "lint every .md file in the repository").action((paths, opts) => {
27307
- const root = findProjectRoot() ?? process.cwd();
27503
+ const root = locateProject()?.main ?? process.cwd();
27308
27504
  if (paths.length === 0 && !opts.all) {
27309
27505
  fail("Give one or more paths, or --all to lint the repository.");
27310
27506
  }
@@ -27352,7 +27548,7 @@ var init_lint_prose = __esm({
27352
27548
 
27353
27549
  // src/index.ts
27354
27550
  init_esm();
27355
- import { spawnSync as spawnSync2 } from "node:child_process";
27551
+ import { spawnSync as spawnSync4 } from "node:child_process";
27356
27552
 
27357
27553
  // src/lib/version.ts
27358
27554
  import { readFileSync } from "node:fs";
@@ -27381,15 +27577,7 @@ function resolveVersion(startDir) {
27381
27577
  }
27382
27578
 
27383
27579
  // src/index.ts
27384
- init_project_data();
27385
- init_project();
27386
27580
  init_utils();
27387
- try {
27388
- const root = findProjectRoot();
27389
- if (root && !(["project-data", "design-review"].includes(process.argv[2] ?? "") && process.argv[3] === "migrate")) ensureProjectGitignore(root);
27390
- } catch (error) {
27391
- fail(error.message);
27392
- }
27393
27581
  var program2 = new Command().name("acstack").description("CLI for the ACStack agentic coding stack").version(resolveVersion(import.meta.dirname));
27394
27582
  var commands = await Promise.all([
27395
27583
  Promise.resolve().then(() => (init_project_data2(), project_data_exports)),
@@ -27417,7 +27605,7 @@ var known = new Set(
27417
27605
  program2.commands.flatMap((c) => [c.name(), ...c.aliases()])
27418
27606
  );
27419
27607
  if (first && !first.startsWith("-") && !known.has(first)) {
27420
- const result = spawnSync2(`acstack-${first}`, process.argv.slice(3), {
27608
+ const result = spawnSync4(`acstack-${first}`, process.argv.slice(3), {
27421
27609
  stdio: "inherit"
27422
27610
  });
27423
27611
  const failedToSpawn = result.error !== void 0 && "code" in result.error;