forge-orkes 0.62.0 → 0.64.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.
Files changed (56) hide show
  1. package/bin/create-forge.js +250 -99
  2. package/experimental/conventions/README.md +87 -0
  3. package/experimental/conventions/install.sh +179 -0
  4. package/experimental/conventions/source/rules/code-quality.md +17 -0
  5. package/experimental/conventions/source/rules/laravel-http.md +21 -0
  6. package/experimental/conventions/source/rules/laravel-jobs.md +19 -0
  7. package/experimental/conventions/source/rules/laravel-migrations.md +19 -0
  8. package/experimental/conventions/source/rules/laravel-services.md +21 -0
  9. package/experimental/conventions/source/rules/python.md +14 -0
  10. package/experimental/conventions/source/rules/testing.md +15 -0
  11. package/experimental/conventions/source/rules/vue-inertia.md +24 -0
  12. package/experimental/conventions/source/skills/laravel-conventions/SKILL.md +124 -0
  13. package/experimental/conventions/source/skills/python-conventions/SKILL.md +100 -0
  14. package/experimental/conventions/source/skills/testing-standards/SKILL.md +142 -0
  15. package/experimental/conventions/source/skills/vue-conventions/SKILL.md +92 -0
  16. package/experimental/conventions/uninstall.sh +97 -0
  17. package/experimental/m10/README.md +130 -0
  18. package/experimental/m10/install.sh +198 -0
  19. package/experimental/m10/source/hooks/forge-branch-guard.sh +61 -0
  20. package/experimental/m10/source/hooks/forge-claim-check-doctor.sh +77 -0
  21. package/experimental/m10/source/hooks/forge-claim-check.sh +113 -0
  22. package/experimental/m10/source/hooks/forge-session-id.sh +22 -0
  23. package/experimental/m10/source/mcp-server/README.md +74 -0
  24. package/experimental/m10/source/mcp-server/example.mcp.json +8 -0
  25. package/experimental/m10/source/mcp-server/index.js +460 -0
  26. package/experimental/m10/source/mcp-server/package.json +17 -0
  27. package/experimental/m10/source/skills/orchestrating/SKILL.md +223 -0
  28. package/experimental/m10/source/skills/orchestrating/bootstrap-checks.md +70 -0
  29. package/experimental/m10/uninstall.sh +69 -0
  30. package/package.json +3 -2
  31. package/template/.claude/hooks/forge-release-fold.sh +9 -1
  32. package/template/.claude/settings.json +1 -7
  33. package/template/.claude/skills/jarvis/SKILL.md +204 -0
  34. package/template/.claude/skills/upgrading/SKILL.md +105 -176
  35. package/template/.forge/FORGE.md +2 -2
  36. package/template/.forge/checks/forge-board-render.sh +11 -5
  37. package/template/.forge/checks/forge-jarvis-answer.sh +79 -0
  38. package/template/.forge/checks/forge-jarvis-awareness.sh +128 -0
  39. package/template/.forge/checks/forge-jarvis-bridge-clean.sh +130 -0
  40. package/template/.forge/checks/forge-jarvis-dispatch.sh +176 -0
  41. package/template/.forge/checks/forge-jarvis-instruments.sh +146 -0
  42. package/template/.forge/checks/forge-jarvis-notify-logonly.sh +40 -0
  43. package/template/.forge/checks/forge-jarvis-promote.sh +103 -0
  44. package/template/.forge/checks/forge-jarvis-prwatch.sh +92 -0
  45. package/template/.forge/checks/forge-jarvis-relay.sh +215 -0
  46. package/template/.forge/checks/forge-jarvis.sh +160 -0
  47. package/template/.forge/checks/tests/forge-jarvis-answer.test.sh +75 -0
  48. package/template/.forge/checks/tests/forge-jarvis-bridge-clean.test.sh +94 -0
  49. package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +162 -0
  50. package/template/.forge/checks/tests/forge-jarvis-instruments.test.sh +64 -0
  51. package/template/.forge/checks/tests/forge-jarvis-promote.test.sh +102 -0
  52. package/template/.forge/checks/tests/forge-jarvis-prwatch.test.sh +66 -0
  53. package/template/.forge/checks/tests/forge-jarvis-relay.test.sh +135 -0
  54. package/template/.forge/checks/tests/forge-jarvis.test.sh +122 -0
  55. package/template/.forge/migrations/0.63.0-origin-first-upgrading.md +49 -0
  56. package/template/.forge/templates/project.yml +11 -0
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env sh
2
+ # forge-jarvis-promote.sh — the promotion VOICE (m-37, Brief-R2 step 4 B6; contract
3
+ # handoff-step4-jarvis.md REV2; FR-208; NFR-038; operator ruling 3 "trigger-not-approve").
4
+ #
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 + the
7
+ # fold), never a session's word. The merged-but-not-yet-validated units are the ones a
8
+ # promotion covers, by CONTAINMENT: a unit becomes validated when its landed SHA is an
9
+ # ancestor of the signed promotion tag's commit (ADR-024) — verified offline, host-side.
10
+ # 2. TRIGGER the step-2 seam: gh workflow run promote.yml -f sha=<full-sha>
11
+ #
12
+ # That is ALL it does. It NEVER approves, waives, or lowers anything: the flow STOPS at
13
+ # GitHub's Environment gates (both taps: deploy + mint) until the operator presses there.
14
+ # Triggering is not approving — the approval is not Jarvis's to give (operator ruling 3).
15
+ # There is deliberately NO Environment-approval call anywhere in this file; the neg-control in
16
+ # the test suite greps this file to prove it (an approval path would fail the acceptance test).
17
+ #
18
+ # forge-jarvis-promote.sh <full-sha> [--dry-run]
19
+ # <full-sha> the full 40-hex deploy SHA. The promotion tag is promotion/<full-sha>
20
+ # (FROZEN with the operator); a short sha is refused — containment needs the
21
+ # full one, and Jarvis never invents or truncates a sha.
22
+ # --dry-run SHOW host state, then PRINT the trigger command without running it.
23
+ #
24
+ # 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
+ # FORGE_JARVIS_FOLD default .claude/hooks/forge-release-fold.sh (per-unit release_state)
27
+ # FORGE_JARVIS_GH default `gh` (the trigger — never an approval)
28
+ # BEST-EFFORT: a missing board/fold degrades to a stated '(unavailable)' — the show still
29
+ # renders; a missing gh in a real (non-dry-run) trigger is a clear error, never a silent success.
30
+ set -u
31
+
32
+ SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
33
+ REPO_ROOT="$(cd "$SELF_DIR/../.." && pwd)" # .forge/checks → repo root
34
+ 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
+ GH="${FORGE_JARVIS_GH:-gh}"
37
+ WORKFLOW="promote.yml"
38
+
39
+ SHA=""
40
+ DRY_RUN=0
41
+ while [ $# -gt 0 ]; do
42
+ case "$1" in
43
+ --dry-run) DRY_RUN=1; shift ;;
44
+ --*) shift ;; # tolerate/ignore unknown flags
45
+ *) [ -z "$SHA" ] && SHA="$1"; shift ;;
46
+ esac
47
+ done
48
+
49
+ # ── refuse a missing or non-full sha (Jarvis never invents/truncates a sha) ─────────────────────
50
+ case "$SHA" in
51
+ "") printf 'forge-jarvis-promote: need a full deploy SHA — usage: forge-jarvis-promote.sh <full-sha> [--dry-run]\n' >&2; exit 2 ;;
52
+ esac
53
+ if ! printf '%s' "$SHA" | grep -qE '^[0-9a-f]{40}$'; then
54
+ printf 'forge-jarvis-promote: %s is not a full 40-hex SHA. Promotion validates by containment against promotion/<full-sha> — pass the full deploy SHA, not a short one.\n' "$SHA" >&2
55
+ exit 2
56
+ fi
57
+
58
+ # ── 1. SHOW what this SHA carries — host truth, recomputed now (never a session's claim) ─────────
59
+ printf '── Promote %s ────────────────────────────────────\n' "$SHA"
60
+ printf '\n> What this promotion covers (board projection — merged units validate by containment):\n'
61
+ if [ -f "$BOARD" ]; then
62
+ _emit="$(sh "$BOARD" --emit 2>/dev/null)"
63
+ if [ -n "$_emit" ]; then
64
+ # release-relevant units: merged (candidate -> validated) + validated (already covered).
65
+ # line-oriented JSON -> grep/sed, no jq (Article X native-first) — same parse as awareness.
66
+ _rel="$(printf '%s\n' "$_emit" | grep '"forge_id"' | grep -E '"release_state":[[:space:]]*"(merged|validated)"')"
67
+ if [ -n "$_rel" ]; then
68
+ printf '%s\n' "$_rel" \
69
+ | sed -E 's/.*"forge_id":[[:space:]]*"([^"]+)".*"release_state":[[:space:]]*"([^"]+)".*/ \1 release_state=\2/' \
70
+ | head -60
71
+ else
72
+ printf ' (no merged units in the board projection — nothing waiting on a promotion)\n'
73
+ fi
74
+ else
75
+ printf ' (board projection unavailable — degraded, not fatal)\n'
76
+ fi
77
+ else
78
+ printf ' (forge-board-render.sh not found — projection unavailable)\n'
79
+ fi
80
+ printf '\n Validation is by CONTAINMENT: each merged unit becomes validated when its landed SHA is an\n'
81
+ printf ' ancestor of the signed promotion/%s tag commit (ADR-024) — host-verified offline, not a claim.\n' "$SHA"
82
+
83
+ # ── 2. TRIGGER the step-2 seam — and STOP. Jarvis triggers; the operator presses the gates. ──────
84
+ CMD="$GH workflow run $WORKFLOW -f sha=$SHA"
85
+ printf '\n> Trigger (step-2 seam — Jarvis triggers; the operator presses GitHub Environment gates):\n'
86
+ if [ "$DRY_RUN" -eq 1 ]; then
87
+ printf ' [--dry-run] would run: %s\n' "$CMD"
88
+ exit 0
89
+ fi
90
+ if ! command -v "$GH" >/dev/null 2>&1; then
91
+ printf ' gh not found — cannot trigger the promotion. Run it from a host that has gh: %s\n' "$CMD" >&2
92
+ exit 3
93
+ fi
94
+ printf ' %s\n' "$CMD"
95
+ # gh workflow run promote.yml -f sha=<full-sha> — the trigger, never an approval call.
96
+ "$GH" workflow run "$WORKFLOW" -f sha="$SHA"
97
+ _rc=$?
98
+ if [ "$_rc" -eq 0 ]; then
99
+ printf '\n[ok] Triggered promote.yml for %s. It now STOPS at GitHub Environment gates — press them there; Jarvis does not.\n' "$SHA"
100
+ else
101
+ printf '\n[!] gh workflow run exited %s — the promotion was NOT triggered. Check the workflow name / host auth.\n' "$_rc" >&2
102
+ fi
103
+ exit "$_rc"
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env sh
2
+ # forge-jarvis-prwatch.sh — the bounded "merged, on staging — come try" watch
3
+ # (m-37, Brief-R2 step 4 B4(c); contract handoff-step4-jarvis.md REV2).
4
+ #
5
+ # The runner's `done` ping fires at PR-open; step-2 auto-merge + the staging deploy land minutes
6
+ # LATER, so that flagship "come try it" moment never appears in the notify log. This watch is the
7
+ # SECOND bounded source for it: on a `done`, poll `gh` for the PR's merge + staging-deploy state
8
+ # with a HARD bound and, on merge+deploy, emit the come-try text once.
9
+ #
10
+ # STRICTLY BOUNDED + self-terminating (NFR-036 / Fork 3): it stops on merge+deploy, on a closed
11
+ # PR, or when the iteration/wall bound is hit — it NEVER becomes a standing daemon and owns no
12
+ # durable state beyond the at-most-once come-try marker (keyed by PR number).
13
+ #
14
+ # HOST TRUTH ONLY: merge + deploy state come from `gh`, never from a build session's words
15
+ # (operator ruling 5). The gh binary is env-overridable (FORGE_JARVIS_GH) so fixtures drive tests.
16
+ #
17
+ # --pr <num> the PR to watch (required)
18
+ # --repo <owner/repo> default: the current repo (gh resolves it)
19
+ # --staging-env <name> deployment environment that means "on staging" (default: staging)
20
+ # --max-iterations <N> hard poll bound (default 30)
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 (default: cwd/.relay-markers)
23
+ # --dry-run print the resolved plan (pr, repo, env, bound) and exit; no polling
24
+ set -u
25
+
26
+ PR=""; REPO=""; ENV_STAGING="staging"; MAX_ITER=30; INTERVAL=20; MARKER_DIR=""
27
+ GH="${FORGE_JARVIS_GH:-gh}"
28
+ DRY=0
29
+ while [ $# -gt 0 ]; do
30
+ case "$1" in
31
+ --pr) PR="${2:-}"; shift 2 ;;
32
+ --repo) REPO="${2:-}"; shift 2 ;;
33
+ --staging-env) ENV_STAGING="${2:-staging}"; shift 2 ;;
34
+ --max-iterations) MAX_ITER="${2:-30}"; shift 2 ;;
35
+ --interval) INTERVAL="${2:-20}"; shift 2 ;;
36
+ --marker-dir) MARKER_DIR="${2:-}"; shift 2 ;;
37
+ --dry-run) DRY=1; shift ;;
38
+ *) shift ;;
39
+ esac
40
+ done
41
+ [ -n "$PR" ] || { printf 'forge-jarvis-prwatch: --pr <num> is required\n' >&2; exit 2; }
42
+ [ -n "$MARKER_DIR" ] || MARKER_DIR="./.relay-markers"
43
+ REPO_ARG=""; [ -n "$REPO" ] && REPO_ARG="--repo $REPO"
44
+
45
+ if [ "$DRY" = 1 ]; then
46
+ printf 'prwatch plan: pr=%s repo=%s env=%s bound=%s×%ss marker-dir=%s\n' \
47
+ "$PR" "${REPO:-<current>}" "$ENV_STAGING" "$MAX_ITER" "$INTERVAL" "$MARKER_DIR"
48
+ exit 0
49
+ fi
50
+
51
+ # --- host-truth probes (gh only) -------------------------------------------------------------
52
+ # The two probes are literal gh queries (env-overridable via $GH for fixtures):
53
+ # merge state → `gh pr view <num> --json state --jq .state`
54
+ # staging deploy → `gh api repos/{owner}/{repo}/deployments?environment=<env>` + its statuses
55
+ # Both read host truth; neither consults anything a build session emitted.
56
+ pr_merge_state() { # → MERGED | CLOSED | OPEN (empty on gh error → treated as OPEN, keep polling)
57
+ # shellcheck disable=SC2086
58
+ "$GH" pr view "$PR" $REPO_ARG --json state --jq '.state' 2>/dev/null
59
+ }
60
+ deploy_succeeded() { # → exit 0 iff the latest staging deployment is in a success state
61
+ # shellcheck disable=SC2086
62
+ _st="$("$GH" api "repos/{owner}/{repo}/deployments?environment=$ENV_STAGING&per_page=1" \
63
+ --jq '.[0].statuses_url' 2>/dev/null)"
64
+ [ -n "$_st" ] || return 1
65
+ _s="$("$GH" api "$_st?per_page=1" --jq '.[0].state' 2>/dev/null)"
66
+ [ "$_s" = success ]
67
+ }
68
+
69
+ fire_come_try() { # at-most-once, keyed by PR (the ONE owned state class)
70
+ mkdir -p "$MARKER_DIR" 2>/dev/null || true
71
+ _mk="$MARKER_DIR/pr-$PR-cometry.relayed"
72
+ [ -f "$_mk" ] && return 0 # already fired — never re-buzz
73
+ _text="🚀 Merged, on staging — come try: PR #$PR is live on $ENV_STAGING."
74
+ printf '%s\toffset=pr-%s\t%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo now)" "$PR" "$_text" > "$_mk" 2>/dev/null || true
75
+ printf '%s\n' "$_text" # the skill fires PushNotification with this
76
+ }
77
+
78
+ # --- the bounded poll loop -------------------------------------------------------------------
79
+ i=0
80
+ while [ "$i" -lt "$MAX_ITER" ]; do
81
+ case "$(pr_merge_state)" in
82
+ MERGED)
83
+ if deploy_succeeded; then fire_come_try; exit 0; fi ;; # the come-try moment — fire + stop
84
+ CLOSED)
85
+ exit 0 ;; # closed without a live deploy — stop, no fire
86
+ *) : ;; # OPEN (or gh hiccup) — keep polling
87
+ esac
88
+ i=$((i + 1))
89
+ [ "$i" -lt "$MAX_ITER" ] && [ "$INTERVAL" -gt 0 ] && sleep "$INTERVAL"
90
+ done
91
+ # bound reached without a merge+deploy — self-terminate cleanly, no fire (never a daemon).
92
+ exit 0
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env sh
2
+ # forge-jarvis-relay.sh — the callback-relay CORE (m-37, Brief-R2 step 4 B4(a/b/c/e);
3
+ # contract handoff-step4-jarvis.md REV2). Turns one notify-log line into a relay
4
+ # decision: HOST-TRUTH recompute → typed relay text → at-most-once offset marker.
5
+ #
6
+ # The runner writes every §8 ping to a slice's notify.log (tab-separated
7
+ # `timestamp<TAB>type<TAB>payload`; forge-slice-notify.sh / -notify-logonly.sh). Under a
8
+ # live Jarvis, that log IS the event bus: the skill arms a Monitor `tail -F` on it and pipes
9
+ # each new line through THIS helper, then fires PushNotification with the returned text.
10
+ #
11
+ # THE LOAD-BEARING RULE (operator ruling 5): every relayed STATE claim (merged / landed /
12
+ # validated) is recomputed from HOST truth (the fold / gh / board) BEFORE relay — NEVER taken
13
+ # from the payload. A payload that claims what the host does not show relays the DISCREPANCY,
14
+ # loudly. A question (ambiguity) is relayed verbatim — it is a question, not a state claim.
15
+ #
16
+ # OWNED STATE (the ONE admitted class, NFR-036): durable marker files keyed by the log-line
17
+ # OFFSET (1-based line number — stable in an append-only log; the runner's internal ping-dedup
18
+ # keys never reach the log, F10). Marker present → already relayed → never re-buzz (at-most-once).
19
+ # Everything else Jarvis "knows" is recomputed; kill Jarvis and only the live connection + armed
20
+ # watches are lost — the markers on disk survive and re-drive the while-you-were-away catch-up.
21
+ #
22
+ # MODES
23
+ # --one "<tsv-line>" [--milestone m-N] pure text-builder: print the relay text for ONE
24
+ # line; NO marker, NO side effects (the unit tests hit).
25
+ # --scan <logfile> [--marker-dir <dir>] [--milestone m-N] [--mode live|catchup]
26
+ # stateful driver: relay every UNMARKED line, write
27
+ # its marker. live → one text line per new ping;
28
+ # catchup → ONE "while you were away" summary. Both
29
+ # write markers, so a line is relayed at-most-once.
30
+ # --audit-sources <?logfile> self-grep: fail if any state claim is sourced from
31
+ # the payload rather than fold/gh/board (acceptance #3).
32
+ #
33
+ # HOST-TRUTH COMMANDS (env-overridable so tests inject stubs, and so degradation is explicit):
34
+ # FORGE_JARVIS_FOLD default .claude/hooks/forge-release-fold.sh (release_state recompute)
35
+ # FORGE_JARVIS_GH default `gh` (PR/check facts — plan-02 leans on it)
36
+ # FORGE_JARVIS_BOARD default .forge/checks/forge-board-render.sh (rollup recompute)
37
+ #
38
+ # BEST-EFFORT: a fold/gh/board that is absent or errors degrades to release_state=unknown and
39
+ # says so — never a silent success, never a hard fail of the relay path.
40
+ set -u
41
+
42
+ SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
43
+ REPO_ROOT="$(cd "$SELF_DIR/../.." && pwd)" # .forge/checks → repo root
44
+ FOLD="${FORGE_JARVIS_FOLD:-$REPO_ROOT/.claude/hooks/forge-release-fold.sh}"
45
+
46
+ # ── host truth: the slice's release_state, recomputed from the fold (never the payload) ───────
47
+ host_release_state() { # host_release_state <milestone-id> → prints unmerged|merged|validated|unknown
48
+ _mid="${1:-}"
49
+ [ -n "$_mid" ] || { printf 'unknown'; return 0; }
50
+ _out="$("$FOLD" "$_mid" 2>/dev/null)" || { printf 'unknown'; return 0; }
51
+ _rs="$(printf '%s\n' "$_out" | grep -m1 'release_state=' | sed 's/.*release_state=//; s/[[:space:]].*//')"
52
+ [ -n "$_rs" ] && printf '%s' "$_rs" || printf 'unknown'
53
+ }
54
+
55
+ # Does the payload TEXT assert a landing/merge/promotion? (only then can it contradict the host)
56
+ payload_claims_landed() { # payload_claims_landed <payload>
57
+ printf '%s' "${1:-}" | grep -qiE 'merged|landed|validated|promoted|on main|is live|shipped|deployed'
58
+ }
59
+
60
+ # ── build the relay text for ONE parsed ping (the pure host-truth text-builder) ───────────────
61
+ # Reads: $2=type $3=payload ; $MILESTONE (optional, for fold). Prints one line to stdout.
62
+ build_relay_text() { # build_relay_text <type> <payload>
63
+ _type="${1:-note}"; _payload="${2:-}"
64
+ case "$_type" in
65
+ ambiguity)
66
+ # A QUESTION — relay verbatim, named. Legal: it is not a state claim (acceptance #2).
67
+ printf '❓ Needs you (ambiguity): %s' "$_payload" ;;
68
+ fork)
69
+ # payload is a PATH to a sketch the operator LOOKS AT (§8) — name it.
70
+ printf '🎨 Design fork — review the sketch: %s' "$_payload" ;;
71
+ irreversible)
72
+ # a gate on an irreversible act — your call; relay verbatim, named.
73
+ printf '⚠️ Irreversible action — your call: %s' "$_payload" ;;
74
+ budget)
75
+ # the runner's OWN spend tally — no host source to recompute against; relay verbatim, named.
76
+ printf '💸 Budget cap reached: %s' "$_payload" ;;
77
+ done|halt)
78
+ # STATE-CLAIM territory. Recompute host truth FIRST; build the claim from it, not the payload.
79
+ _rs="$(host_release_state "${MILESTONE:-}")"
80
+ if [ "$_type" = done ]; then _base='✅ Slice done'; else _base='🛑 Slice HALTED'; fi
81
+ if payload_claims_landed "$_payload"; then
82
+ if [ "$_rs" = unmerged ]; then
83
+ # The standing lesson: payload says merged, host says not. Relay the host, loudly.
84
+ printf '⚠️ DISCREPANCY — payload claims landed/merged, but host shows release_state=unmerged. %s (host truth wins). Payload said: %s' "$_base" "$_payload"
85
+ elif [ "$_rs" = unknown ]; then
86
+ printf '⚠️ %s — payload claims landed/merged but the host could NOT be verified (release_state=unknown). Payload said: %s' "$_base" "$_payload"
87
+ else
88
+ # payload claims landed AND host agrees (merged/validated) — no discrepancy.
89
+ printf '%s — host confirms: release_state=%s. %s' "$_base" "$_rs" "$_payload"
90
+ fi
91
+ else
92
+ printf '%s — host: release_state=%s. %s' "$_base" "$_rs" "$_payload"
93
+ fi ;;
94
+ *)
95
+ # unknown type — relay raw, named (never silently drop).
96
+ printf '(%s): %s' "$_type" "$_payload" ;;
97
+ esac
98
+ }
99
+
100
+ # Parse a TSV log line (ts<TAB>type<TAB>payload) then build text. Prints one line.
101
+ relay_one_line() { # relay_one_line <tsv-line>
102
+ _line="$1"
103
+ _type="$(printf '%s' "$_line" | cut -f2)"
104
+ _payload="$(printf '%s' "$_line" | cut -f3-)"
105
+ # A malformed line with no tabs → cut returns the whole line for f2/f3; treat as a raw note.
106
+ case "$_line" in
107
+ *" "*) : ;; # has a tab — well-formed
108
+ *) _type='note'; _payload="$_line" ;;
109
+ esac
110
+ build_relay_text "$_type" "$_payload"
111
+ }
112
+
113
+ # ── mode dispatch ─────────────────────────────────────────────────────────────────────────────
114
+ MILESTONE=""
115
+ MODE_LIVE="live"
116
+ MARKER_DIR=""
117
+
118
+ case "${1:-}" in
119
+ --one)
120
+ shift
121
+ LINE="${1:-}"; shift 2>/dev/null || true
122
+ while [ $# -gt 0 ]; do case "$1" in --milestone) MILESTONE="${2:-}"; shift 2 ;; *) shift ;; esac; done
123
+ relay_one_line "$LINE"; printf '\n'
124
+ exit 0 ;;
125
+
126
+ --list-logs)
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 ;;
169
+
170
+ --scan)
171
+ shift
172
+ LOG="${1:-}"; shift 2>/dev/null || true
173
+ while [ $# -gt 0 ]; do
174
+ case "$1" in
175
+ --marker-dir) MARKER_DIR="${2:-}"; shift 2 ;;
176
+ --milestone) MILESTONE="${2:-}"; shift 2 ;;
177
+ --mode) MODE_LIVE="${2:-live}"; shift 2 ;;
178
+ *) shift ;;
179
+ esac
180
+ done
181
+ [ -n "$LOG" ] && [ -f "$LOG" ] || { printf 'forge-jarvis-relay: --scan needs an existing logfile\n' >&2; exit 2; }
182
+ [ -n "$MARKER_DIR" ] || MARKER_DIR="$(dirname "$LOG")/relay-markers"
183
+ mkdir -p "$MARKER_DIR" 2>/dev/null || true
184
+
185
+ _n=0; _fresh=0; _summary=""
186
+ # Read every line; relay only those whose offset (line number) has no marker yet.
187
+ while IFS= read -r _l || [ -n "$_l" ]; do
188
+ _n=$((_n + 1))
189
+ _marker="$MARKER_DIR/$_n.relayed"
190
+ [ -f "$_marker" ] && continue # at-most-once: already relayed, never re-buzz
191
+ _text="$(relay_one_line "$_l")"
192
+ # Write the marker BEFORE emitting — a crash after emit must not double-buzz; a crash
193
+ # before emit leaves it unmarked and it re-surfaces as while-you-were-away (no silent loss).
194
+ 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 || true
195
+ _fresh=$((_fresh + 1))
196
+ if [ "$MODE_LIVE" = catchup ]; then
197
+ _summary="${_summary}${_summary:+ | }$_text"
198
+ else
199
+ printf '%s\n' "$_text" # live: one relay text per new ping → one buzz
200
+ fi
201
+ done < "$LOG"
202
+
203
+ if [ "$MODE_LIVE" = catchup ] && [ "$_fresh" -gt 0 ]; then
204
+ printf '📨 While you were away — %s ping(s): %s\n' "$_fresh" "$_summary"
205
+ fi
206
+ # exit 0 whether or not anything was fresh (a no-op scan is success, not failure).
207
+ exit 0 ;;
208
+
209
+ *)
210
+ printf 'usage: forge-jarvis-relay.sh --one "<tsv-line>" [--milestone m-N]\n' >&2
211
+ printf ' forge-jarvis-relay.sh --scan <logfile> [--marker-dir <dir>] [--milestone m-N] [--mode live|catchup]\n' >&2
212
+ printf ' forge-jarvis-relay.sh --list-logs [--worktree-root <dir>]\n' >&2
213
+ printf ' forge-jarvis-relay.sh --audit-sources\n' >&2
214
+ exit 2 ;;
215
+ esac
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env sh
2
+ # forge-jarvis.sh — the Jarvis launcher (m-37, Brief-R2 step 4 B2; contract handoff-step4-jarvis.md REV2).
3
+ #
4
+ # Starts the repo's ONE standing front door: a named `claude remote-control` server whose
5
+ # PRE-CREATED session is Jarvis — intake, dispatch, callback relay, awareness (B3-B5 attach
6
+ # to the session this launcher creates; this script is only the presence). Operator-started
7
+ # when he sits down (presence ops v1-minimal, operator-ruled 2026-07-20): no login items,
8
+ # no watchdogs, no daemon. The launcher is STATELESS — killing the server loses only the
9
+ # live connection and armed watches; everything re-establishes at restart from files
10
+ # (Amendment 2; B1 probe viii: dispatched work survives Jarvis's death via nohup-&).
11
+ #
12
+ # RESOLVED COMMAND SHAPE (operator ruling 6, 2026-07-21):
13
+ # caffeinate -s claude remote-control --name <sticky-name> --spawn worktree --capacity <N>
14
+ #
15
+ # caffeinate -s B1 probe (iii): sleep suspends local COMPUTE (dispatches, Monitor-
16
+ # triggered autonomous turns, sandbox allocation) while the RC
17
+ # connection persists — "present but asleep". -s keeps the machine
18
+ # awake on AC POWER ONLY; on battery, normal sleep + queue-until-wake
19
+ # apply. One word in the launch command, not nursing machinery.
20
+ # (caffeinate absent on PATH → warn + launch unwrapped, portably.)
21
+ # --name B1 probe (ii): the phone lists sessions by their first typed message;
22
+ # the pre-created session's --name is the ONLY sticky, recognizable
23
+ # discoverability anchor. Default: jarvis-<repo-basename>; a per-machine
24
+ # suffix names each machine on multi-machine repos (forge:
25
+ # jarvis-forge-personal / jarvis-forge-work).
26
+ # --spawn worktree on-demand sessions get isolated worktrees (step 0, platform-enforced).
27
+ # NOTE (probe ii finding): spawned bridge worktrees live under
28
+ # .claude/worktrees/ and do NOT auto-clean — B5 presence-ops owns that.
29
+ # --capacity sane for one operator; default 4.
30
+ #
31
+ # AUTH — NO TOKEN EXPORT (B1 probe iv): headless `claude -p` dispatches authenticate via
32
+ # the machine's KEYCHAIN credential with CLAUDE_CODE_OAUTH_TOKEN unset. This launcher must
33
+ # never export a token; if a thinner env ever surfaces the need, `claude setup-token` is
34
+ # the operator's minting path — never committed, never set here.
35
+ #
36
+ # PERMISSIONS — NO FLAGS NEEDED FOR THE PRE-CREATED SESSION (B1 probe vii): typed Bash,
37
+ # Monitor-arm, and PushNotification all run prompt-free in the pre-created session as-is;
38
+ # `--permission-mode` governs only SPAWNED sessions. Nothing to provision here. (Probe vii
39
+ # still owes an explicit check for any OTHER tool a later phase uses unattended.)
40
+ #
41
+ # CONFIG — optional `jarvis:` block in .forge/project.yml (board-block precedent):
42
+ # jarvis:
43
+ # enabled: true # explicit false = this launcher refuses (door switched off)
44
+ # name_suffix: "" # appended: jarvis-<repo>-<suffix>; env FORGE_JARVIS_NAME_SUFFIX
45
+ # # overrides per machine (the multi-machine seam)
46
+ # capacity: 4 # RC server session capacity
47
+ # NO block → sensible defaults (the launcher is operator-invoked, not ambient — unlike
48
+ # the board, absence does not mean inert).
49
+ #
50
+ # USAGE:
51
+ # .forge/checks/forge-jarvis.sh [--dry-run] [--name <full-name>] [--capacity <N>]
52
+ # --dry-run print the fully resolved command on stdout and exit 0 — never executes.
53
+ # --name full sticky-name override (skips derivation entirely).
54
+ # --capacity override config/default capacity.
55
+ #
56
+ # BOUNDARIES (contract "Not this step"): no routing, no approval surface, no new
57
+ # notification infrastructure, no Jarvis-owned state. This script keeps none.
58
+ set -u
59
+
60
+ # --- args -------------------------------------------------------------------
61
+ DRY_RUN=0
62
+ NAME_OVERRIDE=""
63
+ CAP_OVERRIDE=""
64
+ while [ $# -gt 0 ]; do
65
+ case "$1" in
66
+ --dry-run) DRY_RUN=1; shift ;;
67
+ --name) shift; NAME_OVERRIDE="${1:-}"; [ $# -gt 0 ] && shift || true ;;
68
+ --name=*) NAME_OVERRIDE="${1#*=}"; shift ;;
69
+ --capacity) shift; CAP_OVERRIDE="${1:-}"; [ $# -gt 0 ] && shift || true ;;
70
+ --capacity=*) CAP_OVERRIDE="${1#*=}"; shift ;;
71
+ *) printf 'forge-jarvis: unknown argument: %s\n' "$1" >&2; exit 2 ;;
72
+ esac
73
+ done
74
+
75
+ # --- repo (refusal, not fallback: --spawn worktree needs a real git repo) ----
76
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || {
77
+ printf 'forge-jarvis: not inside a git repo — `claude remote-control --spawn worktree` needs one.\n' >&2
78
+ exit 2
79
+ }
80
+ PROJECT="$ROOT/.forge/project.yml"
81
+ # Repo identity for the sticky name: the git COMMON dir's parent, not the checkout's
82
+ # toplevel — stable across worktrees (a worktree basename like m-37 must never leak
83
+ # into the phone-visible name). The presence itself belongs on the MAIN checkout
84
+ # (the shape: Jarvis runs on the repo's main checkout in a real terminal).
85
+ COMMON="$(git rev-parse --git-common-dir)"
86
+ REPO_BASE="$(basename "$(cd "$(dirname "$COMMON")" && pwd)")"
87
+
88
+ # --- config: read a scalar under the top-level `jarvis:` block ---------------
89
+ jarvis_cfg() { # $1 = key under jarvis:
90
+ [ -f "$PROJECT" ] || return 0
91
+ awk -v k="$1" '
92
+ /^jarvis:/ { inb=1; next }
93
+ inb && /^[A-Za-z]/ { inb=0 }
94
+ inb && $0 ~ "^[[:space:]]+"k":" {
95
+ sub(/^[[:space:]]*[^:]+:[[:space:]]*/, "")
96
+ sub(/[[:space:]]*#.*/, "")
97
+ gsub(/^["'"'"']|["'"'"']$/, "")
98
+ print; exit
99
+ }
100
+ ' "$PROJECT"
101
+ }
102
+
103
+ # --- enabled gate (explicit false refuses; absent block = defaults) ----------
104
+ if [ "$(jarvis_cfg enabled)" = "false" ]; then
105
+ printf 'forge-jarvis: jarvis.enabled is false in .forge/project.yml — the door is switched off.\n' >&2
106
+ exit 2
107
+ fi
108
+
109
+ # --- sticky name (probe ii: the only discoverability anchor) -----------------
110
+ if [ -n "$NAME_OVERRIDE" ]; then
111
+ NAME="$NAME_OVERRIDE"
112
+ else
113
+ NAME="jarvis-$REPO_BASE"
114
+ # per-machine seam: env overrides config (multi-machine repos name each machine)
115
+ SUFFIX="${FORGE_JARVIS_NAME_SUFFIX:-$(jarvis_cfg name_suffix)}"
116
+ [ -n "$SUFFIX" ] && NAME="$NAME-$SUFFIX"
117
+ fi
118
+
119
+ # --- capacity ----------------------------------------------------------------
120
+ CAP="${CAP_OVERRIDE:-$(jarvis_cfg capacity)}"
121
+ CAP="${CAP:-4}"
122
+ case "$CAP" in
123
+ *[!0-9]*|'') printf 'forge-jarvis: capacity must be a positive integer, got: %s\n' "$CAP" >&2; exit 2 ;;
124
+ esac
125
+
126
+ # --- resolve -----------------------------------------------------------------
127
+ # Printed exactly as executed (modulo the caffeinate degradation below).
128
+ RESOLVED="caffeinate -s claude remote-control --name $NAME --spawn worktree --capacity $CAP"
129
+
130
+ if [ "$DRY_RUN" = "1" ]; then
131
+ printf '%s\n' "$RESOLVED"
132
+ exit 0
133
+ fi
134
+
135
+ # ============================================================================
136
+ # B4 SEAM — DO NOT REMOVE (contract B4(a)).
137
+ # At boot Jarvis arms a Monitor tail (`tail -F` — capital F) on each active
138
+ # slice's notify log, the watch list recomputed by globbing live slice
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
146
+
147
+ # --- launch ------------------------------------------------------------------
148
+ command -v claude >/dev/null 2>&1 || {
149
+ printf 'forge-jarvis: `claude` not found on PATH.\n' >&2
150
+ exit 2
151
+ }
152
+ printf 'forge-jarvis: starting %s (capacity %s) in %s\n' "$NAME" "$CAP" "$ROOT" >&2
153
+ cd "$ROOT" || exit 2
154
+ if command -v caffeinate >/dev/null 2>&1; then
155
+ exec caffeinate -s claude remote-control --name "$NAME" --spawn worktree --capacity "$CAP"
156
+ else
157
+ # non-macOS: no caffeinate — launch unwrapped; sleep then queues-until-wake (probe iii).
158
+ printf 'forge-jarvis: caffeinate not found — launching without keep-awake (lid-closed autonomy degrades to queue-until-wake).\n' >&2
159
+ exec claude remote-control --name "$NAME" --spawn worktree --capacity "$CAP"
160
+ fi
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env sh
2
+ # Fixture test suite for .forge/checks/forge-jarvis-answer.sh (m-37, step 4 B4(d); plan 68-02).
3
+ #
4
+ # Contract under test (handoff-step4-jarvis.md REV2 B4(d); NFR-037; acceptance #2):
5
+ # - the answer lands at the EXACT m-35 path: <work-dir>/phase-<N>/answer.md, with the reply text.
6
+ # - after writing, the slice is re-dispatched via forge-jarvis-dispatch.sh (stubbed here).
7
+ # - NEG-CONTROL: no answer content → nothing written, nothing re-dispatched (slice stays parked).
8
+ # - the reply may arrive via --answer or via stdin.
9
+ #
10
+ # The dispatch is stubbed via FORGE_JARVIS_DISPATCH (records that it was called) so no real runner
11
+ # spawns. Each assertion prints WHY. RED-friendly: a missing script fails every case.
12
+ set -u
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ ANSWER="$SCRIPT_DIR/../forge-jarvis-answer.sh"
16
+ [ -f "$ANSWER" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$ANSWER" >&2
17
+
18
+ ROOT="$(mktemp -d)"
19
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
20
+ PASSED=0; FAILED=0
21
+ ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
22
+ bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
23
+
24
+ # stub dispatch: record the call so we can assert re-dispatch happened (no real runner spawns)
25
+ STUBD="$ROOT/dispatch-stub.sh"
26
+ CALLED="$ROOT/dispatch.called"
27
+ printf '#!/usr/bin/env sh\nprintf "%%s\\n" "$*" > "%s"\n' "$CALLED" > "$STUBD"
28
+ chmod +x "$STUBD"
29
+ SLICE="$ROOT/slice"; mkdir -p "$SLICE"
30
+
31
+ # 1. answer via --answer → lands at the EXACT m-35 path with the reply text
32
+ WD="$ROOT/w1"; rm -f "$CALLED"
33
+ FORGE_JARVIS_DISPATCH="$STUBD" sh "$ANSWER" --work-dir "$WD" --phase 3 --slice "$SLICE" --answer "Use JWT." >/dev/null 2>&1
34
+ if [ -f "$WD/phase-3/answer.md" ] && grep -q 'Use JWT.' "$WD/phase-3/answer.md"; then
35
+ ok "answer lands at the EXACT m-35 path <work-dir>/phase-3/answer.md with the reply text"
36
+ else
37
+ bad "m-35 answer path" "missing or wrong content at $WD/phase-3/answer.md"
38
+ fi
39
+
40
+ # 2. re-dispatch invoked (the stub recorded the call, carrying --slice + --work-dir)
41
+ if [ -f "$CALLED" ] && grep -q -- "--slice $SLICE" "$CALLED" && grep -q -- "--work-dir $WD" "$CALLED"; then
42
+ ok "slice re-dispatched after the answer landed (forge-jarvis-dispatch called with --slice/--work-dir)"
43
+ else
44
+ bad "re-dispatch" "dispatch not called or missing args: $(cat "$CALLED" 2>/dev/null)"
45
+ fi
46
+
47
+ # 3. NEG-CONTROL: no answer (empty stdin) → nothing written, NO re-dispatch, non-zero exit
48
+ WD2="$ROOT/w2"; rm -f "$CALLED"
49
+ FORGE_JARVIS_DISPATCH="$STUBD" sh "$ANSWER" --work-dir "$WD2" --phase 3 --slice "$SLICE" </dev/null >/dev/null 2>&1; rc=$?
50
+ if [ "$rc" != 0 ] && [ ! -f "$WD2/phase-3/answer.md" ] && [ ! -f "$CALLED" ]; then
51
+ ok "neg-control: no answer → nothing written, no re-dispatch (slice stays parked), non-zero exit"
52
+ else
53
+ bad "neg-control no-answer" "rc=$rc wrote=$([ -f "$WD2/phase-3/answer.md" ] && echo yes) dispatched=$([ -f "$CALLED" ] && echo yes)"
54
+ fi
55
+
56
+ # 4. answer via stdin also works (phone/typed reply piped in)
57
+ WD3="$ROOT/w3"; rm -f "$CALLED"
58
+ printf 'sessions, not JWT' | FORGE_JARVIS_DISPATCH="$STUBD" sh "$ANSWER" --work-dir "$WD3" --phase 7 --slice "$SLICE" >/dev/null 2>&1
59
+ if [ -f "$WD3/phase-7/answer.md" ] && grep -q 'sessions, not JWT' "$WD3/phase-7/answer.md" && [ -f "$CALLED" ]; then
60
+ ok "answer via stdin lands at phase-7/answer.md and re-dispatches"
61
+ else
62
+ bad "stdin answer" "missing write or dispatch for stdin path"
63
+ fi
64
+
65
+ # 5. --dry-run: nothing written, nothing dispatched
66
+ WD4="$ROOT/w4"; rm -f "$CALLED"
67
+ FORGE_JARVIS_DISPATCH="$STUBD" sh "$ANSWER" --work-dir "$WD4" --phase 2 --slice "$SLICE" --answer "x" --dry-run >/dev/null 2>&1
68
+ if [ ! -f "$WD4/phase-2/answer.md" ] && [ ! -f "$CALLED" ]; then
69
+ ok "--dry-run writes nothing and dispatches nothing"
70
+ else
71
+ bad "dry-run side effects" "dry-run wrote or dispatched"
72
+ fi
73
+
74
+ printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
75
+ [ "$FAILED" = 0 ]