cairn-ai 0.2.6 → 0.2.8

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 (65) hide show
  1. package/.agents/workflows/cairn-memory.md +2 -2
  2. package/.agents/workflows/cairn-plan.md +3 -3
  3. package/.agents/workflows/cairn-review.md +2 -2
  4. package/.agents/workflows/cairn-work.md +4 -4
  5. package/.claude/agents/explorer.md +1 -1
  6. package/.claude/agents/worker.md +1 -1
  7. package/.claude/commands/cairn-memory.md +1 -1
  8. package/.claude/commands/cairn-plan.md +1 -1
  9. package/.claude/commands/cairn-review.md +1 -1
  10. package/.claude/commands/cairn-work.md +1 -1
  11. package/.codex-plugin/plugin.json +1 -1
  12. package/README.de.md +3 -2
  13. package/README.es.md +3 -2
  14. package/README.fr.md +3 -2
  15. package/README.ja.md +3 -2
  16. package/README.ko.md +16 -8
  17. package/README.md +14 -8
  18. package/README.pt.md +3 -2
  19. package/README.zh.md +3 -2
  20. package/agents/worker.md +3 -1
  21. package/commands/cairn-install.md +2 -1
  22. package/commands/cairn-memory.md +2 -2
  23. package/commands/cairn-plan.md +3 -3
  24. package/commands/cairn-review.md +2 -2
  25. package/commands/cairn-work.md +4 -4
  26. package/docs/model-guidance/README.md +5 -5
  27. package/docs/model-guidance/claude.md +1 -1
  28. package/docs/model-guidance/codex.md +3 -3
  29. package/package.json +4 -2
  30. package/scripts/cairn-cleanup.mjs +12 -6
  31. package/scripts/cairn-goal-cli.mjs +239 -0
  32. package/scripts/cairn-goal-evidence.mjs +186 -0
  33. package/scripts/cairn-goal-operations-evidence.mjs +124 -0
  34. package/scripts/cairn-goal-operations-mutations.mjs +63 -0
  35. package/scripts/cairn-goal-operations.mjs +187 -0
  36. package/scripts/cairn-goal-policy.mjs +36 -0
  37. package/scripts/cairn-goal-state-validation.mjs +199 -0
  38. package/scripts/cairn-goal-state.mjs +124 -0
  39. package/scripts/cairn-goal.mjs +33 -1078
  40. package/scripts/cairn-lifecycle-commands.mjs +200 -0
  41. package/scripts/cairn-lifecycle-config.mjs +153 -0
  42. package/scripts/cairn-lifecycle-integrity.mjs +218 -0
  43. package/scripts/cairn-lifecycle-legacy.mjs +142 -0
  44. package/scripts/cairn-lifecycle-lock.mjs +205 -0
  45. package/scripts/cairn-lifecycle-mirror.mjs +91 -0
  46. package/scripts/cairn-lifecycle-staging.mjs +152 -0
  47. package/scripts/cairn-lifecycle-transaction-journal.mjs +210 -0
  48. package/scripts/cairn-lifecycle-transaction-targets.mjs +119 -0
  49. package/scripts/cairn-lifecycle-transaction.mjs +12 -0
  50. package/scripts/cairn-lifecycle.mjs +103 -1185
  51. package/scripts/cairn-paths.mjs +35 -5
  52. package/scripts/cairn-safe-fs.mjs +61 -35
  53. package/scripts/cairn-state-context.mjs +156 -0
  54. package/scripts/cairn-state-hook.mjs +26 -0
  55. package/scripts/cairn-state-init.mjs +137 -0
  56. package/scripts/cairn-state.mjs +32 -286
  57. package/scripts/cairn-toolcheck-detect.mjs +229 -0
  58. package/scripts/cairn-toolcheck-runtime.mjs +178 -0
  59. package/scripts/cairn-toolcheck.mjs +79 -424
  60. package/scripts/cairn.mjs +17 -3
  61. package/skills/cairn-memory/SKILL.md +3 -2
  62. package/skills/cairn-plan/SKILL.md +7 -4
  63. package/skills/cairn-review/SKILL.md +4 -4
  64. package/skills/cairn-work/SKILL.md +23 -7
  65. package/scripts/cairn-state.sh +0 -167
