@rubytech/create-maxy-code 0.1.349 → 0.1.350

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 (22) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md +544 -0
  3. package/payload/platform/docs/superpowers/specs/2026-06-23-account-filesystem-schema-design.md +170 -0
  4. package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +68 -0
  5. package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +108 -0
  6. package/payload/platform/scripts/lib/provision-account-dir.sh +22 -3
  7. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  8. package/payload/platform/services/claude-session-manager/dist/http-server.js +45 -1
  9. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  10. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +11 -0
  11. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  12. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +18 -0
  13. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  14. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +1 -0
  15. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  16. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +16 -0
  17. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  18. package/payload/platform/templates/account-schema/SCHEMA.md +54 -0
  19. package/payload/server/public/assets/data-CBrgiyPM.js +1 -0
  20. package/payload/server/public/data.html +1 -1
  21. package/payload/server/server.js +60 -16
  22. package/payload/server/public/assets/data-CH6GNBO3.js +0 -1
@@ -0,0 +1,170 @@
1
+ # Account filesystem schema: seed it, and enforce file↔graph stewardship — design
2
+
3
+ Source task: `.tasks/1091-account-filesystem-schema-enforcement.md`. Backstop
4
+ (out of scope here): `.tasks/1092-account-dir-standing-reconcile-and-quarantine.md`.
5
+
6
+ ## Problem (recap)
7
+
8
+ The account data dir (`data/accounts/<id>/`) should be a consistent projection of
9
+ the graph ontology, but that doctrine lives only inside the `data-manager`
10
+ specialist the main agent often routes around, nothing seeds a canonical file
11
+ layout, and nothing stops an agent inventing folders. The observed failure: an
12
+ agent invented `projects/<name>/deploy|sources|design|archive` with no ontology
13
+ basis, justified as "there was no canonical schema to copy from."
14
+
15
+ ## Resolved gating decision
16
+
17
+ `provision_account_dir` runs **unprivileged** on the Pi target — at install and
18
+ at runtime when the `account_create` admin tool calls it via `execFileSync`
19
+ inside the `systemctl --user` brand service. `chattr +i` needs root
20
+ (`CAP_LINUX_IMMUTABLE`). So the task's Decision C1 (OS-immutable top level)
21
+ **cannot take effect on the real target**.
22
+
23
+ **Decision (operator, 2026-06-23): drop C1 entirely.** No `chattr`/`chflags`
24
+ code ships. The PreToolUse Write-hook becomes the sole tool-layer hard guard;
25
+ top-level strays created via bash fall to the Task 1092 reconcile. This also
26
+ removes the chattr observability line and the upgrade/uninstall-unlock risk from
27
+ the task.
28
+
29
+ ## What ships
30
+
31
+ Three layers, honest about hard-enforceable vs cooperative.
32
+
33
+ ### A. Seeded schema (one standard, every account)
34
+
35
+ `provision_account_dir` (the function both `setup-account.sh` and the
36
+ `account_create` lifecycle tool call) gains an idempotent seed step:
37
+
38
+ - **Create the operator-data skeleton:** `projects/`, `contacts/`, `documents/`.
39
+ - **Create the tool-owned skeleton:** `url-get/`, `output/`, `generated/`,
40
+ `extracted/`, `uploads/` (`agents/`, `specialists/` already created earlier in
41
+ the function).
42
+ - **Copy `SCHEMA.md`** to the account root from a shipped template
43
+ (`platform/templates/account-schema/SCHEMA.md`). The template is the **single
44
+ source of truth** for the allowed top-level set (see below) and the flatness
45
+ rule.
46
+ - **Log** `[acct-schema] seeded dirs=<n>` once per provision. Idempotent: a
47
+ re-provision over an existing account re-creates nothing and overwrites
48
+ `SCHEMA.md` with the shipped version (Rubytech-controlled, like `IDENTITY.md`).
49
+
50
+ Operator-data buckets are **flat**: files live directly under one entity folder
51
+ (`projects/<name>/file`, `contacts/<name>/file`) or, for `documents/`, directly
52
+ in the bucket or one document-folder deep. Flatness is what makes
53
+ `projects/<name>/deploy/` a machine-detectable violation.
54
+
55
+ ### B. `DATA_STEWARDSHIP` directive (cooperative layer)
56
+
57
+ A `DATA_STEWARDSHIP` const beside `PLATFORM_BOUNDARY` in
58
+ `platform/services/claude-session-manager/src/system-prompt.ts`, rendered as a
59
+ `<data-stewardship>` section in `renderAppendBlock` next to `<platform-boundary>`
60
+ — joined into `--append-system-prompt` on **every spawn, every role**. It states:
61
+ the account-dir layout is fixed by the seeded `SCHEMA.md`; never author new
62
+ top-level folders or nested subtrees under an entity folder; route
63
+ reorganisation/moves through `data-manager` and non-trivial graph writes through
64
+ `database-operator`; the existing carve-out (a one-line update against an
65
+ in-context, unambiguously-classified node) stays direct.
66
+
67
+ `runSystemPromptSelfTest` is extended to assert `<data-stewardship>` /
68
+ `</data-stewardship>` appear in the rendered block.
69
+
70
+ ### C. PreToolUse Write-hook (hard guard, no bash removal)
71
+
72
+ A new hook `platform/plugins/admin/hooks/fs-schema-guard.sh`, seeded into the
73
+ account `.claude/settings.json` as a PreToolUse hook on `Write`, `Edit`, and
74
+ `NotebookEdit` (added alongside the existing `archive-ingest-surface-gate.sh`
75
+ entries on those three matchers).
76
+
77
+ Behaviour:
78
+
79
+ 1. Read the stdin JSON envelope. Fail **closed** on terminal stdin (no envelope).
80
+ 2. Extract the target path: `tool_input.file_path` for Write/Edit,
81
+ `tool_input.notebook_path` for NotebookEdit (via python3, matching the
82
+ existing `extract_tool_input_field` pattern — never jq, never command parsing).
83
+ 3. Resolve the path against the account dir. If the resolved path is **outside**
84
+ the account data dir, **allow** (exit 0) — this hook governs the account-dir
85
+ schema only; platform-source writes are PLATFORM_BOUNDARY's concern.
86
+ 4. Compute the path relative to the account dir. Let `seg0` be its first segment.
87
+ 5. **Top-level check:** if `seg0` is a single path segment (a top-level file or
88
+ dir) not in the allowed top-level set → **block** (exit 2),
89
+ `reason=top-level`.
90
+ 6. **Over-deep check:** if `seg0 ∈ {projects, contacts}` and the path has more
91
+ than `bucket/entity/file` depth (i.e. a directory component beyond the single
92
+ entity folder), or `seg0 == documents` and the path exceeds
93
+ `documents/folder/file` → **block** (exit 2), `reason=over-deep`.
94
+ 7. Tool-owned dirs (everything in the allowed set except the three operator-data
95
+ buckets) pass at **any** depth.
96
+ 8. Otherwise allow (exit 0).
97
+ 9. Every block logs `[fs-guard] blocked path=<rel> reason=<top-level|over-deep>`
98
+ to stderr (operator-greppable). No task numbers / internal refs in any
99
+ operator-visible string. The exit-2 stderr message cites the schema in plain
100
+ terms.
101
+
102
+ **Allowed top-level set — single source.** The set lives once, as a fenced
103
+ ` ```allowed-top-level ` block in `platform/templates/account-schema/SCHEMA.md`
104
+ (newline-delimited). The hook parses that fenced block from the **account's**
105
+ seeded `SCHEMA.md` at runtime (deterministic structured data, not prose control
106
+ flow). Shipped contents:
107
+
108
+ ```
109
+ projects
110
+ contacts
111
+ documents
112
+ url-get
113
+ output
114
+ generated
115
+ extracted
116
+ uploads
117
+ agents
118
+ specialists
119
+ sites
120
+ public
121
+ cache
122
+ secrets
123
+ state
124
+ logs
125
+ tmp
126
+ SCHEMA.md
127
+ account.json
128
+ AGENTS.md
129
+ .claude
130
+ .git
131
+ .quarantine
132
+ ```
133
+
134
+ A unit test asserts the hook's parse of the template equals this documented set,
135
+ so the human doc and the enforcement never drift. Adding a future plugin
136
+ top-level dir is a one-line edit to this block.
137
+
138
+ ## Out of scope (unchanged from task)
139
+
140
+ - The standing reconcile / quarantine — Task 1092 (named as the backstop here,
141
+ not built).
142
+ - Removing or narrowing the main agent's `Bash`/`Write`/`Edit` tools — rejected.
143
+ - Auto-repairing drift — `data-manager` fixes on dispatch.
144
+ - Per-account schema variants.
145
+ - Any change to the graph schema or the `database-operator` write path.
146
+ - `chattr`/`chflags` OS-immutability (Decision C1) — dropped per the gating
147
+ decision above.
148
+
149
+ ## Verification
150
+
151
+ - **Seed:** a fresh `provision_account_dir` run produces the documented skeleton
152
+ and `SCHEMA.md`; the seeded allowed-top-level set matches the template exactly.
153
+ Re-provision is idempotent.
154
+ - **Write-hook:** a `Write` to `projects/Acme/deploy/sources/a.txt` blocks
155
+ (`over-deep`); `projects/Acme/a.txt` and `documents/a.pdf` allow; a write into
156
+ `output/` or `generated/` at any depth allows; a write to a novel top-level dir
157
+ `whim/x` blocks (`top-level`); a write outside the account dir allows.
158
+ - **Directive:** `runSystemPromptSelfTest` asserts the `<data-stewardship>` tag.
159
+ - **No tool regression:** the main agent retains `Bash`/`Write`/`Edit`; existing
160
+ hook + spawn + specialist tests pass; `platform/ui` (if touched) builds/lints
161
+ clean. The new hook does not interfere with the existing PreToolUse gate on the
162
+ same matchers (both run; either can block).
163
+ - **Device:** after publish + upgrade, a real account shows the seeded skeleton,
164
+ `SCHEMA.md`, and a blocked whim-write in the logs.
165
+
166
+ ## Observability
167
+
168
+ - `provision_account_dir` logs `[acct-schema] seeded dirs=<n>`.
169
+ - The Write-hook logs `[fs-guard] blocked path=<rel> reason=<top-level|over-deep>`
170
+ per block.
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # Regression test for fs-schema-guard.sh.
3
+ #
4
+ # Covers:
5
+ # 1. Non-Write/Edit/NotebookEdit tool -> ALLOW (early exit)
6
+ # 2. Write to projects/Acme/a.txt (flat) -> ALLOW
7
+ # 3. Write to documents/a.pdf (loose) -> ALLOW
8
+ # 4. Write to output/deep/nested/x.png (tool-owned) -> ALLOW (any depth)
9
+ # 5. Write to projects/Acme/deploy/sources/a.txt -> BLOCK over-deep
10
+ # 6. Write to projects/Acme/deploy/a.txt -> BLOCK over-deep
11
+ # 7. Write to whim/x.txt (novel top-level) -> BLOCK top-level
12
+ # 8. Write to an absolute path outside the account dir -> ALLOW (not our concern)
13
+ # 9. NotebookEdit over-deep (notebook_path) -> BLOCK over-deep
14
+ # 10. Empty stdin (cannot inspect) -> BLOCK fail-closed
15
+ # 11. Allowed-set parse matches the documented set -> assertion
16
+ set -u
17
+
18
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/fs-schema-guard.sh"
19
+ [ -x "$HOOK" ] || { echo "FAIL: $HOOK not executable" >&2; exit 1; }
20
+
21
+ # A scratch account dir with a seeded SCHEMA.md (copied from the shipped template).
22
+ REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
23
+ TEMPLATE="$REPO_ROOT/templates/account-schema/SCHEMA.md"
24
+ [ -f "$TEMPLATE" ] || { echo "FAIL: template missing at $TEMPLATE" >&2; exit 1; }
25
+ ACCT=$(mktemp -d)
26
+ trap 'rm -rf "$ACCT"' EXIT
27
+ cp "$TEMPLATE" "$ACCT/SCHEMA.md"
28
+ mkdir -p "$ACCT/projects/Acme" "$ACCT/documents" "$ACCT/output"
29
+
30
+ PASS=0; FAIL=0
31
+ # Run one case in the account dir as cwd.
32
+ # $1 name $2 stdin $3 expected_exit $4 expected_log_re ("" = none)
33
+ run_case() {
34
+ local name="$1" stdin="$2" exp="$3" re="$4" ef actual
35
+ ef=$(mktemp)
36
+ ( cd "$ACCT" && printf '%s' "$stdin" | bash "$HOOK" >/dev/null 2>"$ef" )
37
+ actual=$?
38
+ local sc; sc=$(cat "$ef"); rm -f "$ef"
39
+ local ok=1
40
+ [ "$actual" -eq "$exp" ] || ok=0
41
+ if [ -n "$re" ] && ! printf '%s' "$sc" | grep -Eq "$re"; then ok=0; fi
42
+ if [ $ok -eq 1 ]; then echo "PASS: $name (exit=$actual)"; PASS=$((PASS+1));
43
+ else echo "FAIL: $name (exit=$actual, want $exp; log want /$re/)" >&2; FAIL=$((FAIL+1)); fi
44
+ }
45
+
46
+ mkenv() { printf '{"hook_event_name":"PreToolUse","tool_name":"%s","tool_input":{"%s":"%s"}}' "$1" "$2" "$3"; }
47
+
48
+ run_case "non-write allow" "$(mkenv Bash command 'ls')" 0 ""
49
+ run_case "flat project allow" "$(mkenv Write file_path 'projects/Acme/a.txt')" 0 ""
50
+ run_case "loose document allow" "$(mkenv Write file_path 'documents/a.pdf')" 0 ""
51
+ run_case "tool-owned deep allow" "$(mkenv Write file_path 'output/deep/nested/x.png')" 0 ""
52
+ run_case "over-deep block" "$(mkenv Write file_path 'projects/Acme/deploy/sources/a.txt')" 2 "fs-guard. blocked path=projects/Acme/deploy/sources/a.txt reason=over-deep"
53
+ run_case "over-deep one block" "$(mkenv Write file_path 'projects/Acme/deploy/a.txt')" 2 "reason=over-deep"
54
+ run_case "top-level block" "$(mkenv Write file_path 'whim/x.txt')" 2 "fs-guard. blocked path=whim/x.txt reason=top-level"
55
+ run_case "outside acct allow" "$(mkenv Write file_path '/etc/passwd')" 0 ""
56
+ run_case "notebook over-deep" "$(mkenv NotebookEdit notebook_path 'projects/Acme/deploy/n.ipynb')" 2 "reason=over-deep"
57
+ run_case "empty stdin block" "" 2 "no stdin"
58
+ # JSON null file_path must coerce to empty (allow), not the literal string "None".
59
+ run_case "null path allow" '{"hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":null}}' 0 ""
60
+
61
+ # Allowed-set parse == documented set.
62
+ EXPECT="projects contacts documents url-get output generated extracted uploads agents specialists sites public cache secrets state logs tmp SCHEMA.md account.json AGENTS.md .claude .git"
63
+ GOT=$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md" | tr '\n' ' ' | sed 's/ *$//')
64
+ if [ "$GOT" = "$EXPECT" ]; then echo "PASS: allowed-set parse"; PASS=$((PASS+1));
65
+ else echo "FAIL: allowed-set parse: got [$GOT]" >&2; FAIL=$((FAIL+1)); fi
66
+
67
+ echo "----- $PASS passed, $FAIL failed -----"
68
+ [ $FAIL -eq 0 ]
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # fs-schema-guard — PreToolUse Write/Edit/NotebookEdit path guard.
3
+ #
4
+ # Enforces the seeded account-dir file schema (see <accountDir>/SCHEMA.md):
5
+ # - the target's first path segment must be in the allowed top-level set
6
+ # (parsed from the fenced ```allowed-top-level block of the account's
7
+ # SCHEMA.md — our own structured data, not CLI prose);
8
+ # - a target under an operator-data bucket (projects/ contacts/) may be at
9
+ # most <bucket>/<entity>/<file> deep; documents/ may be at most
10
+ # <bucket>/<folder>/<file> deep. Tool-owned dirs pass at any depth.
11
+ #
12
+ # The hook governs the account dir only. A path resolving outside the account
13
+ # dir (cwd) is allowed — platform-source writes are PLATFORM_BOUNDARY's concern.
14
+ #
15
+ # Exit codes: 0 = allow, 2 = block (stderr shown to the agent). Fail closed when
16
+ # the tool call cannot be inspected (tty or empty stdin). Every block logs
17
+ # [fs-guard] blocked path=<rel> reason=<top-level|over-deep>
18
+ # No task numbers / internal refs in any operator-visible string.
19
+
20
+ set -uo pipefail
21
+
22
+ # Fail closed if attached to a terminal (no JSON envelope coming).
23
+ if [ -t 0 ]; then
24
+ echo "Blocked: fs-schema-guard received no stdin (cannot inspect the write). Failing closed." >&2
25
+ exit 2
26
+ fi
27
+ INPUT=$(cat)
28
+ if [ -z "$INPUT" ]; then
29
+ echo "Blocked: fs-schema-guard received no stdin (cannot inspect the write). Failing closed." >&2
30
+ exit 2
31
+ fi
32
+
33
+ field() {
34
+ printf '%s' "$INPUT" | python3 -c "
35
+ import sys, json
36
+ try:
37
+ d = json.load(sys.stdin)
38
+ v = (d.get('$1', {}) or {}).get('$2', '') if '$1' else d.get('$2', '')
39
+ print(v if isinstance(v, str) else '')
40
+ except Exception:
41
+ print('')
42
+ " 2>/dev/null || echo ""
43
+ }
44
+
45
+ TOOL_NAME=$(field '' tool_name)
46
+ case "$TOOL_NAME" in
47
+ Write|Edit) FILE_PATH=$(field tool_input file_path) ;;
48
+ NotebookEdit) FILE_PATH=$(field tool_input notebook_path) ;;
49
+ *) exit 0 ;;
50
+ esac
51
+ [ -z "$FILE_PATH" ] && exit 0
52
+
53
+ ACCOUNT_DIR="$PWD"
54
+
55
+ # Resolve to an account-relative path; allow anything outside the account dir.
56
+ REL=$(FILE_PATH="$FILE_PATH" ACCOUNT_DIR="$ACCOUNT_DIR" python3 -c '
57
+ import os, sys
58
+ fp = os.environ["FILE_PATH"]; acct = os.environ["ACCOUNT_DIR"]
59
+ ab = fp if os.path.isabs(fp) else os.path.join(acct, fp)
60
+ ab = os.path.normpath(ab); acct = os.path.normpath(acct)
61
+ if ab == acct or not ab.startswith(acct + os.sep):
62
+ print("") # outside the account dir (or the dir itself)
63
+ else:
64
+ print(os.path.relpath(ab, acct))
65
+ ' 2>/dev/null || echo "")
66
+ [ -z "$REL" ] && exit 0
67
+
68
+ # Parse the allowed top-level set from the account's seeded SCHEMA.md.
69
+ ALLOWED=""
70
+ if [ -f "$ACCOUNT_DIR/SCHEMA.md" ]; then
71
+ ALLOWED=$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCOUNT_DIR/SCHEMA.md")
72
+ fi
73
+ # Fail open on a missing/empty schema (an un-seeded legacy account must not have
74
+ # every write blocked). A re-provision seeds SCHEMA.md, after which the guard
75
+ # applies.
76
+ [ -z "$ALLOWED" ] && exit 0
77
+
78
+ SEG0=${REL%%/*}
79
+
80
+ # Top-level check.
81
+ if ! printf '%s\n' "$ALLOWED" | grep -qxF "$SEG0"; then
82
+ echo "[fs-guard] blocked path=$REL reason=top-level" >&2
83
+ echo "Blocked: '$SEG0' is not an allowed top-level entry in this account's data directory. The layout is fixed by SCHEMA.md — place operator data under projects/, contacts/, or documents/, and route any reorganisation through the data-manager specialist." >&2
84
+ exit 2
85
+ fi
86
+
87
+ # Over-deep check for operator-data buckets. Count path segments.
88
+ depth=$(printf '%s' "$REL" | awk -F/ '{print NF}')
89
+ case "$SEG0" in
90
+ projects|contacts)
91
+ # Allowed: <bucket>/<entity>/<file> = 3 segments max; <bucket>/<file> = 2 ok.
92
+ if [ "$depth" -gt 3 ]; then
93
+ echo "[fs-guard] blocked path=$REL reason=over-deep" >&2
94
+ echo "Blocked: $SEG0/ is flat — one folder per entity, then files. '$REL' nests a subtree under an entity folder, which has no basis in the graph ontology. Keep files directly under $SEG0/<name>/, or route a reorganisation through the data-manager specialist." >&2
95
+ exit 2
96
+ fi
97
+ ;;
98
+ documents)
99
+ # Allowed: documents/<file> = 2, documents/<folder>/<file> = 3 max.
100
+ if [ "$depth" -gt 3 ]; then
101
+ echo "[fs-guard] blocked path=$REL reason=over-deep" >&2
102
+ echo "Blocked: documents/ holds files or one folder deep. '$REL' nests deeper, which has no basis in the graph ontology. Route a reorganisation through the data-manager specialist." >&2
103
+ exit 2
104
+ fi
105
+ ;;
106
+ esac
107
+
108
+ exit 0
@@ -22,6 +22,22 @@ provision_account_dir() {
22
22
 
23
23
  mkdir -p "$ACCOUNT_DIR/agents/admin" "$ACCOUNT_DIR/.claude" "$ACCOUNT_DIR/specialists/.claude-plugin" "$ACCOUNT_DIR/specialists/agents"
24
24
 
25
+ # --- Account filesystem schema seed (idempotent) ---------------------------
26
+ # Operator-data buckets (entity-anchored, flat) + tool-owned dirs. agents/ and
27
+ # specialists/ are created above. The seeded SCHEMA.md is the single source of
28
+ # the allowed top-level set, parsed at runtime by the fs-schema write guard.
29
+ local _seed_dirs=(projects contacts documents url-get output generated extracted uploads)
30
+ local _d _seeded=0
31
+ for _d in "${_seed_dirs[@]}"; do
32
+ [ -d "$ACCOUNT_DIR/$_d" ] || { mkdir -p "$ACCOUNT_DIR/$_d" && _seeded=$((_seeded+1)); }
33
+ done
34
+ if [ -f "$TEMPLATES_DIR/account-schema/SCHEMA.md" ]; then
35
+ cp "$TEMPLATES_DIR/account-schema/SCHEMA.md" "$ACCOUNT_DIR/SCHEMA.md"
36
+ else
37
+ echo " [acct-schema] WARNING: template missing at $TEMPLATES_DIR/account-schema/SCHEMA.md — SCHEMA.md not seeded" >&2
38
+ fi
39
+ echo " [acct-schema] seeded dirs=$_seeded"
40
+
25
41
  # Claude Code discovers project-level .claude/ relative to the nearest .git
26
42
  # root. Without a .git in the account dir it traverses up to ~/maxy/.git and
27
43
  # misses accountDir/.claude/. Initialise a repo so the account dir IS the root.
@@ -42,19 +58,22 @@ provision_account_dir() {
42
58
  {
43
59
  "matcher": "Write",
44
60
  "hooks": [
45
- { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
61
+ { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" },
62
+ { "type": "command", "command": "bash $HOOKS_PATH/fs-schema-guard.sh" }
46
63
  ]
47
64
  },
48
65
  {
49
66
  "matcher": "Edit",
50
67
  "hooks": [
51
- { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
68
+ { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" },
69
+ { "type": "command", "command": "bash $HOOKS_PATH/fs-schema-guard.sh" }
52
70
  ]
53
71
  },
54
72
  {
55
73
  "matcher": "NotebookEdit",
56
74
  "hooks": [
57
- { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
75
+ { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" },
76
+ { "type": "command", "command": "bash $HOOKS_PATH/fs-schema-guard.sh" }
58
77
  ]
59
78
  },
60
79
  {
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAa3B,OAAO,EAiBL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAuBzB,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,iBAAiB,CAAA;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAgFhE,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAS1D,eAAO,MAAM,4BAA4B,QAAS,CAAA;AAIlD,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E;;qEAEiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;IAC9B;;gFAE4E;IAC5E,cAAc,EAAE,cAAc,CAAA;IAC9B;;;6BAGyB;IACzB,mBAAmB,CAAC,EAAE,mBAAmB,CAAA;IACzC;;gFAE4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAgHD,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEpG;AAgBD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAUD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAMvE;AAMD,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,MAAM,CAER;AAkCD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAe5F;AAwJD;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AAmBD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAs7EpD"}
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAa3B,OAAO,EAmBL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAuBzB,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,iBAAiB,CAAA;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAgFhE,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAS1D,eAAO,MAAM,4BAA4B,QAAS,CAAA;AAIlD,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E;;qEAEiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;IAC9B;;gFAE4E;IAC5E,cAAc,EAAE,cAAc,CAAA;IAC9B;;;6BAGyB;IACzB,mBAAmB,CAAC,EAAE,mBAAmB,CAAA;IACzC;;gFAE4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAgHD,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEpG;AAgBD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAUD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAMvE;AAMD,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,MAAM,CAER;AAkCD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAe5F;AAwJD;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AAmBD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAk+EpD"}
@@ -31,7 +31,7 @@ import { randomUUID } from 'node:crypto';
31
31
  import { execFileSync } from 'node:child_process';
32
32
  import { buildRcChildEnv, reapplyAdminLevers } from './rc-daemon.js';
33
33
  import { isEffortLevel, isNewSessionEffortLevel, isPermissionMode, SONNET_MODEL } from '../../../lib/models/dist/index.js';
34
- import { spawnClaudeSession, stopSession, writeInputToPty, isLive, onPtyExit, livePtyCount, liveSessionIds, getPtyTrackerForTests, livePidForSession, classifyLiveness, priorExitedCountForSession, PERMISSION_MODES, openFdCount, registerRcSpawnPty, } from './pty-spawner.js';
34
+ import { spawnClaudeSession, stopSession, writeInputToPty, writeInterruptToPty, isLive, onPtyExit, livePtyCount, liveSessionIds, getPtyTrackerForTests, livePidForSession, classifyLiveness, priorExitedCountForSession, PERMISSION_MODES, openFdCount, registerRcSpawnPty, } from './pty-spawner.js';
35
35
  import { writeScopeRecord, removeScopeRecord } from './scope-record.js';
36
36
  import { writeReseatMarker, resolveReseatChain, clearReseatMarker } from './reseat-ledger.js';
37
37
  import { getScopeMainPid, defaultProcessSignals, defaultSystemctlRunner } from './systemd-scope.js';
@@ -1567,6 +1567,50 @@ export function buildHttpApp(deps) {
1567
1567
  timed(deps.logger, 'POST', `/${id}/input`, 200, Date.now() - start);
1568
1568
  return c.json({ ok: true, bytes: result.bytes }, 200);
1569
1569
  });
1570
+ // Task 1090 Phase 1 — POST /:sessionId/interrupt writes one raw control byte
1571
+ // (ESC or Ctrl-C) to the PTY master via writeInterruptToPty: the soft
1572
+ // interrupt, session-preserving, distinct from /stop's SIGTERM recycle. The
1573
+ // byte is a body parameter so the device spike can test both candidates
1574
+ // against one live session. This logs the requested/sent lifecycle; the
1575
+ // deeper post-conditions (generation-stopped, survival, tail-reconcile) are
1576
+ // observed manually during the spike and engineered in Phase 2 on a GO.
1577
+ app.post('/:sessionId/interrupt', async (c) => {
1578
+ const start = Date.now();
1579
+ const id = c.req.param('sessionId');
1580
+ const row = resolveRow(deps.watcher, id);
1581
+ const sessionId = row?.sessionId ?? id;
1582
+ let body;
1583
+ try {
1584
+ body = await c.req.json();
1585
+ }
1586
+ catch {
1587
+ timed(deps.logger, 'POST', `/${id}/interrupt`, 400, Date.now() - start);
1588
+ return c.json({ error: 'body-not-json' }, 400);
1589
+ }
1590
+ if (body.byte !== 'esc' && body.byte !== 'ctrl-c') {
1591
+ timed(deps.logger, 'POST', `/${id}/interrupt`, 400, Date.now() - start);
1592
+ return c.json({ error: 'byte-invalid' }, 400);
1593
+ }
1594
+ const byte = body.byte;
1595
+ const intId = `${sessionId.slice(0, 8)}-${start}`;
1596
+ deps.logger(`[webchat-interrupt] op=interrupt-requested intId=${intId} sessionId=${sessionId.slice(0, 8)} trigger=manual-spike`);
1597
+ const result = writeInterruptToPty(sessionId, byte, deps.logger);
1598
+ if (!result.ok) {
1599
+ if (result.error === 'not-found') {
1600
+ timed(deps.logger, 'POST', `/${id}/interrupt`, 404, Date.now() - start);
1601
+ return c.json({ error: 'session-not-found' }, 404);
1602
+ }
1603
+ if (result.error === 'exited') {
1604
+ timed(deps.logger, 'POST', `/${id}/interrupt`, 410, Date.now() - start);
1605
+ return c.json({ error: 'session-exited' }, 410);
1606
+ }
1607
+ timed(deps.logger, 'POST', `/${id}/interrupt`, 500, Date.now() - start);
1608
+ return c.json({ error: 'pty-write-failed', detail: result.detail }, 500);
1609
+ }
1610
+ deps.logger(`[webchat-interrupt] op=interrupt-sent intId=${intId} byte=${byte} bytes=${result.bytes}`);
1611
+ timed(deps.logger, 'POST', `/${id}/interrupt`, 200, Date.now() - start);
1612
+ return c.json({ ok: true, intId, byte, bytes: result.bytes }, 200);
1613
+ });
1570
1614
  app.get('/:sessionId/log', (c) => {
1571
1615
  const start = Date.now();
1572
1616
  const id = c.req.param('sessionId');