cairn-ai 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +8 -13
- package/README.de.md +2 -0
- package/README.es.md +2 -0
- package/README.fr.md +2 -0
- package/README.ja.md +2 -0
- package/README.ko.md +3 -1
- package/README.md +3 -1
- package/README.pt.md +2 -0
- package/README.zh.md +2 -0
- package/agents/explorer.md +2 -0
- package/agents/worker.md +2 -0
- package/commands/cairn-uninstall.md +3 -1
- package/docs/model-guidance/README.md +10 -0
- package/docs/model-guidance/codex.md +2 -0
- package/package.json +1 -1
- package/scripts/cairn-goal.mjs +34 -8
- package/scripts/cairn-lifecycle.mjs +194 -40
- package/scripts/cairn-state.mjs +44 -23
- package/skills/cairn-plan/SKILL.md +4 -0
- package/skills/cairn-review/SKILL.md +2 -0
- package/skills/cairn-work/SKILL.md +4 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cairn",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Token-efficient agent harness with tool readiness for Codex, Claude Code, and Antigravity.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "WkNam"
|
|
@@ -21,18 +21,13 @@
|
|
|
21
21
|
"Tool Readiness"
|
|
22
22
|
],
|
|
23
23
|
"defaultPrompt": [
|
|
24
|
-
"Use Cairn
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"Implementation/continue requests authorize an active Cairn goal even without the word goal. After MEMORY.md, write an initial plan with triage-plan active; before exploration call update_plan, then create_goal, then start/attach the repository goal. Triage finalizes the repository and UI plans before implementation. Keep roadmap/current-task recovery context; consultation, explanation, and plan-only requests stay goal-free.",
|
|
32
|
-
"Use a test contract first: spend reasoning on requirements, invariants, boundaries, and failure modes; design executable tests before implementation; make the minimum implementation that passes. Treat tool exit codes and machine summaries as authoritative, and expand context only for failing tests.",
|
|
33
|
-
"Missing, failed, skipped, stale, or placeholder evidence never completes work. Advance a UI step only after the matching repository task passes bound evidence; workers report only assigned tasks; active is not complete.",
|
|
34
|
-
"Use goal verify -- <argv>, quiet state mutations, and concise success summaries. Run focused tests and one final full check. Inspect package lifecycle scripts before packaging: run normal npm pack --dry-run by default; never skip content-producing or unknown scripts with --ignore-scripts, and use it only for absent or proven content-neutral scripts while evidence is fresh.",
|
|
35
|
-
"If blocked, pause/block/cancel explicitly. Answer side questions briefly, then resume the current task unless asked to pause, stop, or switch."
|
|
24
|
+
"Use Cairn's repository-local MEMORY.md, PLAN.md, and persisted goal state as the recovery spine; every agent reads root MEMORY.md before planning, implementation, review, or delegation.",
|
|
25
|
+
"For non-trivial work, load the phase skill (cairn-plan, cairn-work, or cairn-review), then restore the active plan and current-task references before acting.",
|
|
26
|
+
"After compaction, restart, handoff, or delegation, restore MEMORY.md → phase skill → active plan → current task references; do not rely on conversation recall.",
|
|
27
|
+
"If state, skill, plan, or a required reference is missing, unreadable, or inconsistent, fail closed: do not edit, delegate, or complete work; report or block the discrepancy.",
|
|
28
|
+
"Missing, failed, skipped, stale, or placeholder evidence never completes work; advance UI state only after bound repository evidence passes.",
|
|
29
|
+
"Before external-state changes, run dry-run/check and bind the resulting evidence to the current task.",
|
|
30
|
+
"Answer side questions briefly, then resume the current task unless asked to pause, stop, or switch."
|
|
36
31
|
]
|
|
37
32
|
}
|
|
38
33
|
}
|
package/README.de.md
CHANGED
|
@@ -108,6 +108,8 @@ cairn toolcheck
|
|
|
108
108
|
|
|
109
109
|
Der custom marketplace lifecycle hält source und versioned runtime getrennt. Staged validation geht jedem commit voraus, das ownership manifest bindet alle managed digests, und ein Fehler führt zu reverse-order rollback. Modified oder unmanaged artifacts werden preserved und als conflict gemeldet. Cairn ändert nur eigene TOML sections und erzwingt keine öffentlichen feature/agent settings.
|
|
110
110
|
|
|
111
|
+
Clean uninstall entfernt den Cairn marketplace cache root nur, wenn alle managed scaffold directories leer sind; unmanaged Inhalte bleiben erhalten. Source repository und repository state, das globale `cairn-ai` package, package-manager caches sowie legacy backups/settings außerhalb des aktuellen ownership manifest werden nicht automatisch gelöscht.
|
|
112
|
+
|
|
111
113
|
Codex verwendet `skills/` und `commands/`. Claude Code verwendet gespiegelte commands und agent definitions unter `.claude/`. Antigravity verwendet `.agents/workflows` und globale skills mirrors.
|
|
112
114
|
|
|
113
115
|
## Antigravity Compatibility
|
package/README.es.md
CHANGED
|
@@ -108,6 +108,8 @@ cairn toolcheck
|
|
|
108
108
|
|
|
109
109
|
El custom marketplace lifecycle mantiene separados source y versioned runtime. Staged validation precede cada commit, el ownership manifest vincula todos los managed digests y un fallo aplica reverse-order rollback. Los artifacts modified o unmanaged se preservan y se reportan como conflict. Cairn modifica solo sus propias TOML sections y no fuerza feature/agent settings públicos.
|
|
110
110
|
|
|
111
|
+
Clean uninstall elimina el Cairn marketplace cache root solo cuando todos los managed scaffold directories están vacíos; cualquier contenido unmanaged se conserva. No elimina el source repository ni su state, el paquete global `cairn-ai`, los package-manager caches ni legacy backups/settings fuera del ownership manifest actual.
|
|
112
|
+
|
|
111
113
|
Codex usa `skills/` y `commands/`. Claude Code usa comandos reflejados y definiciones de agentes bajo `.claude/`. Antigravity usa `.agents/workflows` y mirrors globales de skills.
|
|
112
114
|
|
|
113
115
|
## Antigravity Compatibility
|
package/README.fr.md
CHANGED
|
@@ -108,6 +108,8 @@ cairn toolcheck
|
|
|
108
108
|
|
|
109
109
|
Le custom marketplace lifecycle sépare source et versioned runtime. Staged validation précède chaque commit, le ownership manifest lie tous les managed digests et un échec déclenche un reverse-order rollback. Les artifacts modified ou unmanaged sont preserved et signalés comme conflict. Cairn ne modifie que ses propres TOML sections et ne force aucun feature/agent setting public.
|
|
110
110
|
|
|
111
|
+
Clean uninstall supprime le Cairn marketplace cache root uniquement lorsque tous les managed scaffold directories sont vides ; tout contenu unmanaged est conservé. Il ne supprime ni le source repository et son state, ni le package global `cairn-ai`, les package-manager caches ou les legacy backups/settings hors du ownership manifest courant.
|
|
112
|
+
|
|
111
113
|
Codex utilise `skills/` et `commands/`. Claude Code utilise les commandes miroir et définitions d'agents sous `.claude/`. Antigravity utilise `.agents/workflows` et les global skills mirrors.
|
|
112
114
|
|
|
113
115
|
## Antigravity Compatibility
|
package/README.ja.md
CHANGED
|
@@ -108,6 +108,8 @@ cairn toolcheck
|
|
|
108
108
|
|
|
109
109
|
custom marketplace lifecycle は source と versioned runtime を分離します。各 commit 前に staged validation を行い、ownership manifest が全 managed digest を結び、失敗時は reverse-order rollback します。modified または unmanaged artifacts は preserve して conflict として報告します。Cairn-owned TOML sections だけを変更し、public feature/agent settings は強制しません。
|
|
110
110
|
|
|
111
|
+
Clean uninstall は、すべての managed scaffold directory が空の場合に限り Cairn marketplace cache root を削除し、unmanaged content は保持します。source repository と repository state、global `cairn-ai` package、package-manager cache、現在の ownership manifest 外の legacy backup/settings は自動削除しません。
|
|
112
|
+
|
|
111
113
|
Codex は `skills/` と `commands/` を使います。Claude Code は `.claude/` 配下の mirrored commands と agent definitions を使います。Antigravity は `.agents/workflows` と global skills mirrors を使います。
|
|
112
114
|
|
|
113
115
|
## Antigravity Compatibility
|
package/README.ko.md
CHANGED
|
@@ -103,7 +103,7 @@ cairn toolcheck
|
|
|
103
103
|
- `cairn install`: custom marketplace source, versioned Codex runtime cache, Cairn-owned config section, Claude Code mirror, 현재 Antigravity IDE/CLI skill 표면을 transaction으로 설치합니다.
|
|
104
104
|
- `cairn upgrade`: ownership manifest의 installed digest와 현재 값이 같은 항목만 교체하며, staged validation과 역순 rollback을 적용합니다.
|
|
105
105
|
- `cairn doctor`: ownership manifest, managed digest, 유효 Codex feature, 실제 plugin installed/enabled/version, mirror/runtime locator를 수정 없이 진단합니다.
|
|
106
|
-
- `cairn uninstall`: 수정되지 않은 ownership manifest 항목만
|
|
106
|
+
- `cairn uninstall`: 수정되지 않은 ownership manifest 항목만 제거·복원한 뒤 빈 managed cache scaffold를 정리합니다. modified/unmanaged target과 그 상위 디렉터리는 보존하고 commit 전 실패는 rollback합니다.
|
|
107
107
|
- `cairn toolcheck`: 저장소 스택과 필요한 LSP·검증 도구를 확인하며, 승인된 지원 installer만 실행합니다.
|
|
108
108
|
- `cairn goal ...`: 저장소의 영속 goal을 시작·조회·일시정지·재개·차단·취소·완료합니다. 기본 tool-bound 정책은 `goal verify -- <argv>`가 명령을 직접 실행해 성공 증거를 기록합니다. `goal receipt`는 기존 선언 증거를 가져오는 호환 명령입니다.
|
|
109
109
|
- `cairn-memory`: 도메인 지식을 탐색하고 `MEMORY.md`를 갱신합니다.
|
|
@@ -117,6 +117,8 @@ cairn toolcheck
|
|
|
117
117
|
|
|
118
118
|
`install`과 `upgrade`는 custom marketplace lifecycle을 유지하며 `codex plugin add`를 호출하지 않습니다. candidate를 staging에서 검증한 뒤 commit하고, 모든 managed destination과 digest를 ownership manifest에 기록하며 실패 시 완료된 phase를 역순 rollback합니다. 수정되지 않은 지원 legacy 설치만 release-integrity 검증 뒤 인수하고, 수정되거나 알 수 없는 artifact는 보존한 채 거부합니다. Cairn-owned TOML section만 편집하고 public feature/agent 설정은 강제하지 않습니다. lifecycle command는 교체 대상 cache copy가 아니라 게시/전역 package에서 실행해야 합니다.
|
|
119
119
|
|
|
120
|
+
Clean uninstall은 모든 managed scaffold가 비었을 때만 recursive 삭제 없이 빈 디렉터리 제거로 Cairn marketplace cache root를 정리합니다. 소스 저장소와 저장소의 `MEMORY.md`/`PLAN.md`/`.cairn` 상태, 전역 `cairn-ai` package, package-manager download cache, legacy backup, 현재 ownership manifest 밖의 legacy shared setting은 지우지 않으며 별도의 명시적 정리 판단이 필요합니다.
|
|
121
|
+
|
|
120
122
|
Codex는 `skills/`와 `commands/`를 사용합니다. Claude Code는 `.claude/` 아래의 mirror command와 agent definition을 사용합니다. Antigravity는 `.agents/workflows`와 global skills mirror를 사용합니다.
|
|
121
123
|
|
|
122
124
|
## Antigravity Compatibility
|
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ cairn toolcheck
|
|
|
111
111
|
- `cairn install`: transactionally installs the custom marketplace source, versioned Codex runtime cache, Cairn-owned config sections, Claude Code mirrors, and current Antigravity IDE/CLI skill surfaces.
|
|
112
112
|
- `cairn upgrade`: replaces only ownership-manifest entries whose installed digests still match, with staged validation and reverse-order rollback on failure.
|
|
113
113
|
- `cairn doctor`: validates the ownership manifest, managed digests, effective Codex features, actual installed/enabled/version plugin state, and mirror/runtime locators without repairing them.
|
|
114
|
-
- `cairn uninstall`: removes or restores only ownership-manifest entries that remain unmodified; modified or unmanaged targets are preserved
|
|
114
|
+
- `cairn uninstall`: removes or restores only ownership-manifest entries that remain unmodified, then prunes empty managed cache scaffolds; modified or unmanaged targets and their parent directories are preserved, and pre-commit failures roll back.
|
|
115
115
|
- `cairn toolcheck`: detects repository stacks and checks required LSP and verification tools; installation runs only for an approved supported installer.
|
|
116
116
|
- `cairn goal ...`: starts, inspects, pauses, resumes, blocks, cancels, and completes a persisted repository goal; the default tool-bound policy records successful evidence by executing `goal verify -- <argv>`. The legacy `goal receipt` command imports declared evidence only.
|
|
117
117
|
- `cairn-memory`: explores domain knowledge and updates `MEMORY.md`.
|
|
@@ -125,6 +125,8 @@ For token-efficient execution, Cairn puts reasoning into a focused executable te
|
|
|
125
125
|
|
|
126
126
|
Install and upgrade keep the custom marketplace lifecycle; they do not invoke `codex plugin add`. Candidates are staged and validated before commit, every managed destination and digest is recorded in an ownership manifest, and committed phases roll back in reverse order on failure. An exact unmodified supported legacy installation can be adopted after release-integrity validation; modified or unknown artifacts are preserved and rejected. Cairn edits only its own TOML sections and never forces public feature or agent settings. The source plugin manifest stays validator-friendly; only the installed cache copy gets a `hooks` field. Lifecycle commands must run from the published/global package, not from the cached copy that they replace.
|
|
127
127
|
|
|
128
|
+
Clean uninstall removes the Cairn marketplace cache root only when every managed scaffold is empty, using empty-directory removal rather than recursive deletion. It does not delete the source repository or repository `MEMORY.md`/`PLAN.md`/`.cairn` state, the global `cairn-ai` package, package-manager download caches, legacy backups, or legacy shared settings outside the current ownership manifest; those require a separate explicit cleanup decision.
|
|
129
|
+
|
|
128
130
|
Codex uses `skills/` and `commands/`. Claude Code uses mirrored commands and agent definitions under `.claude/`. Antigravity uses `.agents/workflows` and global skills mirrors.
|
|
129
131
|
|
|
130
132
|
## Antigravity Compatibility
|
package/README.pt.md
CHANGED
|
@@ -108,6 +108,8 @@ cairn toolcheck
|
|
|
108
108
|
|
|
109
109
|
O custom marketplace lifecycle separa source e versioned runtime. Staged validation precede cada commit, o ownership manifest vincula todos os managed digests e uma falha aciona reverse-order rollback. Artifacts modified ou unmanaged sao preserved e relatados como conflict. O Cairn altera apenas suas TOML sections e nao forca feature/agent settings publicos.
|
|
110
110
|
|
|
111
|
+
Clean uninstall remove o Cairn marketplace cache root somente quando todos os managed scaffold directories estao vazios; qualquer conteudo unmanaged e preservado. O source repository e seu state, o package global `cairn-ai`, package-manager caches e legacy backups/settings fora do ownership manifest atual nao sao removidos automaticamente.
|
|
112
|
+
|
|
111
113
|
Codex usa `skills/` e `commands/`. Claude Code usa comandos espelhados e agent definitions em `.claude/`. Antigravity usa `.agents/workflows` e global skills mirrors.
|
|
112
114
|
|
|
113
115
|
## Antigravity Compatibility
|
package/README.zh.md
CHANGED
|
@@ -108,6 +108,8 @@ cairn toolcheck
|
|
|
108
108
|
|
|
109
109
|
custom marketplace lifecycle 将 source 与 versioned runtime 分离。每次 commit 前执行 staged validation,ownership manifest 绑定全部 managed digests,失败时执行 reverse-order rollback。modified 或 unmanaged artifacts 会 preserve 并报告 conflict。Cairn 只修改自身 TOML sections,不强制 public feature/agent settings。
|
|
110
110
|
|
|
111
|
+
Clean uninstall 仅在所有 managed scaffold directory 均为空时删除 Cairn marketplace cache root,并保留任何 unmanaged content。它不会自动删除 source repository 及其 state、全局 `cairn-ai` package、package-manager cache,或当前 ownership manifest 之外的 legacy backup/settings。
|
|
112
|
+
|
|
111
113
|
Codex 使用 `skills/` 和 `commands/`。Claude Code 使用 `.claude/` 下的 mirrored commands 和 agent definitions。Antigravity 使用 `.agents/workflows` 和 global skills mirrors。
|
|
112
114
|
|
|
113
115
|
## Antigravity Compatibility
|
package/agents/explorer.md
CHANGED
|
@@ -10,6 +10,8 @@ Handle read-only discovery and verification tasks.
|
|
|
10
10
|
|
|
11
11
|
Before doing any assigned task, read the project-root `MEMORY.md`. Treat it as the domain knowledge and repository policy index, then read only the relevant `docs/memory/*.md` notes needed for the task.
|
|
12
12
|
|
|
13
|
+
At the start and after compaction, restart, or handoff, restore context in this order: root `MEMORY.md` → the phase skill named by the assignment → the active plan → current-task references → model guidance recorded by that plan. The assignment must identify the plan and current task whenever persisted work is active. If a required reference is missing, unreadable, or inconsistent with the assignment or goal state, report a blocker and do not perform the assigned discovery or delegate it.
|
|
14
|
+
|
|
13
15
|
When subagent tools are available, you may recursively delegate bounded read-only sub-tasks to subagents. Every child subagent must read the project-root `MEMORY.md`, keep the assigned scope, and preserve others' edits.
|
|
14
16
|
|
|
15
17
|
Use for:
|
package/agents/worker.md
CHANGED
|
@@ -10,6 +10,8 @@ Handle narrow, focused tasks.
|
|
|
10
10
|
|
|
11
11
|
Before doing any assigned task, read the project-root `MEMORY.md`. Treat it as the domain knowledge and repository policy index, then read only the relevant `docs/memory/*.md` notes needed for the task.
|
|
12
12
|
|
|
13
|
+
At the start and after compaction, restart, or handoff, restore context in this order: root `MEMORY.md` → the phase skill named by the assignment → the active plan → current-task references → model guidance recorded by that plan. The assignment must identify the plan and current task whenever persisted work is active. If a required reference is missing, unreadable, or inconsistent with the assignment or goal state, report a blocker and do not edit, delegate, or claim completion.
|
|
14
|
+
|
|
13
15
|
When subagent tools are available, you may recursively delegate bounded sub-tasks to subagents. Every child subagent must read the project-root `MEMORY.md`, keep the assigned scope, and preserve others' edits.
|
|
14
16
|
|
|
15
17
|
Use for:
|
|
@@ -17,4 +17,6 @@ Managed items:
|
|
|
17
17
|
- Claude Code `cairn-*` commands and agents mirror files.
|
|
18
18
|
- Antigravity IDE/CLI `cairn-*` skills and workflows mirror files.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
After the transaction commits, Cairn removes empty managed scaffold directories bottom-up and removes the custom marketplace root only after releasing its own lifecycle lock. A directory containing any unmanaged child is preserved; pruning never uses recursive deletion.
|
|
21
|
+
|
|
22
|
+
The source work folder and repository `MEMORY.md`, `PLAN.md`, `docs/`, and `.cairn/state.json` are not deleted. The globally installed `cairn-ai` package, package-manager download caches, legacy backup files, and legacy shared settings not recorded by the current ownership manifest are also outside this command's scope and require a separate, explicit cleanup decision.
|
|
@@ -14,6 +14,7 @@ The goal is not to make prompts long by model. The goal is to stabilize the same
|
|
|
14
14
|
|
|
15
15
|
## Common Rules
|
|
16
16
|
|
|
17
|
+
- On every fresh entry, compaction recovery, restart, delegation, or handoff, restore context in this order: root `MEMORY.md` → current phase skill → active plan → current-task references → model guidance recorded by the plan. If persisted state or an assignment requires a reference that is missing, unreadable, or inconsistent, fail closed: do not edit, delegate, approve, or complete; report or record a blocker.
|
|
17
18
|
- Keep root `MEMORY.md` and `PLAN.md` as indexes only.
|
|
18
19
|
- Keep repository-specific judgment in `docs/memory/` and `docs/plan/`. Keep Cairn's model guidance in the installed plugin and reference it with `cairn://docs/model-guidance/...`.
|
|
19
20
|
- Preserve proper nouns, file names, variable names, service names, alert names, MCP tool names, and agent names exactly as written.
|
|
@@ -36,6 +37,15 @@ The goal is not to make prompts long by model. The goal is to stabilize the same
|
|
|
36
37
|
- Limit each task to two verification passes by default. After two failed passes, record the blocker or split it into sub-tasks instead of continuing a repeated loop.
|
|
37
38
|
- User-visible responses and generated or updated documentation, plans, and memory artifacts must follow the OS locale unless the user asks for another language.
|
|
38
39
|
|
|
40
|
+
## Prompt Cache Shape
|
|
41
|
+
|
|
42
|
+
- Keep `.codex-plugin/plugin.json` `defaultPrompt` as a small, stable policy and recovery kernel. Put detailed bootstrap, delegation, test, package, and verification procedures in the phase skills and this installed guidance.
|
|
43
|
+
- Put static instructions before dynamic goal data. Hook capsules must be deterministic for the same locale, event, and persisted state; prompt text and turn IDs must not change their content.
|
|
44
|
+
- Idle capsules reference root `MEMORY.md` and `cairn-plan`; active-work capsules reference the active plan, current task, and `cairn-work`; all-tasks-complete capsules reference `cairn-review`.
|
|
45
|
+
- A foreign session receives only a generic ownership-conflict capsule on session/prompt events. It must not expose goal, plan, or task details, and it must not make stop hooks block work owned by another session.
|
|
46
|
+
- Character budgets are regression proxies for cache-friendly prompt shape, not measurements of provider cache hits or cost. Cairn does not require provider cache keys, breakpoints, or live API telemetry.
|
|
47
|
+
- Do not add read receipts that claim model attention. Restore references on every re-entry and use readable state plus fresh bound evidence as the enforceable contract.
|
|
48
|
+
|
|
39
49
|
## Delegation Defaults
|
|
40
50
|
|
|
41
51
|
- `explorer`: use for read-only codebase discovery, impact analysis, pattern searches, and read-only verification when available.
|
|
@@ -17,6 +17,7 @@ Codex-family models are strong at small implementation tasks, explicit tool use,
|
|
|
17
17
|
|
|
18
18
|
## Adjustment Rules
|
|
19
19
|
|
|
20
|
+
- On every fresh entry, compaction recovery, restart, delegation, or handoff, restore root `MEMORY.md` → current phase skill → active plan → current-task references → recorded model guidance. If persisted state or the assignment requires a missing, unreadable, or inconsistent reference, fail closed and report a blocker before editing, delegating, approving, or completing.
|
|
20
21
|
- Do not start implementation without a plan.
|
|
21
22
|
- Do not stop before local implementation solely because subagent tools are unavailable; the main agent takes over implementation directly and records that takeover in evidence.
|
|
22
23
|
- Keep work units small. Split large changes into smaller plan tasks.
|
|
@@ -26,6 +27,7 @@ Codex-family models are strong at small implementation tasks, explicit tool use,
|
|
|
26
27
|
- Do not skip LSP, typecheck, lint, dry-run, or verification because a tool is missing. Try repository-local installation or record the failed install attempt as a blocker.
|
|
27
28
|
- Do not continue an open-ended verification loop. Use at most two verification passes per task unless the plan records a risk-based exception.
|
|
28
29
|
- Write user-visible responses and generated or updated documentation, plans, and memory artifacts in the OS locale unless the user asks for another language.
|
|
30
|
+
- Preserve the stable prompt prefix: keep detailed workflow in phase skills/model guidance, and do not vary a hook capsule with prompt text or turn ID when persisted state is unchanged.
|
|
29
31
|
|
|
30
32
|
## Path Guidance
|
|
31
33
|
|
package/package.json
CHANGED
package/scripts/cairn-goal.mjs
CHANGED
|
@@ -10,6 +10,9 @@ import { assertNoSymlinkComponents, safeMkdir, safeWriteFile, withStateLock } fr
|
|
|
10
10
|
export const GOAL_STATE_VERSION = 2;
|
|
11
11
|
export const GOAL_STATUSES = new Set(["active", "paused", "blocked", "cancelled", "completed"]);
|
|
12
12
|
export const TASK_STATUSES = new Set(["pending", "active", "blocked", "completed"]);
|
|
13
|
+
export const PLAN_ID_MAX_LENGTH = 128;
|
|
14
|
+
export const TASK_ID_MAX_LENGTH = 64;
|
|
15
|
+
export const RECOVERY_REFERENCE_MAX_LENGTH = 160;
|
|
13
16
|
|
|
14
17
|
const terminalGoalStatuses = new Set(["cancelled", "completed"]);
|
|
15
18
|
const evidencePolicies = new Set(["declared", "tool-bound"]);
|
|
@@ -48,7 +51,7 @@ export async function readGoalState({ root = process.cwd() } = {}) {
|
|
|
48
51
|
|
|
49
52
|
export async function startGoal({ root = process.cwd(), goal, planId, tasks, completionCriteria = [], requiredEvidence = defaultGoalEvidence, ownerSessionId = null, evidencePolicy = "tool-bound" } = {}) {
|
|
50
53
|
const normalizedGoal = requiredText(goal, "goal");
|
|
51
|
-
const normalizedPlanId =
|
|
54
|
+
const normalizedPlanId = boundedRequiredText(planId, "planId", PLAN_ID_MAX_LENGTH);
|
|
52
55
|
if (!Array.isArray(tasks) || tasks.length === 0) throw new Error("tasks must contain at least one task");
|
|
53
56
|
|
|
54
57
|
return withStateLock(root, async () => {
|
|
@@ -60,6 +63,7 @@ export async function startGoal({ root = process.cwd(), goal, planId, tasks, com
|
|
|
60
63
|
const now = timestamp();
|
|
61
64
|
const goalId = `goal-${randomUUID()}`;
|
|
62
65
|
const normalizedTasks = tasks.map((task, index) => normalizeTask(task, index, index === 0 ? "active" : "pending"));
|
|
66
|
+
for (const task of normalizedTasks) assertRecoveryReferenceBudget(normalizedPlanId, task.id);
|
|
63
67
|
const state = {
|
|
64
68
|
schemaVersion: GOAL_STATE_VERSION,
|
|
65
69
|
revision: 1,
|
|
@@ -206,7 +210,9 @@ async function recordEvidence({
|
|
|
206
210
|
|| state.goal.planId !== expectedIdentity.planId
|
|
207
211
|
)) throw new Error("Goal identity changed during verification; evidence was not recorded");
|
|
208
212
|
if (expectedIdentity && normalizedScope === "task" && (
|
|
209
|
-
task?.id !== expectedIdentity.taskId
|
|
213
|
+
task?.id !== expectedIdentity.taskId
|
|
214
|
+
|| task?.status !== expectedIdentity.taskStatus
|
|
215
|
+
|| task?.assignedAgentId !== expectedIdentity.taskAgentId
|
|
210
216
|
)) throw new Error("Verification task changed during verification; evidence was not recorded");
|
|
211
217
|
if (expectedFingerprint && workspaceFingerprint(root, watchPaths) !== expectedFingerprint) {
|
|
212
218
|
throw new Error("Watched workspace changed during verification; evidence was not recorded");
|
|
@@ -252,12 +258,15 @@ export async function verifyAndRecord({
|
|
|
252
258
|
assertGoalMutable(initialState);
|
|
253
259
|
const normalizedTaskId = normalizedScope === "task" ? requiredText(taskId, "taskId") : undefined;
|
|
254
260
|
const initialTask = normalizedScope === "task" ? taskById(initialState, normalizedTaskId) : null;
|
|
255
|
-
if (initialTask &&
|
|
261
|
+
if (initialTask && !["active", "completed"].includes(initialTask.status)) {
|
|
262
|
+
throw new Error(`Verification task must be active or completed: ${initialTask.id}`);
|
|
263
|
+
}
|
|
256
264
|
const identity = {
|
|
257
265
|
goalId: initialState.goal.id,
|
|
258
266
|
planId: initialState.goal.planId,
|
|
259
267
|
taskId: normalizedTaskId,
|
|
260
268
|
taskStatus: initialTask?.status,
|
|
269
|
+
taskAgentId: initialTask?.assignedAgentId,
|
|
261
270
|
};
|
|
262
271
|
const initialFingerprint = workspaceFingerprint(root, normalizedWatchPaths);
|
|
263
272
|
const result = runner(normalizedArgv[0], normalizedArgv.slice(1), {
|
|
@@ -283,8 +292,11 @@ export async function verifyAndRecord({
|
|
|
283
292
|
if (!postState || postState.goal.id !== identity.goalId || postState.goal.planId !== identity.planId) {
|
|
284
293
|
throw new Error("Goal identity changed during verification; evidence was not recorded");
|
|
285
294
|
}
|
|
286
|
-
if (normalizedScope === "task"
|
|
287
|
-
|
|
295
|
+
if (normalizedScope === "task") {
|
|
296
|
+
const postTask = taskById(postState, normalizedTaskId);
|
|
297
|
+
if (postTask.status !== identity.taskStatus || postTask.assignedAgentId !== identity.taskAgentId) {
|
|
298
|
+
throw new Error("Verification task changed during verification; evidence was not recorded");
|
|
299
|
+
}
|
|
288
300
|
}
|
|
289
301
|
const command = normalizedArgv.map((value) => JSON.stringify(value)).join(" ");
|
|
290
302
|
const state = await recordEvidence({
|
|
@@ -457,7 +469,7 @@ async function mutateGoal(root, mutate) {
|
|
|
457
469
|
function normalizeTask(task, index, defaultStatus) {
|
|
458
470
|
const source = typeof task === "string" ? { title: task } : task;
|
|
459
471
|
if (!source || typeof source !== "object") throw new Error(`tasks[${index}] must be a string or object`);
|
|
460
|
-
const id = source.id === undefined ? `task-${index + 1}` :
|
|
472
|
+
const id = source.id === undefined ? `task-${index + 1}` : boundedRequiredText(source.id, `tasks[${index}].id`, TASK_ID_MAX_LENGTH);
|
|
461
473
|
const title = requiredText(source.title, `tasks[${index}].title`);
|
|
462
474
|
return {
|
|
463
475
|
id,
|
|
@@ -495,7 +507,7 @@ function validateState(value) {
|
|
|
495
507
|
const goal = value.goal;
|
|
496
508
|
requiredText(goal.id, "goal.id");
|
|
497
509
|
requiredText(goal.title, "goal.title");
|
|
498
|
-
|
|
510
|
+
const planId = boundedRequiredText(goal.planId, "goal.planId", PLAN_ID_MAX_LENGTH);
|
|
499
511
|
validGoalStatus(goal.status);
|
|
500
512
|
validEvidencePolicy(goal.evidencePolicy);
|
|
501
513
|
validTimestamp(goal.createdAt);
|
|
@@ -511,7 +523,8 @@ function validateState(value) {
|
|
|
511
523
|
let activeTasks = 0;
|
|
512
524
|
for (const task of value.tasks) {
|
|
513
525
|
if (!task || typeof task !== "object") throw new Error("Each task must be an object");
|
|
514
|
-
|
|
526
|
+
const taskId = boundedRequiredText(task.id, "task.id", TASK_ID_MAX_LENGTH);
|
|
527
|
+
assertRecoveryReferenceBudget(planId, taskId);
|
|
515
528
|
if (ids.has(task.id)) throw new Error(`Duplicate task id: ${task.id}`);
|
|
516
529
|
ids.add(task.id);
|
|
517
530
|
requiredText(task.title, "task.title");
|
|
@@ -622,6 +635,19 @@ function requiredText(value, label) {
|
|
|
622
635
|
return value.trim();
|
|
623
636
|
}
|
|
624
637
|
|
|
638
|
+
function boundedRequiredText(value, label, maxLength) {
|
|
639
|
+
const text = requiredText(value, label);
|
|
640
|
+
if (value !== text) throw new Error(`${label} must not have leading or trailing whitespace`);
|
|
641
|
+
if (text.length > maxLength) throw new Error(`${label} must be at most ${maxLength} characters`);
|
|
642
|
+
return text;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function assertRecoveryReferenceBudget(planId, taskId) {
|
|
646
|
+
if (planId.length + taskId.length > RECOVERY_REFERENCE_MAX_LENGTH) {
|
|
647
|
+
throw new Error(`planId and taskId must total at most ${RECOVERY_REFERENCE_MAX_LENGTH} characters for exact recovery context`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
625
651
|
function validGoalStatus(value) {
|
|
626
652
|
if (!GOAL_STATUSES.has(value)) throw new Error(`Invalid goal status: ${value}`);
|
|
627
653
|
return value;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { realpathSync } from "node:fs";
|
|
4
|
+
import { constants as fsConstants, realpathSync } from "node:fs";
|
|
5
5
|
import {
|
|
6
|
-
copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, writeFile,
|
|
6
|
+
copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, rmdir, writeFile,
|
|
7
7
|
} from "node:fs/promises";
|
|
8
8
|
import { homedir, hostname, tmpdir } from "node:os";
|
|
9
9
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
@@ -47,7 +47,7 @@ if (isCliEntry()) {
|
|
|
47
47
|
const command = process.argv[2] ?? "help";
|
|
48
48
|
if (command === "install" || command === "upgrade") await withLifecycleLock(() => install(command));
|
|
49
49
|
else if (command === "doctor") await doctor();
|
|
50
|
-
else if (command === "uninstall") await withLifecycleLock(uninstall);
|
|
50
|
+
else if (command === "uninstall") await withLifecycleLock(uninstall, { afterRelease: pruneUninstallRoots });
|
|
51
51
|
else help();
|
|
52
52
|
} catch (error) {
|
|
53
53
|
console.error(`Cairn lifecycle error: ${error.message}`);
|
|
@@ -107,7 +107,7 @@ async function install(mode) {
|
|
|
107
107
|
console.log(`Codex versioned runtime: ${versionedPluginRoot}`);
|
|
108
108
|
console.log(`Ownership manifest: ${ownershipPath}`);
|
|
109
109
|
} catch (error) {
|
|
110
|
-
await rollback(transaction);
|
|
110
|
+
if (transaction.state !== "committed") await rollback(transaction);
|
|
111
111
|
throw error;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -119,17 +119,18 @@ async function uninstall() {
|
|
|
119
119
|
const conflicts = [];
|
|
120
120
|
for (const record of ownership.targets) {
|
|
121
121
|
const current = await targetDigest(record.path, record.type);
|
|
122
|
-
if (current !== record.installedDigest) conflicts.push(record.path);
|
|
122
|
+
if (record.type !== "config" && current !== record.installedDigest) conflicts.push(record.path);
|
|
123
123
|
}
|
|
124
124
|
if (conflicts.length > 0) throw new Error(`Refusing to uninstall modified managed artifacts: ${conflicts.join(", ")}`);
|
|
125
125
|
const transaction = await createTransaction();
|
|
126
126
|
try {
|
|
127
127
|
const configRecord = ownership.targets.find((record) => record.type === "config");
|
|
128
|
-
if (configRecord) {
|
|
128
|
+
if (configRecord && await exists(configRecord.path)) {
|
|
129
|
+
const snapshot = await readSharedConfigSnapshot(configRecord.path);
|
|
129
130
|
const staged = join(transaction.stageRoot, "uninstall-config.toml");
|
|
130
131
|
await mkdir(dirname(staged), { recursive: true });
|
|
131
|
-
await writeFile(staged, removeCairnConfig(
|
|
132
|
-
await replaceTarget({ ...configRecord, staged }, transaction);
|
|
132
|
+
await writeFile(staged, removeCairnConfig(snapshot.text));
|
|
133
|
+
await replaceTarget({ ...configRecord, staged, configSourceDigest: snapshot.digest }, transaction);
|
|
133
134
|
}
|
|
134
135
|
injectFailure("uninstall-after-config");
|
|
135
136
|
for (const phase of ["antigravity", "claude"]) {
|
|
@@ -141,11 +142,13 @@ async function uninstall() {
|
|
|
141
142
|
await removeManagedTarget({ id: "ownership", phase: "manifest", path: ownershipPath, type: "file" }, transaction);
|
|
142
143
|
injectFailure("uninstall-after-ownership");
|
|
143
144
|
await finishTransaction(transaction);
|
|
144
|
-
|
|
145
|
+
injectFailure("uninstall-after-commit");
|
|
145
146
|
} catch (error) {
|
|
146
|
-
await rollback(transaction);
|
|
147
|
+
if (transaction.state !== "committed") await rollback(transaction);
|
|
147
148
|
throw error;
|
|
148
149
|
}
|
|
150
|
+
await pruneUninstallScaffolds();
|
|
151
|
+
console.log(t("uninstallComplete"));
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
async function doctor() {
|
|
@@ -164,7 +167,7 @@ async function doctor() {
|
|
|
164
167
|
checks.push(["installed CLI script", await exists(join(versionedPluginRoot, "scripts", "cairn.mjs"))]);
|
|
165
168
|
checks.push(["installed plan template", await exists(join(versionedPluginRoot, "templates", "work-plan.md"))]);
|
|
166
169
|
checks.push(["installed model guidance", await exists(join(versionedPluginRoot, "docs", "model-guidance", "codex.md"))]);
|
|
167
|
-
const config = await
|
|
170
|
+
const config = (await readSharedConfigSnapshot(configPath)).text;
|
|
168
171
|
checks.push(["config marketplace", splitSections(config).some((section) => section.header === "marketplaces.cairn")]);
|
|
169
172
|
checks.push(["config plugin enabled", hasSetting(config, 'plugins."cairn@cairn"', "enabled", "true")]);
|
|
170
173
|
checks.push(["Claude runtime locator", await validRuntimeLocator(claudeRuntimeLocatorPath)]);
|
|
@@ -202,7 +205,7 @@ async function ownershipDigestsValid(ownership) {
|
|
|
202
205
|
|
|
203
206
|
async function stageInstall(stageRoot) {
|
|
204
207
|
const targets = [];
|
|
205
|
-
const add = (id, phase, destination, staged, type = "tree") => targets.push({ id, phase, path: destination, staged, type });
|
|
208
|
+
const add = (id, phase, destination, staged, type = "tree", details = {}) => targets.push({ id, phase, path: destination, staged, type, ...details });
|
|
206
209
|
const sourceStage = join(stageRoot, "codex-source");
|
|
207
210
|
const runtimeStage = join(stageRoot, "codex-runtime");
|
|
208
211
|
await copyPluginCandidate(sourceStage, versionedPluginRoot);
|
|
@@ -237,8 +240,9 @@ async function stageInstall(stageRoot) {
|
|
|
237
240
|
|
|
238
241
|
const configStage = join(stageRoot, "config.toml");
|
|
239
242
|
const hookStates = await trustedHookStates(runtimeStage);
|
|
240
|
-
|
|
241
|
-
|
|
243
|
+
const configSnapshot = await readSharedConfigSnapshot(configPath);
|
|
244
|
+
await writeFile(configStage, updateConfig(configSnapshot.text, hookStates));
|
|
245
|
+
add("codex-config", "config", configPath, configStage, "config", { configSourceDigest: configSnapshot.digest });
|
|
242
246
|
for (const target of targets) await assertRegularTree(target.staged);
|
|
243
247
|
return targets;
|
|
244
248
|
}
|
|
@@ -328,8 +332,10 @@ async function withLifecycleLock(operation, {
|
|
|
328
332
|
timeoutMs = Number(process.env.CAIRN_LIFECYCLE_LOCK_TIMEOUT_MS ?? DEFAULT_LIFECYCLE_LOCK_TIMEOUT_MS),
|
|
329
333
|
retryMs = 25,
|
|
330
334
|
malformedStaleMs = 30_000,
|
|
335
|
+
afterRelease,
|
|
331
336
|
} = {}) {
|
|
332
337
|
await mkdir(dirname(lifecycleLockPath), { recursive: true });
|
|
338
|
+
if (process.env.CAIRN_TEST_PRUNE_LOCK_PARENT_BEFORE_ACQUIRE === "1") await rmdir(dirname(lifecycleLockPath));
|
|
333
339
|
const owner = { schemaVersion: 1, pid: process.pid, hostname: hostname(), nonce: randomUUID(), acquiredAt: new Date().toISOString() };
|
|
334
340
|
const deadline = Date.now() + timeoutMs;
|
|
335
341
|
while (true) {
|
|
@@ -342,21 +348,76 @@ async function withLifecycleLock(operation, {
|
|
|
342
348
|
if (acquired?.nonce !== owner.nonce) throw new Error("Cairn lifecycle lock ownership changed after acquisition");
|
|
343
349
|
break;
|
|
344
350
|
} catch (error) {
|
|
351
|
+
if (error?.code === "ENOENT") {
|
|
352
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for Cairn lifecycle lock after ${timeoutMs}ms`);
|
|
353
|
+
await mkdir(dirname(lifecycleLockPath), { recursive: true });
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
345
356
|
if (error?.code !== "EEXIST") throw error;
|
|
346
357
|
if (await reclaimLifecycleLock(malformedStaleMs)) continue;
|
|
347
358
|
if (Date.now() >= deadline) throw new Error(`Timed out waiting for Cairn lifecycle lock after ${timeoutMs}ms`);
|
|
348
359
|
await delay(Math.min(retryMs, Math.max(1, deadline - Date.now())));
|
|
349
360
|
}
|
|
350
361
|
}
|
|
362
|
+
let operationSucceeded = false;
|
|
351
363
|
try {
|
|
352
|
-
|
|
364
|
+
const result = await operation();
|
|
365
|
+
operationSucceeded = true;
|
|
366
|
+
return result;
|
|
353
367
|
} finally {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (current?.nonce === owner.nonce) await rm(lifecycleLockPath, { force: true });
|
|
357
|
-
} catch (error) {
|
|
358
|
-
if (error?.code !== "ENOENT") throw error;
|
|
368
|
+
if (process.env.CAIRN_TEST_REPLACE_LOCK_BEFORE_RELEASE === "1") {
|
|
369
|
+
await writeFile(lifecycleLockPath, `${JSON.stringify({ ...owner, nonce: randomUUID() })}\n`);
|
|
359
370
|
}
|
|
371
|
+
const released = await releaseLifecycleLock(owner);
|
|
372
|
+
if (operationSucceeded && released && afterRelease) await afterRelease();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function releaseLifecycleLock(owner) {
|
|
377
|
+
let current;
|
|
378
|
+
try {
|
|
379
|
+
current = parseLifecycleLock(await readFile(lifecycleLockPath, "utf8"));
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (error?.code === "ENOENT") return false;
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
if (current?.nonce !== owner.nonce) return false;
|
|
385
|
+
const releasedPath = `${lifecycleLockPath}.released.${owner.nonce}`;
|
|
386
|
+
try {
|
|
387
|
+
await rename(lifecycleLockPath, releasedPath);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
if (error?.code === "ENOENT") return false;
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
const released = parseLifecycleLock(await readFile(releasedPath, "utf8"));
|
|
393
|
+
if (released?.nonce !== owner.nonce) {
|
|
394
|
+
if (!(await exists(lifecycleLockPath))) await rename(releasedPath, lifecycleLockPath);
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
await rm(releasedPath, { force: true });
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function pruneUninstallScaffolds() {
|
|
402
|
+
for (const path of [
|
|
403
|
+
join(marketplaceRoot, "plugins"),
|
|
404
|
+
join(marketplaceRoot, pluginName),
|
|
405
|
+
join(marketplaceRoot, ".agents", "plugins"),
|
|
406
|
+
join(marketplaceRoot, ".agents"),
|
|
407
|
+
transactionRoot,
|
|
408
|
+
]) await removeEmptyDirectory(path);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function pruneUninstallRoots() {
|
|
412
|
+
await removeEmptyDirectory(dirname(lifecycleLockPath));
|
|
413
|
+
await removeEmptyDirectory(marketplaceRoot);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function removeEmptyDirectory(path) {
|
|
417
|
+
try {
|
|
418
|
+
await rmdir(path);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error?.code)) throw error;
|
|
360
421
|
}
|
|
361
422
|
}
|
|
362
423
|
|
|
@@ -406,6 +467,15 @@ function pidIsAlive(pid) {
|
|
|
406
467
|
function delay(ms) { return new Promise((resolvePromise) => setTimeout(resolvePromise, ms)); }
|
|
407
468
|
|
|
408
469
|
async function replaceTarget(target, transaction) {
|
|
470
|
+
let configProjection = null;
|
|
471
|
+
if (target.type === "config") {
|
|
472
|
+
const snapshot = await readSharedConfigSnapshot(target.path);
|
|
473
|
+
configProjection = cairnConfigProjection((await readSharedConfigSnapshot(target.staged)).text);
|
|
474
|
+
if (snapshot.digest !== target.configSourceDigest) {
|
|
475
|
+
await writeFile(target.staged, `${removeCairnConfig(snapshot.text)}${configProjection}`);
|
|
476
|
+
target.configSourceDigest = snapshot.digest;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
409
479
|
const existed = await exists(target.path);
|
|
410
480
|
const backup = join(transaction.backupRoot, String(transaction.entries.length));
|
|
411
481
|
await mkdir(dirname(backup), { recursive: true });
|
|
@@ -423,7 +493,30 @@ async function replaceTarget(target, transaction) {
|
|
|
423
493
|
};
|
|
424
494
|
transaction.entries.push(entry);
|
|
425
495
|
await writeTransactionJournal(transaction);
|
|
426
|
-
if (
|
|
496
|
+
if (target.type === "config" && process.env.CAIRN_TEST_APPEND_CONFIG_BEFORE_REPLACE) {
|
|
497
|
+
const snapshot = await readSharedConfigSnapshot(target.path);
|
|
498
|
+
await writeFile(target.path, `${snapshot.text}${process.env.CAIRN_TEST_APPEND_CONFIG_BEFORE_REPLACE}`);
|
|
499
|
+
}
|
|
500
|
+
if (target.type === "config" && process.env.CAIRN_TEST_REMOVE_CONFIG_BEFORE_REPLACE === "1") {
|
|
501
|
+
await rm(target.path, { force: true });
|
|
502
|
+
}
|
|
503
|
+
if (target.type === "config") {
|
|
504
|
+
let capturedExists = true;
|
|
505
|
+
try {
|
|
506
|
+
await rename(target.path, backup);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (error?.code !== "ENOENT") throw error;
|
|
509
|
+
capturedExists = false;
|
|
510
|
+
}
|
|
511
|
+
if (process.env.CAIRN_TEST_CRASH_AFTER_CONFIG_CAPTURE === "1") process.exit(86);
|
|
512
|
+
const captured = await readSharedConfigSnapshot(backup);
|
|
513
|
+
if (capturedExists) await writeFile(target.staged, `${removeCairnConfig(captured.text)}${configProjection}`);
|
|
514
|
+
else await writeFile(target.staged, configProjection);
|
|
515
|
+
entry.existed = capturedExists;
|
|
516
|
+
entry.previousDigest = capturedExists ? await targetDigest(backup, target.type) : "missing";
|
|
517
|
+
entry.expectedNewDigest = await targetDigest(target.staged, target.type);
|
|
518
|
+
await writeTransactionJournal(transaction);
|
|
519
|
+
} else if (existed) await rename(target.path, backup);
|
|
427
520
|
await mkdir(dirname(target.path), { recursive: true });
|
|
428
521
|
await rename(target.staged, target.path);
|
|
429
522
|
entry.status = "applied";
|
|
@@ -462,7 +555,12 @@ async function rollback(transaction) {
|
|
|
462
555
|
|
|
463
556
|
async function finishTransaction(transaction) {
|
|
464
557
|
transaction.state = "committed";
|
|
465
|
-
|
|
558
|
+
try {
|
|
559
|
+
await writeTransactionJournal(transaction);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
transaction.state = "active";
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
466
564
|
await rm(transaction.root, { recursive: true, force: true });
|
|
467
565
|
await rm(join(transactionRoot, transaction.id), { recursive: true, force: true });
|
|
468
566
|
await rm(transactionPath, { force: true });
|
|
@@ -568,6 +666,18 @@ async function rollbackEntries(transaction) {
|
|
|
568
666
|
const destinationExists = await exists(entry.path);
|
|
569
667
|
const backupExists = await exists(entry.backup);
|
|
570
668
|
if (backupExists) {
|
|
669
|
+
if (entry.type === "config" && entry.status === "prepared") {
|
|
670
|
+
const previousConfig = (await readSharedConfigSnapshot(entry.backup)).text;
|
|
671
|
+
if (destinationExists) {
|
|
672
|
+
const currentConfig = (await readSharedConfigSnapshot(entry.path)).text;
|
|
673
|
+
await writeFile(entry.path, `${removeCairnConfig(currentConfig)}${cairnConfigProjection(previousConfig)}`);
|
|
674
|
+
await rm(entry.backup, { force: true });
|
|
675
|
+
} else {
|
|
676
|
+
await mkdir(dirname(entry.path), { recursive: true });
|
|
677
|
+
await rename(entry.backup, entry.path);
|
|
678
|
+
}
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
571
681
|
if (!entry.existed) throw new Error(`Cannot recover transaction because an unexpected backup exists: ${entry.path}`);
|
|
572
682
|
if (await targetDigest(entry.backup, entry.type) !== entry.previousDigest) {
|
|
573
683
|
throw new Error(`Cannot recover transaction because its durable backup changed: ${entry.path}`);
|
|
@@ -576,8 +686,8 @@ async function rollbackEntries(transaction) {
|
|
|
576
686
|
const current = await targetDigest(entry.path, entry.type);
|
|
577
687
|
if (current !== entry.expectedNewDigest) throw new Error(`Cannot recover transaction because destination changed: ${entry.path}`);
|
|
578
688
|
if (entry.type === "config") {
|
|
579
|
-
const currentConfig = await
|
|
580
|
-
const previousConfig = await
|
|
689
|
+
const currentConfig = (await readSharedConfigSnapshot(entry.path)).text;
|
|
690
|
+
const previousConfig = (await readSharedConfigSnapshot(entry.backup)).text;
|
|
581
691
|
await writeFile(entry.path, `${removeCairnConfig(currentConfig)}${cairnConfigProjection(previousConfig)}`);
|
|
582
692
|
await rm(entry.backup, { force: true });
|
|
583
693
|
continue;
|
|
@@ -586,12 +696,14 @@ async function rollbackEntries(transaction) {
|
|
|
586
696
|
}
|
|
587
697
|
await mkdir(dirname(entry.path), { recursive: true });
|
|
588
698
|
await rename(entry.backup, entry.path);
|
|
699
|
+
} else if (entry.type === "config" && entry.status === "prepared" && !destinationExists) {
|
|
700
|
+
continue;
|
|
589
701
|
} else if (!entry.existed) {
|
|
590
702
|
if (destinationExists) {
|
|
591
703
|
const current = await targetDigest(entry.path, entry.type);
|
|
592
704
|
if (current !== entry.expectedNewDigest) throw new Error(`Cannot recover new transaction target because it changed: ${entry.path}`);
|
|
593
705
|
if (entry.type === "config") {
|
|
594
|
-
const preserved = removeCairnConfig(await
|
|
706
|
+
const preserved = removeCairnConfig((await readSharedConfigSnapshot(entry.path)).text);
|
|
595
707
|
if (preserved.length > 0) await writeFile(entry.path, preserved);
|
|
596
708
|
else await rm(entry.path, { force: true });
|
|
597
709
|
} else await rm(entry.path, { recursive: true, force: true });
|
|
@@ -624,7 +736,7 @@ async function assertTargetsReplaceable(targets, ownership) {
|
|
|
624
736
|
}
|
|
625
737
|
const record = records.get(target.id);
|
|
626
738
|
if (!record) {
|
|
627
|
-
if (target.type === "config" && cairnConfigProjection(await
|
|
739
|
+
if (target.type === "config" && cairnConfigProjection((await readSharedConfigSnapshot(target.path)).text).length === 0) continue;
|
|
628
740
|
throw new Error(`Refusing to overwrite unmanaged artifact: ${target.path}`);
|
|
629
741
|
}
|
|
630
742
|
if (target.id === "codex-runtime" && ownership?.version !== releaseVersion) {
|
|
@@ -632,13 +744,14 @@ async function assertTargetsReplaceable(targets, ownership) {
|
|
|
632
744
|
}
|
|
633
745
|
if (record.path !== target.path || record.type !== target.type) throw new Error(`Ownership manifest target mismatch: ${target.id}`);
|
|
634
746
|
const current = await targetDigest(target.path, target.type);
|
|
635
|
-
if (current !== record.installedDigest) throw new Error(`Managed artifact was modified: ${target.path}`);
|
|
747
|
+
if (target.type !== "config" && current !== record.installedDigest) throw new Error(`Managed artifact was modified: ${target.path}`);
|
|
636
748
|
}
|
|
637
749
|
}
|
|
638
750
|
|
|
639
751
|
async function assertOwnershipDigests(ownership) {
|
|
640
752
|
for (const record of ownership.targets) {
|
|
641
|
-
|
|
753
|
+
const current = await targetDigest(record.path, record.type);
|
|
754
|
+
if (record.type !== "config" && current !== record.installedDigest) {
|
|
642
755
|
throw new Error(`Managed artifact was modified: ${record.path}`);
|
|
643
756
|
}
|
|
644
757
|
}
|
|
@@ -742,7 +855,7 @@ function versionedRuntimeRoot(version) {
|
|
|
742
855
|
async function anyManagedArtifactExists(targets) {
|
|
743
856
|
for (const target of targets) {
|
|
744
857
|
if (!(await exists(target.path))) continue;
|
|
745
|
-
if (target.type !== "config" || cairnConfigProjection(await
|
|
858
|
+
if (target.type !== "config" || cairnConfigProjection((await readSharedConfigSnapshot(target.path)).text).length > 0) return true;
|
|
746
859
|
}
|
|
747
860
|
return false;
|
|
748
861
|
}
|
|
@@ -762,7 +875,7 @@ async function adoptLegacy(targets, transaction) {
|
|
|
762
875
|
}
|
|
763
876
|
}
|
|
764
877
|
if (target.type === "config") {
|
|
765
|
-
const projection = cairnConfigProjection(await
|
|
878
|
+
const projection = cairnConfigProjection((await readSharedConfigSnapshot(target.path)).text);
|
|
766
879
|
if (!projection.includes("[marketplaces.cairn]") || !projection.includes('[plugins."cairn@cairn"]')) {
|
|
767
880
|
throw new Error(`Legacy Cairn config cannot be identified: ${target.path}`);
|
|
768
881
|
}
|
|
@@ -859,7 +972,7 @@ async function verifyLegacyMirror(target) {
|
|
|
859
972
|
|
|
860
973
|
export async function targetDigest(path, type) {
|
|
861
974
|
if (!(await exists(path))) return "missing";
|
|
862
|
-
if (type === "config") return sha(cairnConfigProjection(await
|
|
975
|
+
if (type === "config") return sha(cairnConfigProjection((await readSharedConfigSnapshot(path)).text));
|
|
863
976
|
const info = await lstat(path);
|
|
864
977
|
if (info.isSymbolicLink() || (!info.isFile() && !info.isDirectory())) throw new Error(`Managed artifact is not a regular file/tree: ${path}`);
|
|
865
978
|
if (info.isFile()) return sha(await readFile(path));
|
|
@@ -905,19 +1018,60 @@ export function updateConfig(config, hookStates, { marketplacePath = marketplace
|
|
|
905
1018
|
}
|
|
906
1019
|
|
|
907
1020
|
export function removeCairnConfig(config) {
|
|
908
|
-
return splitSections(config).
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (section.header === 'plugins."cairn@cairn"') return false;
|
|
912
|
-
return !(section.header.startsWith("hooks.state.") && section.header.includes("cairn@cairn:"));
|
|
913
|
-
}).map((section) => section.text).join("");
|
|
1021
|
+
return splitSections(config).map((section) => isCairnConfigSection(section)
|
|
1022
|
+
? splitTrailingTomlTrivia(section.text).trivia
|
|
1023
|
+
: section.text).join("");
|
|
914
1024
|
}
|
|
915
1025
|
|
|
916
1026
|
function cairnConfigProjection(config) {
|
|
917
|
-
return splitSections(config).filter(
|
|
1027
|
+
return splitSections(config).filter(isCairnConfigSection)
|
|
1028
|
+
.map((section) => splitTrailingTomlTrivia(section.text).body).join("");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function isCairnConfigSection(section) {
|
|
1032
|
+
return section.header === "marketplaces.cairn"
|
|
918
1033
|
|| section.header === 'plugins."cairn@cairn"'
|
|
919
|
-
|| (section.header?.startsWith("hooks.state.") && section.header.includes("cairn@cairn:"))
|
|
920
|
-
|
|
1034
|
+
|| (section.header?.startsWith("hooks.state.") && section.header.includes("cairn@cairn:"));
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function splitTrailingTomlTrivia(text) {
|
|
1038
|
+
const lines = text.split(/(?<=\n)/);
|
|
1039
|
+
let boundary = lines.length;
|
|
1040
|
+
while (boundary > 0) {
|
|
1041
|
+
const line = lines[boundary - 1];
|
|
1042
|
+
if (line.length === 0 || line.trim().length === 0 || line.trimStart().startsWith("#")) boundary -= 1;
|
|
1043
|
+
else break;
|
|
1044
|
+
}
|
|
1045
|
+
return { body: lines.slice(0, boundary).join(""), trivia: lines.slice(boundary).join("") };
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
async function readSharedConfigSnapshot(path) {
|
|
1049
|
+
let pathInfo;
|
|
1050
|
+
try {
|
|
1051
|
+
pathInfo = await lstat(path);
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
if (error?.code === "ENOENT") return { exists: false, text: "", digest: "missing" };
|
|
1054
|
+
throw error;
|
|
1055
|
+
}
|
|
1056
|
+
if (pathInfo.isSymbolicLink() || !pathInfo.isFile()) throw new Error(`Managed config is not a regular file: ${path}`);
|
|
1057
|
+
let handle;
|
|
1058
|
+
try {
|
|
1059
|
+
const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0);
|
|
1060
|
+
handle = await open(path, flags);
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
if (["ELOOP", "EFTYPE", "ENXIO"].includes(error?.code)) throw new Error(`Managed config is not a regular file: ${path}`);
|
|
1063
|
+
throw error;
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
const openedInfo = await handle.stat();
|
|
1067
|
+
if (!openedInfo.isFile() || openedInfo.dev !== pathInfo.dev || openedInfo.ino !== pathInfo.ino) {
|
|
1068
|
+
throw new Error(`Managed config changed identity while opening: ${path}`);
|
|
1069
|
+
}
|
|
1070
|
+
const text = await handle.readFile("utf8");
|
|
1071
|
+
return { exists: true, text, digest: sha(text) };
|
|
1072
|
+
} finally {
|
|
1073
|
+
await handle.close();
|
|
1074
|
+
}
|
|
921
1075
|
}
|
|
922
1076
|
|
|
923
1077
|
export function ensureSetting(config, sectionName, key, value) {
|
package/scripts/cairn-state.mjs
CHANGED
|
@@ -117,7 +117,7 @@ const planTemplateKo = `# PLAN
|
|
|
117
117
|
|
|
118
118
|
const contextLimits = {
|
|
119
119
|
goalTitle: 64,
|
|
120
|
-
|
|
120
|
+
roadmapTaskId: 24,
|
|
121
121
|
taskTitle: 40,
|
|
122
122
|
roadmapWindow: 3,
|
|
123
123
|
};
|
|
@@ -158,7 +158,9 @@ export async function runStateResult(event = "manual", {
|
|
|
158
158
|
|
|
159
159
|
if (event === "session-start" || event === "user-prompt-submit") {
|
|
160
160
|
const state = await readGoalState({ root: resolvedRoot });
|
|
161
|
-
if (!isGoalOwnedBySession(state, payload?.session_id))
|
|
161
|
+
if (state?.goal?.status === "active" && !isGoalOwnedBySession(state, payload?.session_id)) {
|
|
162
|
+
return foreignSessionContextResult({ ko, event });
|
|
163
|
+
}
|
|
162
164
|
return contextResult({ ko, state, event });
|
|
163
165
|
}
|
|
164
166
|
|
|
@@ -222,57 +224,76 @@ async function writeIfMissing(root, path, content) {
|
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
function contextResult({ ko, state, event }) {
|
|
225
|
-
const base = ko ? "Cairn: 루트 MEMORY.md를 읽으세요." : "Cairn: read root MEMORY.md.";
|
|
227
|
+
const base = ko ? "Cairn kernel: 루트 MEMORY.md를 먼저 읽으세요." : "Cairn kernel: read root MEMORY.md first.";
|
|
226
228
|
const idlePolicy = event === "user-prompt-submit"
|
|
227
229
|
? (ko
|
|
228
|
-
? " 구현/계속
|
|
229
|
-
: "
|
|
230
|
-
:
|
|
231
|
-
|
|
230
|
+
? " 구현/계속 실행이면 cairn-plan을 로드하고 active plan과 current task를 복원하거나 생성하세요. 상담·설명·계획 전용은 goal 없이 처리하세요."
|
|
231
|
+
: " For implementation/continue, load cairn-plan and restore or create the active plan and current task. Keep consultation, explanation, and plan-only requests goal-free.")
|
|
232
|
+
: (ko
|
|
233
|
+
? " 중요 작업 전에 cairn-plan을 로드하고 active plan과 current task를 복원하세요."
|
|
234
|
+
: " Before non-trivial work, load cairn-plan and restore the active plan and current task.");
|
|
235
|
+
const failClosed = ko
|
|
236
|
+
? " state, skill, plan 또는 required reference가 없거나 읽을 수 없거나 일치하지 않으면 수정·위임·완료하지 말고 보고하거나 차단하세요."
|
|
237
|
+
: " If state, skill, plan, or a required ref is missing, unreadable, or inconsistent, do not edit, delegate, or complete; report/block.";
|
|
238
|
+
if (!state || state.goal.status !== "active") return contextHookResult({ event, message: `${base}${idlePolicy}${failClosed}` });
|
|
232
239
|
const task = state.tasks.find((item) => item.status === "active")
|
|
233
240
|
?? state.tasks.find((item) => item.status === "pending")
|
|
234
241
|
?? state.tasks.find((item) => item.status === "blocked");
|
|
235
|
-
const
|
|
242
|
+
const planId = state.goal.planId;
|
|
243
|
+
const phase = task ? "cairn-work" : "cairn-review";
|
|
236
244
|
const activeHeader = ko
|
|
237
|
-
?
|
|
238
|
-
:
|
|
245
|
+
? `${base} ${phase}를 로드하고 정확한 plan ${planId}${task ? "과 아래 current task" : "의 완료 기준"}를 복원하세요. 압축·재시작·handoff·위임 뒤 다시 읽으세요.${failClosed}`
|
|
246
|
+
: `${base} Load ${phase}; restore exact plan ${planId}${task ? " and the current task below" : " completion criteria"}. Re-read after compaction/restart/handoff/delegation.${failClosed}`;
|
|
239
247
|
if (!task) {
|
|
240
248
|
return contextHookResult({
|
|
241
249
|
event,
|
|
242
250
|
message: [activeHeader, taskRoadmap(ko, state), activeGoalCompletionMessage(ko, state)].join("\n"),
|
|
243
251
|
});
|
|
244
252
|
}
|
|
245
|
-
const taskId =
|
|
253
|
+
const taskId = task.id;
|
|
246
254
|
const taskTitle = clipText(task.title, contextLimits.taskTitle);
|
|
247
|
-
const continuation =
|
|
248
|
-
? `현재: ${taskId} (${taskTitle}). 바인딩된 증거 뒤에만 완료하세요.`
|
|
249
|
-
: `Current: ${taskId} (${taskTitle}). Complete only after bound evidence.`;
|
|
250
|
-
const resume = event === "user-prompt-submit"
|
|
255
|
+
const continuation = taskId.length > 48
|
|
251
256
|
? (ko
|
|
252
|
-
?
|
|
253
|
-
: `
|
|
257
|
+
? `정확한 current task: ${taskId}. 바인딩된 증거 뒤에만 완료하세요.`
|
|
258
|
+
: `Exact task: ${taskId}. Complete after bound evidence.`)
|
|
259
|
+
: (ko
|
|
260
|
+
? `현재: ${taskId} (${taskTitle}). 바인딩된 증거 뒤에만 완료하세요.`
|
|
261
|
+
: `Current: ${taskId} (${taskTitle}). Complete only after bound evidence.`);
|
|
262
|
+
const resume = event === "user-prompt-submit"
|
|
263
|
+
? (taskId.length > 48
|
|
264
|
+
? (ko
|
|
265
|
+
? "곁가지 질문 뒤 일시정지·중단·전환 요청이 없으면 위의 정확한 task를 재개하세요."
|
|
266
|
+
: "Resume that exact task after side questions unless paused, stopped, or switched.")
|
|
267
|
+
: (ko
|
|
268
|
+
? `곁가지 질문 뒤 일시정지·중단·전환 요청이 없으면 ${taskId}를 재개하세요.`
|
|
269
|
+
: `After a side question, resume ${taskId} unless asked to pause, stop, or switch.`))
|
|
254
270
|
: "";
|
|
255
271
|
return contextHookResult({ event, message: [activeHeader, taskRoadmap(ko, state), continuation, resume].filter(Boolean).join("\n") });
|
|
256
272
|
}
|
|
257
273
|
|
|
274
|
+
function foreignSessionContextResult({ ko, event }) {
|
|
275
|
+
const message = ko
|
|
276
|
+
? "Cairn kernel: 루트 MEMORY.md를 먼저 읽으세요. 저장소 goal을 다른 session이 소유합니다. cairn goal status로 상태를 확인하고 소유권이 해결될 때까지 Cairn 작업을 시작·수정·위임·완료하지 마세요. 이 capsule은 goal, plan, task 상세를 노출하지 않습니다."
|
|
277
|
+
: "Cairn kernel: read root MEMORY.md first. A repository goal is owned by another session. Inspect cairn goal status; until ownership is resolved, do not start, edit, delegate, or complete Cairn work. This capsule does not expose goal, plan, or task details.";
|
|
278
|
+
return contextHookResult({ event, message });
|
|
279
|
+
}
|
|
280
|
+
|
|
258
281
|
function taskRoadmap(ko, state) {
|
|
259
|
-
const formatTask = (task, index) => `${index + 1}. ${clipText(task.id, contextLimits.
|
|
282
|
+
const formatTask = (task, index) => `${index + 1}. ${clipText(task.id, contextLimits.roadmapTaskId)} [${task.status}] ${clipText(task.title, contextLimits.taskTitle)}`;
|
|
260
283
|
if (state.tasks.length <= contextLimits.roadmapWindow) {
|
|
261
284
|
return `${ko ? "작업 단계" : "Work steps"}:\n${state.tasks.map(formatTask).join("\n")}`;
|
|
262
285
|
}
|
|
263
286
|
|
|
264
287
|
const currentIndex = Math.max(0, state.tasks.findIndex((task) => task.status === "active" || task.status === "pending"));
|
|
265
|
-
const maxStart = state.tasks.length - contextLimits.roadmapWindow;
|
|
266
|
-
const start = Math.min(Math.max(0, currentIndex - 1), maxStart);
|
|
267
|
-
const visible = state.tasks.slice(start, start + contextLimits.roadmapWindow);
|
|
268
288
|
const counts = Object.fromEntries([...TASK_STATUS_NAMES].map((status) => [status, state.tasks.filter((task) => task.status === status).length]));
|
|
269
289
|
const countSummary = Object.entries(counts).filter(([, count]) => count > 0).map(([status, count]) => `${status} ${count}`).join(", ");
|
|
270
|
-
const omitted = state.tasks.length -
|
|
290
|
+
const omitted = state.tasks.length - 1;
|
|
271
291
|
const header = ko
|
|
272
292
|
? `작업 단계 (${state.tasks.length}개; ${countSummary})`
|
|
273
293
|
: `Work steps (${state.tasks.length}; ${countSummary})`;
|
|
274
294
|
const omission = ko ? `… ${omitted}개 단계 생략` : `… ${omitted} steps omitted`;
|
|
275
|
-
|
|
295
|
+
const currentPosition = currentIndex + 1;
|
|
296
|
+
return `${header}. ${ko ? `현재 위치 ${currentPosition}` : `Current position ${currentPosition}`}; ${omission}`;
|
|
276
297
|
}
|
|
277
298
|
|
|
278
299
|
function contextHookResult({ event, message }) {
|
|
@@ -17,6 +17,10 @@ Do not treat planning as optional for implementation work that is more than a na
|
|
|
17
17
|
|
|
18
18
|
Plan from the whole work down. First understand the full requested outcome and affected surfaces, then classify the work into small executable tasks. If one task is still too broad to verify cleanly, split that task into sub-tasks.
|
|
19
19
|
|
|
20
|
+
## Re-entry and Required References
|
|
21
|
+
|
|
22
|
+
At the start and after compaction, restart, or handoff, restore context in this order: root `MEMORY.md` → `cairn-plan` → the active plan → current-task references → model guidance recorded by the plan. A new implementation request with no persisted goal may create its initial plan through the procedure below. If persisted state names a plan or task and that reference is missing, unreadable, or inconsistent, do not explore, edit, delegate, or complete work; report or record a blocker first. Do not add a read-receipt field that claims the model read a file: filesystem/state consistency and bound tool evidence remain the fail-closed authority.
|
|
23
|
+
|
|
20
24
|
## Runtime Location
|
|
21
25
|
|
|
22
26
|
Resolve Cairn's read-only runtime from `references/cairn-runtime.json` next to this `SKILL.md`. Its `pluginRoot` contains Cairn's `scripts/`, `templates/`, commands, agents, and model guidance. In a source checkout where the locator does not exist, resolve the plugin root from this `SKILL.md` location (`../..`). Never search the target repository for Cairn scripts, templates, or model guidance, and never derive the plugin root from the current working directory.
|
|
@@ -13,6 +13,8 @@ Review is not another implementation loop. Confirm that the completed task satis
|
|
|
13
13
|
|
|
14
14
|
Every review run and every delegated review agent must read the project-root `MEMORY.md` before doing assigned work.
|
|
15
15
|
|
|
16
|
+
At the start and after compaction, restart, delegation, or handoff, restore context in this order: root `MEMORY.md` → `cairn-review` → the active plan → completed/current-task references → model guidance recorded by the plan. Confirm those references agree with persisted goal state before reviewing. If any required reference is missing, unreadable, or inconsistent, do not approve, delegate, record final review, or complete the goal; report a blocker. Do not use a read-receipt field as evidence that a model understood a reference.
|
|
17
|
+
|
|
16
18
|
When subagent tools are available, each agent may recursively delegate bounded review sub-tasks to subagents. Every child subagent must read the project-root `MEMORY.md`, keep the assigned scope, and preserve others' edits.
|
|
17
19
|
|
|
18
20
|
Resolve any `cairn://...` resource through `references/cairn-runtime.json` next to this `SKILL.md` (or this skill's `../..` source root during development). Never look for Cairn runtime resources in the target repository.
|
|
@@ -21,6 +21,10 @@ Delegated subagents must provide a final report before leaving. After the orches
|
|
|
21
21
|
|
|
22
22
|
If the user asks a side question, status question, or narrow clarification while the selected task is still active, use the active goal's ordered task roadmap to preserve position, answer briefly, and then resume the current task unless the user explicitly asks to pause, stop, or switch tasks.
|
|
23
23
|
|
|
24
|
+
## Re-entry and Required References
|
|
25
|
+
|
|
26
|
+
At the start and after compaction, restart, delegation, or handoff, restore context in this order: root `MEMORY.md` → `cairn-work` → the active plan → current-task references → model guidance recorded by the plan. Confirm that the plan task, persisted current task, and worker assignment agree before acting. If state, skill, plan, assignment, or a required reference is missing, unreadable, or inconsistent, do not edit, delegate, record completion evidence, or complete work; report a blocker to the orchestrator. Do not introduce read receipts as proof of model attention: current state, readable references, and fresh tool-bound evidence are the authority.
|
|
27
|
+
|
|
24
28
|
When subagent tools are available, each agent may recursively delegate bounded sub-tasks to subagents. Every child subagent must read the project-root `MEMORY.md`, keep the assigned scope, and preserve others' edits.
|
|
25
29
|
|
|
26
30
|
## Runtime Location
|