forge-orkes 0.64.0 → 0.64.2
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/package.json +1 -1
- package/template/.claude/skills/jarvis/SKILL.md +2 -2
- package/template/.forge/checks/forge-jarvis-answer.sh +12 -6
- package/template/.forge/checks/forge-jarvis-awareness.sh +10 -5
- package/template/.forge/checks/forge-jarvis-bridge-clean.sh +6 -3
- package/template/.forge/checks/forge-jarvis-dispatch.sh +29 -7
- package/template/.forge/checks/forge-jarvis-instruments.sh +7 -4
- package/template/.forge/checks/forge-jarvis-notify-logonly.sh +6 -0
- package/template/.forge/checks/forge-jarvis-promote.sh +26 -13
- package/template/.forge/checks/forge-jarvis-prwatch.sh +24 -7
- package/template/.forge/checks/forge-jarvis-relay.sh +117 -90
- package/template/.forge/checks/forge-jarvis.sh +17 -16
- package/template/.forge/checks/tests/forge-jarvis-awareness.test.sh +118 -0
- package/template/.forge/checks/tests/forge-jarvis-notify-logonly.test.sh +63 -0
package/package.json
CHANGED
|
@@ -164,7 +164,7 @@ RC on-demand `--spawn worktree` sessions leave `.claude/worktrees/bridge-{cse-id
|
|
|
164
164
|
.forge/checks/forge-jarvis-bridge-clean.sh # then reap
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
Liveness is
|
|
167
|
+
Liveness is decided in this order (FR-207 gap-closure): dirty = keep; a git lock naming a **live** server pid = keep; a lock with a **dead** pid = orphan even when freshly modified (unlock-then-remove — RC locks every spawned bridge under the server's pid, so a fresh-looking bridge whose server died is still reapable); mtime freshness applies only to **unlocked** bridges. `git worktree remove` refusing a dirty worktree stays the second net; only `bridge-*` basenames are ever eligible. Run it as light housekeeping, not on a timer — there is no daemon.
|
|
168
168
|
|
|
169
169
|
### The hub trigger stays watched, not built
|
|
170
170
|
|
|
@@ -181,7 +181,7 @@ The last thing Jarvis does — and the sharpest boundary. On the operator's **ex
|
|
|
181
181
|
|
|
182
182
|
The helper does exactly two things, in order:
|
|
183
183
|
|
|
184
|
-
1. **Shows what the SHA carries** — recomputed live from the board projection (
|
|
184
|
+
1. **Shows what the SHA carries** — recomputed live from the board projection (whose rows carry the fold-derived `release_state`), never a session's word. The merged-but-not-yet-validated units are the ones this promotion covers, **by containment**: a unit becomes `validated` when its landed SHA is an ancestor of the signed `promotion/<sha>` tag's commit (ADR-024), verified offline on the host.
|
|
185
185
|
2. **Triggers the step-2 seam** — `gh workflow run promote.yml -f sha=<full-sha>` — and stops. The flow halts at GitHub's Environment gates (both taps: deploy + mint) until you press them there.
|
|
186
186
|
|
|
187
187
|
### Jarvis never approves — it refuses if asked
|
|
@@ -29,7 +29,6 @@ SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
29
29
|
DISPATCH="${FORGE_JARVIS_DISPATCH:-$SELF_DIR/forge-jarvis-dispatch.sh}"
|
|
30
30
|
|
|
31
31
|
WORK_DIR=""; PHASE=""; SLICE=""; ANSWER=""; ANSWER_FILE=""; DRY=0
|
|
32
|
-
FWD=""
|
|
33
32
|
while [ $# -gt 0 ]; do
|
|
34
33
|
case "$1" in
|
|
35
34
|
--work-dir) WORK_DIR="${2:-}"; shift 2 ;;
|
|
@@ -38,14 +37,22 @@ while [ $# -gt 0 ]; do
|
|
|
38
37
|
--answer) ANSWER="${2:-}"; shift 2 ;;
|
|
39
38
|
--answer-file) ANSWER_FILE="${2:-}"; shift 2 ;;
|
|
40
39
|
--dry-run) DRY=1; shift ;;
|
|
41
|
-
--) shift;
|
|
42
|
-
*)
|
|
40
|
+
--) shift; break ;;
|
|
41
|
+
*) printf 'forge-jarvis-answer: unknown argument: %s\n' "$1" >&2; exit 2 ;;
|
|
43
42
|
esac
|
|
44
43
|
done
|
|
44
|
+
# After `--`, the remaining "$@" is forwarded to dispatch verbatim — argument
|
|
45
|
+
# boundaries preserved (never flattened to a string; args with spaces survive).
|
|
45
46
|
|
|
46
47
|
[ -n "$WORK_DIR" ] && [ -n "$PHASE" ] && [ -n "$SLICE" ] || {
|
|
47
48
|
printf 'forge-jarvis-answer: --work-dir, --phase and --slice are all required\n' >&2; exit 2; }
|
|
48
49
|
|
|
50
|
+
# The phase index comes from a park ping (semi-trusted runner log) and is built
|
|
51
|
+
# into a write path — a non-numeric value could traverse out of the work dir.
|
|
52
|
+
case "$PHASE" in
|
|
53
|
+
*[!0-9]*|'') printf 'forge-jarvis-answer: --phase must be a numeric phase index, got: %s\n' "$PHASE" >&2; exit 2 ;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
49
56
|
# Resolve the answer text: --answer, else --answer-file, else stdin.
|
|
50
57
|
if [ -z "$ANSWER" ]; then
|
|
51
58
|
if [ -n "$ANSWER_FILE" ] && [ -f "$ANSWER_FILE" ]; then
|
|
@@ -65,7 +72,7 @@ DEST="$WORK_DIR/phase-$PHASE/answer.md" # the EXACT m-35 convention
|
|
|
65
72
|
|
|
66
73
|
if [ "$DRY" = 1 ]; then
|
|
67
74
|
printf 'answer plan: write → %s ; then re-dispatch %s --slice %s --work-dir %s %s\n' \
|
|
68
|
-
"$DEST" "$DISPATCH" "$SLICE" "$WORK_DIR" "
|
|
75
|
+
"$DEST" "$DISPATCH" "$SLICE" "$WORK_DIR" "$*"
|
|
69
76
|
exit 0
|
|
70
77
|
fi
|
|
71
78
|
|
|
@@ -75,5 +82,4 @@ printf '%s\n' "$ANSWER" > "$DEST" || { printf 'forge-jarvis-answer: could not wr
|
|
|
75
82
|
printf 'forge-jarvis-answer: answer landed at %s\n' "$DEST" >&2
|
|
76
83
|
|
|
77
84
|
# 2) re-dispatch the slice so the runner resumes past the park (m-35 prompt re-assembly)
|
|
78
|
-
|
|
79
|
-
"$DISPATCH" --slice "$SLICE" --work-dir "$WORK_DIR" $FWD
|
|
85
|
+
"$DISPATCH" --slice "$SLICE" --work-dir "$WORK_DIR" "$@"
|
|
@@ -47,7 +47,7 @@ while [ $# -gt 0 ]; do
|
|
|
47
47
|
case "$1" in
|
|
48
48
|
--dry-run) DRY_RUN=1; shift ;;
|
|
49
49
|
--worktree-root) WT_ROOT="${2:-}"; shift 2 ;;
|
|
50
|
-
*)
|
|
50
|
+
*) printf 'forge-jarvis-awareness: unknown argument: %s\n' "$1" >&2; exit 2 ;;
|
|
51
51
|
esac
|
|
52
52
|
done
|
|
53
53
|
|
|
@@ -73,7 +73,7 @@ list_slices() {
|
|
|
73
73
|
done
|
|
74
74
|
else
|
|
75
75
|
git -C "$REPO_ROOT" worktree list --porcelain 2>/dev/null | awk -v tab="$TAB" '
|
|
76
|
-
/^worktree / { p = $
|
|
76
|
+
/^worktree / { p = substr($0, 10) } # full remainder — paths may contain spaces
|
|
77
77
|
/^branch / {
|
|
78
78
|
if ($2 ~ /refs\/heads\/forge\/m-/) {
|
|
79
79
|
b = $2; sub(/refs\/heads\//, "", b) # forge/m-37[-<session>]
|
|
@@ -108,9 +108,14 @@ elif [ -f "$BOARD" ]; then
|
|
|
108
108
|
# no jq dependency (Article X native-first).
|
|
109
109
|
_inflight="$(printf '%s\n' "$_emit" | grep '"forge_id"' | grep -vE '"status":[[:space:]]*"complete"')"
|
|
110
110
|
if [ -n "$_inflight" ]; then
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
# Order-independent field extraction — one sed per key, so a key-order change
|
|
112
|
+
# in the board emit degrades a field to '?' instead of silently dropping the line.
|
|
113
|
+
printf '%s\n' "$_inflight" | head -40 | while IFS= read -r _ln; do
|
|
114
|
+
_id="$(printf '%s' "$_ln" | sed -n 's/.*"forge_id":[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
115
|
+
_st="$(printf '%s' "$_ln" | sed -n 's/.*"status":[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
116
|
+
_rs="$(printf '%s' "$_ln" | sed -n 's/.*"release_state":[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
117
|
+
printf ' %s status=%s release_state=%s\n' "${_id:-?}" "${_st:-?}" "${_rs:-?}"
|
|
118
|
+
done
|
|
114
119
|
else
|
|
115
120
|
printf ' (all board units complete — nothing in flight)\n'
|
|
116
121
|
fi
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
# forge-jarvis-bridge-clean.sh [--dry-run] [--stale-hours N] [--worktrees-root <dir>]
|
|
25
25
|
# --dry-run list orphan candidates; remove NOTHING (default is to remove).
|
|
26
26
|
# --stale-hours N freshness window; a bridge modified within N hours is LIVE (default 6).
|
|
27
|
-
# --
|
|
27
|
+
# --worktree-root override the scan root (tests); default: <repo>/.claude/worktrees.
|
|
28
|
+
# (--worktrees-root accepted as an alias — historical spelling.)
|
|
28
29
|
set -u
|
|
29
30
|
|
|
30
31
|
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
@@ -37,8 +38,10 @@ while [ $# -gt 0 ]; do
|
|
|
37
38
|
case "$1" in
|
|
38
39
|
--dry-run) DRY_RUN=1; shift ;;
|
|
39
40
|
--stale-hours) STALE_HOURS="${2:-6}"; shift 2 ;;
|
|
40
|
-
--
|
|
41
|
-
|
|
41
|
+
# both spellings accepted — the family standard is --worktree-root (relay/awareness/
|
|
42
|
+
# instruments); the plural was this script's original flag and stays as an alias.
|
|
43
|
+
--worktree-root|--worktrees-root) WT_ROOT="${2:-}"; shift 2 ;;
|
|
44
|
+
*) printf 'forge-jarvis-bridge-clean: unknown argument: %s\n' "$1" >&2; exit 2 ;;
|
|
42
45
|
esac
|
|
43
46
|
done
|
|
44
47
|
[ -n "$WT_ROOT" ] || WT_ROOT="$REPO_ROOT/.claude/worktrees"
|
|
@@ -121,18 +121,40 @@ materialize_work_order() {
|
|
|
121
121
|
fi
|
|
122
122
|
fi
|
|
123
123
|
_tmp="$WORKTREE/slice.yml.tmp"
|
|
124
|
+
# Write-target hygiene: refuse a pre-existing tmp (incl. a pre-committed symlink —
|
|
125
|
+
# a redirect would follow it and truncate whatever it points at).
|
|
126
|
+
if [ -e "$_tmp" ] || [ -L "$_tmp" ]; then
|
|
127
|
+
printf 'forge-jarvis-dispatch: REFUSED — %s already exists; remove it and re-dispatch. RELAY this refusal to the operator.\n' "$_tmp" >&2
|
|
128
|
+
return 2
|
|
129
|
+
fi
|
|
124
130
|
printf '# Materialized by forge-jarvis-dispatch from %s (delete to re-materialize)\nphases:\n' "${_mdir#"$WORKTREE"/}" > "$_tmp"
|
|
125
131
|
_count=0
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
# Quoted glob iteration, numeric phase order — never word-split ls output
|
|
133
|
+
# (phase/plan names are repo content; unquoted expansion would let a crafted
|
|
134
|
+
# name pull arbitrary *.md into the runner's prompt).
|
|
135
|
+
_names=$(for _d in "$_mdir"/*/; do [ -d "$_d" ] || continue; basename "$_d"; done | sort -n)
|
|
136
|
+
while IFS= read -r _p; do
|
|
137
|
+
[ -n "$_p" ] || continue
|
|
138
|
+
_have=0
|
|
139
|
+
for _f in "$_mdir/$_p"/plan-*.md; do [ -f "$_f" ] && { _have=1; break; }; done
|
|
140
|
+
[ "$_have" = 1 ] || continue
|
|
130
141
|
mkdir -p "$WORKTREE/phases/$_p"
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
_pl="$WORKTREE/phases/$_p/plan.md"
|
|
143
|
+
if [ -L "$_pl" ]; then
|
|
144
|
+
rm -f "$_tmp"
|
|
145
|
+
printf 'forge-jarvis-dispatch: REFUSED — %s is a symlink; refusing to write through it. RELAY this refusal to the operator.\n' "$_pl" >&2
|
|
146
|
+
return 2
|
|
147
|
+
fi
|
|
148
|
+
: > "$_pl"
|
|
149
|
+
for _f in "$_mdir/$_p"/plan-*.md; do
|
|
150
|
+
[ -f "$_f" ] || continue
|
|
151
|
+
cat "$_f" >> "$_pl"
|
|
152
|
+
done
|
|
133
153
|
printf ' - %s\n' "$_p" >> "$_tmp"
|
|
134
154
|
_count=$((_count + 1))
|
|
135
|
-
done
|
|
155
|
+
done <<MATERIALIZE_EOF
|
|
156
|
+
$_names
|
|
157
|
+
MATERIALIZE_EOF
|
|
136
158
|
if [ "$_count" -eq 0 ]; then
|
|
137
159
|
rm -f "$_tmp"
|
|
138
160
|
printf 'forge-jarvis-dispatch: REFUSED — %s has no phase plans (plan-*.md); nothing to dispatch. RELAY this refusal to the operator.\n' "$_mdir" >&2
|
|
@@ -49,7 +49,7 @@ while [ $# -gt 0 ]; do
|
|
|
49
49
|
--since) SINCE="${2:-}"; shift 2 ;;
|
|
50
50
|
--worktree-root) WT_ROOT="${2:-}"; shift 2 ;;
|
|
51
51
|
--dry-run) DRY_RUN=1; shift ;;
|
|
52
|
-
*)
|
|
52
|
+
*) printf 'forge-jarvis-instruments: unknown argument: %s\n' "$1" >&2; exit 2 ;;
|
|
53
53
|
esac
|
|
54
54
|
done
|
|
55
55
|
|
|
@@ -70,12 +70,15 @@ if [ -z "$LOGS" ]; then
|
|
|
70
70
|
done
|
|
71
71
|
else
|
|
72
72
|
_globbed="$(git -C "$REPO_ROOT" worktree list --porcelain 2>/dev/null | awk '
|
|
73
|
-
/^worktree / { p = $
|
|
73
|
+
/^worktree / { p = substr($0, 10) } # full remainder — paths may contain spaces
|
|
74
74
|
/^branch / { if ($2 ~ /refs\/heads\/forge\/m-/) print p "/.forge/runner-work/notify.log" }')"
|
|
75
|
-
|
|
75
|
+
while IFS= read -r _l; do
|
|
76
|
+
[ -n "$_l" ] || continue
|
|
76
77
|
[ -f "$_l" ] && LOGS="${LOGS}${LOGS:+
|
|
77
78
|
}$_l"
|
|
78
|
-
done
|
|
79
|
+
done <<GLOBBED_EOF
|
|
80
|
+
$_globbed
|
|
81
|
+
GLOBBED_EOF
|
|
79
82
|
fi
|
|
80
83
|
fi
|
|
81
84
|
|
|
@@ -30,6 +30,12 @@ set -u
|
|
|
30
30
|
TYPE="${1:-note}"
|
|
31
31
|
PAYLOAD="${2:-}"
|
|
32
32
|
|
|
33
|
+
# The log is TAB-separated, one ping per line, and the relay parses it as such —
|
|
34
|
+
# scrub field-forging bytes from caller input so a crafted payload cannot mint
|
|
35
|
+
# extra pings or extra fields (noise-bounded anyway: state claims are host-recomputed).
|
|
36
|
+
TYPE=$(printf '%s' "$TYPE" | tr '\n\t' ' ')
|
|
37
|
+
PAYLOAD=$(printf '%s' "$PAYLOAD" | tr '\n\t' ' ')
|
|
38
|
+
|
|
33
39
|
# Durable local record — the ONLY thing this channel does. Default location travels with
|
|
34
40
|
# the slice (FORGE_SLICE_WORK_DIR), but a Jarvis dispatch always sets FORGE_SLICE_NOTIFY_LOG
|
|
35
41
|
# explicitly INTO the slice worktree.
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
# handoff-step4-jarvis.md REV2; FR-208; NFR-038; operator ruling 3 "trigger-not-approve").
|
|
4
4
|
#
|
|
5
5
|
# The operator says "promote <sha>". Jarvis does exactly two things, in order:
|
|
6
|
-
# 1. SHOW what that SHA carries — recomputed from HOST truth (the board projection
|
|
7
|
-
# fold), never a session's word. The merged-but-not-yet-
|
|
8
|
-
# promotion covers, by CONTAINMENT: a unit becomes
|
|
9
|
-
# ancestor of the signed promotion tag's commit
|
|
6
|
+
# 1. SHOW what that SHA carries — recomputed from HOST truth (the board projection, whose
|
|
7
|
+
# rows carry fold-derived release_state), never a session's word. The merged-but-not-yet-
|
|
8
|
+
# validated units are the ones a promotion covers, by CONTAINMENT: a unit becomes
|
|
9
|
+
# validated when its landed SHA is an ancestor of the signed promotion tag's commit
|
|
10
|
+
# (ADR-024) — verified offline, host-side.
|
|
10
11
|
# 2. TRIGGER the step-2 seam: gh workflow run promote.yml -f sha=<full-sha>
|
|
11
12
|
#
|
|
12
13
|
# That is ALL it does. It NEVER approves, waives, or lowers anything: the flow STOPS at
|
|
@@ -22,8 +23,8 @@
|
|
|
22
23
|
# --dry-run SHOW host state, then PRINT the trigger command without running it.
|
|
23
24
|
#
|
|
24
25
|
# HOST-TRUTH commands (env-overridable so tests inject stubs and degradation is explicit):
|
|
25
|
-
# FORGE_JARVIS_BOARD default .forge/checks/forge-board-render.sh (the rollup projection
|
|
26
|
-
#
|
|
26
|
+
# FORGE_JARVIS_BOARD default .forge/checks/forge-board-render.sh (the rollup projection;
|
|
27
|
+
# its rows already carry the fold-derived release_state)
|
|
27
28
|
# FORGE_JARVIS_GH default `gh` (the trigger — never an approval)
|
|
28
29
|
# BEST-EFFORT: a missing board/fold degrades to a stated '(unavailable)' — the show still
|
|
29
30
|
# renders; a missing gh in a real (non-dry-run) trigger is a clear error, never a silent success.
|
|
@@ -32,7 +33,6 @@ set -u
|
|
|
32
33
|
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
33
34
|
REPO_ROOT="$(cd "$SELF_DIR/../.." && pwd)" # .forge/checks → repo root
|
|
34
35
|
BOARD="${FORGE_JARVIS_BOARD:-$REPO_ROOT/.forge/checks/forge-board-render.sh}"
|
|
35
|
-
FOLD="${FORGE_JARVIS_FOLD:-$REPO_ROOT/.claude/hooks/forge-release-fold.sh}"
|
|
36
36
|
GH="${FORGE_JARVIS_GH:-gh}"
|
|
37
37
|
WORKFLOW="promote.yml"
|
|
38
38
|
|
|
@@ -41,8 +41,14 @@ DRY_RUN=0
|
|
|
41
41
|
while [ $# -gt 0 ]; do
|
|
42
42
|
case "$1" in
|
|
43
43
|
--dry-run) DRY_RUN=1; shift ;;
|
|
44
|
-
--*)
|
|
45
|
-
|
|
44
|
+
--*)
|
|
45
|
+
# LOUD refusal, never a silent swallow: this is the one helper where a typo'd
|
|
46
|
+
# flag (--dryrun) falling through would fire a REAL workflow trigger.
|
|
47
|
+
printf 'forge-jarvis-promote: unknown flag: %s — usage: forge-jarvis-promote.sh <full-sha> [--dry-run]\n' "$1" >&2; exit 2 ;;
|
|
48
|
+
*)
|
|
49
|
+
if [ -z "$SHA" ]; then SHA="$1"; shift; else
|
|
50
|
+
printf 'forge-jarvis-promote: unexpected extra argument: %s\n' "$1" >&2; exit 2
|
|
51
|
+
fi ;;
|
|
46
52
|
esac
|
|
47
53
|
done
|
|
48
54
|
|
|
@@ -65,9 +71,14 @@ if [ -f "$BOARD" ]; then
|
|
|
65
71
|
# line-oriented JSON -> grep/sed, no jq (Article X native-first) — same parse as awareness.
|
|
66
72
|
_rel="$(printf '%s\n' "$_emit" | grep '"forge_id"' | grep -E '"release_state":[[:space:]]*"(merged|validated)"')"
|
|
67
73
|
if [ -n "$_rel" ]; then
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
# Order-independent field extraction — one sed per key (kept aligned with awareness),
|
|
75
|
+
# so a key-order change in the board emit degrades a field to '?' rather than
|
|
76
|
+
# silently dropping every line into a misleading "(no merged units)".
|
|
77
|
+
printf '%s\n' "$_rel" | head -60 | while IFS= read -r _ln; do
|
|
78
|
+
_id="$(printf '%s' "$_ln" | sed -n 's/.*"forge_id":[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
79
|
+
_rs="$(printf '%s' "$_ln" | sed -n 's/.*"release_state":[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
80
|
+
printf ' %s release_state=%s\n' "${_id:-?}" "${_rs:-?}"
|
|
81
|
+
done
|
|
71
82
|
else
|
|
72
83
|
printf ' (no merged units in the board projection — nothing waiting on a promotion)\n'
|
|
73
84
|
fi
|
|
@@ -93,7 +104,9 @@ if ! command -v "$GH" >/dev/null 2>&1; then
|
|
|
93
104
|
fi
|
|
94
105
|
printf ' %s\n' "$CMD"
|
|
95
106
|
# gh workflow run promote.yml -f sha=<full-sha> — the trigger, never an approval call.
|
|
96
|
-
|
|
107
|
+
# Pinned to REPO_ROOT: gh resolves the target repo from cwd, and this project's cwd is
|
|
108
|
+
# known to snap between checkouts across turns — the trigger must always hit THIS repo.
|
|
109
|
+
(cd "$REPO_ROOT" && "$GH" workflow run "$WORKFLOW" -f sha="$SHA")
|
|
97
110
|
_rc=$?
|
|
98
111
|
if [ "$_rc" -eq 0 ]; then
|
|
99
112
|
printf '\n[ok] Triggered promote.yml for %s. It now STOPS at GitHub Environment gates — press them there; Jarvis does not.\n' "$SHA"
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
# --staging-env <name> deployment environment that means "on staging" (default: staging)
|
|
20
20
|
# --max-iterations <N> hard poll bound (default 30)
|
|
21
21
|
# --interval <seconds> sleep between polls (default 20; 0 = no sleep, for fixtures)
|
|
22
|
-
# --marker-dir <dir> where the at-most-once come-try marker lives
|
|
22
|
+
# --marker-dir <dir> where the at-most-once come-try marker lives
|
|
23
|
+
# (default: <git-common-dir>/forge/relay-markers — stable across
|
|
24
|
+
# worktrees and cwds; cwd/.relay-markers only outside a repo)
|
|
23
25
|
# --dry-run print the resolved plan (pr, repo, env, bound) and exit; no polling
|
|
24
26
|
set -u
|
|
25
27
|
|
|
@@ -35,12 +37,28 @@ while [ $# -gt 0 ]; do
|
|
|
35
37
|
--interval) INTERVAL="${2:-20}"; shift 2 ;;
|
|
36
38
|
--marker-dir) MARKER_DIR="${2:-}"; shift 2 ;;
|
|
37
39
|
--dry-run) DRY=1; shift ;;
|
|
38
|
-
*)
|
|
40
|
+
*) printf 'forge-jarvis-prwatch: unknown argument: %s\n' "$1" >&2; exit 2 ;;
|
|
39
41
|
esac
|
|
40
42
|
done
|
|
41
43
|
[ -n "$PR" ] || { printf 'forge-jarvis-prwatch: --pr <num> is required\n' >&2; exit 2; }
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
# The PR number is carried by a runner `done` ping (semi-trusted log content) and is used
|
|
45
|
+
# in the marker filename and as a gh argument — require a plain number before any use.
|
|
46
|
+
case "$PR" in
|
|
47
|
+
*[!0-9]*|'') printf 'forge-jarvis-prwatch: --pr must be numeric, got: %s\n' "$PR" >&2; exit 2 ;;
|
|
48
|
+
esac
|
|
49
|
+
# --repo, when given, must be a plain owner/repo — anything else could smuggle gh flags.
|
|
50
|
+
if [ -n "$REPO" ]; then
|
|
51
|
+
case "$REPO" in
|
|
52
|
+
*/*) case "$REPO" in *[!A-Za-z0-9._/-]*|*/*/*) printf 'forge-jarvis-prwatch: --repo must be owner/repo, got: %s\n' "$REPO" >&2; exit 2 ;; esac ;;
|
|
53
|
+
*) printf 'forge-jarvis-prwatch: --repo must be owner/repo, got: %s\n' "$REPO" >&2; exit 2 ;;
|
|
54
|
+
esac
|
|
55
|
+
fi
|
|
56
|
+
# At-most-once marker home: stable per machine+repo (git common dir — shared across this
|
|
57
|
+
# repo's worktrees, never cwd-relative droppings), falling back to cwd only outside a repo.
|
|
58
|
+
if [ -z "$MARKER_DIR" ]; then
|
|
59
|
+
_cd="$(git rev-parse --git-common-dir 2>/dev/null)"
|
|
60
|
+
if [ -n "$_cd" ]; then MARKER_DIR="$_cd/forge/relay-markers"; else MARKER_DIR="./.relay-markers"; fi
|
|
61
|
+
fi
|
|
44
62
|
|
|
45
63
|
if [ "$DRY" = 1 ]; then
|
|
46
64
|
printf 'prwatch plan: pr=%s repo=%s env=%s bound=%s×%ss marker-dir=%s\n' \
|
|
@@ -54,11 +72,10 @@ fi
|
|
|
54
72
|
# staging deploy → `gh api repos/{owner}/{repo}/deployments?environment=<env>` + its statuses
|
|
55
73
|
# Both read host truth; neither consults anything a build session emitted.
|
|
56
74
|
pr_merge_state() { # → MERGED | CLOSED | OPEN (empty on gh error → treated as OPEN, keep polling)
|
|
57
|
-
#
|
|
58
|
-
"$GH" pr view "$PR" $
|
|
75
|
+
# ${REPO:+…} expands to exactly two argv words when --repo was given, none otherwise.
|
|
76
|
+
"$GH" pr view "$PR" ${REPO:+--repo "$REPO"} --json state --jq '.state' 2>/dev/null
|
|
59
77
|
}
|
|
60
78
|
deploy_succeeded() { # → exit 0 iff the latest staging deployment is in a success state
|
|
61
|
-
# shellcheck disable=SC2086
|
|
62
79
|
_st="$("$GH" api "repos/{owner}/{repo}/deployments?environment=$ENV_STAGING&per_page=1" \
|
|
63
80
|
--jq '.[0].statuses_url' 2>/dev/null)"
|
|
64
81
|
[ -n "$_st" ] || return 1
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
#
|
|
33
33
|
# HOST-TRUTH COMMANDS (env-overridable so tests inject stubs, and so degradation is explicit):
|
|
34
34
|
# FORGE_JARVIS_FOLD default .claude/hooks/forge-release-fold.sh (release_state recompute)
|
|
35
|
-
#
|
|
36
|
-
#
|
|
35
|
+
# (gh-based facts live in forge-jarvis-prwatch.sh, which documents its own FORGE_JARVIS_GH;
|
|
36
|
+
# the board recompute lives in awareness/promote — this helper consults the fold only.)
|
|
37
37
|
#
|
|
38
38
|
# BEST-EFFORT: a fold/gh/board that is absent or errors degrades to release_state=unknown and
|
|
39
39
|
# says so — never a silent success, never a hard fail of the relay path.
|
|
@@ -45,9 +45,12 @@ FOLD="${FORGE_JARVIS_FOLD:-$REPO_ROOT/.claude/hooks/forge-release-fold.sh}"
|
|
|
45
45
|
|
|
46
46
|
# ── host truth: the slice's release_state, recomputed from the fold (never the payload) ───────
|
|
47
47
|
host_release_state() { # host_release_state <milestone-id> → prints unmerged|merged|validated|unknown
|
|
48
|
+
# Kept textually aligned with fold_state in forge-jarvis-awareness.sh — same guard,
|
|
49
|
+
# same invocation, same degradation, so a fold-format change is a mechanical two-file edit.
|
|
48
50
|
_mid="${1:-}"
|
|
49
51
|
[ -n "$_mid" ] || { printf 'unknown'; return 0; }
|
|
50
|
-
|
|
52
|
+
[ -f "$FOLD" ] || { printf 'unknown'; return 0; }
|
|
53
|
+
_out="$(sh "$FOLD" "$_mid" 2>/dev/null)" || { printf 'unknown'; return 0; }
|
|
51
54
|
_rs="$(printf '%s\n' "$_out" | grep -m1 'release_state=' | sed 's/.*release_state=//; s/[[:space:]].*//')"
|
|
52
55
|
[ -n "$_rs" ] && printf '%s' "$_rs" || printf 'unknown'
|
|
53
56
|
}
|
|
@@ -110,102 +113,126 @@ relay_one_line() { # relay_one_line <tsv-line>
|
|
|
110
113
|
build_relay_text "$_type" "$_payload"
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
# ── mode
|
|
116
|
+
# ── modes — one function per mode, each owning its own arg loop ───────────────────────────────
|
|
114
117
|
MILESTONE=""
|
|
115
|
-
|
|
118
|
+
RELAY_MODE="live" # 'live' | 'catchup' — how --scan emits (not a boolean)
|
|
116
119
|
MARKER_DIR=""
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# The watch set the skill arms `tail -F` on — recomputed from a DISK GLOB on EVERY call,
|
|
128
|
-
# never held in memory (Amendment 2: kill Jarvis and the armed watches re-derive from disk).
|
|
129
|
-
# Live slices are worktrees on a forge/m-* branch; each runner writes to
|
|
130
|
-
# <worktree>/.forge/runner-work/notify.log (the dispatch's FORGE_SLICE_NOTIFY_LOG). Emit the
|
|
131
|
-
# EXPECTED path even when the log does not exist yet — `tail -F` (capital F) is chosen exactly
|
|
132
|
-
# so a not-yet-created log is picked up when the first ping lands (F8).
|
|
133
|
-
shift
|
|
134
|
-
WT_ROOT=""
|
|
135
|
-
while [ $# -gt 0 ]; do case "$1" in --worktree-root) WT_ROOT="${2:-}"; shift 2 ;; *) shift ;; esac; done
|
|
136
|
-
if [ -n "$WT_ROOT" ]; then
|
|
137
|
-
# override path (tests): disk-glob the given root for slice worktrees.
|
|
138
|
-
for _wt in "$WT_ROOT"/*/; do
|
|
139
|
-
[ -d "$_wt" ] || continue
|
|
140
|
-
printf '%s.forge/runner-work/notify.log\n' "$_wt"
|
|
141
|
-
done
|
|
142
|
-
else
|
|
143
|
-
# live path: `git worktree list` is the on-disk registry of worktrees; glob it to the
|
|
144
|
-
# forge/m-* slice branches and emit each slice's notify-log path.
|
|
145
|
-
git worktree list --porcelain 2>/dev/null | awk '
|
|
146
|
-
/^worktree / { p=$2 }
|
|
147
|
-
/^branch / { if ($2 ~ /refs\/heads\/forge\/m-/) print p "/.forge/runner-work/notify.log" }
|
|
148
|
-
'
|
|
149
|
-
fi
|
|
150
|
-
exit 0 ;;
|
|
151
|
-
|
|
152
|
-
--audit-sources)
|
|
153
|
-
# Static guard: the done/halt (state-claim) branch must recompute from the fold, and the
|
|
154
|
-
# authoritative release-state var must NEVER be assigned from the payload. Catches a future
|
|
155
|
-
# rewrite that sources a merge verdict from the session's own payload rather than host truth.
|
|
156
|
-
# The guard's own two grep lines carry a #@audit tag and are excluded from the scan so their
|
|
157
|
-
# literal patterns cannot self-match the file they scan.
|
|
158
|
-
_body="$(grep -vE '#@audit' "$0")"
|
|
159
|
-
if ! printf '%s\n' "$_body" | grep -qE 'host_release_state|FORGE_JARVIS_FOLD|forge-release-fold'; then #@audit
|
|
160
|
-
printf 'forge-jarvis-relay: AUDIT FAIL — no host-truth (fold) recompute in the relay path\n' >&2
|
|
161
|
-
exit 3
|
|
162
|
-
fi
|
|
163
|
-
if printf '%s\n' "$_body" | grep -qE '_rs=["'\''$]*_payload'; then #@audit
|
|
164
|
-
printf 'forge-jarvis-relay: AUDIT FAIL — a state claim is sourced from the payload, not the host\n' >&2
|
|
165
|
-
exit 3
|
|
166
|
-
fi
|
|
167
|
-
printf 'relay speaks host truth only\n'
|
|
168
|
-
exit 0 ;;
|
|
121
|
+
mode_one() {
|
|
122
|
+
LINE="${1:-}"; shift 2>/dev/null || true
|
|
123
|
+
while [ $# -gt 0 ]; do case "$1" in
|
|
124
|
+
--milestone) MILESTONE="${2:-}"; shift 2 ;;
|
|
125
|
+
*) printf 'forge-jarvis-relay: unknown argument: %s\n' "$1" >&2; exit 2 ;;
|
|
126
|
+
esac; done
|
|
127
|
+
relay_one_line "$LINE"; printf '\n'
|
|
128
|
+
exit 0
|
|
129
|
+
}
|
|
169
130
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
131
|
+
mode_list_logs() {
|
|
132
|
+
# The watch set the skill arms `tail -F` on — recomputed from a DISK GLOB on EVERY call,
|
|
133
|
+
# never held in memory (Amendment 2: kill Jarvis and the armed watches re-derive from disk).
|
|
134
|
+
# Live slices are worktrees on a forge/m-* branch; each runner writes to
|
|
135
|
+
# <worktree>/.forge/runner-work/notify.log (the dispatch's FORGE_SLICE_NOTIFY_LOG). Emit the
|
|
136
|
+
# EXPECTED path even when the log does not exist yet — `tail -F` (capital F) is chosen exactly
|
|
137
|
+
# so a not-yet-created log is picked up when the first ping lands (F8).
|
|
138
|
+
WT_ROOT=""
|
|
139
|
+
while [ $# -gt 0 ]; do case "$1" in
|
|
140
|
+
--worktree-root) WT_ROOT="${2:-}"; shift 2 ;;
|
|
141
|
+
*) printf 'forge-jarvis-relay: unknown argument: %s\n' "$1" >&2; exit 2 ;;
|
|
142
|
+
esac; done
|
|
143
|
+
if [ -n "$WT_ROOT" ]; then
|
|
144
|
+
# override path (tests): disk-glob the given root for slice worktrees.
|
|
145
|
+
for _wt in "$WT_ROOT"/*/; do
|
|
146
|
+
[ -d "$_wt" ] || continue
|
|
147
|
+
printf '%s.forge/runner-work/notify.log\n' "$_wt"
|
|
180
148
|
done
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
149
|
+
else
|
|
150
|
+
# live path: `git worktree list` is the on-disk registry of worktrees; glob it to the
|
|
151
|
+
# forge/m-* slice branches and emit each slice's notify-log path. Pinned with -C so the
|
|
152
|
+
# watch list never depends on the caller's cwd (the cwd-reset footgun: a snapped cwd
|
|
153
|
+
# would silently arm ZERO watches).
|
|
154
|
+
git -C "$REPO_ROOT" worktree list --porcelain 2>/dev/null | awk '
|
|
155
|
+
/^worktree / { p=substr($0, 10) } # full remainder — paths may contain spaces
|
|
156
|
+
/^branch / { if ($2 ~ /refs\/heads\/forge\/m-/) print p "/.forge/runner-work/notify.log" }
|
|
157
|
+
'
|
|
158
|
+
fi
|
|
159
|
+
exit 0
|
|
160
|
+
}
|
|
184
161
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
162
|
+
mode_audit_sources() {
|
|
163
|
+
# Static guard: the done/halt (state-claim) branch must recompute from the fold, and the
|
|
164
|
+
# authoritative release-state var must NEVER be assigned from the payload. Catches a future
|
|
165
|
+
# rewrite that sources a merge verdict from the session's own payload rather than host truth.
|
|
166
|
+
# The guard's own two grep lines carry a #@audit tag and are excluded from the scan so their
|
|
167
|
+
# literal patterns cannot self-match the file they scan.
|
|
168
|
+
_body="$(grep -vE '#@audit' "$0")"
|
|
169
|
+
if ! printf '%s\n' "$_body" | grep -qE 'host_release_state|FORGE_JARVIS_FOLD|forge-release-fold'; then #@audit
|
|
170
|
+
printf 'forge-jarvis-relay: AUDIT FAIL — no host-truth (fold) recompute in the relay path\n' >&2
|
|
171
|
+
exit 3
|
|
172
|
+
fi
|
|
173
|
+
if printf '%s\n' "$_body" | grep -qE '_rs=["'\''$]*_payload'; then #@audit
|
|
174
|
+
printf 'forge-jarvis-relay: AUDIT FAIL — a state claim is sourced from the payload, not the host\n' >&2
|
|
175
|
+
exit 3
|
|
176
|
+
fi
|
|
177
|
+
printf 'relay speaks host truth only\n'
|
|
178
|
+
exit 0
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
mode_scan() {
|
|
182
|
+
LOG="${1:-}"; shift 2>/dev/null || true
|
|
183
|
+
while [ $# -gt 0 ]; do
|
|
184
|
+
case "$1" in
|
|
185
|
+
--marker-dir) MARKER_DIR="${2:-}"; shift 2 ;;
|
|
186
|
+
--milestone) MILESTONE="${2:-}"; shift 2 ;;
|
|
187
|
+
--mode) RELAY_MODE="${2:-live}"; shift 2 ;;
|
|
188
|
+
*) printf 'forge-jarvis-relay: unknown argument: %s\n' "$1" >&2; exit 2 ;;
|
|
189
|
+
esac
|
|
190
|
+
done
|
|
191
|
+
[ -n "$LOG" ] || { printf 'forge-jarvis-relay: --scan needs a logfile path\n' >&2; exit 2; }
|
|
192
|
+
# A dispatched-but-not-yet-pinged slice has no log YET — --list-logs deliberately emits
|
|
193
|
+
# expected paths, so catchup over that set must compose: a missing log is an EMPTY log
|
|
194
|
+
# (nothing to relay, exit 0), never a refusal. A missing path ARGUMENT stays exit 2 above.
|
|
195
|
+
[ -f "$LOG" ] || exit 0
|
|
196
|
+
[ -n "$MARKER_DIR" ] || MARKER_DIR="$(dirname "$LOG")/relay-markers"
|
|
197
|
+
mkdir -p "$MARKER_DIR" 2>/dev/null || true
|
|
202
198
|
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
_n=0; _fresh=0; _summary=""
|
|
200
|
+
# Read every line; relay only those whose offset (line number) has no marker yet.
|
|
201
|
+
while IFS= read -r _l || [ -n "$_l" ]; do
|
|
202
|
+
_n=$((_n + 1))
|
|
203
|
+
_marker="$MARKER_DIR/$_n.relayed"
|
|
204
|
+
[ -f "$_marker" ] && continue # at-most-once: already relayed, never re-buzz
|
|
205
|
+
_text="$(relay_one_line "$_l")"
|
|
206
|
+
# Write the marker BEFORE emitting — a crash after emit must not double-buzz; a crash
|
|
207
|
+
# before emit leaves it unmarked and it re-surfaces as while-you-were-away (no silent loss).
|
|
208
|
+
# A FAILED marker write must not emit either: emitting unmarked would re-buzz on every
|
|
209
|
+
# scan (at-most-once silently inverted). Say so loudly and skip — the ping stays unmarked
|
|
210
|
+
# and re-surfaces once the marker dir is fixed.
|
|
211
|
+
if ! printf '%s\toffset=%s\t%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo now)" "$_n" "$_text" > "$_marker" 2>/dev/null; then
|
|
212
|
+
printf 'forge-jarvis-relay: WARNING — cannot write marker %s; relay of line %s SUPPRESSED to preserve at-most-once. Fix the marker dir.\n' "$_marker" "$_n" >&2
|
|
213
|
+
continue
|
|
214
|
+
fi
|
|
215
|
+
_fresh=$((_fresh + 1))
|
|
216
|
+
if [ "$RELAY_MODE" = catchup ]; then
|
|
217
|
+
_summary="${_summary}${_summary:+ | }$_text"
|
|
218
|
+
else
|
|
219
|
+
printf '%s\n' "$_text" # live: one relay text per new ping → one buzz
|
|
205
220
|
fi
|
|
206
|
-
|
|
207
|
-
exit 0 ;;
|
|
221
|
+
done < "$LOG"
|
|
208
222
|
|
|
223
|
+
if [ "$RELAY_MODE" = catchup ] && [ "$_fresh" -gt 0 ]; then
|
|
224
|
+
printf '📨 While you were away — %s ping(s): %s\n' "$_fresh" "$_summary"
|
|
225
|
+
fi
|
|
226
|
+
# exit 0 whether or not anything was fresh (a no-op scan is success, not failure).
|
|
227
|
+
exit 0
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# ── mode dispatch ─────────────────────────────────────────────────────────────────────────────
|
|
231
|
+
case "${1:-}" in
|
|
232
|
+
--one) shift; mode_one "$@" ;;
|
|
233
|
+
--list-logs) shift; mode_list_logs "$@" ;;
|
|
234
|
+
--audit-sources) shift; mode_audit_sources ;;
|
|
235
|
+
--scan) shift; mode_scan "$@" ;;
|
|
209
236
|
*)
|
|
210
237
|
printf 'usage: forge-jarvis-relay.sh --one "<tsv-line>" [--milestone m-N]\n' >&2
|
|
211
238
|
printf ' forge-jarvis-relay.sh --scan <logfile> [--marker-dir <dir>] [--milestone m-N] [--mode live|catchup]\n' >&2
|
|
@@ -115,6 +115,13 @@ else
|
|
|
115
115
|
SUFFIX="${FORGE_JARVIS_NAME_SUFFIX:-$(jarvis_cfg name_suffix)}"
|
|
116
116
|
[ -n "$SUFFIX" ] && NAME="$NAME-$SUFFIX"
|
|
117
117
|
fi
|
|
118
|
+
# The resolved name reaches the phone's session list AND the --dry-run line the
|
|
119
|
+
# operator may copy-paste — keep it to a safe, phone-legible charset regardless of
|
|
120
|
+
# where it came from (override flag, config name_suffix, env).
|
|
121
|
+
case "$NAME" in
|
|
122
|
+
*[!A-Za-z0-9._-]*|'')
|
|
123
|
+
printf 'forge-jarvis: sticky name must be [A-Za-z0-9._-] only, got: %s\n' "$NAME" >&2; exit 2 ;;
|
|
124
|
+
esac
|
|
118
125
|
|
|
119
126
|
# --- capacity ----------------------------------------------------------------
|
|
120
127
|
CAP="${CAP_OVERRIDE:-$(jarvis_cfg capacity)}"
|
|
@@ -124,25 +131,19 @@ case "$CAP" in
|
|
|
124
131
|
esac
|
|
125
132
|
|
|
126
133
|
# --- resolve -----------------------------------------------------------------
|
|
127
|
-
#
|
|
128
|
-
|
|
134
|
+
# The ONE command shape, built once — --dry-run prints exactly the argv launch
|
|
135
|
+
# execs (modulo the caffeinate degradation below), so the two can never drift.
|
|
136
|
+
set -- claude remote-control --name "$NAME" --spawn worktree --capacity "$CAP"
|
|
129
137
|
|
|
130
138
|
if [ "$DRY_RUN" = "1" ]; then
|
|
131
|
-
printf '%s\n' "
|
|
139
|
+
printf 'caffeinate -s %s\n' "$*"
|
|
132
140
|
exit 0
|
|
133
141
|
fi
|
|
134
142
|
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
# worktrees, never from memory (Amendment 2). That boot arming act attaches
|
|
140
|
-
# HERE — as the launcher's instruction to the pre-created session — once the
|
|
141
|
-
# relay (B4) exists. Deliberately a no-op today: B2 ships the presence only,
|
|
142
|
-
# and building any relay glue ahead of B4 is out of this phase's scope.
|
|
143
|
-
# ============================================================================
|
|
144
|
-
jarvis_boot_arm() { :; }
|
|
145
|
-
jarvis_boot_arm
|
|
143
|
+
# Boot arming (contract B4(a)) is the PRE-CREATED SESSION's act, not the launcher's:
|
|
144
|
+
# the session arms Monitor `tail -F` watches on the live notify logs per
|
|
145
|
+
# .claude/skills/jarvis/SKILL.md ("Session binding" / arm-the-watches). The launcher
|
|
146
|
+
# stays presence-only and deliberately keeps no relay glue.
|
|
146
147
|
|
|
147
148
|
# --- launch ------------------------------------------------------------------
|
|
148
149
|
command -v claude >/dev/null 2>&1 || {
|
|
@@ -152,9 +153,9 @@ command -v claude >/dev/null 2>&1 || {
|
|
|
152
153
|
printf 'forge-jarvis: starting %s (capacity %s) in %s\n' "$NAME" "$CAP" "$ROOT" >&2
|
|
153
154
|
cd "$ROOT" || exit 2
|
|
154
155
|
if command -v caffeinate >/dev/null 2>&1; then
|
|
155
|
-
exec caffeinate -s
|
|
156
|
+
exec caffeinate -s "$@"
|
|
156
157
|
else
|
|
157
158
|
# non-macOS: no caffeinate — launch unwrapped; sleep then queues-until-wake (probe iii).
|
|
158
159
|
printf 'forge-jarvis: caffeinate not found — launching without keep-awake (lid-closed autonomy degrades to queue-until-wake).\n' >&2
|
|
159
|
-
exec
|
|
160
|
+
exec "$@"
|
|
160
161
|
fi
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Fixture test for .forge/checks/forge-jarvis-awareness.sh (m-37 B5; FR-203/204, NFR-036).
|
|
3
|
+
#
|
|
4
|
+
# Pins the B5 "what's in play?" surface that shipped untested: the slice glob
|
|
5
|
+
# (--worktree-root seam), per-slice fold recompute + degradation, the board
|
|
6
|
+
# projection parse (including key-order independence) + degradation, the
|
|
7
|
+
# always-present per-machine caveat, and the loud unknown-flag refusal.
|
|
8
|
+
# No real repo/network — pure fixtures via the existing env seams.
|
|
9
|
+
set -u
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
AWARE="$SCRIPT_DIR/../forge-jarvis-awareness.sh"
|
|
13
|
+
[ -f "$AWARE" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$AWARE" >&2
|
|
14
|
+
|
|
15
|
+
ROOT="$(mktemp -d)"
|
|
16
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
17
|
+
|
|
18
|
+
PASSED=0; FAILED=0
|
|
19
|
+
ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
|
|
20
|
+
bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
|
|
21
|
+
|
|
22
|
+
# fixture slice worktrees (the --worktree-root seam globs this dir)
|
|
23
|
+
WT="$ROOT/worktrees"; mkdir -p "$WT/m-41" "$WT/m-42"
|
|
24
|
+
|
|
25
|
+
# stub fold: records invocation, answers a fixed state
|
|
26
|
+
FOLDSTUB="$ROOT/fold.sh"
|
|
27
|
+
cat > "$FOLDSTUB" <<'EOF'
|
|
28
|
+
#!/bin/sh
|
|
29
|
+
echo "called $1" >> "${FOLD_CALLS:-/dev/null}"
|
|
30
|
+
echo "release_state=merged"
|
|
31
|
+
EOF
|
|
32
|
+
chmod +x "$FOLDSTUB"
|
|
33
|
+
|
|
34
|
+
# stub board: two units, one complete one executing — and the executing row uses a
|
|
35
|
+
# DIFFERENT key order (release_state before status) to pin the order-independent parse.
|
|
36
|
+
BOARDSTUB="$ROOT/board.sh"
|
|
37
|
+
cat > "$BOARDSTUB" <<'EOF'
|
|
38
|
+
#!/bin/sh
|
|
39
|
+
[ "$1" = "--emit" ] || exit 2
|
|
40
|
+
printf '{"forge_id": "m-40", "status": "complete", "release_state": "validated"}\n'
|
|
41
|
+
printf '{"forge_id": "m-41", "release_state": "unmerged", "status": "executing"}\n'
|
|
42
|
+
EOF
|
|
43
|
+
chmod +x "$BOARDSTUB"
|
|
44
|
+
|
|
45
|
+
# 1. --dry-run: slices listed, fold NOT invoked
|
|
46
|
+
CALLS="$ROOT/fold-calls"; : > "$CALLS"
|
|
47
|
+
OUT="$(FOLD_CALLS="$CALLS" FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$BOARDSTUB" \
|
|
48
|
+
sh "$AWARE" --dry-run --worktree-root "$WT" 2>&1)"
|
|
49
|
+
if printf '%s' "$OUT" | grep -q 'm-41' && printf '%s' "$OUT" | grep -q '(dry-run)' && [ ! -s "$CALLS" ]; then
|
|
50
|
+
ok '--dry-run lists slices without invoking the fold'
|
|
51
|
+
else
|
|
52
|
+
bad '--dry-run lists slices without invoking the fold' "$OUT"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# 2. real run: per-slice fold recompute rendered
|
|
56
|
+
OUT="$(FOLD_CALLS="$CALLS" FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$BOARDSTUB" \
|
|
57
|
+
sh "$AWARE" --worktree-root "$WT" 2>&1)"
|
|
58
|
+
if printf '%s' "$OUT" | grep -q 'release_state=merged' && grep -q 'called m-41' "$CALLS"; then
|
|
59
|
+
ok 'live run recomputes each slice release_state from the fold'
|
|
60
|
+
else
|
|
61
|
+
bad 'live run recomputes each slice release_state from the fold' "$OUT"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# 3. fold missing → unknown, exit 0 (degraded, never fatal)
|
|
65
|
+
OUT="$(FORGE_JARVIS_FOLD="$ROOT/no-such-fold" FORGE_JARVIS_BOARD="$BOARDSTUB" \
|
|
66
|
+
sh "$AWARE" --worktree-root "$WT" 2>&1)"; RC=$?
|
|
67
|
+
if [ "$RC" -eq 0 ] && printf '%s' "$OUT" | grep -q 'release_state=unknown'; then
|
|
68
|
+
ok 'missing fold degrades to release_state=unknown, exit 0'
|
|
69
|
+
else
|
|
70
|
+
bad 'missing fold degrades to release_state=unknown, exit 0' "rc=$RC out=$OUT"
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# 4. board projection: in-flight only, key-order-independent field extraction
|
|
74
|
+
OUT="$(FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$BOARDSTUB" \
|
|
75
|
+
sh "$AWARE" --worktree-root "$WT" 2>&1)"
|
|
76
|
+
if printf '%s' "$OUT" | grep -q 'm-41 status=executing release_state=unmerged' \
|
|
77
|
+
&& ! printf '%s' "$OUT" | grep -q 'm-40 status=complete'; then
|
|
78
|
+
ok 'board parse: in-flight only, fields extracted regardless of key order'
|
|
79
|
+
else
|
|
80
|
+
bad 'board parse: in-flight only, fields extracted regardless of key order' "$OUT"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# 5. board missing → stated unavailable, exit 0
|
|
84
|
+
OUT="$(FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$ROOT/no-such-board" \
|
|
85
|
+
sh "$AWARE" --worktree-root "$WT" 2>&1)"; RC=$?
|
|
86
|
+
if [ "$RC" -eq 0 ] && printf '%s' "$OUT" | grep -qi 'projection unavailable'; then
|
|
87
|
+
ok 'missing board degrades to a stated unavailable, exit 0'
|
|
88
|
+
else
|
|
89
|
+
bad 'missing board degrades to a stated unavailable, exit 0' "rc=$RC out=$OUT"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# 6. per-machine caveat always present (F11)
|
|
93
|
+
if printf '%s' "$OUT" | grep -q 'PER-MACHINE'; then
|
|
94
|
+
ok 'per-machine scope caveat is always stated'
|
|
95
|
+
else
|
|
96
|
+
bad 'per-machine scope caveat is always stated' "$OUT"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# 7. empty worktree root → explicit none-line, exit 0
|
|
100
|
+
EMPTY="$ROOT/empty"; mkdir -p "$EMPTY"
|
|
101
|
+
OUT="$(FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$BOARDSTUB" \
|
|
102
|
+
sh "$AWARE" --worktree-root "$EMPTY" 2>&1)"; RC=$?
|
|
103
|
+
if [ "$RC" -eq 0 ] && printf '%s' "$OUT" | grep -q '(none on this machine)'; then
|
|
104
|
+
ok 'empty glob renders an explicit none-line, not silence'
|
|
105
|
+
else
|
|
106
|
+
bad 'empty glob renders an explicit none-line, not silence' "rc=$RC out=$OUT"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# 8. unknown flag → loud refusal, exit 2
|
|
110
|
+
OUT="$(sh "$AWARE" --no-such-flag 2>&1)"; RC=$?
|
|
111
|
+
if [ "$RC" -eq 2 ] && printf '%s' "$OUT" | grep -q 'unknown argument'; then
|
|
112
|
+
ok 'unknown flag refused loudly (exit 2)'
|
|
113
|
+
else
|
|
114
|
+
bad 'unknown flag refused loudly (exit 2)' "rc=$RC out=$OUT"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
printf '\nforge-jarvis-awareness.test.sh: %s passed, %s failed\n' "$PASSED" "$FAILED"
|
|
118
|
+
[ "$FAILED" -eq 0 ]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Fixture test for .forge/checks/forge-jarvis-notify-logonly.sh (m-37 B3; FR-197).
|
|
3
|
+
#
|
|
4
|
+
# Pins the log-only channel's whole contract: one TSV line per ping at the
|
|
5
|
+
# FORGE_SLICE_NOTIFY_LOG path, field-forging bytes scrubbed from caller input,
|
|
6
|
+
# best-effort exit 0 even when the write fails, and the FORGE_SLICE_WORK_DIR
|
|
7
|
+
# default location. Pure fixtures.
|
|
8
|
+
set -u
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
11
|
+
CHAN="$SCRIPT_DIR/../forge-jarvis-notify-logonly.sh"
|
|
12
|
+
[ -f "$CHAN" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$CHAN" >&2
|
|
13
|
+
|
|
14
|
+
ROOT="$(mktemp -d)"
|
|
15
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
16
|
+
|
|
17
|
+
PASSED=0; FAILED=0
|
|
18
|
+
ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
|
|
19
|
+
bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
|
|
20
|
+
TAB="$(printf '\t')"
|
|
21
|
+
|
|
22
|
+
# 1. appends exactly one TSV line: ts <TAB> type <TAB> payload
|
|
23
|
+
LOG="$ROOT/notify.log"
|
|
24
|
+
FORGE_SLICE_NOTIFY_LOG="$LOG" sh "$CHAN" ambiguity "JWT or sessions?"; RC=$?
|
|
25
|
+
if [ "$RC" -eq 0 ] && [ "$(wc -l < "$LOG" | tr -d ' ')" = 1 ] \
|
|
26
|
+
&& grep -q "${TAB}ambiguity${TAB}JWT or sessions?$" "$LOG"; then
|
|
27
|
+
ok 'one ping → one TSV line (ts, type, payload), exit 0'
|
|
28
|
+
else
|
|
29
|
+
bad 'one ping → one TSV line (ts, type, payload), exit 0' "rc=$RC log=$(cat "$LOG" 2>/dev/null)"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# 2. a payload with embedded newline/tab cannot forge lines or fields
|
|
33
|
+
FORGE_SLICE_NOTIFY_LOG="$LOG" sh "$CHAN" done "line1
|
|
34
|
+
line2${TAB}forged-field"
|
|
35
|
+
LINES="$(wc -l < "$LOG" | tr -d ' ')"
|
|
36
|
+
FIELDS="$(tail -1 "$LOG" | awk -F"$TAB" '{print NF}')"
|
|
37
|
+
if [ "$LINES" = 2 ] && [ "$FIELDS" = 3 ]; then
|
|
38
|
+
ok 'newline/tab in payload scrubbed — no forged lines, still 3 TSV fields'
|
|
39
|
+
else
|
|
40
|
+
bad 'newline/tab in payload scrubbed — no forged lines, still 3 TSV fields' "lines=$LINES fields=$FIELDS"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# 3. best-effort contract: unwritable log target → still exit 0 (never a slice failure)
|
|
44
|
+
FORGE_SLICE_NOTIFY_LOG="$ROOT/no-perm/notify.log" sh -c 'mkdir -p "'"$ROOT"'/no-perm" && chmod 555 "'"$ROOT"'/no-perm"'
|
|
45
|
+
FORGE_SLICE_NOTIFY_LOG="$ROOT/no-perm/notify.log" sh "$CHAN" done "cannot land"; RC=$?
|
|
46
|
+
chmod 755 "$ROOT/no-perm" 2>/dev/null
|
|
47
|
+
if [ "$RC" -eq 0 ]; then
|
|
48
|
+
ok 'unwritable log → exit 0 (best-effort, never a slice failure)'
|
|
49
|
+
else
|
|
50
|
+
bad 'unwritable log → exit 0 (best-effort, never a slice failure)' "rc=$RC"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# 4. default location: FORGE_SLICE_WORK_DIR/notify.log when NOTIFY_LOG is unset
|
|
54
|
+
WORK="$ROOT/work"; mkdir -p "$WORK"
|
|
55
|
+
( unset FORGE_SLICE_NOTIFY_LOG 2>/dev/null; FORGE_SLICE_WORK_DIR="$WORK" sh "$CHAN" note "hello" )
|
|
56
|
+
if [ -f "$WORK/notify.log" ] && grep -q "${TAB}note${TAB}hello$" "$WORK/notify.log"; then
|
|
57
|
+
ok 'default location is FORGE_SLICE_WORK_DIR/notify.log'
|
|
58
|
+
else
|
|
59
|
+
bad 'default location is FORGE_SLICE_WORK_DIR/notify.log' "$(ls "$WORK" 2>/dev/null)"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
printf '\nforge-jarvis-notify-logonly.test.sh: %s passed, %s failed\n' "$PASSED" "$FAILED"
|
|
63
|
+
[ "$FAILED" -eq 0 ]
|