cairn-ai 0.2.6 → 0.2.7
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/.agents/workflows/cairn-memory.md +1 -1
- package/.agents/workflows/cairn-plan.md +2 -2
- package/.agents/workflows/cairn-review.md +1 -1
- package/.agents/workflows/cairn-work.md +3 -3
- package/.claude/agents/explorer.md +1 -1
- package/.claude/agents/worker.md +1 -1
- package/.claude/commands/cairn-memory.md +1 -1
- package/.claude/commands/cairn-plan.md +1 -1
- package/.claude/commands/cairn-review.md +1 -1
- package/.claude/commands/cairn-work.md +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.de.md +3 -2
- package/README.es.md +3 -2
- package/README.fr.md +3 -2
- package/README.ja.md +3 -2
- package/README.ko.md +7 -6
- package/README.md +5 -6
- package/README.pt.md +3 -2
- package/README.zh.md +3 -2
- package/agents/worker.md +1 -1
- package/commands/cairn-install.md +2 -1
- package/commands/cairn-memory.md +1 -1
- package/commands/cairn-plan.md +2 -2
- package/commands/cairn-review.md +1 -1
- package/commands/cairn-work.md +3 -3
- package/docs/model-guidance/README.md +4 -4
- package/docs/model-guidance/claude.md +1 -1
- package/docs/model-guidance/codex.md +2 -2
- package/package.json +4 -2
- package/scripts/cairn-cleanup.mjs +12 -6
- package/scripts/cairn-goal-cli.mjs +233 -0
- package/scripts/cairn-goal-evidence.mjs +179 -0
- package/scripts/cairn-goal-operations-evidence.mjs +124 -0
- package/scripts/cairn-goal-operations-mutations.mjs +63 -0
- package/scripts/cairn-goal-operations.mjs +177 -0
- package/scripts/cairn-goal-policy.mjs +33 -0
- package/scripts/cairn-goal-state-validation.mjs +199 -0
- package/scripts/cairn-goal-state.mjs +89 -0
- package/scripts/cairn-goal.mjs +32 -1078
- package/scripts/cairn-lifecycle-commands.mjs +200 -0
- package/scripts/cairn-lifecycle-config.mjs +149 -0
- package/scripts/cairn-lifecycle-integrity.mjs +218 -0
- package/scripts/cairn-lifecycle-legacy.mjs +142 -0
- package/scripts/cairn-lifecycle-lock.mjs +205 -0
- package/scripts/cairn-lifecycle-mirror.mjs +91 -0
- package/scripts/cairn-lifecycle-staging.mjs +152 -0
- package/scripts/cairn-lifecycle-transaction-journal.mjs +210 -0
- package/scripts/cairn-lifecycle-transaction-targets.mjs +119 -0
- package/scripts/cairn-lifecycle-transaction.mjs +12 -0
- package/scripts/cairn-lifecycle.mjs +103 -1185
- package/scripts/cairn-paths.mjs +35 -5
- package/scripts/cairn-safe-fs.mjs +61 -35
- package/scripts/cairn-state-context.mjs +148 -0
- package/scripts/cairn-state-hook.mjs +24 -0
- package/scripts/cairn-state-init.mjs +137 -0
- package/scripts/cairn-state.mjs +6 -286
- package/scripts/cairn-toolcheck-detect.mjs +229 -0
- package/scripts/cairn-toolcheck-runtime.mjs +178 -0
- package/scripts/cairn-toolcheck.mjs +79 -424
- package/scripts/cairn.mjs +17 -3
- package/skills/cairn-memory/SKILL.md +1 -1
- package/skills/cairn-plan/SKILL.md +3 -3
- package/skills/cairn-review/SKILL.md +1 -1
- package/skills/cairn-work/SKILL.md +4 -4
- package/scripts/cairn-state.sh +0 -167
package/scripts/cairn-state.mjs
CHANGED
|
@@ -1,130 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
5
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import {
|
|
4
|
+
import { contextResult, foreignSessionContextResult, prependContext } from "./cairn-state-context.mjs";
|
|
5
|
+
import { continuationReason, stopAllowedMessage, stopHookOutput } from "./cairn-state-hook.mjs";
|
|
6
|
+
import { initializeProject } from "./cairn-state-init.mjs";
|
|
7
|
+
import { evaluateStop, isGoalOwnedBySession, reconcileWorktreeState } from "./cairn-goal.mjs";
|
|
7
8
|
import { resolveRepoRoot } from "./cairn-paths.mjs";
|
|
8
|
-
import { assertNoSymlinkComponents, safeMkdir, safeWriteFile } from "./cairn-safe-fs.mjs";
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
This file is a short index of persistent repository knowledge.
|
|
13
|
-
|
|
14
|
-
## Domain Knowledge
|
|
15
|
-
|
|
16
|
-
- Link detailed notes under \`docs/memory/\`.
|
|
17
|
-
|
|
18
|
-
## Policy
|
|
19
|
-
|
|
20
|
-
- Prefer precise repository exploration before implementation.
|
|
21
|
-
- Preserve proper nouns, file names, variable names, service names, alert names, MCP tool names, and agent names exactly as written.
|
|
22
|
-
- Read only the detailed memory files needed for the current task.
|
|
23
|
-
- Write user-visible responses and generated or updated documentation, plans, and memory artifacts in the OS locale unless the user asks for another language.
|
|
24
|
-
|
|
25
|
-
## Update Rules
|
|
26
|
-
|
|
27
|
-
- Keep root files short.
|
|
28
|
-
- Move detailed domain knowledge to \`docs/memory/<domain>.md\`.
|
|
29
|
-
- Record facts with source paths, commands, and observed behavior.
|
|
30
|
-
`;
|
|
31
|
-
|
|
32
|
-
const memoryTemplateKo = `# MEMORY
|
|
33
|
-
|
|
34
|
-
이 파일은 지속적으로 필요한 저장소 지식의 짧은 색인입니다.
|
|
35
|
-
|
|
36
|
-
## Domain Knowledge
|
|
37
|
-
|
|
38
|
-
- 자세한 기록은 \`docs/memory/\` 아래에 연결합니다.
|
|
39
|
-
|
|
40
|
-
## Policy
|
|
41
|
-
|
|
42
|
-
- 구현 전에는 저장소를 정확히 탐색합니다.
|
|
43
|
-
- 고유명사, 파일 이름, 변수 이름, 서비스 이름, 알림 이름, MCP 도구 이름, 에이전트 이름은 쓰인 그대로 보존합니다.
|
|
44
|
-
- 현재 작업에 필요한 상세 메모리 파일만 읽습니다.
|
|
45
|
-
- 사용자가 다른 언어를 요청하지 않는 한, 사용자에게 보이는 응답과 생성 또는 갱신하는 문서, 계획, 메모리 산출물은 OS locale 언어로 작성합니다.
|
|
46
|
-
|
|
47
|
-
## Update Rules
|
|
48
|
-
|
|
49
|
-
- 루트 파일은 짧게 유지합니다.
|
|
50
|
-
- 자세한 도메인 지식은 \`docs/memory/<domain>.md\`로 옮깁니다.
|
|
51
|
-
- 사실은 출처 경로, 명령, 관찰된 동작과 함께 기록합니다.
|
|
52
|
-
`;
|
|
53
|
-
|
|
54
|
-
const planTemplate = `# PLAN
|
|
55
|
-
|
|
56
|
-
This file is a short index of active and completed work plans.
|
|
57
|
-
|
|
58
|
-
## Active Plans
|
|
59
|
-
|
|
60
|
-
- Link detailed plans under \`docs/plan/\`.
|
|
61
|
-
|
|
62
|
-
## Completed Plans
|
|
63
|
-
|
|
64
|
-
- Move completed topics here with evidence links.
|
|
65
|
-
|
|
66
|
-
## Planning Rules
|
|
67
|
-
|
|
68
|
-
- Read project-root \`MEMORY.md\` first when it exists; if it is absent, continue without repository memory.
|
|
69
|
-
- For non-trivial implementation or continuation of planned work, first write or restore a plan with \`triage-plan\` active and synchronize it before exploration. Known-target Git/GitHub operations stay plan/goal-free unless they require code edits, conflict resolution, destructive recovery, release/deploy, or design.
|
|
70
|
-
- Plans must be decision-complete before implementation.
|
|
71
|
-
- Run complexity triage before applying agent, plugin, or delegated workflow guidance.
|
|
72
|
-
- Record the selected Light Path or Heavy Path and the checked Heavy Path signals in \`docs/plan/<topic>.md\`.
|
|
73
|
-
- Record request, planning, and code checkpoints. When the route changes before editing, synchronize the plan artifact, repository goal task roadmap through \`goal replan\`, and native UI plan. After editing starts, a new Heavy Path signal promotes Light Path to Heavy Path; stop further edits, mark affected evidence stale, synchronize all three roadmaps, and repeat the code checkpoint.
|
|
74
|
-
- Keep models inherited. Record requested/effective reasoning effort per task: Light planning/implementation/verification=\`medium\`; Heavy planning/review/implementation=\`high\`; final verification/review=\`xhigh\`. A route change also synchronizes the reasoning effort profile, preserving completed profiles and recalculating incomplete profiles. Unsupported host/value means effective=\`inherited\` with no model/global config change.
|
|
75
|
-
- Split implementation into small module tasks.
|
|
76
|
-
- Detect repository stack and required LSP/check tools before implementation.
|
|
77
|
-
- Record missing required tools and suggested commands. Run only pinned, supported installation steps after explicit user approval.
|
|
78
|
-
- Each task normally passes exactly two gates.
|
|
79
|
-
- Module acceptance verification.
|
|
80
|
-
- Surface integration verification.
|
|
81
|
-
- Run dry-run or check mode before external-state mutation when available.
|
|
82
|
-
- Write user-visible responses and generated or updated documentation, plans, and memory artifacts in the OS locale unless the user asks for another language.
|
|
83
|
-
- Use at most two verification passes per task by default.
|
|
84
|
-
- If a gate fails, diagnose once, shrink the task or split it into sub-tasks, and rerun both gates.
|
|
85
|
-
- After two failed passes, record the blocker in \`docs/plan/<topic>.md\`.
|
|
86
|
-
`;
|
|
87
|
-
|
|
88
|
-
const planTemplateKo = `# PLAN
|
|
89
|
-
|
|
90
|
-
이 파일은 진행 중이거나 완료된 작업 계획의 짧은 색인입니다.
|
|
91
|
-
|
|
92
|
-
## Active Plans
|
|
93
|
-
|
|
94
|
-
- 상세 계획은 \`docs/plan/\` 아래에 연결합니다.
|
|
95
|
-
|
|
96
|
-
## Completed Plans
|
|
97
|
-
|
|
98
|
-
- 완료된 주제는 증거 링크와 함께 이곳으로 옮깁니다.
|
|
99
|
-
|
|
100
|
-
## Planning Rules
|
|
101
|
-
|
|
102
|
-
- 프로젝트 루트 \`MEMORY.md\`가 있으면 먼저 읽고, 없으면 저장소 메모리 없이 계속 진행합니다.
|
|
103
|
-
- 비단순 구현 또는 계획된 작업 재개는 먼저 \`triage-plan\`이 active인 계획을 쓰거나 복원하고 탐색 전에 동기화합니다. 대상이 확정된 Git/GitHub 운영은 코드 수정·충돌 해결·파괴적 복구·릴리스/배포·설계가 필요하지 않으면 plan/goal 없이 실행합니다.
|
|
104
|
-
- 계획은 구현 전에 의사결정이 완료된 상태여야 합니다.
|
|
105
|
-
- 에이전트, 플러그인, 위임 워크플로 지침을 적용하기 전에 복잡도 트리아지를 실행합니다.
|
|
106
|
-
- 선택한 Light Path 또는 Heavy Path와 확인한 Heavy Path 신호를 \`docs/plan/<topic>.md\`에 기록합니다.
|
|
107
|
-
- 요청, 계획, 코드 체크포인트를 기록합니다. 편집 전에 경로가 바뀌면 plan artifact, 저장소 goal task roadmap, native UI plan을 동기화합니다. 편집 뒤 새 Heavy Path 신호가 나오면 추가 편집을 중단하고 관련 증거를 stale로 표시한 뒤 세 roadmap과 코드 체크포인트를 다시 맞춥니다.
|
|
108
|
-
- 모델은 상속하고 task별 requested/effective reasoning effort를 기록합니다. Light 계획/구현/검증은 \`medium\`, Heavy 계획/검토/구현은 \`high\`, 최종 검증/검토는 \`xhigh\`입니다. 경로 변경 시 reasoning effort profile도 동기화하고 완료 profile은 보존하며 미완료 profile은 재계산합니다. 미지원 host/value는 model/global config 변경 없이 effective=\`inherited\`입니다.
|
|
109
|
-
- 구현은 작은 모듈 작업으로 나눕니다.
|
|
110
|
-
- 구현 전에 저장소 스택과 필요한 LSP/check 도구를 감지합니다.
|
|
111
|
-
- 필요한 도구가 없으면 누락 상태와 제안 명령을 기록합니다. 명시적 사용자 승인 뒤에만 고정된 지원 설치 단계를 실행합니다.
|
|
112
|
-
- 각 작업은 보통 정확히 두 게이트를 통과합니다.
|
|
113
|
-
- 모듈 수용 검증.
|
|
114
|
-
- 표면 통합 검증.
|
|
115
|
-
- 외부 상태 변경 전에는 가능한 경우 dry-run 또는 check mode를 실행합니다.
|
|
116
|
-
- 사용자가 다른 언어를 요청하지 않는 한, 사용자에게 보이는 응답과 생성 또는 갱신하는 문서, 계획, 메모리 산출물은 OS locale 언어로 작성합니다.
|
|
117
|
-
- 기본적으로 작업당 검증은 최대 두 번만 수행합니다.
|
|
118
|
-
- 게이트가 실패하면 한 번 진단하고, 작업을 줄이거나 sub-task로 나눈 뒤 두 게이트를 다시 실행합니다.
|
|
119
|
-
- 두 번 실패한 뒤에는 blocker를 \`docs/plan/<topic>.md\`에 기록합니다.
|
|
120
|
-
`;
|
|
121
|
-
|
|
122
|
-
const contextLimits = {
|
|
123
|
-
goalTitle: 64,
|
|
124
|
-
roadmapTaskId: 24,
|
|
125
|
-
taskTitle: 40,
|
|
126
|
-
roadmapWindow: 3,
|
|
127
|
-
};
|
|
10
|
+
const recoverableGoalStatuses = new Set(["active", "paused", "blocked"]);
|
|
128
11
|
|
|
129
12
|
if (isCliEntry()) {
|
|
130
13
|
const event = process.argv[2] ?? "manual";
|
|
@@ -163,7 +46,7 @@ export async function runStateResult(event = "manual", {
|
|
|
163
46
|
if (event === "session-start" || event === "user-prompt-submit") {
|
|
164
47
|
const reconciled = await reconcileWorktreeState({ root: resolvedRoot });
|
|
165
48
|
const state = reconciled.state;
|
|
166
|
-
if (state?.goal?.status
|
|
49
|
+
if (recoverableGoalStatuses.has(state?.goal?.status) && !isGoalOwnedBySession(state, payload?.session_id)) {
|
|
167
50
|
return foreignSessionContextResult({ ko, event });
|
|
168
51
|
}
|
|
169
52
|
const result = contextResult({ ko, state, event });
|
|
@@ -211,169 +94,6 @@ export async function runStateResult(event = "manual", {
|
|
|
211
94
|
};
|
|
212
95
|
}
|
|
213
96
|
|
|
214
|
-
async function initializeProject(root, ko) {
|
|
215
|
-
await safeMkdir(root, "docs/memory");
|
|
216
|
-
await safeMkdir(root, "docs/plan");
|
|
217
|
-
await writeIfMissing(root, join(root, "MEMORY.md"), ko ? memoryTemplateKo : memoryTemplate);
|
|
218
|
-
await writeIfMissing(root, join(root, "PLAN.md"), ko ? planTemplateKo : planTemplate);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async function writeIfMissing(root, path, content) {
|
|
222
|
-
await assertNoSymlinkComponents(root, path);
|
|
223
|
-
try {
|
|
224
|
-
await readFile(path, "utf8");
|
|
225
|
-
} catch (error) {
|
|
226
|
-
if (error?.code !== "ENOENT") throw error;
|
|
227
|
-
try {
|
|
228
|
-
await safeWriteFile(root, path, content, { encoding: "utf8", flag: "wx" });
|
|
229
|
-
} catch (writeError) {
|
|
230
|
-
if (writeError?.code !== "EEXIST") throw writeError;
|
|
231
|
-
await assertNoSymlinkComponents(root, path, { allowMissing: false });
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function prependContext(result, notice) {
|
|
237
|
-
const specific = result.hookOutput?.hookSpecificOutput;
|
|
238
|
-
if (!specific) return { ...result, message: `${notice}\n${result.message}` };
|
|
239
|
-
return {
|
|
240
|
-
...result,
|
|
241
|
-
message: `${notice}\n${result.message}`,
|
|
242
|
-
hookOutput: {
|
|
243
|
-
...result.hookOutput,
|
|
244
|
-
hookSpecificOutput: {
|
|
245
|
-
...specific,
|
|
246
|
-
additionalContext: `${notice}\n${specific.additionalContext}`,
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function contextResult({ ko, state, event }) {
|
|
253
|
-
const base = ko ? "Cairn kernel: 루트 MEMORY.md는 선택 사항이며, 있으면 읽으세요." : "Cairn kernel: root MEMORY.md is optional; read it when present.";
|
|
254
|
-
const idlePolicy = event === "user-prompt-submit"
|
|
255
|
-
? (ko
|
|
256
|
-
? " 비단순 구현이나 계획된 작업 재개는 cairn-plan과 plan/task를 복원·생성하세요. 대상이 확정된 Git/GitHub 상태·fetch·checkout·merge·push·PR 작업은 코드 수정·충돌 해결·파괴적 복구·릴리스/배포·설계가 필요하지 않으면 plan/goal 없이 실행하세요. 상담·설명·계획 전용도 goal 없이 처리하세요."
|
|
257
|
-
: " For non-trivial implementation or planned-work continuation, load cairn-plan and restore/create its plan and task. Known-target Git/GitHub operations stay plan/goal-free unless they require code edits, conflict resolution, destructive recovery, release/deploy, or design. Consultation, explanation, and plan-only requests are also goal-free.")
|
|
258
|
-
: (ko
|
|
259
|
-
? " 중요 작업 전에 cairn-plan을 로드하고 active plan과 current task를 복원하세요."
|
|
260
|
-
: " Before non-trivial work, load cairn-plan and restore the active plan and current task.");
|
|
261
|
-
const failClosed = ko
|
|
262
|
-
? " active state와 그 skill·plan·required task reference가 없거나 읽을 수 없거나 일치하지 않을 때만 중단하세요."
|
|
263
|
-
: " Stop only if active state and its skill, plan, or required task ref is missing, unreadable, or inconsistent.";
|
|
264
|
-
if (!state || state.goal.status !== "active") return contextHookResult({ event, message: `${base}${idlePolicy}${failClosed}` });
|
|
265
|
-
const task = state.tasks.find((item) => item.status === "active")
|
|
266
|
-
?? state.tasks.find((item) => item.status === "pending")
|
|
267
|
-
?? state.tasks.find((item) => item.status === "blocked");
|
|
268
|
-
const planId = state.goal.planId;
|
|
269
|
-
const phase = task ? "cairn-work" : "cairn-review";
|
|
270
|
-
const activeHeader = ko
|
|
271
|
-
? `${base} ${phase}를 로드하고 정확한 plan ${planId}${task ? "과 아래 current task" : "의 완료 기준"}를 복원하세요. 압축·재시작·handoff·위임 뒤 다시 읽으세요.${failClosed}`
|
|
272
|
-
: `${base} Load ${phase}; restore exact plan ${planId}${task ? " and the current task below" : " completion criteria"}. Re-read after compaction/restart/handoff/delegation.${failClosed}`;
|
|
273
|
-
if (!task) {
|
|
274
|
-
return contextHookResult({
|
|
275
|
-
event,
|
|
276
|
-
message: [activeHeader, taskRoadmap(ko, state), activeGoalCompletionMessage(ko, state)].join("\n"),
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
const taskId = task.id;
|
|
280
|
-
const taskTitle = clipText(task.title, contextLimits.taskTitle);
|
|
281
|
-
const continuation = taskId.length > 48
|
|
282
|
-
? (ko
|
|
283
|
-
? `정확한 current task: ${taskId}. 바인딩된 증거 뒤에만 완료하세요.`
|
|
284
|
-
: `Exact task: ${taskId}. Complete after bound evidence.`)
|
|
285
|
-
: (ko
|
|
286
|
-
? `현재: ${taskId} (${taskTitle}). 바인딩된 증거 뒤에만 완료하세요.`
|
|
287
|
-
: `Current: ${taskId} (${taskTitle}). Complete only after bound evidence.`);
|
|
288
|
-
const resume = event === "user-prompt-submit"
|
|
289
|
-
? (taskId.length > 48
|
|
290
|
-
? (ko
|
|
291
|
-
? "곁가지 질문 뒤 일시정지·중단·전환 요청이 없으면 위의 정확한 task를 재개하세요."
|
|
292
|
-
: "Resume that exact task after side questions unless paused, stopped, or switched.")
|
|
293
|
-
: (ko
|
|
294
|
-
? `곁가지 질문 뒤 일시정지·중단·전환 요청이 없으면 ${taskId}를 재개하세요.`
|
|
295
|
-
: `After a side question, resume ${taskId} unless asked to pause, stop, or switch.`))
|
|
296
|
-
: "";
|
|
297
|
-
return contextHookResult({ event, message: [activeHeader, taskRoadmap(ko, state), continuation, resume].filter(Boolean).join("\n") });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function foreignSessionContextResult({ ko, event }) {
|
|
301
|
-
const message = ko
|
|
302
|
-
? "Cairn kernel: 루트 MEMORY.md는 선택 사항이며, 있으면 읽으세요. 저장소 goal을 다른 session이 소유합니다. cairn goal status로 상태를 확인하고 소유권이 해결될 때까지 Cairn 작업을 시작·수정·위임·완료하지 마세요. 이 capsule은 goal, plan, task 상세를 노출하지 않습니다."
|
|
303
|
-
: "Cairn kernel: root MEMORY.md is optional; read it when present. 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.";
|
|
304
|
-
return contextHookResult({ event, message });
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function taskRoadmap(ko, state) {
|
|
308
|
-
const formatTask = (task, index) => `${index + 1}. ${clipText(task.id, contextLimits.roadmapTaskId)} [${task.status}] ${clipText(task.title, contextLimits.taskTitle)}`;
|
|
309
|
-
if (state.tasks.length <= contextLimits.roadmapWindow) {
|
|
310
|
-
return `${ko ? "작업 단계" : "Work steps"}:\n${state.tasks.map(formatTask).join("\n")}`;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const currentIndex = Math.max(0, state.tasks.findIndex((task) => task.status === "active" || task.status === "pending"));
|
|
314
|
-
const counts = Object.fromEntries([...TASK_STATUS_NAMES].map((status) => [status, state.tasks.filter((task) => task.status === status).length]));
|
|
315
|
-
const countSummary = Object.entries(counts).filter(([, count]) => count > 0).map(([status, count]) => `${status} ${count}`).join(", ");
|
|
316
|
-
const omitted = state.tasks.length - 1;
|
|
317
|
-
const header = ko
|
|
318
|
-
? `작업 단계 (${state.tasks.length}개; ${countSummary})`
|
|
319
|
-
: `Work steps (${state.tasks.length}; ${countSummary})`;
|
|
320
|
-
const omission = ko ? `… ${omitted}개 단계 생략` : `… ${omitted} steps omitted`;
|
|
321
|
-
const currentPosition = currentIndex + 1;
|
|
322
|
-
return `${header}. ${ko ? `현재 위치 ${currentPosition}` : `Current position ${currentPosition}`}; ${omission}`;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function contextHookResult({ event, message }) {
|
|
326
|
-
return {
|
|
327
|
-
status: 0,
|
|
328
|
-
message,
|
|
329
|
-
hookOutput: {
|
|
330
|
-
hookSpecificOutput: {
|
|
331
|
-
hookEventName: event === "session-start" ? "SessionStart" : "UserPromptSubmit",
|
|
332
|
-
additionalContext: message,
|
|
333
|
-
},
|
|
334
|
-
},
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function continuationReason(reason, payload) {
|
|
339
|
-
const continuation = payload?.stop_hook_active ? " This turn was already continued; the active goal remains persisted for the next session." : "";
|
|
340
|
-
return `${reason}${continuation}`;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function stopHookOutput(gate, payload) {
|
|
344
|
-
if (!gate.block) return { continue: true };
|
|
345
|
-
const reason = continuationReason(gate.reason, payload);
|
|
346
|
-
if (payload?.stop_hook_active) {
|
|
347
|
-
return { continue: true, systemMessage: reason };
|
|
348
|
-
}
|
|
349
|
-
return { decision: "block", reason };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function stopAllowedMessage({ ko, state, event }) {
|
|
353
|
-
if (state?.goal?.status) {
|
|
354
|
-
return ko
|
|
355
|
-
? `Cairn 종료 게이트: 목표 상태가 ${state.goal.status}이므로 종료를 허용합니다.`
|
|
356
|
-
: `Cairn stop gate: goal status is ${state.goal.status}; allowing stop.`;
|
|
357
|
-
}
|
|
358
|
-
return ko
|
|
359
|
-
? `${event === "subagent-stop" ? "서브에이전트" : "작업"} 종료 게이트: 활성 Cairn 목표가 없어 종료를 허용합니다.`
|
|
360
|
-
: `${event === "subagent-stop" ? "Subagent" : "Turn"} stop gate: no active Cairn goal; allowing stop.`;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function activeGoalCompletionMessage(ko, state) {
|
|
364
|
-
const goalTitle = clipText(state.goal.title, contextLimits.goalTitle);
|
|
365
|
-
return ko
|
|
366
|
-
? `활성 목표 "${goalTitle}"의 모든 task가 완료되었습니다. 완료 기준을 확인하고 명시적으로 complete 상태로 전이하세요.`
|
|
367
|
-
: `All tasks for active goal "${goalTitle}" are complete. Verify criteria and explicitly transition the goal to completed.`;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const TASK_STATUS_NAMES = ["active", "pending", "blocked", "completed"];
|
|
371
|
-
|
|
372
|
-
function clipText(value, maxLength) {
|
|
373
|
-
const text = String(value ?? "");
|
|
374
|
-
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
97
|
function resolveRoot(root, payload) {
|
|
378
98
|
return resolveRepoRoot({
|
|
379
99
|
explicitRoot: root ?? null,
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
import { commandOk } from "./cairn-toolcheck-runtime.mjs";
|
|
4
|
+
|
|
5
|
+
export async function collectEntries(base) {
|
|
6
|
+
const output = [];
|
|
7
|
+
async function walk(dir) {
|
|
8
|
+
for (const entry of await readdir(dir)) {
|
|
9
|
+
if ([".git", ".cairn", "node_modules", "vendor", "target", ".venv", "__pycache__"].includes(entry)) continue;
|
|
10
|
+
const path = join(dir, entry);
|
|
11
|
+
const relativePath = normalizePath(relative(base, path));
|
|
12
|
+
if (relativePath === "test/fixtures" || relativePath.startsWith("test/fixtures/")) continue;
|
|
13
|
+
const info = await lstat(path);
|
|
14
|
+
if (info.isSymbolicLink()) continue;
|
|
15
|
+
if (info.isDirectory()) await walk(path);
|
|
16
|
+
else output.push(relativePath);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
await walk(base);
|
|
20
|
+
return output;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function detectStacks(paths) {
|
|
24
|
+
const stacks = new Set();
|
|
25
|
+
if (hasBasename(paths, "package.json")) stacks.add("javascript");
|
|
26
|
+
if (hasBasename(paths, "tsconfig.json") || paths.some((path) => path.endsWith(".ts") || path.endsWith(".tsx"))) stacks.add("typescript");
|
|
27
|
+
if (hasBasename(paths, "pyproject.toml") || paths.some((path) => path.endsWith(".py"))) stacks.add("python");
|
|
28
|
+
if (hasBasename(paths, "composer.json") || paths.some((path) => path.endsWith(".php"))) stacks.add("php");
|
|
29
|
+
if (hasBasename(paths, "pom.xml", "build.gradle", "build.gradle.kts") || paths.some((path) => path.endsWith(".java"))) stacks.add("java");
|
|
30
|
+
if (paths.some((path) => path.endsWith(".kt") || path.endsWith(".kts"))) stacks.add("kotlin");
|
|
31
|
+
if (hasBasename(paths, "Package.swift") || paths.some((path) => hasPathSegmentWithSuffix(path, ".xcodeproj", ".xcworkspace") || path.endsWith(".swift"))) stacks.add("swift");
|
|
32
|
+
if (hasBasename(paths, "go.mod") || paths.some((path) => path.endsWith(".go"))) stacks.add("go");
|
|
33
|
+
if (hasBasename(paths, "Cargo.toml") || paths.some((path) => path.endsWith(".rs"))) stacks.add("rust");
|
|
34
|
+
return [...stacks];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildRequirements(stacks, entries = [], _commandAvailable = commandOk, platform = process.platform) {
|
|
38
|
+
const requirements = [];
|
|
39
|
+
if (stacks.includes("javascript")) {
|
|
40
|
+
requirements.push(unsafeReq("node", ["--version"], "JavaScript runtime for package scripts and CLI checks", "automatic runtime installation is unavailable"));
|
|
41
|
+
const manager = packageManager(entries);
|
|
42
|
+
requirements.push(unsafeReq(manager, ["--version"], `JavaScript package manager selected from ${packageManagerSource(entries)}`, "automatic package-manager installation is unavailable"));
|
|
43
|
+
}
|
|
44
|
+
if (stacks.includes("typescript")) {
|
|
45
|
+
requirements.push(unsafeReq("typescript-language-server", ["--version"], "TypeScript LSP server", "automatic package installation is disabled without pinned package versions"));
|
|
46
|
+
requirements.push(unsafeReq("tsc", ["--version"], "TypeScript compiler verification", "automatic package installation is disabled without pinned package versions"));
|
|
47
|
+
}
|
|
48
|
+
if (stacks.includes("python")) {
|
|
49
|
+
requirements.push(unsafeReq("basedpyright", ["--version"], "Python LSP/type checker", "automatic Python package installation is disabled without pinned package versions"));
|
|
50
|
+
requirements.push(unsafeReq("ruff", ["--version"], "Python lint and format checker", "automatic Python package installation is disabled without pinned package versions"));
|
|
51
|
+
}
|
|
52
|
+
if (stacks.includes("php")) {
|
|
53
|
+
const composerProject = hasBasename(entries, "composer.json");
|
|
54
|
+
requirements.push(req("php", ["--version"], "PHP runtime for Composer scripts and CLI checks"));
|
|
55
|
+
if (composerProject) requirements.push(req("composer", ["--version"], "PHP package manager for project-local tool installation"));
|
|
56
|
+
requirements.push(unsafeReq("phpactor", ["--version"], "PHP LSP server", "automatic Composer package installation is disabled without pinned package versions"));
|
|
57
|
+
requirements.push(unsafeReq("phpstan", ["--version"], "PHP static analysis verification", "automatic Composer package installation is disabled without pinned package versions"));
|
|
58
|
+
requirements.push(unsafeReq("php-cs-fixer", ["--version"], "PHP formatting verification", "automatic Composer package installation is disabled without pinned package versions"));
|
|
59
|
+
}
|
|
60
|
+
if (stacks.includes("java")) {
|
|
61
|
+
addJvmRequirements(requirements);
|
|
62
|
+
requirements.push(unsafeReq("jdtls", ["--version"], "Java LSP server", "checksum-free JDTLS latest downloads are disabled"));
|
|
63
|
+
addJvmBuildToolRequirements(requirements, entries);
|
|
64
|
+
}
|
|
65
|
+
if (stacks.includes("kotlin")) {
|
|
66
|
+
addJvmRequirements(requirements);
|
|
67
|
+
requirements.push(req("kotlin-lsp.sh", ["--help"], "Kotlin LSP server"));
|
|
68
|
+
requirements.push(req("kotlinc", ["-version"], "Kotlin compiler verification"));
|
|
69
|
+
addJvmBuildToolRequirements(requirements, entries);
|
|
70
|
+
}
|
|
71
|
+
if (stacks.includes("swift")) {
|
|
72
|
+
requirements.push(req("swift", ["--version"], "Swift toolchain for build and test commands"));
|
|
73
|
+
requirements.push(req("sourcekit-lsp", ["--version"], "Swift LSP server"));
|
|
74
|
+
if (entries.some((path) => hasPathSegmentWithSuffix(path, ".xcodeproj", ".xcworkspace"))) {
|
|
75
|
+
requirements.push(req("xcodebuild", ["-version"], "Xcode build verification"));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (stacks.includes("go")) {
|
|
79
|
+
requirements.push(unsafeReq("gopls", ["version"], "Go LSP server", "unpinned go install targets are disabled"));
|
|
80
|
+
requirements.push(unsafeReq("golangci-lint", ["--version"], "Go lint verification", "unpinned go install targets are disabled"));
|
|
81
|
+
}
|
|
82
|
+
if (stacks.includes("rust")) {
|
|
83
|
+
requirements.push(unsafeReq("rust-analyzer", ["--version"], "Rust LSP server", "automatic rustup changes are disabled without a pinned toolchain manifest"));
|
|
84
|
+
requirements.push(unsafeReq("cargo", ["clippy", "--version"], "Rust clippy verification", "automatic rustup changes are disabled without a pinned toolchain manifest"));
|
|
85
|
+
}
|
|
86
|
+
return requirements;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function addJvmRequirements(requirements) {
|
|
90
|
+
if (!requirements.some((requirement) => requirement.name === "java")) {
|
|
91
|
+
requirements.push(req("java", ["--version"], "JVM runtime for build and test commands"));
|
|
92
|
+
}
|
|
93
|
+
if (!requirements.some((requirement) => requirement.name === "javac")) {
|
|
94
|
+
requirements.push(req("javac", ["--version"], "JVM compiler verification"));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function addJvmBuildToolRequirements(requirements, entries = []) {
|
|
99
|
+
const gradleWindows = findBasename(entries, "gradlew.bat");
|
|
100
|
+
const gradleUnix = findBasename(entries, "gradlew");
|
|
101
|
+
if (gradleWindows) pushUnique(requirements, req(repositoryCommand(gradleWindows), ["--version"], "Gradle wrapper availability", null, { localOnly: true }));
|
|
102
|
+
else if (gradleUnix) pushUnique(requirements, req(repositoryCommand(gradleUnix), ["--version"], "Gradle wrapper availability", null, { localOnly: true }));
|
|
103
|
+
else if (hasBasename(entries, "build.gradle", "build.gradle.kts")) pushUnique(requirements, req("gradle", ["--version"], "Gradle availability"));
|
|
104
|
+
const mavenWindows = findBasename(entries, "mvnw.cmd", "mvnw.bat");
|
|
105
|
+
const mavenUnix = findBasename(entries, "mvnw");
|
|
106
|
+
if (mavenWindows) pushUnique(requirements, req(repositoryCommand(mavenWindows), ["--version"], "Maven wrapper availability", null, { localOnly: true }));
|
|
107
|
+
else if (mavenUnix) pushUnique(requirements, req(repositoryCommand(mavenUnix), ["--version"], "Maven wrapper availability", null, { localOnly: true }));
|
|
108
|
+
else if (hasBasename(entries, "pom.xml")) pushUnique(requirements, req("mvn", ["--version"], "Maven availability"));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function pushUnique(requirements, requirement) {
|
|
112
|
+
if (!requirements.some((candidate) => candidate.name === requirement.name)) requirements.push(requirement);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Kept for API compatibility. Callers may show these commands, but buildRequirements
|
|
116
|
+
// no longer attaches an unpinned command to an automatically executable requirement.
|
|
117
|
+
export function composerInstall(packages) {
|
|
118
|
+
return { command: "composer", args: ["require", "--dev", ...packages] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function pythonInstall(uv, project, tool, platform = process.platform) {
|
|
122
|
+
if (uv && project) return { command: "uv", args: ["add", "--dev", tool] };
|
|
123
|
+
if (uv) return { command: "uv", args: ["tool", "install", tool] };
|
|
124
|
+
if (platform === "win32") return { command: "py", args: ["-3", "-m", "pip", "install", "--user", tool] };
|
|
125
|
+
return { command: "python3", args: ["-m", "pip", "install", "--user", tool] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function jdtlsInstall(_platform = process.platform) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function req(command, args, reason, installCommand = null, options = {}) {
|
|
133
|
+
return {
|
|
134
|
+
name: command,
|
|
135
|
+
command,
|
|
136
|
+
args,
|
|
137
|
+
reason,
|
|
138
|
+
install: installCommand,
|
|
139
|
+
installUnavailableReason: options.installUnavailableReason ?? null,
|
|
140
|
+
localOnly: options.localOnly ?? false,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function unsafeReq(command, args, reason, installUnavailableReason) {
|
|
145
|
+
return req(command, args, reason, null, { installUnavailableReason });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function packageManager(entries = []) {
|
|
149
|
+
return selectPackageManager(entries).manager;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function packageManagerSource(entries = []) {
|
|
153
|
+
const selected = selectPackageManager(entries);
|
|
154
|
+
return selected.path ?? "the npm default (no lockfile found)";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function installDev(pm, packages) {
|
|
158
|
+
if (pm === "bun") return { command: "bun", args: ["add", "-d", ...packages] };
|
|
159
|
+
if (pm === "pnpm") return { command: "pnpm", args: ["add", "-D", ...packages] };
|
|
160
|
+
if (pm === "yarn") return { command: "yarn", args: ["add", "-D", ...packages] };
|
|
161
|
+
return { command: "npm", args: ["install", "--save-dev", ...packages] };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizePath(path) {
|
|
165
|
+
return path.split(sep).join("/");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function pathBasename(path) {
|
|
169
|
+
const normalized = normalizePath(path);
|
|
170
|
+
return normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hasBasename(paths, ...names) {
|
|
174
|
+
const expected = new Set(names);
|
|
175
|
+
return paths.some((path) => expected.has(pathBasename(path)));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function findBasename(paths, ...names) {
|
|
179
|
+
const expected = new Set(names);
|
|
180
|
+
return [...paths]
|
|
181
|
+
.filter((path) => expected.has(pathBasename(path)))
|
|
182
|
+
.sort(compareRepositoryPaths)[0] ?? null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function hasPathSegmentWithSuffix(path, ...suffixes) {
|
|
186
|
+
return normalizePath(path)
|
|
187
|
+
.split("/")
|
|
188
|
+
.some((segment) => suffixes.some((suffix) => segment.endsWith(suffix)));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function repositoryCommand(path) {
|
|
192
|
+
const normalized = normalizePath(path);
|
|
193
|
+
return normalized.startsWith("./") ? normalized : `./${normalized}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function selectPackageManager(entries) {
|
|
197
|
+
const lockfiles = [
|
|
198
|
+
{ manager: "bun", names: new Set(["bun.lock", "bun.lockb"]), priority: 0 },
|
|
199
|
+
{ manager: "pnpm", names: new Set(["pnpm-lock.yaml"]), priority: 1 },
|
|
200
|
+
{ manager: "yarn", names: new Set(["yarn.lock"]), priority: 2 },
|
|
201
|
+
{ manager: "npm", names: new Set(["package-lock.json", "npm-shrinkwrap.json"]), priority: 3 },
|
|
202
|
+
];
|
|
203
|
+
const candidates = [];
|
|
204
|
+
|
|
205
|
+
for (const path of entries) {
|
|
206
|
+
const normalized = normalizePath(path);
|
|
207
|
+
const name = pathBasename(normalized);
|
|
208
|
+
const match = lockfiles.find(({ names }) => names.has(name));
|
|
209
|
+
if (!match) continue;
|
|
210
|
+
candidates.push({
|
|
211
|
+
manager: match.manager,
|
|
212
|
+
path: normalized,
|
|
213
|
+
depth: normalized.split("/").length - 1,
|
|
214
|
+
priority: match.priority,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
candidates.sort((left, right) => left.depth - right.depth
|
|
219
|
+
|| left.priority - right.priority
|
|
220
|
+
|| left.path.localeCompare(right.path));
|
|
221
|
+
return candidates[0] ?? { manager: "npm", path: null };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function compareRepositoryPaths(left, right) {
|
|
225
|
+
const leftPath = normalizePath(left);
|
|
226
|
+
const rightPath = normalizePath(right);
|
|
227
|
+
const depth = leftPath.split("/").length - rightPath.split("/").length;
|
|
228
|
+
return depth || leftPath.localeCompare(rightPath);
|
|
229
|
+
}
|