@reddb-io/red-skills 4.4.1 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/build-info.mjs +87 -0
- package/bin/cli-frontdoor.mjs +14 -0
- package/bin/red-skills-brain.mjs +17 -22
- package/bin/red-skills-content.mjs +17 -0
- package/bin/red-skills-hook.mjs +45 -0
- package/bin/red-skills-memory.mjs +17 -22
- package/bin/red-skills-redskilled-mcp.mjs +2 -0
- package/bin/red-skills-resource.mjs +21 -0
- package/bin/runtime-compatibility.mjs +17 -0
- package/bin/runtime-paths.mjs +13 -0
- package/dist/code-nav.bundle.min.mjs +1 -1
- package/dist/opencode-host.bundle.min.mjs +1 -1
- package/dist/redskilled-mcp.bundle.min.mjs +36 -36
- package/dist/redskilled-web.bundle.min.mjs +311 -0
- package/dist/redskilled.bundle.min.mjs +306 -292
- package/dist/release.bundle.min.mjs +1 -1
- package/dist/rsp-core.bundle.min.mjs +1 -1
- package/dist/rsp.bundle.min.mjs +1 -1
- package/dist/statusline.bundle.min.mjs +1 -1
- package/package.json +12 -4
- package/runtime/hook-routes.json +125 -0
- package/runtime/plugins/brain/scripts/bootstrap.mjs +656 -0
- package/runtime/plugins/brain/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/hooks/branch-lock-codex.sh +105 -0
- package/runtime/plugins/dev/hooks/code-nav-mcp.sh +63 -0
- package/runtime/plugins/dev/hooks/command-guard.sh +893 -0
- package/runtime/plugins/dev/hooks/ensure-codex-statusline.mjs +144 -0
- package/runtime/plugins/dev/hooks/red-fetch.mjs +38 -0
- package/runtime/plugins/dev/hooks/redskilled-mcp.sh +64 -0
- package/runtime/plugins/dev/hooks/rsp-hook.sh +343 -0
- package/runtime/plugins/dev/hooks/tests/command-guard.test.sh +378 -0
- package/runtime/plugins/dev/hooks/tests/rsp-hook.test.sh +229 -0
- package/runtime/plugins/dev/scripts/memory-bridge.sh +230 -0
- package/runtime/plugins/dev/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/scripts/tests/memory-bridge.test.sh +555 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/gradle-pre-worktree.sh +45 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/cargo.sh +27 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/gradle.sh +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/only-mine.sh +32 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-cargo +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-envelope +50 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-gradle +42 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-heartbeat +46 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-validation +79 -0
- package/runtime/plugins/dev/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/scripts/install-runtime-shim.sh +181 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
- package/runtime/plugins/dev/skills/engineering/wizard/template.sh +211 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +230 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
- package/runtime/plugins/internal/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/internal/skills/maintainer/create-plugin/scripts/create-plugin.sh +308 -0
- package/runtime/plugins/memory/scripts/bootstrap.mjs +659 -0
- package/runtime/plugins/memory/scripts/structural-smoke.sh +7 -0
- package/scripts/check-skills-boundary.mjs +32 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# branch-lock-hook.sh — PreToolUse(Bash) hook that blocks the agent from
|
|
3
|
+
# switching away from the locked branch. Self-contained: composes the three
|
|
4
|
+
# pure modules (lock-store, scope-resolver, git-command-classifier) into one
|
|
5
|
+
# allow/block verdict, with no dependency on the git-guardrails skill.
|
|
6
|
+
#
|
|
7
|
+
# Reads the Claude Code PreToolUse payload on stdin, extracts the Bash command,
|
|
8
|
+
# and:
|
|
9
|
+
# 1. resolves the project root (CLAUDE_PROJECT_DIR, else the git toplevel);
|
|
10
|
+
# 2. exits 0 (allow) when the checkout is an /afk worktree — scope exemption;
|
|
11
|
+
# 3. exits 0 (allow) when no lock file is present — unlocked is the default;
|
|
12
|
+
# 4. classifies the command; on "block" prints a clear message to stderr and
|
|
13
|
+
# exits 2 (the Claude Code convention for "deny this tool call").
|
|
14
|
+
#
|
|
15
|
+
# Any verdict other than block is a silent exit 0, so the hook never adds noise
|
|
16
|
+
# to allowed commands.
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
allow() {
|
|
21
|
+
printf '{}'
|
|
22
|
+
exit 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
deny() {
|
|
26
|
+
local reason="$1"
|
|
27
|
+
printf '%s\n' "$reason" >&2
|
|
28
|
+
jq -nc --arg reason "$reason" '{
|
|
29
|
+
decision: "block",
|
|
30
|
+
reason: $reason,
|
|
31
|
+
hookSpecificOutput: {
|
|
32
|
+
hookEventName: "PreToolUse",
|
|
33
|
+
permissionDecision: "deny",
|
|
34
|
+
permissionDecisionReason: $reason
|
|
35
|
+
}
|
|
36
|
+
}'
|
|
37
|
+
exit 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
41
|
+
# shellcheck source=lib/lock-store.sh
|
|
42
|
+
source "$HERE/lib/lock-store.sh"
|
|
43
|
+
# shellcheck source=lib/scope-resolver.sh
|
|
44
|
+
source "$HERE/lib/scope-resolver.sh"
|
|
45
|
+
# shellcheck source=lib/git-command-classifier.sh
|
|
46
|
+
source "$HERE/lib/git-command-classifier.sh"
|
|
47
|
+
# shellcheck source=lib/dev-config.sh
|
|
48
|
+
source "$HERE/lib/dev-config.sh"
|
|
49
|
+
|
|
50
|
+
INPUT="$(timeout "${RED_SKILLS_HOOK_STDIN_TIMEOUT_S:-5s}" cat 2>/dev/null || true)"
|
|
51
|
+
COMMAND="$(jq -r '.tool_input.command // empty' <<<"$INPUT" 2>/dev/null)"
|
|
52
|
+
[[ -z "$COMMAND" ]] && allow
|
|
53
|
+
|
|
54
|
+
# Project root: prefer the harness-provided dir, fall back to the git toplevel.
|
|
55
|
+
ROOT="${CLAUDE_PROJECT_DIR:-}"
|
|
56
|
+
if [[ -z "$ROOT" ]]; then
|
|
57
|
+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
58
|
+
fi
|
|
59
|
+
[[ -z "$ROOT" ]] && allow
|
|
60
|
+
|
|
61
|
+
# Per-directory plugin gate (ADR 0067): the dev plugin's hooks are installed
|
|
62
|
+
# globally but must stay fully inert in any repo that did not opt in. Exit before
|
|
63
|
+
# any lock/scope work unless `plugins.dev.enabled: true` is set here.
|
|
64
|
+
dev_plugin_enabled "$ROOT/.red/config.yaml" || allow
|
|
65
|
+
|
|
66
|
+
# Scope: /afk worktrees are exempt even when a lock is active.
|
|
67
|
+
scope_should_enforce "$ROOT" || allow
|
|
68
|
+
|
|
69
|
+
# Untouchable primary (ADR 0083 §2): an agent may never move the primary
|
|
70
|
+
# checkout's branch. This block is unconditional — it no longer arms with the
|
|
71
|
+
# `dev.lock.primary-branch` toggle (which stays readable for backward
|
|
72
|
+
# compatibility but can no longer *enable* switching; /red-doctor may flag it as
|
|
73
|
+
# redundant). Human terminals are unaffected: this is an agent-only pre-tool hook
|
|
74
|
+
# (ADR 0006).
|
|
75
|
+
if [[ "$(classify_primary_branch_switch_guard "$COMMAND")" == "block" ]]; then
|
|
76
|
+
deny "$(cat <<EOF
|
|
77
|
+
BLOCKED by the untouchable-primary rule (ADR 0083): an agent can never switch
|
|
78
|
+
the primary checkout's branch or destroy work in it, regardless of
|
|
79
|
+
configuration or lock state. The command '$COMMAND' would move the agent's
|
|
80
|
+
primary checkout to another branch or destroy work in it (branch switch,
|
|
81
|
+
'git reset' in any form, 'git stash', or 'git rebase --autostash'). Parallel
|
|
82
|
+
human WIP lives in this primary checkout, and these commands have destroyed
|
|
83
|
+
in-progress work before.
|
|
84
|
+
|
|
85
|
+
Do branch work in an isolated worktree under .red/tmp/worktrees/manual/<slug> instead
|
|
86
|
+
('git worktree add .red/tmp/worktrees/manual/<slug> -b <branch> origin/main'). If the local
|
|
87
|
+
trunk diverged from origin, leave it alone and base on the fresh remote ref
|
|
88
|
+
(ADR 0083) — never reset or stash the primary to reconcile it.
|
|
89
|
+
|
|
90
|
+
Allowed in the primary checkout: git commit, git worktree add, read-only git,
|
|
91
|
+
and other non-destructive commands. To change the primary branch, ask the user.
|
|
92
|
+
EOF
|
|
93
|
+
)"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
LOCKFILE="$ROOT/.red/tmp/branch-lock.yaml"
|
|
97
|
+
LOCK_BRANCH="$(lock_store_read "$LOCKFILE")" || allow # absent => unlocked
|
|
98
|
+
|
|
99
|
+
if [[ "$(classify_git_command "$LOCK_BRANCH" "$COMMAND")" == "block" ]]; then
|
|
100
|
+
deny "$(cat <<EOF
|
|
101
|
+
BLOCKED by branch lock: this session is locked to '$LOCK_BRANCH'.
|
|
102
|
+
The command '$COMMAND' would switch the agent away from the locked branch or
|
|
103
|
+
discard working-tree changes (stash, clean -f, reset --hard, whole-tree restore).
|
|
104
|
+
|
|
105
|
+
Allowed while locked: switching back to '$LOCK_BRANCH', targeted file restore
|
|
106
|
+
('git checkout -- <path>', 'git restore <path>'), read-only stash, dry-run clean,
|
|
107
|
+
soft/mixed reset, and 'git worktree add'. To change or release the lock, ask the
|
|
108
|
+
user — they drive it with '/branch-lock <branch>' or '/branch-lock clear'.
|
|
109
|
+
EOF
|
|
110
|
+
)"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
allow
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# branch-lock-session-start.sh — SessionStart hook that offers, once at session
|
|
3
|
+
# start, to lock the agent to the current branch. Self-contained: reuses the
|
|
4
|
+
# same two pure modules as the PreToolUse hook (lock-store, scope-resolver) so
|
|
5
|
+
# the prompt obeys the exact same scope rule as enforcement.
|
|
6
|
+
#
|
|
7
|
+
# On SessionStart it:
|
|
8
|
+
# 1. resolves the project root (CLAUDE_PROJECT_DIR, else the git toplevel);
|
|
9
|
+
# 2. stays silent (exit 0, no output) inside an /afk worktree — the lock
|
|
10
|
+
# protects the interactive primary checkout, never the autonomous loop;
|
|
11
|
+
# 3. stays silent when a lock is already present — nothing to offer;
|
|
12
|
+
# 4. stays silent on a detached HEAD — there is no branch to lock;
|
|
13
|
+
# 5. otherwise emits a SessionStart additionalContext block instructing the
|
|
14
|
+
# agent to ask the user whether to lock to the current branch before doing
|
|
15
|
+
# anything else.
|
|
16
|
+
#
|
|
17
|
+
# The hook never writes the lock itself: it only injects the instruction. The
|
|
18
|
+
# agent runs `branch-lock.sh set <branch>` (or the user's /branch-lock command)
|
|
19
|
+
# on a "yes", and leaves the repo unlocked on a "no".
|
|
20
|
+
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
# shellcheck source=lib/lock-store.sh
|
|
25
|
+
source "$HERE/lib/lock-store.sh"
|
|
26
|
+
# shellcheck source=lib/scope-resolver.sh
|
|
27
|
+
source "$HERE/lib/scope-resolver.sh"
|
|
28
|
+
|
|
29
|
+
# session_start_should_prompt <root> <lockfile> <branch>
|
|
30
|
+
# Returns 0 (prompt) only in the primary checkout, while unlocked, with a real
|
|
31
|
+
# branch checked out. Returns 1 (stay silent) otherwise. No output.
|
|
32
|
+
session_start_should_prompt() {
|
|
33
|
+
local _root="$1" _lockfile="$2" _branch="$3"
|
|
34
|
+
scope_should_enforce "$_root" || return 1 # worktree => exempt
|
|
35
|
+
lock_store_is_locked "$_lockfile" && return 1 # already locked => nothing to offer
|
|
36
|
+
[[ -z "$_branch" || "$_branch" == "HEAD" ]] && return 1 # detached / no branch
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# session_start_prompt_text <branch>
|
|
41
|
+
# The instruction injected into the agent's context.
|
|
42
|
+
session_start_prompt_text() {
|
|
43
|
+
local _branch="$1"
|
|
44
|
+
cat <<EOF
|
|
45
|
+
branch-lock: this session is on branch '$_branch' in the primary checkout, and the branch is not locked. Before doing anything else, ask the user whether to lock the agent to '$_branch' for this session, showing the branch name. If they say yes, lock it with the /branch-lock command (or run scripts/branch-lock.sh set '$_branch'). If they say no, leave the repo unlocked and proceed. Do not lock without an explicit yes.
|
|
46
|
+
EOF
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main() {
|
|
50
|
+
# Drain stdin (SessionStart sends a JSON payload we don't need to inspect).
|
|
51
|
+
cat >/dev/null 2>&1 || true
|
|
52
|
+
|
|
53
|
+
local root="${CLAUDE_PROJECT_DIR:-}"
|
|
54
|
+
if [[ -z "$root" ]]; then
|
|
55
|
+
root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
56
|
+
fi
|
|
57
|
+
[[ -z "$root" ]] && exit 0
|
|
58
|
+
|
|
59
|
+
local lockfile="$root/.red/tmp/branch-lock.yaml"
|
|
60
|
+
local branch
|
|
61
|
+
branch="$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
62
|
+
|
|
63
|
+
session_start_should_prompt "$root" "$lockfile" "$branch" || exit 0
|
|
64
|
+
|
|
65
|
+
local text
|
|
66
|
+
text="$(session_start_prompt_text "$branch")"
|
|
67
|
+
|
|
68
|
+
if command -v jq >/dev/null 2>&1; then
|
|
69
|
+
jq -nc --arg ctx "$text" \
|
|
70
|
+
'{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $ctx}}'
|
|
71
|
+
else
|
|
72
|
+
# Fallback: bare stdout is added to the session context too.
|
|
73
|
+
printf '%s\n' "$text"
|
|
74
|
+
fi
|
|
75
|
+
exit 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Run main only when executed directly, not when sourced by tests.
|
|
79
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
80
|
+
main "$@"
|
|
81
|
+
fi
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# branch-lock.sh — the CLI behind the /branch-lock command. Sets, clears, and
|
|
3
|
+
# reports the branch lock, keeping lib/lock-store.sh the single writer of the
|
|
4
|
+
# lock file. The lock always lives at <root>/.red/tmp/branch-lock.yaml, where
|
|
5
|
+
# <root> is CLAUDE_PROJECT_DIR (set by the harness) or the git toplevel.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# branch-lock.sh set [branch] lock to <branch> (default: current branch),
|
|
9
|
+
# atomic relock-then-switch — rewrite the lock
|
|
10
|
+
# target first so the move "back to the lock
|
|
11
|
+
# target" is itself allowed by the hook, then
|
|
12
|
+
# switch the working tree if not already there.
|
|
13
|
+
# branch-lock.sh clear release the lock.
|
|
14
|
+
# branch-lock.sh status print the locked branch, or "unlocked".
|
|
15
|
+
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
|
|
18
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
# shellcheck source=lib/lock-store.sh
|
|
20
|
+
source "$HERE/lib/lock-store.sh"
|
|
21
|
+
|
|
22
|
+
resolve_root() {
|
|
23
|
+
local root="${CLAUDE_PROJECT_DIR:-}"
|
|
24
|
+
[[ -z "$root" ]] && root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
25
|
+
[[ -z "$root" ]] && { echo "branch-lock: not inside a git repo and CLAUDE_PROJECT_DIR unset" >&2; return 1; }
|
|
26
|
+
printf '%s' "$root"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
main() {
|
|
30
|
+
local action="${1:-status}"
|
|
31
|
+
local root lockfile
|
|
32
|
+
root="$(resolve_root)" || return 1
|
|
33
|
+
lockfile="$root/.red/tmp/branch-lock.yaml"
|
|
34
|
+
|
|
35
|
+
case "$action" in
|
|
36
|
+
set)
|
|
37
|
+
local branch="${2:-}"
|
|
38
|
+
[[ -z "$branch" ]] && branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
|
|
39
|
+
[[ -z "$branch" ]] && { echo "branch-lock: no branch given and HEAD is detached" >&2; return 1; }
|
|
40
|
+
# Atomic relock-then-switch: rewrite the target before moving, so the
|
|
41
|
+
# subsequent switch is "return to the lock target" and the hook allows it.
|
|
42
|
+
lock_store_write "$lockfile" "$branch" || return 1
|
|
43
|
+
local current; current="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
44
|
+
if [[ -n "$current" && "$current" != "$branch" ]]; then
|
|
45
|
+
git switch "$branch" || { echo "branch-lock: locked to '$branch' but switch failed (resolve manually)" >&2; return 1; }
|
|
46
|
+
fi
|
|
47
|
+
echo "branch-lock: locked to '$branch'"
|
|
48
|
+
;;
|
|
49
|
+
clear)
|
|
50
|
+
lock_store_clear "$lockfile"
|
|
51
|
+
echo "branch-lock: cleared (unlocked)"
|
|
52
|
+
;;
|
|
53
|
+
status)
|
|
54
|
+
local b
|
|
55
|
+
if b="$(lock_store_read "$lockfile")"; then
|
|
56
|
+
echo "branch-lock: locked to '$b'"
|
|
57
|
+
else
|
|
58
|
+
echo "branch-lock: unlocked"
|
|
59
|
+
fi
|
|
60
|
+
;;
|
|
61
|
+
*)
|
|
62
|
+
echo "usage: branch-lock.sh <set [branch]|clear|status>" >&2
|
|
63
|
+
return 2
|
|
64
|
+
;;
|
|
65
|
+
esac
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
main "$@"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lib/dev-config.sh — tiny runtime reader for `.red/config.yaml` dev flags.
|
|
3
|
+
#
|
|
4
|
+
# The hook must be dormant by default: missing config file or missing key means
|
|
5
|
+
# disabled. This parser intentionally mirrors the constrained YAML shape used by
|
|
6
|
+
# the dev runtime: nested 2-space mappings with scalar leaves. It exposes the
|
|
7
|
+
# few boolean flags the dev hooks need (ADR 0043 lock guard, ADR 0067 gate).
|
|
8
|
+
|
|
9
|
+
# _dev_config_scalar_true <config.yaml> <dotted-key...>
|
|
10
|
+
# Returns 0 when ANY of the given dotted keys is the exact scalar `true`, 1
|
|
11
|
+
# otherwise (including a missing file). Shared parser for the public readers.
|
|
12
|
+
_dev_config_scalar_true() {
|
|
13
|
+
local _file="$1"
|
|
14
|
+
shift
|
|
15
|
+
local -a _keys=("$@")
|
|
16
|
+
[[ -f "$_file" ]] || return 1
|
|
17
|
+
|
|
18
|
+
local -a _stack=()
|
|
19
|
+
local -a _indents=()
|
|
20
|
+
local _raw _line _indent_str _indent _rest _key _value _full _i
|
|
21
|
+
local _inner _before_close _close_len _tail
|
|
22
|
+
|
|
23
|
+
while IFS= read -r _raw || [[ -n "$_raw" ]]; do
|
|
24
|
+
_raw="${_raw%$'\r'}"
|
|
25
|
+
_line="$_raw"
|
|
26
|
+
|
|
27
|
+
if [[ "$_line" != *\"* && "$_line" != *\'* ]]; then
|
|
28
|
+
_line="${_line%%#*}"
|
|
29
|
+
fi
|
|
30
|
+
_line="${_line%"${_line##*[![:space:]]}"}"
|
|
31
|
+
[[ -z "${_line//[[:space:]]/}" ]] && continue
|
|
32
|
+
|
|
33
|
+
_indent_str="${_line%%[![:space:]]*}"
|
|
34
|
+
_indent=${#_indent_str}
|
|
35
|
+
(( _indent % 2 == 0 )) || return 1
|
|
36
|
+
_rest="${_line:$_indent}"
|
|
37
|
+
|
|
38
|
+
# Skip block-sequence items (`- value`) — they never contain dev flags.
|
|
39
|
+
# A top-level sequence (empty stack) mirrors the TS parser: malformed → bail.
|
|
40
|
+
if [[ "$_rest" =~ ^-([[:space:]]|$) ]]; then
|
|
41
|
+
(( ${#_stack[@]} == 0 )) && return 1
|
|
42
|
+
continue
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
[[ "$_rest" =~ ^[A-Za-z_][A-Za-z0-9_-]*: ]] || return 1
|
|
46
|
+
_key="${_rest%%:*}"
|
|
47
|
+
_value="${_rest#*:}"
|
|
48
|
+
_value="${_value#"${_value%%[![:space:]]*}"}"
|
|
49
|
+
|
|
50
|
+
while ((${#_indents[@]} > 0 && _indents[${#_indents[@]} - 1] >= _indent)); do
|
|
51
|
+
local _last_stack=$(( ${#_stack[@]} - 1 ))
|
|
52
|
+
local _last_indent=$(( ${#_indents[@]} - 1 ))
|
|
53
|
+
unset "_stack[$_last_stack]"
|
|
54
|
+
unset "_indents[$_last_indent]"
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
_full=""
|
|
58
|
+
for ((_i = 0; _i < ${#_stack[@]}; _i++)); do
|
|
59
|
+
[[ -n "$_full" ]] && _full+="."
|
|
60
|
+
_full+="${_stack[$_i]}"
|
|
61
|
+
done
|
|
62
|
+
[[ -n "$_full" ]] && _full+="."
|
|
63
|
+
_full+="$_key"
|
|
64
|
+
|
|
65
|
+
if [[ -z "$_value" ]]; then
|
|
66
|
+
_stack+=("$_key")
|
|
67
|
+
_indents+=("$_indent")
|
|
68
|
+
continue
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Strip an inline comment that follows a closing quote (e.g. `key: "v" # note`).
|
|
72
|
+
if [[ "${_value:0:1}" == '"' ]]; then
|
|
73
|
+
_inner="${_value:1}"
|
|
74
|
+
_before_close="${_inner%%\"*}"
|
|
75
|
+
if [[ "$_inner" == *'"'* ]]; then
|
|
76
|
+
_close_len=$(( 1 + ${#_before_close} + 1 ))
|
|
77
|
+
_tail="${_value:$_close_len}"
|
|
78
|
+
if [[ "$_tail" =~ ^[[:space:]]*(#.*)?$ ]]; then
|
|
79
|
+
_value="${_value:0:$_close_len}"
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
elif [[ "${_value:0:1}" == "'" ]]; then
|
|
83
|
+
_inner="${_value:1}"
|
|
84
|
+
_before_close="${_inner%%\'*}"
|
|
85
|
+
if [[ "$_inner" == *"'"* ]]; then
|
|
86
|
+
_close_len=$(( 1 + ${#_before_close} + 1 ))
|
|
87
|
+
_tail="${_value:$_close_len}"
|
|
88
|
+
if [[ "$_tail" =~ ^[[:space:]]*(#.*)?$ ]]; then
|
|
89
|
+
_value="${_value:0:$_close_len}"
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if [[ "$_value" == \"*\" && "$_value" == *\" ]]; then
|
|
95
|
+
_value="${_value:1:${#_value}-2}"
|
|
96
|
+
elif [[ "$_value" == \'*\' && "$_value" == *\' ]]; then
|
|
97
|
+
_value="${_value:1:${#_value}-2}"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
local _k
|
|
101
|
+
for _k in "${_keys[@]}"; do
|
|
102
|
+
if [[ "$_full" == "$_k" ]]; then
|
|
103
|
+
[[ "$_value" == "true" ]] && return 0
|
|
104
|
+
fi
|
|
105
|
+
done
|
|
106
|
+
done < "$_file"
|
|
107
|
+
|
|
108
|
+
return 1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# dev_config_lock_primary_branch_enabled <config.yaml>
|
|
112
|
+
# Returns 0 when the primary-branch lock is on, 1 otherwise. The flag lives at
|
|
113
|
+
# `dev.lock.primary-branch` (top-level) or `plugins.dev.lock.primary-branch`
|
|
114
|
+
# (namespaced); either set to exactly `true` enables the guard.
|
|
115
|
+
dev_config_lock_primary_branch_enabled() {
|
|
116
|
+
_dev_config_scalar_true "$1" "dev.lock.primary-branch" "plugins.dev.lock.primary-branch"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# dev_plugin_enabled <config.yaml>
|
|
120
|
+
# The per-directory plugin activation gate (ADR 0067): returns 0 only when
|
|
121
|
+
# `plugins.dev.enabled` is the exact scalar `true`. A missing file or missing
|
|
122
|
+
# flag → 1 (dormant). Every dev shell hook must early-exit on a non-zero return
|
|
123
|
+
# so it stays fully inert in repos that did not opt into the dev plugin.
|
|
124
|
+
dev_plugin_enabled() {
|
|
125
|
+
_dev_config_scalar_true "$1" "plugins.dev.enabled"
|
|
126
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lib/git-command-classifier.sh — the Git Command Classifier: given the locked
|
|
3
|
+
# branch and a shell command string, decides whether the command would lose the
|
|
4
|
+
# agent's work while a lock is active and must be blocked. Pure / explicit-args,
|
|
5
|
+
# like the sibling lib/ modules. Self-contained: it carries its own small
|
|
6
|
+
# command matcher so the branch-lock hook needs no dependency on the
|
|
7
|
+
# git-guardrails skill.
|
|
8
|
+
#
|
|
9
|
+
# It blocks two families of command:
|
|
10
|
+
# 1. Branch-leaving `git checkout <branch>` / `git switch <branch>` (the
|
|
11
|
+
# original slice) — unless the target is the lock branch itself.
|
|
12
|
+
# 2. The work-loss family (issue #61): commands that throw away the working
|
|
13
|
+
# tree or the stash —
|
|
14
|
+
# - `git stash` / `git stash push` / `git stash save` (bare stash defaults to push)
|
|
15
|
+
# - `git clean` with any force flag (`-f`, `-fd`, `-xfd`, `--force`)
|
|
16
|
+
# - `git reset --hard`
|
|
17
|
+
# - whole-tree restore: `git checkout .`, `git checkout -- .`, `git restore .`
|
|
18
|
+
#
|
|
19
|
+
# Everything else is allowed — switching back to the lock branch, targeted
|
|
20
|
+
# single-file restore (`git checkout -- <path>`, `git restore <path>`), a bare
|
|
21
|
+
# `git checkout`, read-only stash (`list`/`show`), a dry-run clean (`-n`), a
|
|
22
|
+
# soft/mixed reset, an unstage (`git restore --staged`), `git worktree add`
|
|
23
|
+
# (worktrees are how /afk works and are exempt by scope), and any other command.
|
|
24
|
+
#
|
|
25
|
+
# Recognition is intentionally conservative: it scans the token stream for a
|
|
26
|
+
# `git` token, skips any global options between `git` and the subcommand
|
|
27
|
+
# (`git -C <path> checkout x`, `git --git-dir=… switch y`, `git -c k=v …`), and
|
|
28
|
+
# classifies the recognised subcommand, so a compound command
|
|
29
|
+
# (`cd x && git reset --hard`) and a global-option form are both classified.
|
|
30
|
+
#
|
|
31
|
+
# Public surface:
|
|
32
|
+
# classify_git_command <lock_branch> <command>
|
|
33
|
+
# Echoes exactly "block" or "allow" and returns 0.
|
|
34
|
+
#
|
|
35
|
+
# classify_primary_branch_switch_guard <command>
|
|
36
|
+
# Echoes exactly "block" for commands that would move or destroy work in the
|
|
37
|
+
# primary checkout, and "allow" otherwise. This is the config-flag guard
|
|
38
|
+
# from ADR 0043 (unlike classify_git_command it has no lock branch). It
|
|
39
|
+
# blocks: branch movement (switch/create/checkout to a branch); `git reset`
|
|
40
|
+
# in ANY form; every `git stash` subcommand; and `git rebase --autostash`
|
|
41
|
+
# (issue #1024). Parallel human WIP lives in the primary checkout and these
|
|
42
|
+
# commands have destroyed in-progress work before; do branch work in an
|
|
43
|
+
# isolated worktree under .red/tmp/ instead. Worktrees are exempt by scope,
|
|
44
|
+
# so this classifier only ever runs against the primary checkout.
|
|
45
|
+
|
|
46
|
+
# _git_subcommand_index <toks-name> <git-index>
|
|
47
|
+
# Given the name of the token array and the index of a `git` token, echoes the
|
|
48
|
+
# index of the git subcommand, skipping any leading global options. Global
|
|
49
|
+
# options that take a separate argument (`-C <path>`, `-c <name=value>`,
|
|
50
|
+
# `--git-dir <path>`, `--work-tree <path>`, `--namespace <name>`,
|
|
51
|
+
# `--exec-path <path>`, `--super-prefix <path>`) consume the following token;
|
|
52
|
+
# the `--opt=value` form is self-contained, and bare flags (`-p`, `--no-pager`,
|
|
53
|
+
# `--bare`, …) consume only themselves. Echoes the array length when no
|
|
54
|
+
# subcommand follows.
|
|
55
|
+
_git_subcommand_index() {
|
|
56
|
+
local -n _t="$1"
|
|
57
|
+
local k=$(( $2 + 1 ))
|
|
58
|
+
local len=${#_t[@]}
|
|
59
|
+
while (( k < len )); do
|
|
60
|
+
local tok="${_t[k]}"
|
|
61
|
+
case "$tok" in
|
|
62
|
+
-C|-c|--git-dir|--work-tree|--namespace|--exec-path|--super-prefix)
|
|
63
|
+
k=$(( k + 2 )); continue ;; # option + its separate argument
|
|
64
|
+
--*=*|-*)
|
|
65
|
+
k=$(( k + 1 )); continue ;; # `--opt=value` or a bare flag
|
|
66
|
+
*)
|
|
67
|
+
break ;; # the subcommand token
|
|
68
|
+
esac
|
|
69
|
+
done
|
|
70
|
+
echo "$k"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# classify_git_command <lock_branch> <command>
|
|
74
|
+
classify_git_command() {
|
|
75
|
+
local _lock="$1" _cmd="$2"
|
|
76
|
+
local -a toks
|
|
77
|
+
read -ra toks <<<"$_cmd"
|
|
78
|
+
local n=${#toks[@]} i j
|
|
79
|
+
|
|
80
|
+
for ((i = 0; i < n; i++)); do
|
|
81
|
+
[[ "${toks[i]}" == "git" ]] || continue
|
|
82
|
+
local s; s="$(_git_subcommand_index toks "$i")" # subcommand index, past global options
|
|
83
|
+
local sub="${toks[s]:-}"
|
|
84
|
+
case "$sub" in
|
|
85
|
+
worktree)
|
|
86
|
+
echo "allow"; return 0
|
|
87
|
+
;;
|
|
88
|
+
stash)
|
|
89
|
+
# bare `git stash` defaults to push; push/save discard the working tree.
|
|
90
|
+
local op="${toks[s + 1]:-}"
|
|
91
|
+
if [[ -z "$op" || "$op" == "push" || "$op" == "save" ]]; then
|
|
92
|
+
echo "block"; return 0
|
|
93
|
+
fi
|
|
94
|
+
echo "allow"; return 0 # list / show / pop / apply / drop …
|
|
95
|
+
;;
|
|
96
|
+
clean)
|
|
97
|
+
# any force flag makes clean destructive; -n/--dry-run is safe.
|
|
98
|
+
for ((j = s + 1; j < n; j++)); do
|
|
99
|
+
local t="${toks[j]}"
|
|
100
|
+
[[ "$t" == "--force" ]] && { echo "block"; return 0; }
|
|
101
|
+
# short flag bundle containing 'f' (-f, -fd, -xfd, …) but not -n.
|
|
102
|
+
if [[ "$t" == -[^-]* && "$t" == *f* ]]; then echo "block"; return 0; fi
|
|
103
|
+
done
|
|
104
|
+
echo "allow"; return 0
|
|
105
|
+
;;
|
|
106
|
+
reset)
|
|
107
|
+
for ((j = s + 1; j < n; j++)); do
|
|
108
|
+
[[ "${toks[j]}" == "--hard" ]] && { echo "block"; return 0; }
|
|
109
|
+
done
|
|
110
|
+
echo "allow"; return 0
|
|
111
|
+
;;
|
|
112
|
+
restore)
|
|
113
|
+
# whole-tree restore (`git restore .`) loses work; a targeted path or an
|
|
114
|
+
# unstage (`--staged <path>`) is allowed, mirroring `checkout -- <path>`.
|
|
115
|
+
for ((j = s + 1; j < n; j++)); do
|
|
116
|
+
local t="${toks[j]}"
|
|
117
|
+
[[ "$t" == -* ]] && continue # skip flags (--staged, --source=…, …)
|
|
118
|
+
if [[ "$t" == "." ]]; then echo "block"; return 0; fi
|
|
119
|
+
break # a named path: allowed
|
|
120
|
+
done
|
|
121
|
+
echo "allow"; return 0
|
|
122
|
+
;;
|
|
123
|
+
checkout|switch)
|
|
124
|
+
local target="" sawdoubledash=0
|
|
125
|
+
for ((j = s + 1; j < n; j++)); do
|
|
126
|
+
local t="${toks[j]}"
|
|
127
|
+
if [[ "$t" == "--" ]]; then sawdoubledash=1; continue; fi
|
|
128
|
+
if [[ "$t" == "-" ]]; then target="-"; break; fi # `switch -` = previous branch
|
|
129
|
+
[[ "$t" == -* ]] && continue # skip flags (-b, -c, -q, --track, …)
|
|
130
|
+
target="$t"; break
|
|
131
|
+
done
|
|
132
|
+
# whole-tree restore via checkout (`git checkout .`, `git checkout -- .`).
|
|
133
|
+
if [[ "$target" == "." ]]; then echo "block"; return 0; fi
|
|
134
|
+
if (( sawdoubledash )); then echo "allow"; return 0; fi # file restore
|
|
135
|
+
if [[ -z "$target" ]]; then echo "allow"; return 0; fi # bare checkout
|
|
136
|
+
if [[ "$target" == "$_lock" ]]; then echo "allow"; return 0; fi # back to lock
|
|
137
|
+
echo "block"; return 0
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
done
|
|
141
|
+
|
|
142
|
+
echo "allow"; return 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# classify_primary_branch_switch_guard <command>
|
|
146
|
+
classify_primary_branch_switch_guard() {
|
|
147
|
+
local _cmd="$1"
|
|
148
|
+
local -a toks
|
|
149
|
+
read -ra toks <<<"$_cmd"
|
|
150
|
+
local n=${#toks[@]} i j
|
|
151
|
+
|
|
152
|
+
for ((i = 0; i < n; i++)); do
|
|
153
|
+
[[ "${toks[i]}" == "git" ]] || continue
|
|
154
|
+
local s; s="$(_git_subcommand_index toks "$i")" # subcommand index, past global options
|
|
155
|
+
local sub="${toks[s]:-}"
|
|
156
|
+
case "$sub" in
|
|
157
|
+
worktree)
|
|
158
|
+
echo "allow"; return 0
|
|
159
|
+
;;
|
|
160
|
+
reset)
|
|
161
|
+
# `git reset` in ANY form (--hard/--soft/--mixed, with or without a
|
|
162
|
+
# pathspec) can throw away parallel human WIP in the primary checkout
|
|
163
|
+
# (issue #1024). All forms blocked; use a worktree for branch work.
|
|
164
|
+
echo "block"; return 0
|
|
165
|
+
;;
|
|
166
|
+
stash)
|
|
167
|
+
# `git stash` and every subcommand (push/save/pop/apply/list/show/…)
|
|
168
|
+
# blocked in the primary checkout (issue #1024) — the whole family
|
|
169
|
+
# touches or hides the primary's working tree.
|
|
170
|
+
echo "block"; return 0
|
|
171
|
+
;;
|
|
172
|
+
rebase)
|
|
173
|
+
# `git rebase --autostash` silently stashes/pops the primary's WIP
|
|
174
|
+
# around the rebase (issue #1024); a plain rebase is not this vector.
|
|
175
|
+
for ((j = s + 1; j < n; j++)); do
|
|
176
|
+
[[ "${toks[j]}" == "--autostash" ]] && { echo "block"; return 0; }
|
|
177
|
+
done
|
|
178
|
+
echo "allow"; return 0
|
|
179
|
+
;;
|
|
180
|
+
checkout|switch)
|
|
181
|
+
local target="" sawdoubledash=0
|
|
182
|
+
for ((j = s + 1; j < n; j++)); do
|
|
183
|
+
local t="${toks[j]}"
|
|
184
|
+
if [[ "$t" == "--" ]]; then sawdoubledash=1; continue; fi
|
|
185
|
+
if [[ "$t" == "-" ]]; then target="-"; break; fi
|
|
186
|
+
[[ "$t" == -* ]] && continue
|
|
187
|
+
target="$t"; break
|
|
188
|
+
done
|
|
189
|
+
if (( sawdoubledash )); then echo "allow"; return 0; fi
|
|
190
|
+
if [[ -z "$target" ]]; then echo "allow"; return 0; fi
|
|
191
|
+
if [[ "$target" == "." ]]; then echo "allow"; return 0; fi
|
|
192
|
+
echo "block"; return 0
|
|
193
|
+
;;
|
|
194
|
+
esac
|
|
195
|
+
done
|
|
196
|
+
|
|
197
|
+
echo "allow"; return 0
|
|
198
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lib/lock-store.sh — the Lock Store: owns the branch-lock.yaml file end to end,
|
|
3
|
+
# following the pure / explicit-args contract of the afk lib/ modules.
|
|
4
|
+
#
|
|
5
|
+
# The lock file records the single branch the agent is pinned to in the primary
|
|
6
|
+
# checkout. Its presence means "locked"; its absence means "unlocked". Content
|
|
7
|
+
# is the branch name on one line (a bare YAML scalar — valid YAML, trivially
|
|
8
|
+
# inspectable). It lives under the gitignored .red/tmp/ so every checkout and
|
|
9
|
+
# machine locks independently and nothing is ever committed.
|
|
10
|
+
#
|
|
11
|
+
# Reads no globals. The lock-file path is a parameter on every call, so the
|
|
12
|
+
# Module stays unit-testable by sourcing it directly. Writes are atomic
|
|
13
|
+
# (temp-in-same-dir then mv) so a concurrent reader never sees a half-written
|
|
14
|
+
# file and a relock can never leave the target empty.
|
|
15
|
+
#
|
|
16
|
+
# Public surface:
|
|
17
|
+
# lock_store_write <lockfile> <branch>
|
|
18
|
+
# Atomically writes <branch> as the lock target. Creates the parent dir if
|
|
19
|
+
# missing. Returns 2 on missing arguments.
|
|
20
|
+
#
|
|
21
|
+
# lock_store_read <lockfile>
|
|
22
|
+
# Echoes the locked branch name (trailing whitespace stripped) and returns
|
|
23
|
+
# 0 when locked. Prints nothing and returns 1 when the file is absent
|
|
24
|
+
# (= unlocked), so absence is strictly opt-out.
|
|
25
|
+
#
|
|
26
|
+
# lock_store_clear <lockfile>
|
|
27
|
+
# Removes the lock file. Idempotent — returns 0 whether or not it existed.
|
|
28
|
+
#
|
|
29
|
+
# lock_store_is_locked <lockfile>
|
|
30
|
+
# Returns 0 when a non-empty lock file is present, 1 otherwise. No output.
|
|
31
|
+
|
|
32
|
+
# lock_store_write <lockfile> <branch>
|
|
33
|
+
lock_store_write() {
|
|
34
|
+
local _path="$1" _branch="$2"
|
|
35
|
+
[[ -z "$_path" || -z "$_branch" ]] && { printf '[lock-store] lock_store_write: need <lockfile> <branch>\n' >&2; return 2; }
|
|
36
|
+
mkdir -p "$(dirname "$_path")"
|
|
37
|
+
local tmp
|
|
38
|
+
tmp="$(mktemp "$(dirname "$_path")/.branch-lock.XXXXXX")"
|
|
39
|
+
printf '%s\n' "$_branch" > "$tmp"
|
|
40
|
+
mv -f "$tmp" "$_path"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# lock_store_read <lockfile>
|
|
44
|
+
lock_store_read() {
|
|
45
|
+
local _path="$1"
|
|
46
|
+
[[ -s "$_path" ]] || return 1
|
|
47
|
+
local branch
|
|
48
|
+
# strip trailing whitespace/newlines; keep the first line only
|
|
49
|
+
IFS= read -r branch < "$_path"
|
|
50
|
+
branch="${branch%"${branch##*[![:space:]]}"}"
|
|
51
|
+
[[ -n "$branch" ]] || return 1
|
|
52
|
+
printf '%s' "$branch"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# lock_store_clear <lockfile>
|
|
56
|
+
lock_store_clear() {
|
|
57
|
+
local _path="$1"
|
|
58
|
+
[[ -z "$_path" ]] && { printf '[lock-store] lock_store_clear: need <lockfile>\n' >&2; return 2; }
|
|
59
|
+
rm -f "$_path"
|
|
60
|
+
return 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# lock_store_is_locked <lockfile>
|
|
64
|
+
lock_store_is_locked() {
|
|
65
|
+
local _path="$1"
|
|
66
|
+
lock_store_read "$_path" >/dev/null 2>&1
|
|
67
|
+
}
|