@etiquekit/etq 0.0.1 → 1.0.1

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 (83) hide show
  1. package/AGENTS.md +28 -0
  2. package/QuickStart.md +94 -0
  3. package/README.md +77 -9
  4. package/bin/etiquette +1149 -0
  5. package/bin/etiquette-core +313 -0
  6. package/docs/ARCHITECTURE.md +100 -0
  7. package/docs/CODEX_CLIENT_COMPATIBILITY.md +83 -0
  8. package/docs/CONCEPTS.md +128 -0
  9. package/docs/CONCEPT_STATUS.md +67 -0
  10. package/docs/CORE_PROFILE.md +70 -0
  11. package/docs/LANE_PROVISIONING.md +157 -0
  12. package/docs/README.md +29 -0
  13. package/docs/RELEASE_SURFACE_AUDIT.md +156 -0
  14. package/docs/SEAT_DISCIPLINE.md +68 -0
  15. package/docs/SEAT_PROVISIONING.md +100 -0
  16. package/docs/TEAM_HANDOFF.md +190 -0
  17. package/docs/WORKTREE_QOL.md +92 -0
  18. package/docs/contracts/ledger-entry/README.md +83 -0
  19. package/docs/contracts/ledger-entry/ledger-entry.v0.1.md +116 -0
  20. package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +90 -0
  21. package/docs/contracts/ledger-entry/ledger-entry.v0.md +123 -0
  22. package/docs/contracts/ledger-entry/ledger-query-cli.v0.1.md +64 -0
  23. package/package.json +42 -11
  24. package/scripts/install.sh +144 -0
  25. package/scripts/uninstall.sh +58 -0
  26. package/templates/etiquette-vanilla-v0/README.md +69 -0
  27. package/templates/etiquette-vanilla-v0/bin/stamp-vanilla +33 -0
  28. package/templates/etiquette-vanilla-v0/source/control-seat/.llm-substrate/HANDOFF.md +22 -0
  29. package/templates/etiquette-vanilla-v0/source/control-seat/README.md +16 -0
  30. package/templates/etiquette-vanilla-v0/source/control-seat/bin/_lib/http.sh +121 -0
  31. package/templates/etiquette-vanilla-v0/source/control-seat/bin/_lib/sanitize.sh +191 -0
  32. package/templates/etiquette-vanilla-v0/source/control-seat/bin/_lib/state.sh +67 -0
  33. package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +55 -0
  34. package/templates/etiquette-vanilla-v0/source/control-seat/bin/compact-local-events +71 -0
  35. package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/post-tool-use +64 -0
  36. package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/session-end +64 -0
  37. package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/session-start +64 -0
  38. package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/stop +64 -0
  39. package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/user-prompt-submit +64 -0
  40. package/templates/etiquette-vanilla-v0/source/control-seat/bin/local-event +53 -0
  41. package/templates/etiquette-vanilla-v0/source/control-seat/bin/post-receipt +143 -0
  42. package/templates/etiquette-vanilla-v0/source/control-seat/bin/post-telemetry +112 -0
  43. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-consume-pass +115 -0
  44. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +20 -0
  45. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-init +86 -0
  46. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-post-readiness +171 -0
  47. package/templates/etiquette-vanilla-v0/source/control-seat/bin/session-init +132 -0
  48. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +39 -0
  49. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +53 -0
  50. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/ledger/LEDGER.md +7 -0
  51. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/playbacks/PLAYBACK_TEMPLATE.md +31 -0
  52. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/receipts/.gitkeep +1 -0
  53. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/tasks/CONTROL-001.yaml +13 -0
  54. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/README.md +6 -0
  55. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/bin/kernel-doctor +11 -0
  56. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/playback.v0.md +37 -0
  57. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/receipt.v0.md +25 -0
  58. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/seat-readiness.v0.md +22 -0
  59. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/task.v0.md +18 -0
  60. package/templates/etiquette-vanilla-v0/validate-vanilla.sh +43 -0
  61. package/templates/github-actions/README.md +30 -0
  62. package/templates/github-actions/w1-webhook-wake-canary.yml +111 -0
  63. package/templates/hosted-receiver/README.md +41 -0
  64. package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +129 -0
  65. package/templates/seat-packs-v0/README.md +72 -0
  66. package/templates/seat-packs-v0/bin/provision-seat +70 -0
  67. package/templates/seat-packs-v0/source/claude-code-seat/.llm-substrate/HANDOFF.md +24 -0
  68. package/templates/seat-packs-v0/source/claude-code-seat/README.md +19 -0
  69. package/templates/seat-packs-v0/source/claude-code-seat/bin/seat-doctor +8 -0
  70. package/templates/seat-packs-v0/source/codex-seat/.llm-substrate/HANDOFF.md +25 -0
  71. package/templates/seat-packs-v0/source/codex-seat/README.md +22 -0
  72. package/templates/seat-packs-v0/source/codex-seat/bin/seat-doctor +8 -0
  73. package/templates/seat-packs-v0/source/gemini-seat/.llm-substrate/HANDOFF.md +23 -0
  74. package/templates/seat-packs-v0/source/gemini-seat/README.md +19 -0
  75. package/templates/seat-packs-v0/source/gemini-seat/bin/seat-doctor +8 -0
  76. package/templates/seat-packs-v0/source/ollama-seat/.llm-substrate/HANDOFF.md +24 -0
  77. package/templates/seat-packs-v0/source/ollama-seat/README.md +19 -0
  78. package/templates/seat-packs-v0/source/ollama-seat/bin/seat-doctor +8 -0
  79. package/templates/seat-packs-v0/source/openrouter-seat/.llm-substrate/HANDOFF.md +24 -0
  80. package/templates/seat-packs-v0/source/openrouter-seat/README.md +19 -0
  81. package/templates/seat-packs-v0/source/openrouter-seat/bin/seat-doctor +8 -0
  82. package/templates/seat-packs-v0/validate-seat-packs.sh +27 -0
  83. package/bin/etq +0 -4
