instar 0.28.73 → 0.28.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +51 -23
- package/dist/commands/server.js.map +1 -1
- package/dist/core/BranchManager.d.ts.map +1 -1
- package/dist/core/BranchManager.js +2 -0
- package/dist/core/BranchManager.js.map +1 -1
- package/dist/core/GitSync.d.ts.map +1 -1
- package/dist/core/GitSync.js +2 -0
- package/dist/core/GitSync.js.map +1 -1
- package/dist/core/HandoffManager.d.ts.map +1 -1
- package/dist/core/HandoffManager.js +2 -0
- package/dist/core/HandoffManager.js.map +1 -1
- package/dist/core/SessionManager.d.ts +4 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +15 -4
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SourceTreeGuard.d.ts +69 -0
- package/dist/core/SourceTreeGuard.d.ts.map +1 -0
- package/dist/core/SourceTreeGuard.js +378 -0
- package/dist/core/SourceTreeGuard.js.map +1 -0
- package/dist/core/StateManager.d.ts.map +1 -1
- package/dist/core/StateManager.js +46 -9
- package/dist/core/StateManager.js.map +1 -1
- package/dist/core/types.d.ts +15 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts +8 -0
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +31 -1
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +28 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +79 -1
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/TelegramMarkdownFormatter.d.ts +76 -0
- package/dist/messaging/TelegramMarkdownFormatter.d.ts.map +1 -0
- package/dist/messaging/TelegramMarkdownFormatter.js +493 -0
- package/dist/messaging/TelegramMarkdownFormatter.js.map +1 -0
- package/dist/messaging/imessage/IMessageAdapter.d.ts +3 -6
- package/dist/messaging/imessage/IMessageAdapter.d.ts.map +1 -1
- package/dist/messaging/imessage/IMessageAdapter.js +20 -21
- package/dist/messaging/imessage/IMessageAdapter.js.map +1 -1
- package/dist/messaging/imessage/NativeBackend.d.ts +26 -0
- package/dist/messaging/imessage/NativeBackend.d.ts.map +1 -1
- package/dist/messaging/imessage/NativeBackend.js +133 -0
- package/dist/messaging/imessage/NativeBackend.js.map +1 -1
- package/dist/messaging/telegramFormatMetrics.d.ts +22 -0
- package/dist/messaging/telegramFormatMetrics.d.ts.map +1 -0
- package/dist/messaging/telegramFormatMetrics.js +38 -0
- package/dist/messaging/telegramFormatMetrics.js.map +1 -0
- package/dist/messaging/types.d.ts +7 -0
- package/dist/messaging/types.d.ts.map +1 -1
- package/dist/messaging/types.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +5 -5
- package/src/templates/scripts/telegram-reply.sh +52 -8
- package/upgrades/0.28.74.md +48 -0
- package/upgrades/0.28.75.md +29 -0
- package/upgrades/side-effects/0.28.75.md +53 -0
- package/upgrades/side-effects/source-tree-guard.md +340 -0
- package/upgrades/side-effects/telegram-markdown-renderer-pr1.md +124 -0
- /package/upgrades/side-effects/{capability-map-user-provenance-fallback.md → 0.28.73.md} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-04-
|
|
5
|
-
"instarVersion": "0.28.
|
|
4
|
+
"generatedAt": "2026-04-25T09:16:20.363Z",
|
|
5
|
+
"instarVersion": "0.28.75",
|
|
6
6
|
"entryCount": 186,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -1112,7 +1112,7 @@
|
|
|
1112
1112
|
"type": "template",
|
|
1113
1113
|
"domain": "operations",
|
|
1114
1114
|
"sourcePath": "src/templates/scripts/telegram-reply.sh",
|
|
1115
|
-
"contentHash": "
|
|
1115
|
+
"contentHash": "3d08c63c6280d0a7ba94a345c259673a461ee5c1d116cb47c95c7626c67cee23",
|
|
1116
1116
|
"since": "2025-01-01"
|
|
1117
1117
|
},
|
|
1118
1118
|
"template:whatsapp-reply.sh": {
|
|
@@ -1408,7 +1408,7 @@
|
|
|
1408
1408
|
"type": "subsystem",
|
|
1409
1409
|
"domain": "sessions",
|
|
1410
1410
|
"sourcePath": "src/core/SessionManager.ts",
|
|
1411
|
-
"contentHash": "
|
|
1411
|
+
"contentHash": "b47fdc0040c512bada61574b4fd1505da3c9bbe2adc793915d6f5bd479faad06",
|
|
1412
1412
|
"since": "2025-01-01"
|
|
1413
1413
|
},
|
|
1414
1414
|
"subsystem:auto-updater": {
|
|
@@ -1464,7 +1464,7 @@
|
|
|
1464
1464
|
"type": "subsystem",
|
|
1465
1465
|
"domain": "communication",
|
|
1466
1466
|
"sourcePath": "src/lifeline/TelegramLifeline.ts",
|
|
1467
|
-
"contentHash": "
|
|
1467
|
+
"contentHash": "22d7d3827ba85d345862e55457fd2cca83e4f38201da61e20a7f0e539052322b",
|
|
1468
1468
|
"since": "2025-01-01"
|
|
1469
1469
|
},
|
|
1470
1470
|
"subsystem:orphan-process-reaper": {
|
|
@@ -3,18 +3,52 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Usage:
|
|
5
5
|
# ./telegram-reply.sh TOPIC_ID "message text"
|
|
6
|
+
# ./telegram-reply.sh --format markdown TOPIC_ID "**bold**"
|
|
6
7
|
# echo "message text" | ./telegram-reply.sh TOPIC_ID
|
|
7
8
|
# cat <<'EOF' | ./telegram-reply.sh TOPIC_ID
|
|
8
9
|
# Multi-line message here
|
|
9
10
|
# EOF
|
|
10
11
|
#
|
|
12
|
+
# Flags:
|
|
13
|
+
# --format <mode> Override server-side format mode for this send.
|
|
14
|
+
# Valid: plain, code, markdown, legacy-passthrough
|
|
15
|
+
# ('html' is reserved for trusted internal callers.)
|
|
16
|
+
# When absent, the server's configured default applies.
|
|
17
|
+
#
|
|
11
18
|
# Reads INSTAR_PORT from environment (default: 4040).
|
|
12
19
|
|
|
20
|
+
FORMAT=""
|
|
21
|
+
|
|
22
|
+
# Parse leading flags before positional args.
|
|
23
|
+
while [ $# -gt 0 ]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--format)
|
|
26
|
+
FORMAT="$2"
|
|
27
|
+
shift 2
|
|
28
|
+
;;
|
|
29
|
+
--format=*)
|
|
30
|
+
FORMAT="${1#--format=}"
|
|
31
|
+
shift
|
|
32
|
+
;;
|
|
33
|
+
--)
|
|
34
|
+
shift
|
|
35
|
+
break
|
|
36
|
+
;;
|
|
37
|
+
-*)
|
|
38
|
+
echo "Unknown flag: $1" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
;;
|
|
41
|
+
*)
|
|
42
|
+
break
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
done
|
|
46
|
+
|
|
13
47
|
TOPIC_ID="$1"
|
|
14
48
|
shift
|
|
15
49
|
|
|
16
50
|
if [ -z "$TOPIC_ID" ]; then
|
|
17
|
-
echo "Usage: telegram-reply.sh TOPIC_ID [message]" >&2
|
|
51
|
+
echo "Usage: telegram-reply.sh [--format MODE] TOPIC_ID [message]" >&2
|
|
18
52
|
exit 1
|
|
19
53
|
fi
|
|
20
54
|
|
|
@@ -38,22 +72,32 @@ if [ -f ".instar/config.json" ]; then
|
|
|
38
72
|
AUTH_TOKEN=$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null)
|
|
39
73
|
fi
|
|
40
74
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
75
|
+
# Build JSON body (text + optional format).
|
|
76
|
+
JSON_BODY=$(python3 -c '
|
|
77
|
+
import sys, json
|
|
78
|
+
msg = sys.argv[1]
|
|
79
|
+
fmt = sys.argv[2]
|
|
80
|
+
body = {"text": msg}
|
|
81
|
+
if fmt:
|
|
82
|
+
body["format"] = fmt
|
|
83
|
+
print(json.dumps(body))
|
|
84
|
+
' "$MSG" "$FORMAT" 2>/dev/null)
|
|
85
|
+
|
|
86
|
+
if [ -z "$JSON_BODY" ]; then
|
|
87
|
+
# Fallback if python3 not available: basic escape, no format override.
|
|
88
|
+
ESCAPED=$(printf '%s' "$MSG" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g')
|
|
89
|
+
JSON_BODY="{\"text\":\"${ESCAPED}\"}"
|
|
46
90
|
fi
|
|
47
91
|
|
|
48
92
|
if [ -n "$AUTH_TOKEN" ]; then
|
|
49
93
|
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "http://localhost:${PORT}/telegram/reply/${TOPIC_ID}" \
|
|
50
94
|
-H 'Content-Type: application/json' \
|
|
51
95
|
-H "Authorization: Bearer ${AUTH_TOKEN}" \
|
|
52
|
-
-d "
|
|
96
|
+
-d "$JSON_BODY")
|
|
53
97
|
else
|
|
54
98
|
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "http://localhost:${PORT}/telegram/reply/${TOPIC_ID}" \
|
|
55
99
|
-H 'Content-Type: application/json' \
|
|
56
|
-
-d "
|
|
100
|
+
-d "$JSON_BODY")
|
|
57
101
|
fi
|
|
58
102
|
|
|
59
103
|
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Upgrade Guide — v0.28.74
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
The scope-coherence stop hook (`scope-coherence-checkpoint.js`) previously
|
|
8
|
+
suggested running `/grounding` as the only recovery path when the
|
|
9
|
+
implementation-depth threshold was crossed. `/grounding` is a Claude-Code-side
|
|
10
|
+
skill convention; Instar does not ship it, so agents whose harness lacks that
|
|
11
|
+
skill saw a dead-end suggestion with no working manual reset path.
|
|
12
|
+
|
|
13
|
+
The checkpoint message now lists three real options: read the relevant
|
|
14
|
+
spec/proposal, invoke `/grounding` if the harness has it, or reset directly via
|
|
15
|
+
the local Instar server's reset endpoint. The endpoint already exists and is
|
|
16
|
+
what `session-start.sh` invokes automatically — this release just surfaces it
|
|
17
|
+
as a manual fallback in the message agents see when they hit the checkpoint.
|
|
18
|
+
|
|
19
|
+
No code paths changed beyond the human-readable message string. The reset
|
|
20
|
+
endpoint and the depth-accumulation logic are unchanged.
|
|
21
|
+
|
|
22
|
+
## What to Tell Your User
|
|
23
|
+
|
|
24
|
+
- The scope-coherence checkpoint now points to a manual reset path that always works, so if the suggested skill is not installed in your setup you still have a one-line recovery I can run for you.
|
|
25
|
+
|
|
26
|
+
## Summary of New Capabilities
|
|
27
|
+
|
|
28
|
+
| Capability | How to Use |
|
|
29
|
+
|-----------|-----------|
|
|
30
|
+
| Manual scope-coherence reset surfaced in checkpoint message | `curl -X POST http://localhost:4040/scope-coherence/reset` |
|
|
31
|
+
|
|
32
|
+
## Evidence
|
|
33
|
+
|
|
34
|
+
Cluster `cluster-scope-coherence-hook-counter-never-resets-across-sessions`
|
|
35
|
+
flagged that `/grounding` is not a registered skill or command in Instar and
|
|
36
|
+
that there is no user-facing reset path shown in the checkpoint message.
|
|
37
|
+
|
|
38
|
+
Verification: `ls skills/` confirms no `grounding` skill ships with Instar.
|
|
39
|
+
The reset endpoint is implemented at `src/server/routes.ts:2925` and is
|
|
40
|
+
invoked by `.instar/hooks/instar/session-start.sh` when the local server is
|
|
41
|
+
healthy — so the underlying counter reset already works on every session
|
|
42
|
+
start; this release makes the manual fallback path discoverable from the
|
|
43
|
+
hook's own message.
|
|
44
|
+
|
|
45
|
+
Reproduction: trigger the checkpoint (implementation depth ≥ 20, cooldown
|
|
46
|
+
elapsed, server running) and inspect the `reason` field in the hook's
|
|
47
|
+
output. Before: ends with `or /grounding`. After: ends with the three-option
|
|
48
|
+
block including the explicit curl command for manual reset.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Upgrade Guide — v0.28.75
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
`StateManager` now discriminates permission errors (`EPERM`/`EACCES`) from JSON corruption when reading state files. Previously, any failure in `getSession`, `listSessions`, `getJobState`, or `get` was reported as "Corrupted ... file" — including `EPERM` from launchd-spawned processes lacking macOS Full Disk Access on `~/Documents`. A new internal helper `describeReadError(err, filePath)` classifies failures into `permission`, `parse`, or `io` and feeds the discriminated reason into the DegradationReporter and `console.warn` line.
|
|
8
|
+
|
|
9
|
+
The null-return contract is preserved exactly. `feature`, `primary`, `fallback`, and `impact` strings are unchanged. Only the `reason` content is improved. No API surface changes.
|
|
10
|
+
|
|
11
|
+
Closes feedback cluster `cluster-degradation-statemanager-getjobstate-corrupted-job-state-fi` (45 reports across `ai-guy` and `sagemind` agents) and prevents future reports from being mis-clustered as corruption.
|
|
12
|
+
|
|
13
|
+
## What to Tell Your User
|
|
14
|
+
|
|
15
|
+
- **Clearer state-file errors**: when I can't read a state file because of a permissions issue (common on macOS when I'm started by a system service that doesn't have Full Disk Access on your Documents folder), I'll now say so plainly instead of telling you the file is corrupted. That should make the fix obvious — granting Full Disk Access in System Settings — instead of sending you on a corruption hunt.
|
|
16
|
+
|
|
17
|
+
## Summary of New Capabilities
|
|
18
|
+
|
|
19
|
+
| Capability | How to Use |
|
|
20
|
+
|-----------|-----------|
|
|
21
|
+
| Discriminated state-file read errors | automatic — appears in degradation reports and warnings |
|
|
22
|
+
|
|
23
|
+
## Evidence
|
|
24
|
+
|
|
25
|
+
Field repro: cluster `cluster-degradation-statemanager-getjobstate-corrupted-job-state-fi` shows 45 reports of `EPERM: operation not permitted, open '<path>'` mislabeled as "Corrupted job state file". Reporters: `ai-guy` and `sagemind` running instar 0.28.73 against `.instar/state/jobs/*.json` and `.instar/state/agent-attention-topic.json` files in `~/Documents/Projects/*`.
|
|
26
|
+
|
|
27
|
+
Post-change verification: `tests/unit/StateManager.test.ts` 26/26 passing, including a new `discriminates permission errors from corruption (EPERM/EACCES)` test that chmod 0o000s a file and asserts the warning emitted is keyed `permission`, not `parse` or `Corrupted`.
|
|
28
|
+
|
|
29
|
+
Build: clean.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Side Effects — fix(StateManager): discriminate EPERM/EACCES from JSON corruption
|
|
2
|
+
|
|
3
|
+
**Cluster**: `cluster-degradation-statemanager-getjobstate-corrupted-job-state-fi`
|
|
4
|
+
**Risk**: LOW (diagnostic-only)
|
|
5
|
+
**Spec**: `docs/specs/fix-statemanager-eperm-discrimination.md`
|
|
6
|
+
|
|
7
|
+
## What changes (summary)
|
|
8
|
+
|
|
9
|
+
`StateManager.ts` adds a `describeReadError(err, filePath)` helper that classifies read errors into `permission` (EPERM/EACCES), `parse` (SyntaxError), and `io` (other). All four read sites — `getSession`, `listSessions`, `getJobState`, `get` — use the helper to populate `console.warn` and the `DegradationReporter.report({reason})` field. The null-return contract, the `feature` field, and `primary`/`fallback`/`impact` strings are preserved.
|
|
10
|
+
|
|
11
|
+
## 1. What user-facing behavior changes?
|
|
12
|
+
|
|
13
|
+
The DegradationReporter `reason` text changes for state-file read failures. Permission errors now read "Permission denied reading <path> (EPERM). On macOS, launchd-spawned processes need Full Disk Access to read under ~/Documents." instead of "Corrupted job state file: EPERM: operation not permitted, open '<path>'". JSON parse errors now read "Corrupted state file <path> (JSON parse failed): <message>". I/O errors get a generic "Failed to read <path> (<code>): <message>".
|
|
14
|
+
|
|
15
|
+
No CLI surface, dashboard view, or API endpoint changes shape. Anyone consuming the DegradationReporter feed sees better-targeted reasons; anyone grouping by `feature`/`fallback` is unaffected.
|
|
16
|
+
|
|
17
|
+
## 2. What ledger / state / on-disk artifacts change?
|
|
18
|
+
|
|
19
|
+
None. The change is in the catch handler of read paths. No writes added, no schema changes, no new files. State files on disk are read identically.
|
|
20
|
+
|
|
21
|
+
## 3. What downstream agents / consumers rely on the previous behavior?
|
|
22
|
+
|
|
23
|
+
The Outside-Dawn feedback pipeline currently clusters reports by `feature` and (sometimes) substring-match on `reason`. The cluster `cluster-degradation-statemanager-getjobstate-corrupted-job-state-fi` was formed by substring "Corrupted job state file" in reasons. After this fix, EPERM reports will land in a NEW cluster keyed off "Permission denied reading" — that's the intended outcome (the reporter pipeline auto-clusters new substrings into new clusters). The old cluster is being closed out as `fixed` in this same release.
|
|
24
|
+
|
|
25
|
+
No agent code consumes the `reason` text programmatically (verified: grep for `Corrupted job state file` and `Corrupted state file` across `src/` shows zero hits outside `StateManager.ts`).
|
|
26
|
+
|
|
27
|
+
## 4. What tests verify the change?
|
|
28
|
+
|
|
29
|
+
- `tests/unit/StateManager.test.ts` — 25 pre-existing tests pass unchanged. The "Corrupted File Handling" group still asserts `null` returns for genuine parse-error inputs.
|
|
30
|
+
- New test: `discriminates permission errors from corruption (EPERM/EACCES)`. Chmod 0o000s a file, captures `console.warn` calls, asserts that any warning emitted contains "permission" and none contain "parse"/"Corrupted". Skips when running as root (where chmod is bypassed) and tolerates sandboxed runners that no-op chmod.
|
|
31
|
+
|
|
32
|
+
## 5. What rollback path exists?
|
|
33
|
+
|
|
34
|
+
`git revert` of the StateManager.ts diff + patch release. The revert is safe at any time:
|
|
35
|
+
- No persistent state needs cleanup (the change writes nothing new).
|
|
36
|
+
- Agents updated to the fixed version that subsequently downgrade keep working — the same `feature` names and null-return contract apply.
|
|
37
|
+
- The new feedback cluster (keyed off "Permission denied reading") simply stops receiving new reports; existing reports remain in their cluster.
|
|
38
|
+
|
|
39
|
+
## 6. What's the blast radius if the change is wrong?
|
|
40
|
+
|
|
41
|
+
Bounded to log/feedback-feed cosmetics. The worst case is a confusing `reason` string in a degradation report; no functional path is changed because:
|
|
42
|
+
- The catch handler still returns null on every error path it previously returned null on.
|
|
43
|
+
- The `feature` and `fallback` semantics are unchanged, so the scheduler/loader logic that gates on degradation reports continues to work.
|
|
44
|
+
- The new helper has no I/O of its own — it's a pure string formatter over the caught error.
|
|
45
|
+
|
|
46
|
+
A bug in `describeReadError` (e.g., throwing inside the formatter) would be caught by the existing outer null-return contract test cases, which would fail and block the release.
|
|
47
|
+
|
|
48
|
+
## 7. What invariants must hold after this change?
|
|
49
|
+
|
|
50
|
+
- `getSession`, `listSessions`, `getJobState`, `get` still return `null` (or skip the file in `listSessions`) on ANY read failure. Verified by tests.
|
|
51
|
+
- `DegradationReporter.report({feature, primary, fallback, impact})` for these paths still passes the same constants — only `reason` content is discriminated. Verified by inspection.
|
|
52
|
+
- `console.warn` still emits one line per failure, prefixed `[StateManager] <method> <kind>:`. Operators tailing logs see strictly more information than before.
|
|
53
|
+
- No new module-load side effects: the helper is a plain function in the same module, not a new import or class.
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Side-Effects Review — Destructive tool target guard
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `source-tree-guard`
|
|
4
|
+
**Date:** `2026-04-24`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required (brittle safety-guard carve-out; see §4)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds a new primitive `src/core/SourceTreeGuard.ts` that refuses to let
|
|
11
|
+
destructive managers (`GitSyncManager`, `BranchManager`,
|
|
12
|
+
`HandoffManager`) be constructed against the instar source tree. The
|
|
13
|
+
guard is wired as the first statement of each manager's constructor and
|
|
14
|
+
throws `SourceTreeGuardError` (code `INSTAR_SOURCE_TREE_GUARD`) before
|
|
15
|
+
any collaborator touches anything. Detection is the OR of three layers
|
|
16
|
+
(marker file `.instar-source-tree`, canonical `origin` URL in the
|
|
17
|
+
resolved common git dir's config, or `package.json name === "instar"`
|
|
18
|
+
plus two-of-N signature files). Path resolution closes the
|
|
19
|
+
uncreated-subdirectory bypass via a nearest-existing-ancestor walk, and
|
|
20
|
+
handles worktrees via `.git`-file parsing with
|
|
21
|
+
`basename(dirname(gitdir)) === "worktrees"` common-git-dir resolution.
|
|
22
|
+
Fail-closed is two-tier: detector-level inability to canonicalize/ascend
|
|
23
|
+
returns TRUE; layer-level inability to evaluate returns FALSE for that
|
|
24
|
+
sub-check and the OR across layers decides.
|
|
25
|
+
|
|
26
|
+
Files touched:
|
|
27
|
+
|
|
28
|
+
- `src/core/SourceTreeGuard.ts` (new)
|
|
29
|
+
- `src/core/GitSync.ts` (+1 import, +1 assert at constructor top)
|
|
30
|
+
- `src/core/BranchManager.ts` (+1 import, +1 assert at constructor top)
|
|
31
|
+
- `src/core/HandoffManager.ts` (+1 import, +1 assert at constructor top)
|
|
32
|
+
- `.instar-source-tree` (new — marker file at repo root)
|
|
33
|
+
- `tests/unit/SourceTreeGuard.test.ts` (new — 34 tests)
|
|
34
|
+
- `tests/integration/source-tree-guard-wiring.test.ts` (new — 10 tests)
|
|
35
|
+
- `docs/specs/DESTRUCTIVE-TOOL-TARGET-GUARDS-SPEC.md` (spec, pre-existing)
|
|
36
|
+
- `docs/specs/reports/destructive-tool-target-guards-convergence.md` (report, pre-existing)
|
|
37
|
+
|
|
38
|
+
Incident context: the 2026-04-22 branch-lifecycle e2e fixture wiped 1,893
|
|
39
|
+
files from the real instar checkout because destructive components
|
|
40
|
+
trusted an incoming `projectDir` without verification. This PR delivers
|
|
41
|
+
the tactical guardrail described in the spec. E2E sandbox hardening,
|
|
42
|
+
Adriana's autostash fix, the CI mutation detector, and the
|
|
43
|
+
SafeGitExecutor centralization are explicitly deferred to separate PRs.
|
|
44
|
+
|
|
45
|
+
## Decision-point inventory
|
|
46
|
+
|
|
47
|
+
- `src/core/SourceTreeGuard.ts :: isInstarSourceTree` — **add** — new
|
|
48
|
+
detector returning boolean based on the 3-layer OR.
|
|
49
|
+
- `src/core/SourceTreeGuard.ts :: assertNotInstarSourceTree` — **add** —
|
|
50
|
+
new assertion wrapper that throws `SourceTreeGuardError`.
|
|
51
|
+
- `GitSyncManager` constructor — **modify** — first statement now calls
|
|
52
|
+
the assertion; no other behavior change.
|
|
53
|
+
- `BranchManager` constructor — **modify** — same.
|
|
54
|
+
- `HandoffManager` constructor — **modify** — same.
|
|
55
|
+
|
|
56
|
+
No existing decision points are removed or repurposed.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 1. Over-block
|
|
61
|
+
|
|
62
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
63
|
+
|
|
64
|
+
Concrete over-block scenarios considered:
|
|
65
|
+
|
|
66
|
+
- A parallel-dev worktree *of the instar source* used as a destructive
|
|
67
|
+
target. Blocked today. This is intended — the worktree's common git
|
|
68
|
+
dir is the real repo, so `git add -A` from the worktree mutates the
|
|
69
|
+
real repo. If someone legitimately needs this, the fix is to loosen
|
|
70
|
+
the guard deliberately, not to silently permit it.
|
|
71
|
+
- A fork that keeps `package.json.name === "instar"`, ships two of the
|
|
72
|
+
signature files (likely — they're core infra), and carries the
|
|
73
|
+
`.instar-source-tree` marker (only if the forker copied it; it's in
|
|
74
|
+
the upstream repo but not automatically propagated by `git clone`).
|
|
75
|
+
Fork remotes are NOT on the canonical list, so layer (b) does not
|
|
76
|
+
match. If a fork has the marker and keeps the name+signature, it
|
|
77
|
+
blocks — the fork author can delete the marker in their repo.
|
|
78
|
+
- A developer who manually sets their working dir to the instar source
|
|
79
|
+
to run a one-off script. Blocked only for destructive-manager
|
|
80
|
+
construction — read-only tools are untouched. This is the entire
|
|
81
|
+
purpose of the guard.
|
|
82
|
+
- Tests that deliberately construct a manager pointed at the instar
|
|
83
|
+
source to exercise error paths. Use a `mkdtemp` sandbox instead (the
|
|
84
|
+
integration tests in this PR follow that convention).
|
|
85
|
+
|
|
86
|
+
No legitimate input is rejected that shouldn't be. The one "blocked by
|
|
87
|
+
design" case (worktrees of the source) matches the incident shape and
|
|
88
|
+
is a deliberate guard.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 2. Under-block
|
|
93
|
+
|
|
94
|
+
**What failure modes does this still miss?**
|
|
95
|
+
|
|
96
|
+
- Destructive components not on the three-manager list. Addressed by
|
|
97
|
+
the pre-ship enumeration below: every direct git invocation under
|
|
98
|
+
`src/` was inventoried. See §"Pre-ship grep enumeration evidence."
|
|
99
|
+
- Destructive work launched via child processes or shell scripts
|
|
100
|
+
(e.g. `nuke.ts`, `init.ts`) that bypass the manager layer. These are
|
|
101
|
+
either operator-initiated (`nuke`) or target a fresh dir (`init`),
|
|
102
|
+
not the incident shape. The SafeGitExecutor follow-up PR centralizes
|
|
103
|
+
these.
|
|
104
|
+
- Fork of instar that renames `package.json`, changes canonical remote,
|
|
105
|
+
and drops the marker. By design — if all three layers disagree this
|
|
106
|
+
is not the instar source tree, guard passes.
|
|
107
|
+
- A manager constructed against a fresh clone that doesn't have the
|
|
108
|
+
marker yet AND has a non-canonical remote (e.g. contributor fork)
|
|
109
|
+
AND has been renamed. Same as above — by design not caught.
|
|
110
|
+
|
|
111
|
+
Under-block is scoped to "things out of this PR's lane" and enumerated
|
|
112
|
+
in the spec's Out-of-Scope Follow-Ups. Nothing silently degrades.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 3. Level-of-abstraction fit
|
|
117
|
+
|
|
118
|
+
**Is this at the right layer?**
|
|
119
|
+
|
|
120
|
+
Constructor-time on the three destructive managers is the correct
|
|
121
|
+
layer for this PR. It is the point at which `projectDir` first becomes
|
|
122
|
+
trusted state — below this (inside `execFileSync('git', ...)`) is too
|
|
123
|
+
late (damage already in flight); above this (scattered across callers)
|
|
124
|
+
is the layer the incident proved unreliable.
|
|
125
|
+
|
|
126
|
+
A lower-level primitive (`SafeGitExecutor`) will additionally funnel at
|
|
127
|
+
the `execFileSync` boundary in a follow-up PR. The constructor wire-in
|
|
128
|
+
remains as belt-and-suspenders even after that refactor.
|
|
129
|
+
|
|
130
|
+
A higher-level gate does NOT already exist for this — the incident is
|
|
131
|
+
specifically the absence of such a gate. This change creates it.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 4. Signal vs authority compliance
|
|
136
|
+
|
|
137
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
138
|
+
|
|
139
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
140
|
+
|
|
141
|
+
- [ ] No — this change produces a signal consumed by an existing smart gate.
|
|
142
|
+
- [ ] No — this change has no block/allow surface.
|
|
143
|
+
- [ ] Yes — but the logic is a smart gate with full conversational context.
|
|
144
|
+
- [x] ✅ Yes, with brittle logic — **and that is correct** under the
|
|
145
|
+
carve-out in `docs/signal-vs-authority.md` lines 74–77:
|
|
146
|
+
|
|
147
|
+
> **Safety guards on irreversible actions.** `rm -rf /`,
|
|
148
|
+
> force-pushing to main, deleting the database — these can and
|
|
149
|
+
> should be hard-blocked by brittle pattern matchers, because the
|
|
150
|
+
> cost of a false pass is catastrophic and the cost of a false
|
|
151
|
+
> block is merely "try again with the right arguments."
|
|
152
|
+
|
|
153
|
+
This guard is exactly that category. False-pass cost: 1,893-file wipe +
|
|
154
|
+
force-push recovery (established empirically on 2026-04-22). False-block
|
|
155
|
+
cost: developer edits a path or touches a marker file once. The
|
|
156
|
+
asymmetry is overwhelming and the principle explicitly excludes this
|
|
157
|
+
class of check from the "brittle = signal only" rule.
|
|
158
|
+
|
|
159
|
+
No judgment gate is being added. No LLM-backed contextual check is
|
|
160
|
+
being bypassed. The check does not reason about message content, agent
|
|
161
|
+
intent, or conversational state — it asks "is this path the source
|
|
162
|
+
tree?" and acts on the yes/no. Fully compliant with the
|
|
163
|
+
signal-vs-authority separation.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 5. Interactions
|
|
168
|
+
|
|
169
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
170
|
+
|
|
171
|
+
- **Shadowing:** The guard runs as the FIRST statement of each
|
|
172
|
+
constructor, before any other validation. It cannot shadow existing
|
|
173
|
+
checks — it precedes them. If the guard throws, later validation does
|
|
174
|
+
not run, which is correct (the path is untrusted; any later check
|
|
175
|
+
against it is moot).
|
|
176
|
+
- **Double-fire:** No other component performs an equivalent check
|
|
177
|
+
today (grep confirms). Once `SafeGitExecutor` lands, both will fire;
|
|
178
|
+
that's intentional redundancy, not a bug.
|
|
179
|
+
- **Races:** Pure synchronous fs reads. No shared state. No
|
|
180
|
+
concurrency hazard. The detector is idempotent and side-effect-free.
|
|
181
|
+
- **Feedback loops:** None. The guard does not feed any downstream
|
|
182
|
+
decision system.
|
|
183
|
+
- **Test harness interaction:** All existing `mkdtemp`-based tests
|
|
184
|
+
still pass. The smoke tier (2037 tests) passes locally post-wiring.
|
|
185
|
+
Integration test confirms that a sandbox subdirectory still
|
|
186
|
+
constructs successfully.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 6. External surfaces
|
|
191
|
+
|
|
192
|
+
**Does this change anything visible outside the immediate code path?**
|
|
193
|
+
|
|
194
|
+
- **Other agents on the same machine:** none. Per-process guard only.
|
|
195
|
+
- **Other users of the install base:** behaviour changes only for
|
|
196
|
+
callers constructing the three managers against the instar source
|
|
197
|
+
itself — which no legitimate user does (it would mutate the instar
|
|
198
|
+
source). Forks are intentionally not caught by layer (b); layer (c)
|
|
199
|
+
catches unrenamed forks, which is arguably a feature (forkers who
|
|
200
|
+
kept the instar name probably also don't want their own repo wiped).
|
|
201
|
+
- **External systems:** no network, no LLM, no IPC. Sub-millisecond
|
|
202
|
+
synchronous fs check.
|
|
203
|
+
- **Persistent state:** the `.instar-source-tree` marker file at repo
|
|
204
|
+
root is new persistent state. It's a one-line inert sentinel; its
|
|
205
|
+
only consumer is this guard. Safe to leave in place on rollback.
|
|
206
|
+
- **Timing / runtime conditions:** the guard runs at constructor time,
|
|
207
|
+
before any async work. Worst case ~5ms cold (three fs reads); warm
|
|
208
|
+
cache sub-millisecond. Cannot cause gate-vs-client-timeout issues.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 7. Rollback cost
|
|
213
|
+
|
|
214
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
215
|
+
|
|
216
|
+
- **Hot-fix release:** revert the commit — delete
|
|
217
|
+
`src/core/SourceTreeGuard.ts`, remove the three one-line wire-ins,
|
|
218
|
+
ship as next patch. No code outside this module depends on the new
|
|
219
|
+
symbols. The `.instar-source-tree` marker can be left in place (inert
|
|
220
|
+
without the guard reading it) — avoids a second commit.
|
|
221
|
+
- **Data migration:** none. No persistent state introduced beyond the
|
|
222
|
+
inert marker file.
|
|
223
|
+
- **Agent state repair:** none. Per-process guard, no shared state.
|
|
224
|
+
- **User visibility:** none — rollback restores prior behaviour
|
|
225
|
+
instantly on restart. A legitimate caller that was ever blocked by
|
|
226
|
+
this guard was almost certainly pointed at the wrong tree anyway; the
|
|
227
|
+
rollback window does not expose users to regression.
|
|
228
|
+
|
|
229
|
+
Estimated hot-fix time: minutes.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Conclusion
|
|
234
|
+
|
|
235
|
+
This review produced the following design decisions locked into the
|
|
236
|
+
final code:
|
|
237
|
+
|
|
238
|
+
- Two-tier fail-closed (detector-level = TRUE; layer-level = FALSE for
|
|
239
|
+
that sub-check) to avoid the over-block pathology flagged in the
|
|
240
|
+
convergence review.
|
|
241
|
+
- Worktree common-git-dir resolution via the
|
|
242
|
+
`basename(dirname(gitdir)) === "worktrees"` rule, falling through to
|
|
243
|
+
layer (b) inconclusive on non-standard layouts rather than guessing.
|
|
244
|
+
- Canonical-remote list kept intentionally narrow (three exact URLs
|
|
245
|
+
with three narrow normalization rules: strip whitespace, strip
|
|
246
|
+
trailing slash, strip trailing `.git`). No substring or regex
|
|
247
|
+
matching.
|
|
248
|
+
- Error message deliberately does NOT inline bypass instructions (a
|
|
249
|
+
tutorial-in-the-error-message is a convenient "how to defeat the
|
|
250
|
+
guard" crib sheet that outlives the reason it exists).
|
|
251
|
+
- Pre-ship enumeration converts the three-manager defense from "hope
|
|
252
|
+
we found them all" to "documented inventory at ship time." Findings
|
|
253
|
+
below.
|
|
254
|
+
|
|
255
|
+
The change is clear to ship. It is a necessary-but-not-sufficient step
|
|
256
|
+
toward the SafeGitExecutor centralization, which is scheduled as a
|
|
257
|
+
follow-up PR.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Pre-ship grep enumeration evidence
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
$ grep -rnE "execFileSync\('git'|execSync\('git |spawn\(?'git'|spawnSync\('git'" src/
|
|
265
|
+
src/core/SyncOrchestrator.ts:1163 return execFileSync('git', args, { non-destructive (delegates to GitSync / BranchManager / HandoffManager which are now guarded)
|
|
266
|
+
src/core/ProjectMapper.ts:261 execFileSync('git', ['remote', 'get-url', 'origin'], …) non-destructive (read-only)
|
|
267
|
+
src/core/ProjectMapper.ts:275 execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], …) non-destructive (read-only)
|
|
268
|
+
src/core/ScopeVerifier.ts:449 execFileSync('git', ['remote', 'get-url', 'origin'], …) non-destructive (read-only)
|
|
269
|
+
src/core/BranchManager.ts:533 execFileSync('git', args, { destructive — wrapped by the guarded BranchManager constructor
|
|
270
|
+
src/core/AgentConnector.ts:141 execSync('git --version', …) non-destructive (version query)
|
|
271
|
+
src/core/ParallelDevWiring.ts:35 execFileSync('git', ['remote', 'get-url', 'origin'], …) non-destructive (read-only)
|
|
272
|
+
src/core/GitSync.ts:1006 execFileSync('git', args, { destructive — wrapped by the guarded GitSyncManager constructor
|
|
273
|
+
src/core/WorktreeManager.ts:756 execFileSync('git', […'rev-parse', 'HEAD'], …) non-destructive (read-only)
|
|
274
|
+
src/core/WorktreeManager.ts:763 execFileSync('git', […'rev-parse', '--verify', branch], …) non-destructive (read-only)
|
|
275
|
+
src/core/WorktreeManager.ts:765 execFileSync('git', […'branch', branch], …) branch-create only; targets a worktree path, not the source tree — deferred to SafeGitExecutor PR
|
|
276
|
+
src/core/WorktreeManager.ts:767 execFileSync('git', […'worktree', 'add', …], …) creates a new worktree, does not mutate source — out of scope
|
|
277
|
+
src/core/WorktreeManager.ts:879 execFileSync('git', […'worktree', 'list', …], …) non-destructive (read-only)
|
|
278
|
+
src/core/FileClassifier.ts:298 execFileSync('git', ['checkout', '--ours', …], …) destructive — invoked only during sync under GitSyncManager's control; guard fires at manager construction upstream
|
|
279
|
+
src/core/FileClassifier.ts:302 execFileSync('git', ['add', …], …) destructive — same as above; upstream-guarded
|
|
280
|
+
src/core/FileClassifier.ts:324 execFileSync('git', ['add', relPath], …) destructive — same; upstream-guarded
|
|
281
|
+
src/core/HandoffManager.ts:496 execFileSync('git', args, …) destructive — wrapped by the guarded HandoffManager constructor
|
|
282
|
+
src/server/routes.ts:2285 execFileSync('git', ['remote'], …) non-destructive (read-only)
|
|
283
|
+
src/server/routes.ts:3035 execFileSync('git', ['remote', 'get-url', 'origin'], …) non-destructive (read-only)
|
|
284
|
+
src/lifeline/ServerSupervisor.ts:376 spawnSync('git', ['status'], …) non-destructive (read-only)
|
|
285
|
+
src/lifeline/ServerSupervisor.ts:384 spawnSync('git', ['rebase', '--abort'], …) recovery-only, operates on the agent's OWN repo (not the source tree); orthogonal to this guard
|
|
286
|
+
src/commands/init.ts:386 execFileSync('git', ['init'], …) destructive on target dir — target is a fresh user project, not the source tree; out of scope
|
|
287
|
+
src/commands/init.ts:3329 execFileSync('git', ['remote'], …) non-destructive (read-only)
|
|
288
|
+
src/commands/nuke.ts:142 execFileSync('git', ['add', '-A'], …) destructive — operator-initiated on the agent's own directory; documented and out of scope
|
|
289
|
+
src/commands/nuke.ts:143 execFileSync('git', ['status', '--porcelain'], …) non-destructive (read-only)
|
|
290
|
+
src/commands/nuke.ts:150 execFileSync('git', ['commit', …], …) destructive — same as above
|
|
291
|
+
src/commands/nuke.ts:156 execFileSync('git', ['push'], …) destructive — same
|
|
292
|
+
src/commands/nuke.ts:212 execFileSync('git', ['remote'], …) non-destructive (read-only)
|
|
293
|
+
src/commands/machine.ts:285 execFileSync('git', ['clone', …], …) destructive on a fresh target, not the source tree
|
|
294
|
+
src/commands/setup.ts:114 execFileSync('git', ['rev-parse', '--show-toplevel'], …) non-destructive (read-only)
|
|
295
|
+
src/monitoring/WorktreeReaper.ts:208 execFileSync('git', […'worktree', 'add', …], …) creates a new worktree, does not mutate source
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Summary:
|
|
299
|
+
- 31 direct git invocations under `src/`.
|
|
300
|
+
- Destructive call sites routed through the three guarded managers:
|
|
301
|
+
`GitSync.ts:1006`, `BranchManager.ts:533`, `HandoffManager.ts:496`,
|
|
302
|
+
plus three `FileClassifier` helpers invoked only under
|
|
303
|
+
`GitSyncManager`'s control.
|
|
304
|
+
- Destructive sites NOT routed through the three managers and flagged
|
|
305
|
+
for PR 2 (SafeGitExecutor centralization): `WorktreeManager.ts`
|
|
306
|
+
branch/worktree creation, `commands/init.ts`, `commands/nuke.ts`,
|
|
307
|
+
`commands/machine.ts clone`, `ServerSupervisor.ts rebase --abort`.
|
|
308
|
+
None of these target the instar source tree in their documented call
|
|
309
|
+
patterns — they either operate on fresh targets (`init`, `clone`,
|
|
310
|
+
`worktree add`) or on the agent's own dir (`nuke`, `rebase --abort`).
|
|
311
|
+
- No unguarded manager-layer destructive paths found.
|
|
312
|
+
|
|
313
|
+
This inventory is the "documented enumeration at ship time" the spec's
|
|
314
|
+
pre-ship acceptance criterion requires.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Evidence pointers
|
|
319
|
+
|
|
320
|
+
- Spec: `docs/specs/DESTRUCTIVE-TOOL-TARGET-GUARDS-SPEC.md` (approved,
|
|
321
|
+
converged).
|
|
322
|
+
- Convergence report:
|
|
323
|
+
`docs/specs/reports/destructive-tool-target-guards-convergence.md`.
|
|
324
|
+
- Unit tests: `tests/unit/SourceTreeGuard.test.ts` — 34 tests, all
|
|
325
|
+
green locally. Covers detector layers individually, normalization
|
|
326
|
+
rules, worktree handling (standard + non-standard + malformed +
|
|
327
|
+
relative pointer), subdirectory bypass, uncreated-subdirectory
|
|
328
|
+
bypass, symlink canonicalization, two-tier fail-closed, error
|
|
329
|
+
shape.
|
|
330
|
+
- Integration tests:
|
|
331
|
+
`tests/integration/source-tree-guard-wiring.test.ts` — 10 tests, all
|
|
332
|
+
green. Covers: constructor throws for each of the three managers
|
|
333
|
+
pointed at the real instar source; uncreated-subdirectory-of-instar
|
|
334
|
+
still throws; mkdtemp sandbox construction succeeds for all three;
|
|
335
|
+
subdirectory-of-sandbox also succeeds (no over-block on legitimate
|
|
336
|
+
nested paths); side-effect isolation (state dir NOT created when
|
|
337
|
+
guard fires).
|
|
338
|
+
- Smoke tier pass: `npm run test:smoke` — 2037 tests passed locally
|
|
339
|
+
post-wiring (the push-hook's gate).
|
|
340
|
+
- Full-suite sample: `tsc --noEmit` clean; no new warnings.
|