cairn-ai 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/workflows/cairn-memory.md +2 -2
- package/.agents/workflows/cairn-plan.md +3 -3
- package/.agents/workflows/cairn-review.md +2 -2
- package/.agents/workflows/cairn-work.md +4 -4
- 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 +16 -8
- package/README.md +14 -8
- package/README.pt.md +3 -2
- package/README.zh.md +3 -2
- package/agents/worker.md +3 -1
- package/commands/cairn-install.md +2 -1
- package/commands/cairn-memory.md +2 -2
- package/commands/cairn-plan.md +3 -3
- package/commands/cairn-review.md +2 -2
- package/commands/cairn-work.md +4 -4
- package/docs/model-guidance/README.md +5 -5
- package/docs/model-guidance/claude.md +1 -1
- package/docs/model-guidance/codex.md +3 -3
- package/package.json +4 -2
- package/scripts/cairn-cleanup.mjs +12 -6
- package/scripts/cairn-goal-cli.mjs +239 -0
- package/scripts/cairn-goal-evidence.mjs +186 -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 +187 -0
- package/scripts/cairn-goal-policy.mjs +36 -0
- package/scripts/cairn-goal-state-validation.mjs +199 -0
- package/scripts/cairn-goal-state.mjs +124 -0
- package/scripts/cairn-goal.mjs +33 -1078
- package/scripts/cairn-lifecycle-commands.mjs +200 -0
- package/scripts/cairn-lifecycle-config.mjs +153 -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 +156 -0
- package/scripts/cairn-state-hook.mjs +26 -0
- package/scripts/cairn-state-init.mjs +137 -0
- package/scripts/cairn-state.mjs +32 -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 +3 -2
- package/skills/cairn-plan/SKILL.md +7 -4
- package/skills/cairn-review/SKILL.md +4 -4
- package/skills/cairn-work/SKILL.md +23 -7
- package/scripts/cairn-state.sh +0 -167
package/commands/cairn-review.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Use the `cairn-review` skill.
|
|
4
4
|
|
|
5
|
-
Prerequisite: resolve Cairn from its installed runtime locator. If
|
|
5
|
+
Prerequisite: resolve Cairn from its installed runtime locator. If the shared runtime or locator is missing, unreadable, or inconsistent, stop every dependent mirror; run published/global `cairn doctor`, then `cairn upgrade` to recover it. Never repair through another mirror or look for Cairn runtime files in the target repository.
|
|
6
6
|
|
|
7
7
|
Goal: review completed work against plan, memory, policy, and evidence.
|
|
8
8
|
|
|
9
9
|
Procedure:
|
|
10
10
|
|
|
11
11
|
1. Every agent reads project-root `MEMORY.md` when present and continues without repository memory when absent before review work, then reads the plan task and available memory inputs.
|
|
12
|
-
2. Check changed files, tool readiness evidence, and verification evidence.
|
|
12
|
+
2. Check changed files, tool readiness evidence, and verification evidence. Inspect test, fixture, and verification-command diffs and reject any weakening without a recorded plan reason. For defect fixes, confirm the same-rule sweep was performed and its result recorded.
|
|
13
13
|
3. Delegate independent read-only review to `explorer` when it materially improves speed or quality; use `worker` only for scoped verification reruns or QA artifacts. When subagent tools are available, each agent may recursively delegate bounded sub-tasks to subagents. Tell every delegated agent and child subagent to read the project-root `MEMORY.md` before work, keep scope, and preserve others' edits.
|
|
14
14
|
4. Report findings first, ordered by severity, with file and line references. If no issues are found, say so clearly and mention remaining test gaps or residual risk.
|
|
15
15
|
5. Record review evidence in `docs/plan/<topic>.md` and as the goal-level final review evidence record before completing the goal.
|
package/commands/cairn-work.md
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Use the `cairn-work` skill.
|
|
4
4
|
|
|
5
|
-
Prerequisite: resolve the installed Cairn runtime from the plugin or skill locator. If
|
|
5
|
+
Prerequisite: resolve the installed Cairn runtime from the plugin or skill locator. If the shared runtime or locator is missing, unreadable, or inconsistent, stop every dependent mirror; run published/global `cairn doctor`, then `cairn upgrade` to recover it. Never repair through another mirror or look for Cairn scripts or model guidance in the target repository.
|
|
6
6
|
|
|
7
7
|
Goal: execute the active goal's current task without bypassing complexity triage, then keep advancing tasks until goal-level final review completes or the goal is explicitly paused, blocked, or cancelled.
|
|
8
8
|
|
|
9
9
|
Require three complexity records: the provisional request checkpoint, post-exploration planning checkpoint, and code checkpoint after exact file/caller/test inspection immediately before the first edit. Before editing, evidence may change either route. Every change must synchronize the plan artifact, repository goal task roadmap through `goal replan`, and native UI plan, including reviews and required evidence. After editing begins, 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.
|
|
10
10
|
|
|
11
|
-
Models always inherit. Route reasoning effort per task: Light planning/implementation/verification=`medium`; Heavy planning/review/implementation=`high`; final verification/review=`xhigh`.
|
|
11
|
+
Models always inherit. Route reasoning effort per task: Light planning/implementation/verification=`medium`; Heavy planning/review/implementation=`high`; final verification/review=`xhigh`. Requested is Cairn's desired profile; Effective records the dispatch result. Effective=`inherited` means no task override was confirmed, not that the host is incapable. Claim host capability only from its exposed interface or dispatch response, and distinguish Cairn non-integration from host non-support. Only a new task/worker may receive requested effort through a supported host-native option; never change model/global config. Route changes synchronize the plan artifact, repository goal task roadmap, native UI plan, and reasoning effort profile; preserve completed profiles and recalculate incomplete profiles.
|
|
12
12
|
|
|
13
13
|
Procedure:
|
|
14
14
|
|
|
15
15
|
1. Every agent reads project-root `MEMORY.md` when present and continues without repository memory when absent before selecting or executing a task.
|
|
16
16
|
2. Read `PLAN.md`, the detailed plan, and relevant memory notes.
|
|
17
17
|
3. Resolve and read the `cairn://` Codex or Claude model guidance recorded in the plan through the installed runtime.
|
|
18
|
-
4. Read
|
|
18
|
+
4. Read the current task with `goal task status` (full `goal status` only when receipts are needed) and select only that task. Fold a new request or steer that arrives during an unfinished goal into it with `goal replan` instead of cancelling it, and keep advancing to goal final review. If implementation was requested but no goal exists, create it from the decision-complete plan.
|
|
19
19
|
5. Confirm required tool readiness. If a required LSP/check tool is missing, report it and obtain explicit user approval before `toolcheck --install --yes` or a repository-native install command.
|
|
20
20
|
6. Confirm the complexity triage and selected Light/Heavy Path recorded in the plan. If missing, update the plan before mutating files.
|
|
21
21
|
7. Before implementation, spend reasoning on a focused executable test contract covering requirements, invariants, boundaries, and failure modes. Confirm the intended failure, then constrain implementation to the minimum change that passes it.
|
|
@@ -23,7 +23,7 @@ Procedure:
|
|
|
23
23
|
9. Give implementation the test contract, failing evidence, exact file ownership, and constraints. Require minimal implementation and no repeated broad discovery. Preserve status, final-report, close-and-review, recursive bounded delegation, and main-agent fallback rules.
|
|
24
24
|
10. Treat tool exit codes and bounded summaries as authoritative. Keep success output concise; expand only the failing test and related code context.
|
|
25
25
|
11. For Heavy Path, run the recorded automated test command and record explicit `Tests:` evidence.
|
|
26
|
-
12. Run focused module acceptance, then one final surface integration check through `goal verify -- ...`. Verification defaults to 600,000 ms (10 minutes), may be shortened with `--timeout-ms`, and is capped at 3,600,000 ms (1 hour). The command runs without holding the state lock; evidence is recorded only if the starting goal/task identity and the pre/post watched fingerprint still match.
|
|
26
|
+
12. Run focused module acceptance, then one final surface integration check through `goal verify -- ...`. Verification defaults to 600,000 ms (10 minutes), may be shortened with `--timeout-ms`, and is capped at 3,600,000 ms (1 hour). The command runs without holding the state lock; evidence is recorded only if the starting goal/task identity and the pre/post watched fingerprint still match. The default Git fingerprint excludes ignored files, so explicitly `--watch` every ignored artifact needed by the claim. Never edit, weaken, skip, or delete existing tests, fixtures, or verification commands to make a gate pass unless the plan records the contract change. Reproduce reported defects before fixing, test one hypothesis at a time with a measurement, and after a fix search for other implementations of the same rule or pattern.
|
|
27
27
|
13. Before package verification, inspect package lifecycle scripts. Run normal `npm pack --dry-run` by default. Content-producing or unknown lifecycle scripts must never use `--ignore-scripts`; only absent or proven content-neutral scripts may use it when the prior full check remains fresh.
|
|
28
28
|
14. Record evidence at the semantic boundary that proves the claimed behavior, bound to goal/task/plan. Missing, failed, skipped, stale, or placeholder evidence never completes work; relevant mutation invalidates affected evidence.
|
|
29
29
|
15. Complete the task and continue the goal. Use `--quiet` on successful state mutations; workers report only their assigned task and never select the next one.
|
|
@@ -7,14 +7,14 @@ The goal is not to make prompts long by model. The goal is to stabilize the same
|
|
|
7
7
|
## Application Order
|
|
8
8
|
|
|
9
9
|
1. Identify the active model name. If the model name is not directly exposed, infer it from the user's default model setting and agent role frontmatter.
|
|
10
|
-
2. Resolve this installed guidance root from the active skill's `references/cairn-runtime.json` or the source skill's location. Never look for Cairn model guidance in the target repository.
|
|
10
|
+
2. Resolve this installed guidance root from the active skill's `references/cairn-runtime.json` or the source skill's location. Never look for Cairn model guidance in the target repository. If the shared runtime or locator is missing, unreadable, or inconsistent, stop using every dependent mirror; run published/global `cairn doctor`, then `cairn upgrade` for recovery, rather than repairing through another mirror.
|
|
11
11
|
3. For Claude-family models, apply `cairn://docs/model-guidance/claude.md`.
|
|
12
12
|
4. For Codex-family models, apply `cairn://docs/model-guidance/codex.md`.
|
|
13
13
|
5. If the model is not clearly classified, apply only the common rules and skip model-specific adjustment.
|
|
14
14
|
|
|
15
15
|
## Common Rules
|
|
16
16
|
|
|
17
|
-
- On every fresh entry, compaction recovery, restart, delegation, or handoff, restore context in this order: optional root `MEMORY.md` → current phase skill → active plan → current-task references → model guidance recorded by the plan. If `MEMORY.md` is absent, continue without repository memory and do not invoke another memory service. If persisted state or an assignment requires another reference that is missing, unreadable, or inconsistent, fail closed: do not edit, delegate, approve, or complete; report or record a blocker.
|
|
17
|
+
- On every fresh entry, compaction recovery, restart, delegation, or handoff, restore context in this order: optional root `MEMORY.md` → current phase skill → active plan → current-task references → model guidance recorded by the plan. If `MEMORY.md` is absent, continue without repository memory and do not invoke another memory service. If persisted state or an assignment requires another reference that is missing, unreadable, or inconsistent, fail closed: do not edit, delegate, approve, or complete; report or record a blocker. Delegation means the delegated agent restores on its own entry; the orchestrator does not re-read this chain after dispatching or collecting a subagent, and within one unbroken context it rereads only references that changed, which keeps the cached prompt prefix stable.
|
|
18
18
|
- Keep root `MEMORY.md` and `PLAN.md` as indexes only.
|
|
19
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/...`.
|
|
20
20
|
- Treat the user-home `.cairn/projects/<project-id>/worktrees/<worktree-id>/state.json` as a worktree-bound active-work slot. State without `worktreeId` and state copied into another worktree are stale and are removed on the next hook/goal start. Successful completion or cancellation removes the owning copy after plan evidence is recorded; only paused or blocked work remains for resumption.
|
|
@@ -32,7 +32,7 @@ The goal is not to make prompts long by model. The goal is to stabilize the same
|
|
|
32
32
|
- Treat missing, failed, skipped, stale, or placeholder evidence as failure. Task evidence records must be bound to the current goal, task, and plan.
|
|
33
33
|
- Treat the repository Cairn state as the fail-closed transition authority. Advance the matching Codex UI plan step only after the evidence-gated repository task transition succeeds.
|
|
34
34
|
- Spend reasoning on a focused executable test contract before implementation: requirements, invariants, boundaries, and failure modes. Give implementation only that contract, failing evidence, file scope, and constraints; require the minimum passing change.
|
|
35
|
-
- Run verification through `goal verify -- ...`; its default timeout is 600,000 ms and its maximum is 3,600,000 ms. Treat tool exit codes and bounded machine summaries as authoritative. Record evidence only when the starting goal/task identity and pre/post watched fingerprint still match, and choose the watch set and command at the semantic boundary that proves the claim. Compress success output and expand context only for failing tests and related code.
|
|
35
|
+
- Run verification through `goal verify -- ...`; its default timeout is 600,000 ms and its maximum is 3,600,000 ms. Treat tool exit codes and bounded machine summaries as authoritative. Record evidence only when the starting goal/task identity and pre/post watched fingerprint still match, and choose the watch set and command at the semantic boundary that proves the claim. The default Git fingerprint excludes ignored files; explicitly pass every ignored artifact needed by the claim with `--watch`. Compress success output and expand context only for failing tests and related code.
|
|
36
36
|
- Use a verification ladder: focused contract tests, one full check after the final change, then package dry-run while the full check remains fresh. Inspect package lifecycle scripts first and run normal `npm pack --dry-run` by default. Content-producing or unknown lifecycle scripts must never use `--ignore-scripts`; only absent or proven content-neutral scripts may use it.
|
|
37
37
|
- Every implementation task must pass module acceptance verification and surface integration verification.
|
|
38
38
|
- Before mutating external state, run the closest available dry-run, check, plan, diff, validate, or equivalent command.
|
|
@@ -53,9 +53,9 @@ The goal is not to make prompts long by model. The goal is to stabilize the same
|
|
|
53
53
|
- Models always inherit the host or user default; Cairn never selects or overrides a model.
|
|
54
54
|
- Light Path planning, implementation, and verification request `medium`.
|
|
55
55
|
- Heavy Path planning, review, and implementation request `high`; final verification and review request `xhigh`.
|
|
56
|
-
- Every plan task records `Requested reasoning effort` and `Effective reasoning effort`.
|
|
56
|
+
- Every plan task records `Requested reasoning effort` and `Effective reasoning effort`. Requested is Cairn's desired profile; Effective is the value confirmed by the dispatch result.
|
|
57
57
|
- Only a newly dispatched task or worker may receive the requested value, and only through a host-exposed reasoning-effort option or host-native equivalent. Omit every model override.
|
|
58
|
-
- For an unsupported host or value,
|
|
58
|
+
- For an unsupported host or value, use `Effective reasoning effort: inherited`; this means no task-level override was confirmed and does not prove the host lacks the capability. Describe host capability only from an exposed interface or dispatch result, and distinguish Cairn's choice not to integrate or use an option from host non-support. Do not change the model or global config and do not silently choose a nearby value.
|
|
59
59
|
- A route change synchronizes the plan artifact, repository goal task roadmap through `goal replan`, native UI plan, and reasoning effort profile before edits resume. Preserve completed task profiles as audit history and recalculate incomplete task profiles for the new path.
|
|
60
60
|
|
|
61
61
|
## Delegation Defaults
|
|
@@ -18,7 +18,7 @@ Claude-family models are strong at long-context retention, policy interpretation
|
|
|
18
18
|
- Do not skip the two verification gates even on Light Path.
|
|
19
19
|
- Do not approve a plan that lacks dry-run or check evidence for external-state-changing work.
|
|
20
20
|
- Do not allow open-ended verification loops. Require a blocker or sub-task split after two failed passes.
|
|
21
|
-
- Keep the model inherited.
|
|
21
|
+
- Keep the model inherited. `Requested reasoning effort` is Cairn's desired profile; `Effective reasoning effort` is what the dispatch response confirms. Apply the requested value only to a newly dispatched task or worker through a host-native task/subagent option, never a model override. For an unsupported host or value, record Effective `inherited`; this means no task override was confirmed, not proof of missing host capability. Infer capability only from the exposed interface or dispatch response, distinguish Cairn non-integration from host non-support, and make no model/global configuration change.
|
|
22
22
|
- On Heavy Path, check plan gaps before implementation.
|
|
23
23
|
- Review all three complexity records: the provisional request checkpoint, post-exploration planning checkpoint, and code checkpoint after exact file/caller/test inspection immediately before the first edit. Before editing, evidence may change either route. Every change must synchronize the plan artifact, repository goal task roadmap through `goal replan`, and native UI plan, including reviews and required evidence. After editing begins, a new Heavy signal promotes Light Path to Heavy Path: stop further edits, mark affected evidence stale, synchronize all three roadmaps, and repeat the code checkpoint.
|
|
24
24
|
- Before asking the user, use repository evidence and available `explorer`/`worker` delegation when it materially improves speed or quality.
|
|
@@ -17,9 +17,9 @@ 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 optional root `MEMORY.md` → current phase skill → active plan → current-task references → recorded model guidance. Missing `MEMORY.md` never blocks and must not trigger another memory service. If persisted state or the assignment requires another missing, unreadable, or inconsistent reference, fail closed and report a blocker before editing, delegating, approving, or completing.
|
|
20
|
+
- On every fresh entry, compaction recovery, restart, delegation, or handoff, restore optional root `MEMORY.md` → current phase skill → active plan → current-task references → recorded model guidance. Missing `MEMORY.md` never blocks and must not trigger another memory service. If persisted state or the assignment requires another missing, unreadable, or inconsistent reference, fail closed and report a blocker before editing, delegating, approving, or completing. Delegation means the delegated agent restores on its own entry; the orchestrator does not re-read this chain after dispatching or collecting a subagent, and within one unbroken context it rereads only references that changed, which keeps the cached prompt prefix stable.
|
|
21
21
|
- Do not start implementation without a plan.
|
|
22
|
-
- Keep the model inherited.
|
|
22
|
+
- Keep the model inherited. `Requested reasoning effort` is Cairn's desired profile; `Effective reasoning effort` is what the dispatch response confirms. Only a newly dispatched task or worker may receive the requested effort through the host's task/subagent option, with no model override. For an unsupported host or value, record Effective `inherited`; this means no task override was confirmed, not proof of missing host capability. Infer capability only from the exposed interface or dispatch response, distinguish Cairn non-integration from host non-support, and leave model/global configuration unchanged.
|
|
23
23
|
- Do not treat complexity as a one-time choice. Require the provisional request checkpoint, post-exploration planning checkpoint, and code checkpoint after exact file/caller/test inspection immediately before the first edit. Before editing, evidence may change either route. Every change must synchronize the plan artifact, repository goal task roadmap through `goal replan`, and native UI plan, including reviews and required evidence. After editing begins, a new Heavy signal promotes Light Path to Heavy Path: stop further edits, mark affected evidence stale, synchronize all three roadmaps, and repeat the code checkpoint.
|
|
24
24
|
- 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.
|
|
25
25
|
- Keep work units small. Split large changes into smaller plan tasks.
|
|
@@ -46,7 +46,7 @@ Codex-family models are strong at small implementation tasks, explicit tool use,
|
|
|
46
46
|
### Implementation
|
|
47
47
|
|
|
48
48
|
- Invest Codex reasoning in executable test design first, then hand the bounded failing contract to implementation and require the minimum passing patch.
|
|
49
|
-
- Run verification through `goal verify -- ...`. Its default timeout is 600,000 ms (10 minutes), capped at 3,600,000 ms (1 hour). On success, retain exact argv, tool exit code, pass count, output digest, and watched-workspace fingerprint; commit evidence only when the starting goal/task identity and pre/post watched fingerprint match. Choose the watch set and command at the semantic boundary that proves the claim. Expand diagnostics only for the failing test and its related file/symbol scope; reject stale evidence after related changes.
|
|
49
|
+
- Run verification through `goal verify -- ...`. Its default timeout is 600,000 ms (10 minutes), capped at 3,600,000 ms (1 hour). On success, retain exact argv, tool exit code, pass count, output digest, and watched-workspace fingerprint; commit evidence only when the starting goal/task identity and pre/post watched fingerprint match. Choose the watch set and command at the semantic boundary that proves the claim. The default Git fingerprint excludes ignored files; explicitly `--watch` every ignored artifact needed by the claim. Expand diagnostics only for the failing test and its related file/symbol scope; reject stale evidence after related changes.
|
|
50
50
|
- If acting as the user-called/main agent, orchestrate the work and delegate actual implementation edits to `worker` whenever subagent tools are available, regardless of Light Path or Heavy Path.
|
|
51
51
|
- If acting as the user-called/main agent, immediately relay received subagent status events to the user. If no mid-run reporting channel exists, relay observable events such as assignment, waiting, and final completion.
|
|
52
52
|
- If acting as the user-called/main agent, close or release completed subagents after capturing their final report and evidence, then review the final report and evidence before marking the work complete.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cairn-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Token-efficient agent harness with tool readiness for Codex, Claude Code, and Antigravity",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"commands/",
|
|
33
33
|
"docs/model-guidance/",
|
|
34
34
|
"hooks/",
|
|
35
|
-
"scripts/",
|
|
35
|
+
"scripts/cairn",
|
|
36
|
+
"scripts/*.mjs",
|
|
37
|
+
"scripts/release-integrity-*.json",
|
|
36
38
|
"skills/",
|
|
37
39
|
"templates/",
|
|
38
40
|
"README.md",
|
|
@@ -5,24 +5,28 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import { reconcileWorktreeState } from "./cairn-goal.mjs";
|
|
6
6
|
import { parseRootArgs } from "./cairn-paths.mjs";
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const staleLockName = /^state\.lock\.stale\..+\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
9
9
|
|
|
10
10
|
export async function cleanupProjectCairn({ root = process.cwd(), apply = false } = {}) {
|
|
11
11
|
const repoRoot = resolve(root);
|
|
12
12
|
const legacyRoot = join(repoRoot, ".cairn");
|
|
13
|
-
let
|
|
13
|
+
let rootStat;
|
|
14
14
|
try {
|
|
15
|
-
|
|
15
|
+
rootStat = await lstat(legacyRoot);
|
|
16
16
|
} catch (error) {
|
|
17
17
|
if (error?.code === "ENOENT") return { root: repoRoot, apply, known: [], unknown: [], removed: [], projectDirectoryRemoved: false };
|
|
18
18
|
throw error;
|
|
19
19
|
}
|
|
20
|
+
if (rootStat.isSymbolicLink()) throw new Error(`Legacy Cairn root must not be a symbolic link: ${legacyRoot}`);
|
|
21
|
+
if (!rootStat.isDirectory()) throw new Error(`Legacy Cairn root is not a directory: ${legacyRoot}`);
|
|
20
22
|
|
|
23
|
+
const entries = await readdir(legacyRoot, { withFileTypes: true });
|
|
21
24
|
const known = [];
|
|
22
25
|
const unknown = [];
|
|
23
26
|
for (const entry of entries) {
|
|
24
|
-
const
|
|
25
|
-
|
|
27
|
+
const isKnownFile = entry.isFile() && (entry.name === "state.json" || entry.name === "state.lock" || staleLockName.test(entry.name));
|
|
28
|
+
const isKnownDirectory = entry.isDirectory() && entry.name === "tools";
|
|
29
|
+
(isKnownFile || isKnownDirectory ? known : unknown).push(entry.name);
|
|
26
30
|
}
|
|
27
31
|
if (!apply) return { root: repoRoot, apply, known: known.sort(), unknown: unknown.sort(), removed: [], projectDirectoryRemoved: false };
|
|
28
32
|
|
|
@@ -32,7 +36,9 @@ export async function cleanupProjectCairn({ root = process.cwd(), apply = false
|
|
|
32
36
|
const path = join(legacyRoot, name);
|
|
33
37
|
try {
|
|
34
38
|
const stat = await lstat(path);
|
|
35
|
-
|
|
39
|
+
const expectedType = name === "tools" ? stat.isDirectory() : stat.isFile();
|
|
40
|
+
if (!expectedType) continue;
|
|
41
|
+
await rm(path, { recursive: name === "tools", force: true });
|
|
36
42
|
removed.push(name);
|
|
37
43
|
} catch (error) {
|
|
38
44
|
if (error?.code !== "ENOENT") throw error;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
export async function runGoalCli(args, {
|
|
2
|
+
stdout = console.log,
|
|
3
|
+
defaultGoalEvidence,
|
|
4
|
+
operations,
|
|
5
|
+
}) {
|
|
6
|
+
const [firstCommand = "help", ...remaining] = args;
|
|
7
|
+
const [command = "help", ...rest] = firstCommand === "goal" ? remaining : [firstCommand, ...remaining];
|
|
8
|
+
const { options, positional, passthrough } = parseArgs(rest, command);
|
|
9
|
+
const root = options.root ?? process.cwd();
|
|
10
|
+
const emit = options.quiet ? () => {} : stdout;
|
|
11
|
+
if (command === "start") {
|
|
12
|
+
const tasks = parseTasks(options.tasks ?? positional.slice(0));
|
|
13
|
+
const state = await operations.startGoal({
|
|
14
|
+
root,
|
|
15
|
+
goal: options.goal,
|
|
16
|
+
planId: options.plan,
|
|
17
|
+
tasks,
|
|
18
|
+
completionCriteria: splitValues(options.criteria),
|
|
19
|
+
requiredEvidence: options.requiredEvidence ? splitValues(options.requiredEvidence) : defaultGoalEvidence,
|
|
20
|
+
ownerSessionId: options.session,
|
|
21
|
+
evidencePolicy: options.evidencePolicy ?? options["evidence-policy"] ?? "tool-bound",
|
|
22
|
+
});
|
|
23
|
+
emit(JSON.stringify(state));
|
|
24
|
+
return state;
|
|
25
|
+
}
|
|
26
|
+
if (command === "status") {
|
|
27
|
+
const state = await operations.readGoalState({ root });
|
|
28
|
+
stdout(JSON.stringify(state));
|
|
29
|
+
return state;
|
|
30
|
+
}
|
|
31
|
+
if (command === "task") {
|
|
32
|
+
if (positional[0] === "status" && options.status === undefined) {
|
|
33
|
+
const state = await operations.readGoalState({ root });
|
|
34
|
+
if (!state) {
|
|
35
|
+
stdout(JSON.stringify(null));
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const requestedTaskId = options.task ?? positional[1];
|
|
39
|
+
const task = requestedTaskId === undefined
|
|
40
|
+
? state.tasks.find((candidate) => candidate.status === "active") ?? null
|
|
41
|
+
: state.tasks.find((candidate) => candidate.id === requestedTaskId);
|
|
42
|
+
if (requestedTaskId !== undefined && task === undefined) throw new Error(`Unknown task: ${requestedTaskId}`);
|
|
43
|
+
stdout(JSON.stringify(task));
|
|
44
|
+
return task;
|
|
45
|
+
}
|
|
46
|
+
const taskAction = taskStatusFromAction(positional[0]);
|
|
47
|
+
const state = await operations.setTaskStatus({
|
|
48
|
+
root,
|
|
49
|
+
taskId: options.task ?? (taskAction ? positional[1] : positional[0]),
|
|
50
|
+
status: options.status ?? taskAction ?? positional[1],
|
|
51
|
+
blocker: options.reason,
|
|
52
|
+
});
|
|
53
|
+
emit(JSON.stringify(state));
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
56
|
+
if (command === "replan") {
|
|
57
|
+
const state = await operations.replanGoal({ root, tasks: parseTasks(options.tasks ?? positional.slice(0)) });
|
|
58
|
+
emit(JSON.stringify(state));
|
|
59
|
+
return state;
|
|
60
|
+
}
|
|
61
|
+
if (command === "claim") {
|
|
62
|
+
const state = await operations.claimGoal({ root, sessionId: options.session });
|
|
63
|
+
emit(JSON.stringify(state));
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
if (command === "assign") {
|
|
67
|
+
const state = await operations.assignTask({
|
|
68
|
+
root,
|
|
69
|
+
taskId: options.task ?? positional[0],
|
|
70
|
+
agentId: options.agent ?? positional[1] ?? null,
|
|
71
|
+
});
|
|
72
|
+
emit(JSON.stringify(state));
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
if (command === "receipt") {
|
|
76
|
+
const state = await operations.recordReceipt({
|
|
77
|
+
root,
|
|
78
|
+
taskId: options.task ?? positional[0],
|
|
79
|
+
kind: options.kind,
|
|
80
|
+
scope: options.scope,
|
|
81
|
+
command: options.command,
|
|
82
|
+
exitCode: numberOption(options.exitCode ?? options["exit-code"] ?? options.exit),
|
|
83
|
+
timestamp: options.timestamp,
|
|
84
|
+
goalId: options.goalId ?? options["goal-id"],
|
|
85
|
+
planId: options.plan,
|
|
86
|
+
});
|
|
87
|
+
emit(JSON.stringify(state));
|
|
88
|
+
return state;
|
|
89
|
+
}
|
|
90
|
+
if (command === "verify") {
|
|
91
|
+
const result = await operations.verifyAndRecord({
|
|
92
|
+
root,
|
|
93
|
+
taskId: options.task ?? positional[0],
|
|
94
|
+
kind: options.kind,
|
|
95
|
+
scope: options.scope,
|
|
96
|
+
watchPaths: splitValues(options.watch),
|
|
97
|
+
argv: passthrough,
|
|
98
|
+
timeoutMs: options["timeout-ms"] === undefined
|
|
99
|
+
? 600_000
|
|
100
|
+
: positiveIntegerOption(options["timeout-ms"], "timeout-ms"),
|
|
101
|
+
});
|
|
102
|
+
emit(JSON.stringify({
|
|
103
|
+
id: result.evidence.id,
|
|
104
|
+
scope: result.evidence.scope,
|
|
105
|
+
kind: result.evidence.kind,
|
|
106
|
+
exitCode: result.evidence.exitCode,
|
|
107
|
+
outputDigest: result.evidence.outputDigest,
|
|
108
|
+
workspaceFingerprint: result.evidence.workspaceFingerprint,
|
|
109
|
+
}));
|
|
110
|
+
return result.state;
|
|
111
|
+
}
|
|
112
|
+
if (["pause", "resume", "block", "cancel", "complete"].includes(command)) {
|
|
113
|
+
const state = await operations.transitionGoal({
|
|
114
|
+
root,
|
|
115
|
+
status: { pause: "paused", resume: "active", block: "blocked", cancel: "cancelled", complete: "completed" }[command],
|
|
116
|
+
blocker: options.reason,
|
|
117
|
+
});
|
|
118
|
+
emit(JSON.stringify(state));
|
|
119
|
+
return state;
|
|
120
|
+
}
|
|
121
|
+
throw new Error("Usage: cairn-goal start|status|task|replan|claim|assign|receipt|verify|pause|resume|block|cancel|complete [--root PATH]");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseArgs(args, command) {
|
|
125
|
+
const options = {};
|
|
126
|
+
const positional = [];
|
|
127
|
+
const passthrough = [];
|
|
128
|
+
const booleanOptions = new Set(["quiet"]);
|
|
129
|
+
const allowed = allowedOptions(command);
|
|
130
|
+
const seenCanonicalOptions = new Set();
|
|
131
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
132
|
+
const value = args[index];
|
|
133
|
+
if (value === "--") {
|
|
134
|
+
passthrough.push(...args.slice(index + 1));
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
if (!value.startsWith("--")) {
|
|
138
|
+
positional.push(value);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const option = value.slice(2);
|
|
142
|
+
const equalsIndex = option.indexOf("=");
|
|
143
|
+
const key = equalsIndex === -1 ? option : option.slice(0, equalsIndex);
|
|
144
|
+
const inline = equalsIndex === -1 ? undefined : option.slice(equalsIndex + 1);
|
|
145
|
+
if (!allowed.has(key)) throw new Error(`Unknown option for ${command}: --${key}`);
|
|
146
|
+
const canonicalKey = canonicalOptionKey(key);
|
|
147
|
+
if (seenCanonicalOptions.has(canonicalKey)) throw new Error(`--${canonicalKey} may only be provided once`);
|
|
148
|
+
seenCanonicalOptions.add(canonicalKey);
|
|
149
|
+
if (inline !== undefined) {
|
|
150
|
+
if (!booleanOptions.has(key) && inline.length === 0) throw new Error(`--${key} requires a value`);
|
|
151
|
+
if (booleanOptions.has(key) && !["true", "false"].includes(inline)) throw new Error(`--${key} must be true or false`);
|
|
152
|
+
options[key] = booleanOptions.has(key) ? inline !== "false" : inline;
|
|
153
|
+
} else if (booleanOptions.has(key)) {
|
|
154
|
+
options[key] = true;
|
|
155
|
+
} else {
|
|
156
|
+
const next = args[index + 1];
|
|
157
|
+
if (next === undefined || next === "--" || next.startsWith("--")) throw new Error(`--${key} requires a value`);
|
|
158
|
+
options[key] = next;
|
|
159
|
+
index += 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { options, positional, passthrough };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function canonicalOptionKey(key) {
|
|
166
|
+
return {
|
|
167
|
+
exitCode: "exit-code",
|
|
168
|
+
exit: "exit-code",
|
|
169
|
+
goalId: "goal-id",
|
|
170
|
+
evidencePolicy: "evidence-policy",
|
|
171
|
+
}[key] ?? key;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function allowedOptions(command) {
|
|
175
|
+
const common = ["root", "quiet"];
|
|
176
|
+
const byCommand = {
|
|
177
|
+
start: ["goal", "plan", "tasks", "criteria", "requiredEvidence", "session", "evidencePolicy", "evidence-policy"],
|
|
178
|
+
status: [],
|
|
179
|
+
task: ["task", "status", "reason"],
|
|
180
|
+
replan: ["tasks"],
|
|
181
|
+
claim: ["session"],
|
|
182
|
+
assign: ["task", "agent"],
|
|
183
|
+
receipt: ["task", "kind", "scope", "command", "exitCode", "exit-code", "exit", "timestamp", "goalId", "goal-id", "plan"],
|
|
184
|
+
verify: ["task", "kind", "scope", "watch", "timeout-ms"],
|
|
185
|
+
pause: [],
|
|
186
|
+
resume: [],
|
|
187
|
+
block: ["reason"],
|
|
188
|
+
cancel: [],
|
|
189
|
+
complete: [],
|
|
190
|
+
help: [],
|
|
191
|
+
};
|
|
192
|
+
return new Set([...common, ...(byCommand[command] ?? [])]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseTasks(value) {
|
|
196
|
+
if (Array.isArray(value)) return value;
|
|
197
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error("Provide tasks with --tasks JSON or positional task titles");
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(value);
|
|
200
|
+
if (!Array.isArray(parsed)) throw new Error("--tasks JSON must be an array");
|
|
201
|
+
return parsed;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error instanceof SyntaxError) return value.split("|").map((item) => item.trim()).filter(Boolean);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function splitValues(value) {
|
|
209
|
+
if (!value) return [];
|
|
210
|
+
return String(value).split("|").map((item) => item.trim()).filter(Boolean);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function numberOption(value) {
|
|
214
|
+
if (value === undefined) return undefined;
|
|
215
|
+
const result = Number(value);
|
|
216
|
+
if (!Number.isInteger(result)) throw new Error("exitCode must be an integer");
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function positiveIntegerOption(value, label) {
|
|
221
|
+
const result = Number(value);
|
|
222
|
+
if (!Number.isSafeInteger(result) || result <= 0 || result > 3_600_000) {
|
|
223
|
+
throw new Error(`${label} must be an integer between 1 and 3600000`);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function taskStatusFromAction(value) {
|
|
229
|
+
return {
|
|
230
|
+
start: "active",
|
|
231
|
+
activate: "active",
|
|
232
|
+
active: "active",
|
|
233
|
+
complete: "completed",
|
|
234
|
+
completed: "completed",
|
|
235
|
+
block: "blocked",
|
|
236
|
+
blocked: "blocked",
|
|
237
|
+
pending: "pending",
|
|
238
|
+
}[value] ?? null;
|
|
239
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { lstatSync, readFileSync, readlinkSync, readdirSync } from "node:fs";
|
|
4
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
export function normalizeVerificationArgv(argv) {
|
|
7
|
+
if (!Array.isArray(argv) || argv.length === 0) throw new Error("verification command must follow -- as argv");
|
|
8
|
+
return argv.map((item, index) => {
|
|
9
|
+
if (typeof item !== "string") throw new Error(`verification argv[${index}] must be a string`);
|
|
10
|
+
if (index === 0 && item.trim().length === 0) throw new Error("verification executable must be a non-empty string");
|
|
11
|
+
return item;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeWatchPaths(root, watchPaths) {
|
|
16
|
+
if (!Array.isArray(watchPaths)) throw new Error("watchPaths must be an array");
|
|
17
|
+
const workspaceRoot = resolve(root);
|
|
18
|
+
return [...new Set(watchPaths.map((item, index) => {
|
|
19
|
+
const source = requiredText(item, `watchPaths[${index}]`);
|
|
20
|
+
const absolutePath = resolve(workspaceRoot, source);
|
|
21
|
+
const relativePath = relative(workspaceRoot, absolutePath);
|
|
22
|
+
if (relativePath === ".." || relativePath.startsWith(`..${sep}`)) {
|
|
23
|
+
throw new Error(`watch path must remain inside the workspace: ${source}`);
|
|
24
|
+
}
|
|
25
|
+
const normalizedPath = relativePath === "" ? "." : relativePath.split(sep).join("/");
|
|
26
|
+
if (isInternalStatePath(normalizedPath)) throw new Error(`watch path cannot target internal Cairn or Git state: ${source}`);
|
|
27
|
+
return normalizedPath;
|
|
28
|
+
}))];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function workspaceFingerprint(root = process.cwd(), watchPaths = []) {
|
|
32
|
+
const workspaceRoot = resolve(root);
|
|
33
|
+
const normalizedWatchPaths = normalizeWatchPaths(workspaceRoot, watchPaths);
|
|
34
|
+
const gitPaths = normalizedWatchPaths.length === 0 ? listGitWorkspacePaths(workspaceRoot) : null;
|
|
35
|
+
const paths = normalizedWatchPaths.length > 0
|
|
36
|
+
? listFilesystemPaths(workspaceRoot, normalizedWatchPaths)
|
|
37
|
+
: (gitPaths ?? listFilesystemPaths(workspaceRoot, normalizedWatchPaths));
|
|
38
|
+
const hash = createHash("sha256");
|
|
39
|
+
for (const path of [...new Set(paths)].sort()) hashWorkspacePath(hash, workspaceRoot, path);
|
|
40
|
+
return `sha256:${hash.digest("hex")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function runVerificationEvidence({
|
|
44
|
+
root,
|
|
45
|
+
argv,
|
|
46
|
+
watchPaths,
|
|
47
|
+
runner = spawnSync,
|
|
48
|
+
timeoutMs,
|
|
49
|
+
}) {
|
|
50
|
+
const initialFingerprint = workspaceFingerprint(root, watchPaths);
|
|
51
|
+
const result = runner(argv[0], argv.slice(1), {
|
|
52
|
+
cwd: resolve(root),
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
55
|
+
shell: false,
|
|
56
|
+
timeout: validTimeout(timeoutMs),
|
|
57
|
+
});
|
|
58
|
+
if (result.error?.code === "ETIMEDOUT") throw new Error(`Verification timed out after ${timeoutMs}ms`);
|
|
59
|
+
if (result.error) throw new Error(`Verification could not start: ${result.error.message}`);
|
|
60
|
+
const exitCode = Number.isInteger(result.status) ? result.status : 1;
|
|
61
|
+
const output = combinedOutput(result.stdout, result.stderr);
|
|
62
|
+
if (exitCode !== 0) {
|
|
63
|
+
const detail = boundedText(output, 2000);
|
|
64
|
+
throw new Error(`Verification failed with exit ${exitCode}${detail ? `:\n${detail}` : ""}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const fingerprint = workspaceFingerprint(root, watchPaths);
|
|
68
|
+
if (fingerprint !== initialFingerprint) throw new Error("Watched workspace changed during verification; evidence was not recorded");
|
|
69
|
+
return {
|
|
70
|
+
argv,
|
|
71
|
+
command: argv.map((value) => JSON.stringify(value)).join(" "),
|
|
72
|
+
outputDigest: sha256(output),
|
|
73
|
+
summary: successSummary(output),
|
|
74
|
+
workspaceFingerprint: fingerprint,
|
|
75
|
+
watchPaths,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function listGitWorkspacePaths(root) {
|
|
80
|
+
const result = spawnSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
83
|
+
});
|
|
84
|
+
if (result.status !== 0 || result.error) return null;
|
|
85
|
+
return result.stdout.split("\0").filter(Boolean)
|
|
86
|
+
.filter((path) => !isInternalStatePath(path) && !isCairnRecordPath(path));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Cairn records evidence in these artifacts after verification; including them in the default fingerprint would
|
|
90
|
+
// make every recorded receipt stale. Pass --watch explicitly when a claim is about these files.
|
|
91
|
+
function isCairnRecordPath(path) {
|
|
92
|
+
return path === "PLAN.md" || path === "MEMORY.md" || path.startsWith("docs/plan/") || path.startsWith("docs/memory/");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function listFilesystemPaths(root, watchPaths) {
|
|
96
|
+
const paths = [];
|
|
97
|
+
const visit = (relativePath) => {
|
|
98
|
+
if (isInternalStatePath(relativePath)) return;
|
|
99
|
+
const absolutePath = relativePath === "." ? root : join(root, ...relativePath.split("/"));
|
|
100
|
+
let stat;
|
|
101
|
+
try {
|
|
102
|
+
stat = lstatSync(absolutePath);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error?.code === "ENOENT") {
|
|
105
|
+
paths.push(relativePath);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
if (!stat.isDirectory()) {
|
|
111
|
+
paths.push(relativePath);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const entries = readdirSync(absolutePath).sort();
|
|
115
|
+
if (entries.length === 0) paths.push(relativePath);
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const child = relativePath === "." ? entry : `${relativePath}/${entry}`;
|
|
118
|
+
visit(child);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
for (const path of watchPaths.length > 0 ? watchPaths : ["."]) visit(path);
|
|
122
|
+
return paths;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function hashWorkspacePath(hash, root, relativePath) {
|
|
126
|
+
const absolutePath = relativePath === "." ? root : join(root, ...relativePath.split("/"));
|
|
127
|
+
hash.update(`${relativePath}\0`);
|
|
128
|
+
let stat;
|
|
129
|
+
try {
|
|
130
|
+
stat = lstatSync(absolutePath);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error?.code === "ENOENT") {
|
|
133
|
+
hash.update("missing\0");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
hash.update(`${stat.mode}\0`);
|
|
139
|
+
if (stat.isSymbolicLink()) {
|
|
140
|
+
hash.update(`symlink\0${readlinkSync(absolutePath)}\0`);
|
|
141
|
+
} else if (stat.isFile()) {
|
|
142
|
+
hash.update("file\0");
|
|
143
|
+
hash.update(readFileSync(absolutePath));
|
|
144
|
+
hash.update("\0");
|
|
145
|
+
} else if (stat.isDirectory()) {
|
|
146
|
+
hash.update("directory\0");
|
|
147
|
+
} else {
|
|
148
|
+
hash.update("other\0");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isInternalStatePath(path) {
|
|
153
|
+
const firstSegment = path.split("/")[0];
|
|
154
|
+
return firstSegment === ".git" || firstSegment === ".cairn";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function requiredText(value, label) {
|
|
158
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);
|
|
159
|
+
return value.trim();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function combinedOutput(stdout, stderr) {
|
|
163
|
+
return [stdout, stderr].filter((value) => typeof value === "string" && value.length > 0).join("\n").trim();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function boundedText(value, maxLength) {
|
|
167
|
+
const text = String(value ?? "").trim();
|
|
168
|
+
if (text.length <= maxLength) return text;
|
|
169
|
+
return `…${text.slice(-(maxLength - 1))}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function successSummary(output) {
|
|
173
|
+
const text = String(output ?? "");
|
|
174
|
+
const outputBytes = Buffer.byteLength(text);
|
|
175
|
+
const outputLines = text.length === 0 ? 0 : text.split(/\r?\n/).length;
|
|
176
|
+
return `verification passed; outputBytes=${outputBytes}; outputLines=${outputLines}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function sha256(value) {
|
|
180
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function validTimeout(value) {
|
|
184
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error("timeoutMs must be a positive integer");
|
|
185
|
+
return value;
|
|
186
|
+
}
|