@@ -0,0 +1,64 @@
1
+ #!/bin/sh
2
+ # bin/hooks/post-tool-use — Etiquette Hook Adapter v0.1
3
+ # Maps runtime lifecycle event to etiquette `hook post_tool_use`.
4
+ #
5
+ # DISABLED BY DEFAULT. Enable with:
6
+ # export ETIQUETTE_HOOKS_ENABLED=1
7
+ # export ETIQUETTE_WORKSPACE_ID=<opaque-workspace-id>
8
+ # export ETIQUETTE_CLI_SCRIPT=/path/to/etiquette/packages/control/src/cli.ts
9
+ #
10
+ # Optional input:
11
+ # stdin or $ETIQUETTE_HOOK_PAYLOAD = JSON object payload (sanitized server-side)
12
+ #
13
+ # Contract:
14
+ # - disabled-by-default: silent exit 0 when not opted in
15
+ # - fast-fail: any error is swallowed; never blocks the operator
16
+ # - non-authoritative: writes per-workspace outbox only; no ledger writes
17
+ set -eu
18
+
19
+ if [ "${ETIQUETTE_HOOKS_ENABLED:-0}" != "1" ]; then
20
+ exit 0
21
+ fi
22
+
23
+ CLI_BIN=${ETIQUETTE_CLI_BIN:-bun}
24
+ CLI_SCRIPT=${ETIQUETTE_CLI_SCRIPT:-}
25
+
26
+ if [ -z "$CLI_SCRIPT" ] || [ ! -f "$CLI_SCRIPT" ]; then
27
+ # hook enabled but no cli script located; non-blocking exit
28
+ exit 0
29
+ fi
30
+
31
+ # Payload via env var (preferred) or stdin
32
+ if [ -n "${ETIQUETTE_HOOK_PAYLOAD:-}" ]; then
33
+ payload=$ETIQUETTE_HOOK_PAYLOAD
34
+ elif [ ! -t 0 ]; then
35
+ payload=$(cat)
36
+ else
37
+ payload='{}'
38
+ fi
39
+
40
+ if [ -n "${ETIQUETTE_WORKSPACE_ID:-}" ]; then
41
+ set -- hook post_tool_use --workspace "$ETIQUETTE_WORKSPACE_ID" --payload "$payload"
42
+ else
43
+ set -- hook post_tool_use --payload "$payload"
44
+ fi
45
+
46
+ timeout_seconds=${ETIQUETTE_HOOK_TIMEOUT_SECONDS:-2}
47
+ case "$timeout_seconds" in
48
+ ''|*[!0-9]*) timeout_seconds=2 ;;
49
+ esac
50
+
51
+ # Run with timeout safety. Output goes to /dev/null so a hook never pollutes the
52
+ # runtime's stdout, and timeout/failure never blocks the operator.
53
+ (
54
+ "$CLI_BIN" "$CLI_SCRIPT" "$@" >/dev/null 2>&1
55
+ ) &
56
+ hook_pid=$!
57
+ (
58
+ sleep "$timeout_seconds"
59
+ kill "$hook_pid" 2>/dev/null || true
60
+ ) &
61
+ watchdog_pid=$!
62
+ wait "$hook_pid" 2>/dev/null || true
63
+ kill "$watchdog_pid" 2>/dev/null || true
64
+ exit 0
@@ -0,0 +1,64 @@
1
+ #!/bin/sh
2
+ # bin/hooks/session-end — Etiquette Hook Adapter v0.1
3
+ # Maps runtime lifecycle event to etiquette `hook session_end`.
4
+ #
5
+ # DISABLED BY DEFAULT. Enable with:
6
+ # export ETIQUETTE_HOOKS_ENABLED=1
7
+ # export ETIQUETTE_WORKSPACE_ID=<opaque-workspace-id>
8
+ # export ETIQUETTE_CLI_SCRIPT=/path/to/etiquette/packages/control/src/cli.ts
9
+ #
10
+ # Optional input:
11
+ # stdin or $ETIQUETTE_HOOK_PAYLOAD = JSON object payload (sanitized server-side)
12
+ #
13
+ # Contract:
14
+ # - disabled-by-default: silent exit 0 when not opted in
15
+ # - fast-fail: any error is swallowed; never blocks the operator
16
+ # - non-authoritative: writes per-workspace outbox only; no ledger writes
17
+ set -eu
18
+
19
+ if [ "${ETIQUETTE_HOOKS_ENABLED:-0}" != "1" ]; then
20
+ exit 0
21
+ fi
22
+
23
+ CLI_BIN=${ETIQUETTE_CLI_BIN:-bun}
24
+ CLI_SCRIPT=${ETIQUETTE_CLI_SCRIPT:-}
25
+
26
+ if [ -z "$CLI_SCRIPT" ] || [ ! -f "$CLI_SCRIPT" ]; then
27
+ # hook enabled but no cli script located; non-blocking exit
28
+ exit 0
29
+ fi
30
+
31
+ # Payload via env var (preferred) or stdin
32
+ if [ -n "${ETIQUETTE_HOOK_PAYLOAD:-}" ]; then
33
+ payload=$ETIQUETTE_HOOK_PAYLOAD
34
+ elif [ ! -t 0 ]; then
35
+ payload=$(cat)
36
+ else
37
+ payload='{}'
38
+ fi
39
+
40
+ if [ -n "${ETIQUETTE_WORKSPACE_ID:-}" ]; then
41
+ set -- hook session_end --workspace "$ETIQUETTE_WORKSPACE_ID" --payload "$payload"
42
+ else
43
+ set -- hook session_end --payload "$payload"
44
+ fi
45
+
46
+ timeout_seconds=${ETIQUETTE_HOOK_TIMEOUT_SECONDS:-2}
47
+ case "$timeout_seconds" in
48
+ ''|*[!0-9]*) timeout_seconds=2 ;;
49
+ esac
50
+
51
+ # Run with timeout safety. Output goes to /dev/null so a hook never pollutes the
52
+ # runtime's stdout, and timeout/failure never blocks the operator.
53
+ (
54
+ "$CLI_BIN" "$CLI_SCRIPT" "$@" >/dev/null 2>&1
55
+ ) &
56
+ hook_pid=$!
57
+ (
58
+ sleep "$timeout_seconds"
59
+ kill "$hook_pid" 2>/dev/null || true
60
+ ) &
61
+ watchdog_pid=$!
62
+ wait "$hook_pid" 2>/dev/null || true
63
+ kill "$watchdog_pid" 2>/dev/null || true
64
+ exit 0
@@ -0,0 +1,64 @@
1
+ #!/bin/sh
2
+ # bin/hooks/session-start — Etiquette Hook Adapter v0.1
3
+ # Maps runtime lifecycle event to etiquette `hook session_start`.
4
+ #
5
+ # DISABLED BY DEFAULT. Enable with:
6
+ # export ETIQUETTE_HOOKS_ENABLED=1
7
+ # export ETIQUETTE_WORKSPACE_ID=<opaque-workspace-id>
8
+ # export ETIQUETTE_CLI_SCRIPT=/path/to/etiquette/packages/control/src/cli.ts
9
+ #
10
+ # Optional input:
11
+ # stdin or $ETIQUETTE_HOOK_PAYLOAD = JSON object payload (sanitized server-side)
12
+ #
13
+ # Contract:
14
+ # - disabled-by-default: silent exit 0 when not opted in
15
+ # - fast-fail: any error is swallowed; never blocks the operator
16
+ # - non-authoritative: writes per-workspace outbox only; no ledger writes
17
+ set -eu
18
+
19
+ if [ "${ETIQUETTE_HOOKS_ENABLED:-0}" != "1" ]; then
20
+ exit 0
21
+ fi
22
+
23
+ CLI_BIN=${ETIQUETTE_CLI_BIN:-bun}
24
+ CLI_SCRIPT=${ETIQUETTE_CLI_SCRIPT:-}
25
+
26
+ if [ -z "$CLI_SCRIPT" ] || [ ! -f "$CLI_SCRIPT" ]; then
27
+ # hook enabled but no cli script located; non-blocking exit
28
+ exit 0
29
+ fi
30
+
31
+ # Payload via env var (preferred) or stdin
32
+ if [ -n "${ETIQUETTE_HOOK_PAYLOAD:-}" ]; then
33
+ payload=$ETIQUETTE_HOOK_PAYLOAD
34
+ elif [ ! -t 0 ]; then
35
+ payload=$(cat)
36
+ else
37
+ payload='{}'
38
+ fi
39
+
40
+ if [ -n "${ETIQUETTE_WORKSPACE_ID:-}" ]; then
41
+ set -- hook session_start --workspace "$ETIQUETTE_WORKSPACE_ID" --payload "$payload"
42
+ else
43
+ set -- hook session_start --payload "$payload"
44
+ fi
45
+
46
+ timeout_seconds=${ETIQUETTE_HOOK_TIMEOUT_SECONDS:-2}
47
+ case "$timeout_seconds" in
48
+ ''|*[!0-9]*) timeout_seconds=2 ;;
49
+ esac
50
+
51
+ # Run with timeout safety. Output goes to /dev/null so a hook never pollutes the
52
+ # runtime's stdout, and timeout/failure never blocks the operator.
53
+ (
54
+ "$CLI_BIN" "$CLI_SCRIPT" "$@" >/dev/null 2>&1
55
+ ) &
56
+ hook_pid=$!
57
+ (
58
+ sleep "$timeout_seconds"
59
+ kill "$hook_pid" 2>/dev/null || true
60
+ ) &
61
+ watchdog_pid=$!
62
+ wait "$hook_pid" 2>/dev/null || true
63
+ kill "$watchdog_pid" 2>/dev/null || true
64
+ exit 0
@@ -0,0 +1,64 @@
1
+ #!/bin/sh
2
+ # bin/hooks/stop — Etiquette Hook Adapter v0.1
3
+ # Maps runtime lifecycle event to etiquette `hook stop`.
4
+ #
5
+ # DISABLED BY DEFAULT. Enable with:
6
+ # export ETIQUETTE_HOOKS_ENABLED=1
7
+ # export ETIQUETTE_WORKSPACE_ID=<opaque-workspace-id>
8
+ # export ETIQUETTE_CLI_SCRIPT=/path/to/etiquette/packages/control/src/cli.ts
9
+ #
10
+ # Optional input:
11
+ # stdin or $ETIQUETTE_HOOK_PAYLOAD = JSON object payload (sanitized server-side)
12
+ #
13
+ # Contract:
14
+ # - disabled-by-default: silent exit 0 when not opted in
15
+ # - fast-fail: any error is swallowed; never blocks the operator
16
+ # - non-authoritative: writes per-workspace outbox only; no ledger writes
17
+ set -eu
18
+
19
+ if [ "${ETIQUETTE_HOOKS_ENABLED:-0}" != "1" ]; then
20
+ exit 0
21
+ fi
22
+
23
+ CLI_BIN=${ETIQUETTE_CLI_BIN:-bun}
24
+ CLI_SCRIPT=${ETIQUETTE_CLI_SCRIPT:-}
25
+
26
+ if [ -z "$CLI_SCRIPT" ] || [ ! -f "$CLI_SCRIPT" ]; then
27
+ # hook enabled but no cli script located; non-blocking exit
28
+ exit 0
29
+ fi
30
+
31
+ # Payload via env var (preferred) or stdin
32
+ if [ -n "${ETIQUETTE_HOOK_PAYLOAD:-}" ]; then
33
+ payload=$ETIQUETTE_HOOK_PAYLOAD
34
+ elif [ ! -t 0 ]; then
35
+ payload=$(cat)
36
+ else
37
+ payload='{}'
38
+ fi
39
+
40
+ if [ -n "${ETIQUETTE_WORKSPACE_ID:-}" ]; then
41
+ set -- hook stop --workspace "$ETIQUETTE_WORKSPACE_ID" --payload "$payload"
42
+ else
43
+ set -- hook stop --payload "$payload"
44
+ fi
45
+
46
+ timeout_seconds=${ETIQUETTE_HOOK_TIMEOUT_SECONDS:-2}
47
+ case "$timeout_seconds" in
48
+ ''|*[!0-9]*) timeout_seconds=2 ;;
49
+ esac
50
+
51
+ # Run with timeout safety. Output goes to /dev/null so a hook never pollutes the
52
+ # runtime's stdout, and timeout/failure never blocks the operator.
53
+ (
54
+ "$CLI_BIN" "$CLI_SCRIPT" "$@" >/dev/null 2>&1
55
+ ) &
56
+ hook_pid=$!
57
+ (
58
+ sleep "$timeout_seconds"
59
+ kill "$hook_pid" 2>/dev/null || true
60
+ ) &
61
+ watchdog_pid=$!
62
+ wait "$hook_pid" 2>/dev/null || true
63
+ kill "$watchdog_pid" 2>/dev/null || true
64
+ exit 0
@@ -0,0 +1,64 @@
1
+ #!/bin/sh
2
+ # bin/hooks/user-prompt-submit — Etiquette Hook Adapter v0.1
3
+ # Maps runtime lifecycle event to etiquette `hook user_prompt_submit`.
4
+ #
5
+ # DISABLED BY DEFAULT. Enable with:
6
+ # export ETIQUETTE_HOOKS_ENABLED=1
7
+ # export ETIQUETTE_WORKSPACE_ID=<opaque-workspace-id>
8
+ # export ETIQUETTE_CLI_SCRIPT=/path/to/etiquette/packages/control/src/cli.ts
9
+ #
10
+ # Optional input:
11
+ # stdin or $ETIQUETTE_HOOK_PAYLOAD = JSON object payload (sanitized server-side)
12
+ #
13
+ # Contract:
14
+ # - disabled-by-default: silent exit 0 when not opted in
15
+ # - fast-fail: any error is swallowed; never blocks the operator
16
+ # - non-authoritative: writes per-workspace outbox only; no ledger writes
17
+ set -eu
18
+
19
+ if [ "${ETIQUETTE_HOOKS_ENABLED:-0}" != "1" ]; then
20
+ exit 0
21
+ fi
22
+
23
+ CLI_BIN=${ETIQUETTE_CLI_BIN:-bun}
24
+ CLI_SCRIPT=${ETIQUETTE_CLI_SCRIPT:-}
25
+
26
+ if [ -z "$CLI_SCRIPT" ] || [ ! -f "$CLI_SCRIPT" ]; then
27
+ # hook enabled but no cli script located; non-blocking exit
28
+ exit 0
29
+ fi
30
+
31
+ # Payload via env var (preferred) or stdin
32
+ if [ -n "${ETIQUETTE_HOOK_PAYLOAD:-}" ]; then
33
+ payload=$ETIQUETTE_HOOK_PAYLOAD
34
+ elif [ ! -t 0 ]; then
35
+ payload=$(cat)
36
+ else
37
+ payload='{}'
38
+ fi
39
+
40
+ if [ -n "${ETIQUETTE_WORKSPACE_ID:-}" ]; then
41
+ set -- hook user_prompt_submit --workspace "$ETIQUETTE_WORKSPACE_ID" --payload "$payload"
42
+ else
43
+ set -- hook user_prompt_submit --payload "$payload"
44
+ fi
45
+
46
+ timeout_seconds=${ETIQUETTE_HOOK_TIMEOUT_SECONDS:-2}
47
+ case "$timeout_seconds" in
48
+ ''|*[!0-9]*) timeout_seconds=2 ;;
49
+ esac
50
+
51
+ # Run with timeout safety. Output goes to /dev/null so a hook never pollutes the
52
+ # runtime's stdout, and timeout/failure never blocks the operator.
53
+ (
54
+ "$CLI_BIN" "$CLI_SCRIPT" "$@" >/dev/null 2>&1
55
+ ) &
56
+ hook_pid=$!
57
+ (
58
+ sleep "$timeout_seconds"
59
+ kill "$hook_pid" 2>/dev/null || true
60
+ ) &
61
+ watchdog_pid=$!
62
+ wait "$hook_pid" 2>/dev/null || true
63
+ kill "$watchdog_pid" 2>/dev/null || true
64
+ exit 0
@@ -0,0 +1,53 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5
+
6
+ if [ "$#" -lt 4 ]; then
7
+ echo "usage: sh bin/local-event <task-id> <actor> <action> <message...>" >&2
8
+ exit 2
9
+ fi
10
+
11
+ task_id=$1
12
+ actor=$2
13
+ action=$3
14
+ shift 3
15
+ message=$*
16
+
17
+ event_log=${ETIQUETTE_EVENT_LOG:-"$ROOT/.etiquette/events/local.ndjson"}
18
+ event_dir=$(dirname -- "$event_log")
19
+ mkdir -p "$event_dir"
20
+
21
+ lock_dir="$event_dir/.local-event.lock"
22
+ lock_attempts=0
23
+ while ! mkdir "$lock_dir" 2>/dev/null; do
24
+ lock_attempts=$((lock_attempts + 1))
25
+ if [ "$lock_attempts" -ge 100 ]; then
26
+ echo "timed out waiting for local event lock: $lock_dir" >&2
27
+ exit 1
28
+ fi
29
+ sleep 0.05
30
+ done
31
+ trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT INT TERM
32
+
33
+ if [ -f "$event_log" ]; then
34
+ seq=$(wc -l < "$event_log" | tr -d ' ')
35
+ else
36
+ seq=0
37
+ fi
38
+ seq=$((seq + 1))
39
+ at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
40
+
41
+ json_escape() {
42
+ printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
43
+ }
44
+
45
+ task_id_json=$(json_escape "$task_id")
46
+ actor_json=$(json_escape "$actor")
47
+ action_json=$(json_escape "$action")
48
+ message_json=$(json_escape "$message")
49
+
50
+ printf '{"seq":%s,"at":"%s","task_id":"%s","actor":"%s","action":"%s","message":"%s"}\n' \
51
+ "$seq" "$at" "$task_id_json" "$actor_json" "$action_json" "$message_json" >> "$event_log"
52
+
53
+ printf 'event: %s seq:%s\n' "$event_log" "$seq"
@@ -0,0 +1,143 @@
1
+ #!/bin/sh
2
+ # post-receipt: push a receipt METADATA pointer to the hosted plane.
3
+ #
4
+ # Per hosted-plane-boundary.v0 — content stays in the repo; only the
5
+ # pointer (task_id / seat_id / status / artifact_ref / receipt_ref /
6
+ # validation_summary capped) is sent. Sanitisers reject path/hostname/IP
7
+ # leakage in the refs.
8
+ #
9
+ # Usage:
10
+ # sh bin/post-receipt --task-id DEMO-001 --status success \
11
+ # --artifact-ref repo:main --receipt-ref docs/work/receipts/DEMO-001.md \
12
+ # --validation-summary "product-doctor: ok"
13
+ set -eu
14
+
15
+ ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
16
+ # shellcheck source=_lib/sanitize.sh
17
+ . "$ROOT/bin/_lib/sanitize.sh"
18
+ # shellcheck source=_lib/state.sh
19
+ . "$ROOT/bin/_lib/state.sh"
20
+ # shellcheck source=_lib/http.sh
21
+ . "$ROOT/bin/_lib/http.sh"
22
+
23
+ ALLOWED_STATUSES="success partial blocked failed"
24
+
25
+ task_id=""
26
+ status=""
27
+ artifact_ref=""
28
+ receipt_ref=""
29
+ validation_summary=""
30
+
31
+ while [ "$#" -gt 0 ]; do
32
+ case "$1" in
33
+ --task-id) task_id=$2; shift 2 ;;
34
+ --status) status=$2; shift 2 ;;
35
+ --artifact-ref) artifact_ref=$2; shift 2 ;;
36
+ --receipt-ref) receipt_ref=$2; shift 2 ;;
37
+ --validation-summary) validation_summary=$2; shift 2 ;;
38
+ -h|--help)
39
+ cat <<EOF
40
+ post-receipt — push a receipt metadata pointer
41
+
42
+ sh bin/post-receipt --task-id <id> --status <s> \\
43
+ [--artifact-ref <ref>] [--receipt-ref <repo-relative-path>] \\
44
+ [--validation-summary <≤200 chars>]
45
+
46
+ Status (4):
47
+ $(printf ' - %s\n' $ALLOWED_STATUSES)
48
+
49
+ Requires seat-consume-pass to have populated workspace.json.
50
+ Only metadata flows over the wire; receipt body stays in your repo.
51
+ EOF
52
+ exit 0
53
+ ;;
54
+ *) echo "unknown arg: $1" >&2; exit 2 ;;
55
+ esac
56
+ done
57
+
58
+ [ -n "$task_id" ] || { echo "post-receipt: --task-id required" >&2; exit 2; }
59
+ [ -n "$status" ] || { echo "post-receipt: --status required" >&2; exit 2; }
60
+
61
+ # Status allowlist.
62
+ ok=0
63
+ for s in $ALLOWED_STATUSES; do
64
+ if [ "$s" = "$status" ]; then ok=1; break; fi
65
+ done
66
+ if [ "$ok" -ne 1 ]; then
67
+ printf 'post-receipt: --status "%s" not in allowlist:\n' "$status" >&2
68
+ printf ' - %s\n' $ALLOWED_STATUSES >&2
69
+ exit 2
70
+ fi
71
+
72
+ # task_id length cap (defence; api caps at 128).
73
+ if [ "${#task_id}" -gt 128 ]; then
74
+ echo "post-receipt: --task-id length > 128" >&2
75
+ exit 2
76
+ fi
77
+
78
+ # Sanitise optional refs.
79
+ clean_artifact=""
80
+ if [ -n "$artifact_ref" ]; then
81
+ if ! clean_artifact=$(etq_sanitize_artifact_ref "$artifact_ref"); then
82
+ echo "post-receipt: --artifact-ref rejected" >&2
83
+ exit 2
84
+ fi
85
+ fi
86
+ clean_receipt=""
87
+ if [ -n "$receipt_ref" ]; then
88
+ if ! clean_receipt=$(etq_sanitize_repo_relative "$receipt_ref"); then
89
+ echo "post-receipt: --receipt-ref rejected" >&2
90
+ exit 2
91
+ fi
92
+ fi
93
+ # Validation summary cap at 200 chars (server also enforces).
94
+ if [ "${#validation_summary}" -gt 200 ]; then
95
+ validation_summary=$(printf '%s' "$validation_summary" | cut -c1-200)
96
+ fi
97
+
98
+ # Load workspace.json.
99
+ workspace_file=$(etq_state_workspace_path)
100
+ if [ ! -f "$workspace_file" ]; then
101
+ echo "post-receipt: workspace.json missing — run \`sh bin/seat-consume-pass --pass-id <id>\` first" >&2
102
+ exit 1
103
+ fi
104
+ workspace_id=$(etq_state_read_key "$workspace_file" workspace_id) || exit 1
105
+ pass_id=$(etq_state_read_key "$workspace_file" pass_id) || exit 1
106
+ host=$(etq_state_read_key "$workspace_file" hosted_host) || exit 1
107
+ seat_id=$(etq_state_read_key "$(etq_state_seat_path)" seat_id) || exit 1
108
+ seat_id=$(etq_sanitize_opaque_id "$seat_id") || {
109
+ echo "post-receipt: seat.json contains invalid seat_id; re-run seat-init --force" >&2
110
+ exit 1
111
+ }
112
+
113
+ # Build body with jq for safe JSON construction.
114
+ if ! command -v jq >/dev/null 2>&1; then
115
+ echo "post-receipt: jq required" >&2
116
+ exit 1
117
+ fi
118
+ body=$(jq -nc \
119
+ --arg pass_id "$pass_id" \
120
+ --arg task_id "$task_id" \
121
+ --arg seat_id "$seat_id" \
122
+ --arg status "$status" \
123
+ --arg artifact_ref "$clean_artifact" \
124
+ --arg receipt_ref "$clean_receipt" \
125
+ --arg validation_summary "$validation_summary" \
126
+ '
127
+ {pass_id: $pass_id, task_id: $task_id, seat_id: $seat_id, status: $status}
128
+ + (if $artifact_ref != "" then {artifact_ref: $artifact_ref} else {} end)
129
+ + (if $receipt_ref != "" then {receipt_ref: $receipt_ref} else {} end)
130
+ + (if $validation_summary != "" then {validation_summary: $validation_summary} else {} end)
131
+ ')
132
+
133
+ url="$host/workspaces/$workspace_id/receipts"
134
+ printf 'post-receipt: POST %s task=%s status=%s\n' "$url" "$task_id" "$status"
135
+
136
+ response=$(etq_http_post "$url" "$body") || {
137
+ rc=$?
138
+ echo "post-receipt: ingest failed (http rc=$rc)" >&2
139
+ exit "$rc"
140
+ }
141
+
142
+ printf 'post-receipt: ingested\n'
143
+ printf '%s\n' "$response" | jq -c . 2>/dev/null || printf '%s\n' "$response"
@@ -0,0 +1,112 @@
1
+ #!/bin/sh
2
+ # post-telemetry: push a daily-grain telemetry event to the hosted plane.
3
+ #
4
+ # Per telemetry.v0 — event_type must be in the 8-event allowlist; meta is
5
+ # bounded (keys ≤32 chars, string values ≤64 chars and reject path/hostname
6
+ # patterns) before anything is sent over the wire.
7
+ #
8
+ # Usage:
9
+ # sh bin/post-telemetry --event-type seat_doctor_run
10
+ # sh bin/post-telemetry --event-type seat_doctor_run --count 1 --meta '{"p50_ms":140}'
11
+ set -eu
12
+
13
+ ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
14
+ # shellcheck source=_lib/sanitize.sh
15
+ . "$ROOT/bin/_lib/sanitize.sh"
16
+ # shellcheck source=_lib/state.sh
17
+ . "$ROOT/bin/_lib/state.sh"
18
+ # shellcheck source=_lib/http.sh
19
+ . "$ROOT/bin/_lib/http.sh"
20
+
21
+ ALLOWED_EVENTS="pass_issued pass_revoked pass_consumed seat_readiness_posted seat_verified receipt_submitted seat_doctor_run validate_vanilla_run"
22
+
23
+ event_type=""
24
+ count=1
25
+ meta='{}'
26
+
27
+ while [ "$#" -gt 0 ]; do
28
+ case "$1" in
29
+ --event-type) event_type=$2; shift 2 ;;
30
+ --count) count=$2; shift 2 ;;
31
+ --meta) meta=$2; shift 2 ;;
32
+ -h|--help)
33
+ cat <<EOF
34
+ post-telemetry — push a daily-grain telemetry event
35
+
36
+ sh bin/post-telemetry --event-type <type> [--count N] [--meta JSON]
37
+
38
+ Allowed event_type values (8):
39
+ $(printf ' - %s\n' $ALLOWED_EVENTS)
40
+
41
+ Requires seat-consume-pass to have populated workspace.json.
42
+ EOF
43
+ exit 0
44
+ ;;
45
+ *) echo "unknown arg: $1" >&2; exit 2 ;;
46
+ esac
47
+ done
48
+
49
+ if [ -z "$event_type" ]; then
50
+ echo "post-telemetry: --event-type is required" >&2
51
+ exit 2
52
+ fi
53
+
54
+ # Allowlist check client-side (saves a round trip).
55
+ allowed=0
56
+ for e in $ALLOWED_EVENTS; do
57
+ if [ "$e" = "$event_type" ]; then allowed=1; break; fi
58
+ done
59
+ if [ "$allowed" -ne 1 ]; then
60
+ printf 'post-telemetry: event_type "%s" not in allowlist:\n' "$event_type" >&2
61
+ printf ' - %s\n' $ALLOWED_EVENTS >&2
62
+ exit 2
63
+ fi
64
+
65
+ # count must be a positive integer.
66
+ case "$count" in
67
+ *[!0-9]*) echo "post-telemetry: --count must be a positive integer" >&2; exit 2 ;;
68
+ '') echo "post-telemetry: --count empty" >&2; exit 2 ;;
69
+ esac
70
+
71
+ # Meta must be a JSON object.
72
+ if ! command -v jq >/dev/null 2>&1; then
73
+ echo "post-telemetry: jq required" >&2
74
+ exit 1
75
+ fi
76
+ if ! printf '%s' "$meta" | jq -e 'type == "object"' >/dev/null 2>&1; then
77
+ echo "post-telemetry: --meta must be a JSON object" >&2
78
+ exit 2
79
+ fi
80
+ if ! clean_meta=$(etq_sanitize_meta_json "$meta"); then
81
+ echo "post-telemetry: --meta sanitization failed" >&2
82
+ exit 2
83
+ fi
84
+
85
+ # Load workspace.json.
86
+ workspace_file=$(etq_state_workspace_path)
87
+ if [ ! -f "$workspace_file" ]; then
88
+ echo "post-telemetry: workspace.json missing — run \`sh bin/seat-consume-pass --pass-id <id>\` first" >&2
89
+ exit 1
90
+ fi
91
+ workspace_id=$(etq_state_read_key "$workspace_file" workspace_id) || exit 1
92
+ pass_id=$(etq_state_read_key "$workspace_file" pass_id) || exit 1
93
+ host=$(etq_state_read_key "$workspace_file" hosted_host) || exit 1
94
+
95
+ body=$(jq -nc \
96
+ --arg pass_id "$pass_id" \
97
+ --arg event_type "$event_type" \
98
+ --argjson count "$count" \
99
+ --argjson meta "$clean_meta" \
100
+ '{pass_id: $pass_id, event_type: $event_type, count: $count, meta: $meta}')
101
+
102
+ url="$host/workspaces/$workspace_id/telemetry"
103
+ printf 'post-telemetry: POST %s event_type=%s count=%s\n' "$url" "$event_type" "$count"
104
+
105
+ response=$(etq_http_post "$url" "$body") || {
106
+ rc=$?
107
+ echo "post-telemetry: ingest failed (http rc=$rc)" >&2
108
+ exit "$rc"
109
+ }
110
+
111
+ printf 'post-telemetry: ingested\n'
112
+ printf '%s\n' "$response" | jq -c . 2>/dev/null || printf '%s\n' "$response"