@@ -0,0 +1,124 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, readFile, rename, rm } from "node:fs/promises";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import { cairnHome, goalStateDirectory } from "./cairn-paths.mjs";
5
+ import { assertNoSymlinkComponents, safeMkdir, safeWriteFile, withStateLock } from "./cairn-safe-fs.mjs";
6
+ import { migrateState, validateState } from "./cairn-goal-state-validation.mjs";
7
+
8
+ export function goalStatePath(root = process.cwd()) {
9
+ return join(goalStateDirectory(root), "state.json");
10
+ }
11
+
12
+ export async function readGoalState({ root = process.cwd() } = {}) {
13
+ const state = await readCurrentGoalState(root);
14
+ if (state) return state;
15
+ // Read-only callers (hooks) must not create user-home state directories when there is nothing to migrate.
16
+ if (!(await legacyGoalStateExists(root))) return null;
17
+ return withGoalStateLock(root, () => readGoalStateUnlocked(root));
18
+ }
19
+
20
+ export function stopGuardPath(root = process.cwd()) {
21
+ return join(goalStateDirectory(root), "stop-guard.json");
22
+ }
23
+
24
+ export async function readStopGuard(root = process.cwd()) {
25
+ try {
26
+ const path = stopGuardPath(root);
27
+ await assertNoSymlinkComponents(cairnHome(), path);
28
+ const value = JSON.parse(await readFile(path, "utf8"));
29
+ return value && typeof value === "object" ? value : null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export async function writeStopGuard(root = process.cwd(), value) {
36
+ try {
37
+ await safeWriteFile(cairnHome(), stopGuardPath(root), `${JSON.stringify(value)}\n`, "utf8");
38
+ } catch {
39
+ // The guard only prevents no-progress continuation loops; failing to persist it must not break the stop hook.
40
+ }
41
+ }
42
+
43
+ async function legacyGoalStateExists(root) {
44
+ try {
45
+ await lstat(join(resolve(root), ".cairn", "state.json"));
46
+ return true;
47
+ } catch (error) {
48
+ if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return false;
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ export async function writeGoalState({ root = process.cwd(), state } = {}) {
54
+ return withGoalStateLock(root, () => writeGoalStateUnlocked({ root, state }));
55
+ }
56
+
57
+ export async function writeGoalStateUnlocked({ root = process.cwd(), state } = {}) {
58
+ const validated = validateState(state);
59
+ const path = goalStatePath(root);
60
+ await ensureGoalStateDirectory(root);
61
+ await assertNoSymlinkComponents(cairnHome(), path);
62
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
63
+ try {
64
+ await safeWriteFile(cairnHome(), temporaryPath, `${JSON.stringify(validated, null, 2)}\n`, "utf8");
65
+ await rename(temporaryPath, path);
66
+ } catch (error) {
67
+ await rm(temporaryPath, { force: true }).catch(() => {});
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ export async function readGoalStateUnlocked(root) {
73
+ return await readCurrentGoalState(root) ?? migrateLegacyGoalState(root);
74
+ }
75
+
76
+ async function readCurrentGoalState(root) {
77
+ try {
78
+ const path = goalStatePath(root);
79
+ await assertNoSymlinkComponents(cairnHome(), path);
80
+ return validateState(migrateState(JSON.parse(await readFile(path, "utf8"))));
81
+ } catch (error) {
82
+ if (error?.code === "ENOENT") return null;
83
+ if (error instanceof SyntaxError) throw new Error(`Cairn goal state is invalid JSON: ${error.message}`);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ async function migrateLegacyGoalState(root) {
89
+ const legacyPath = join(resolve(root), ".cairn", "state.json");
90
+ try {
91
+ await assertNoSymlinkComponents(root, legacyPath, { allowMissing: false });
92
+ const state = validateState(migrateState(JSON.parse(await readFile(legacyPath, "utf8"))));
93
+ await writeGoalStateUnlocked({ root, state });
94
+ await rm(legacyPath);
95
+ return state;
96
+ } catch (error) {
97
+ if (error?.code === "ENOENT") return null;
98
+ if (error instanceof SyntaxError) throw new Error(`Cairn goal state is invalid JSON: ${error.message}`);
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ export async function withGoalStateLock(root, operation) {
104
+ await ensureGoalStateDirectory(root);
105
+ return withStateLock(goalStateDirectory(root), operation);
106
+ }
107
+
108
+ async function ensureGoalStateDirectory(root) {
109
+ const home = cairnHome();
110
+ let existingAncestor = home;
111
+ while (true) {
112
+ try {
113
+ const stat = await lstat(existingAncestor);
114
+ if (!stat.isDirectory()) throw new Error(`Cairn home ancestor is not a directory: ${existingAncestor}`);
115
+ break;
116
+ } catch (error) {
117
+ if (error?.code !== "ENOENT") throw error;
118
+ const parent = dirname(existingAncestor);
119
+ if (parent === existingAncestor) throw error;
120
+ existingAncestor = parent;
121
+ }
122
+ }
123
+ await safeMkdir(existingAncestor, relative(existingAncestor, goalStateDirectory(root)));
124
+ }