seisin 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +506 -162
- package/package.json +14 -6
- package/src/browsers.js +52 -0
- package/src/canary.js +149 -0
- package/src/cli.js +175 -52
- package/src/commands/check.js +157 -5
- package/src/commands/explain.js +31 -13
- package/src/commands/hook.js +20 -6
- package/src/commands/init.js +183 -88
- package/src/commands/log.js +48 -9
- package/src/commands/requests.js +122 -70
- package/src/commands/review.js +73 -21
- package/src/commands/run.js +422 -78
- package/src/commands/scan.js +18 -3
- package/src/commands/ui.js +18 -5
- package/src/commands/walls.js +62 -11
- package/src/commands/whose.js +9 -7
- package/src/commands/wire.js +156 -17
- package/src/config.js +505 -37
- package/src/controls.js +244 -0
- package/src/diagnose.js +102 -49
- package/src/env.js +14 -8
- package/src/grants.js +122 -9
- package/src/handoff.js +285 -10
- package/src/help.js +179 -0
- package/src/hook.js +101 -42
- package/src/index.d.ts +217 -0
- package/src/index.js +24 -16
- package/src/inspect/codex.js +64 -0
- package/src/inspect/credentials.js +48 -0
- package/src/inspect/enforce.js +24 -0
- package/src/inspect/files.js +71 -0
- package/src/inspect/home.js +43 -0
- package/src/inspect/inject.js +68 -0
- package/src/inspect/keys.js +74 -0
- package/src/inspect/layout.js +258 -0
- package/src/inspect/limits.js +98 -0
- package/src/inspect/machine.js +107 -0
- package/src/inspect/network.js +37 -0
- package/src/inspect/providers.js +71 -0
- package/src/inspect/read-territory.js +105 -0
- package/src/inspect/role-keys.js +136 -0
- package/src/inspect/shared.js +37 -0
- package/src/inspect/warnings.js +58 -0
- package/src/inspect.js +20 -756
- package/src/intake.js +150 -35
- package/src/keys.js +231 -73
- package/src/kinds.js +249 -0
- package/src/lines.js +26 -0
- package/src/log.js +242 -52
- package/src/mcp.js +57 -58
- package/src/nested.js +12 -3
- package/src/notify.js +66 -20
- package/src/owners.js +358 -39
- package/src/paths.js +54 -2
- package/src/provider-run.js +81 -0
- package/src/redact.js +46 -13
- package/src/render.js +101 -17
- package/src/requests.js +506 -87
- package/src/review.js +50 -25
- package/src/rundir.js +33 -14
- package/src/sandboxes/command.js +93 -0
- package/src/sandboxes/index.js +113 -0
- package/src/sandboxes/mach-profile.js +31 -0
- package/src/sandboxes/srt-mach.mjs +43 -0
- package/src/sandboxes/srt.js +163 -0
- package/src/sandboxes/tartaros.js +80 -0
- package/src/scan.js +181 -19
- package/src/serve.js +396 -230
- package/src/spool.js +16 -45
- package/src/srt.js +296 -49
- package/src/suggest.js +58 -0
- package/src/surface.js +314 -54
- package/src/territory.js +261 -0
- package/src/trace.js +33 -0
- package/src/uilink.js +24 -9
- package/src/views.js +315 -0
- package/src/violations.js +247 -23
- package/src/walls.js +76 -46
- package/src/worktree.js +3 -11
- package/ui/index.html +1429 -538
- package/ui/favicon.svg +0 -1
package/src/commands/requests.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `seisin requests`, `grant` and `
|
|
2
|
+
* `seisin requests`, `grant` and `decline` — a person's end of the loop.
|
|
3
3
|
*
|
|
4
4
|
* These live in the CLI and in the console, and deliberately NOT in the MCP
|
|
5
5
|
* server. An agent may ask and may draft; turning a request into policy takes a
|
|
6
6
|
* person acting in a channel the agent does not have. That is the invariant the
|
|
7
|
-
* whole feature rests on — see docs/
|
|
7
|
+
* whole feature rests on — see docs/requests.md.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { settle, applyGrant, refuseIfBarred, requestsPath, editPolicy, queueOf, queueData, shellId, asFileGrant, lastAgentOf, fileOnlyNote } from "../requests.js";
|
|
10
|
+
import { describeHandoff } from "../handoff.js";
|
|
11
|
+
import { keyHolders } from "../owners.js";
|
|
12
|
+
import { loadConfig } from "../config.js";
|
|
10
13
|
import { read, logPath } from "../log.js";
|
|
11
14
|
import { C, out, safe } from "../render.js";
|
|
12
15
|
|
|
@@ -16,9 +19,9 @@ import { C, out, safe } from "../render.js";
|
|
|
16
19
|
* "Stable while you read it" was the old claim and it is false exactly where it
|
|
17
20
|
* matters. The queue is filled by agents that are still running: on a live
|
|
18
21
|
* repository it grew and shrank between a listing and the next command, and
|
|
19
|
-
* `
|
|
22
|
+
* `seisin decline 1` settled a different request than the one printed as #1 seconds
|
|
20
23
|
* earlier. That is time-of-check-to-time-of-use in the one command whose whole
|
|
21
|
-
* job is deciding a
|
|
24
|
+
* job is deciding a request, and it happened twice in one session.
|
|
22
25
|
*
|
|
23
26
|
* A position is still accepted, because reading a list and typing a number is
|
|
24
27
|
* how anyone will use this. An argument that is not a number is matched against
|
|
@@ -28,8 +31,10 @@ import { C, out, safe } from "../render.js";
|
|
|
28
31
|
* An ambiguous match is refused rather than resolved. Choosing for you is the
|
|
29
32
|
* failure being fixed.
|
|
30
33
|
*/
|
|
31
|
-
function pick(config, n) {
|
|
32
|
-
|
|
34
|
+
function pick(config, n, verb = "grant") {
|
|
35
|
+
if (n === undefined || n === "" || String(n).startsWith("--"))
|
|
36
|
+
throw new Error(`usage: seisin ${verb} <n|id> [--reason "…"] — the numbers and ids are in: seisin requests`);
|
|
37
|
+
const queue = queueOf(config);
|
|
33
38
|
if (!queue.length) throw new Error("no pending requests");
|
|
34
39
|
|
|
35
40
|
const arg = String(n).trim();
|
|
@@ -53,9 +58,11 @@ function pick(config, n) {
|
|
|
53
58
|
: " Use its number, or the id printed under it by `seisin requests`."));
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
export
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
export { queueOf };
|
|
62
|
+
|
|
63
|
+
export function requests(config, argv = []) {
|
|
64
|
+
const queue = queueOf(config, read(logPath(config.root)));
|
|
65
|
+
out(argv.includes("--json") ? JSON.stringify(queueData(queue), null, 2) + "\n" : renderQueue(queue));
|
|
59
66
|
return queue;
|
|
60
67
|
}
|
|
61
68
|
|
|
@@ -64,10 +71,34 @@ export function renderQueue(queue) {
|
|
|
64
71
|
if (queue.length === 0) return `\n ${C.dim}no pending requests${C.off}\n\n`;
|
|
65
72
|
|
|
66
73
|
const lines = [`\n ${C.yellow}${queue.length} pending request(s)${C.off}\n\n`];
|
|
67
|
-
queue.forEach((r, i) =>
|
|
68
|
-
|
|
74
|
+
queue.forEach((r, i) => lines.push(...entryLines(r, i + 1)));
|
|
75
|
+
lines.push(`\n ${C.dim}seisin grant <n|id> [--file] [--reason "…"] · seisin decline <n|id> [--reason "…"]${C.off}\n`);
|
|
76
|
+
// Said, because it bites: grant #1 and the old #2 is #1 now.
|
|
77
|
+
lines.push(` ${C.dim}#n is a position and shifts as requests are settled; the id does not.${C.off}\n\n`);
|
|
78
|
+
return lines.join("");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What one run added to the queue, for the end of `seisin run`. Each entry
|
|
83
|
+
* carries `n`, its number in the whole queue, so `grant <n>` works from here.
|
|
84
|
+
*/
|
|
85
|
+
export function renderAdded(added, older) {
|
|
86
|
+
if (!added.length) return "";
|
|
87
|
+
const lines = ["\n"];
|
|
88
|
+
for (const r of added) lines.push(...entryLines(r, r.n));
|
|
89
|
+
lines.push(`\n ${C.dim}this run: ${added.length} new request(s)` +
|
|
90
|
+
`${older ? ` · ${older} older pending` : ""} — seisin requests${C.off}\n\n`);
|
|
91
|
+
return lines.join("");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function entryLines(r, n) {
|
|
95
|
+
const lines = [];
|
|
96
|
+
{
|
|
97
|
+
const owners = r.declared
|
|
98
|
+
? ` ${C.dim}(${r.owners.length ? `declared by ${r.owners.join(", ")}` : "declared by no role"})${C.off}`
|
|
99
|
+
: r.owners.length ? ` ${C.dim}(owned by ${r.owners.join(", ")})${C.off}` : ` ${C.dim}(unowned)${C.off}`;
|
|
69
100
|
const times = r.times > 1 ? ` ${C.dim}· asked ${r.times}×${C.off}` : "";
|
|
70
|
-
lines.push(` ${C.b}#${
|
|
101
|
+
lines.push(` ${C.b}#${n}${C.off} ${safe(r.role)} wants ${r.action} on ${C.b}${safe(r.grant)}${C.off}${owners}${times}\n`);
|
|
71
102
|
lines.push(` ${C.dim}first asked over ${safe(r.target)}${C.off}\n`);
|
|
72
103
|
// The stable way to name it: a number is a position in a queue that agents
|
|
73
104
|
// are still writing to, and this does not move when the queue does.
|
|
@@ -75,84 +106,105 @@ export function renderQueue(queue) {
|
|
|
75
106
|
// Marked, not moved: the number above is what `grant <n>` is typed against.
|
|
76
107
|
if (r.stale)
|
|
77
108
|
lines.push(` ${C.dim}not asked again in ${r.stale.runs} runs of ${r.role} since — likely no longer needed${C.off}\n`);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return lines.join("");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* One line saying why no worker is running for a request that is still open.
|
|
87
|
-
*
|
|
88
|
-
* Deliberately not a scheduler view. The failure this closes is narrow and
|
|
89
|
-
* expensive: work that was refused admission, left the queue looking idle, and
|
|
90
|
-
* was never done by anybody. A row that says which limit held it back is the
|
|
91
|
-
* whole fix; deciding who retries and when is a separate question that this
|
|
92
|
-
* does not have to answer first.
|
|
93
|
-
*/
|
|
94
|
-
export function handoffNote(h) {
|
|
95
|
-
if (!h) return "";
|
|
96
|
-
switch (h.outcome) {
|
|
97
|
-
case "throttled":
|
|
98
|
-
return `handoff held: ${limitWords(h.limit, h.role)} ${h.max}/${h.max} — still to do`;
|
|
99
|
-
case "cycle":
|
|
100
|
-
return `handoff stopped: ${h.role} is already in this chain — needs a person to split the work`;
|
|
101
|
-
case "depth":
|
|
102
|
-
return `handoff stopped: chain reached ${h.depth} of ${h.max} — needs a person`;
|
|
103
|
-
case "human":
|
|
104
|
-
return `handoff stopped: ${h.reason === "ambiguous" ? "more than one role owns this" : "no role owns this"}`;
|
|
105
|
-
case "route":
|
|
106
|
-
return `handed to ${h.role}`;
|
|
107
|
-
case "resolved":
|
|
108
|
-
return `no handoff needed — the policy moved and ${h.role} owns it now`;
|
|
109
|
-
default:
|
|
110
|
-
return `handoff outcome: ${h.outcome}`;
|
|
109
|
+
// Where it goes, decided now. A route is the expected case and reads as
|
|
110
|
+
// information; anything that needs a person is said in yellow.
|
|
111
|
+
const note = describeHandoff(r.handoff, r.attempt, r.chainStopped);
|
|
112
|
+
if (note) lines.push(` ${r.handoff?.type === "route" ? C.dim : C.yellow}${safe(note)}${C.off}\n`);
|
|
111
113
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
/** The limit names are internal; what a person reads should not be a field name. */
|
|
115
|
-
function limitWords(limit, role) {
|
|
116
|
-
if (limit === "senderChains") return "open chains for this role";
|
|
117
|
-
if (limit === "receiverConcurrent") return `${role ?? "the owner"} already running`;
|
|
118
|
-
return limit ?? "a limit";
|
|
114
|
+
return lines;
|
|
119
115
|
}
|
|
120
116
|
|
|
121
117
|
export function grant(config, argv = []) {
|
|
122
|
-
const req = pick(config, argv[0]);
|
|
123
118
|
const i = argv.indexOf("--reason");
|
|
124
119
|
const reason = i === -1 ? "" : argv[i + 1] ?? "";
|
|
120
|
+
// `--file` grants the file the request was asked over, not the folder it is
|
|
121
|
+
// keyed by. The id stays first, as usage says; the flag may come after it.
|
|
122
|
+
const only = argv.includes("--file");
|
|
123
|
+
const which = argv[0] === "--file" ? argv[1] : argv[0];
|
|
125
124
|
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
let
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
// Looked up, applied and settled under one lock — the one the console's
|
|
126
|
+
// decide takes too. Picking before the lock let this and the console both
|
|
127
|
+
// find the same request open and act on it twice.
|
|
128
|
+
let req;
|
|
129
|
+
const { changed } = underLock(() => editPolicy(config, (before) => {
|
|
130
|
+
const current = loadConfig(config.path, before);
|
|
131
|
+
req = pick(current, which, "grant");
|
|
132
|
+
if (only) req = asFileGrant(req);
|
|
133
|
+
refuseIfBarred(current, req);
|
|
134
|
+
return applyGrant(before, req, reason);
|
|
135
|
+
}, { after: () => settle(requestsPath(config.root), req.key, "granted", reason, { only: req.only }) }));
|
|
136
|
+
// Said once, beside the grant it is about: a literal file is useless to an
|
|
137
|
+
// agent that saves through a temporary beside it.
|
|
138
|
+
const caveat = only ? fileOnlyNote(req.role, lastAgentOf(read(logPath(config.root)), req.role)) : "";
|
|
139
|
+
const warn = caveat ? ` ${C.yellow}${caveat}${C.off}\n` : "";
|
|
140
|
+
// Already in the policy: the request is answered all the same, as the
|
|
141
|
+
// console answers it. Throwing here left it pending forever — the person had
|
|
142
|
+
// said yes, the role had the path, and the queue kept asking.
|
|
143
|
+
if (!changed) {
|
|
144
|
+
out(
|
|
145
|
+
`\n ${C.green}already granted${C.off} ${req.role} → ${C.b}${req.grant}${C.off}\n` +
|
|
146
|
+
` ${C.dim}${config.path} already gives it; nothing written, the request is closed.${C.off}\n` + warn + "\n"
|
|
147
|
+
);
|
|
148
|
+
out(remaining(config));
|
|
149
|
+
return { request: req, grant: req.grant, changed: false };
|
|
136
150
|
}
|
|
137
|
-
if (!changed) throw new Error(`${req.role} already has ${req.grant} — nothing to add`);
|
|
138
151
|
|
|
139
|
-
|
|
152
|
+
// A key another role declares is shared by this grant, not moved: both roles
|
|
153
|
+
// read it from now on. That is sometimes the point, and never a thing to find
|
|
154
|
+
// out later.
|
|
155
|
+
const others = req.action === "read" ? keyHolders(config, req.target).filter((r) => r !== req.role) : [];
|
|
140
156
|
out(
|
|
141
157
|
`\n ${C.green}granted${C.off} ${req.role} → ${C.b}${req.grant}${C.off}\n` +
|
|
158
|
+
(others.length
|
|
159
|
+
? ` ${C.yellow}shared: ${req.grant} is also declared by ${others.join(", ")} — both roles read it now${C.off}\n`
|
|
160
|
+
: "") + warn +
|
|
142
161
|
` ${C.dim}written into ${config.path} with its provenance. It applies on the next run.${C.off}\n\n`
|
|
143
162
|
);
|
|
144
|
-
|
|
163
|
+
out(remaining(config));
|
|
164
|
+
return { request: req, grant: req.grant, shared: others };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A held lock is another decision in flight, not a failure of this one. */
|
|
168
|
+
function underLock(fn) {
|
|
169
|
+
try {
|
|
170
|
+
return fn();
|
|
171
|
+
} catch (e) {
|
|
172
|
+
if (e.code === "ELOCKED")
|
|
173
|
+
throw new Error("another decision is in progress — run this again in a moment");
|
|
174
|
+
throw e;
|
|
175
|
+
}
|
|
145
176
|
}
|
|
146
177
|
|
|
147
|
-
export function
|
|
148
|
-
const req = pick(config, argv[0]);
|
|
178
|
+
export function decline(config, argv = []) {
|
|
149
179
|
const i = argv.indexOf("--reason");
|
|
150
180
|
const reason = i === -1 ? "" : argv[i + 1] ?? "";
|
|
151
181
|
|
|
152
|
-
|
|
182
|
+
// The policy is not edited, but the lock is still the policy's: it is the one
|
|
183
|
+
// every decision takes, so a decline cannot settle what a grant is applying.
|
|
184
|
+
let req;
|
|
185
|
+
underLock(() => editPolicy(config, (before) => {
|
|
186
|
+
req = pick(config, argv[0], "decline");
|
|
187
|
+
return { toml: before, changed: false };
|
|
188
|
+
}, { after: () => settle(requestsPath(config.root), req.key, "declined", reason) }));
|
|
153
189
|
out(
|
|
154
190
|
`\n ${C.yellow}declined${C.off} ${req.role} ✕ ${req.grant}\n` +
|
|
155
191
|
` ${C.dim}${reason || "no reason recorded"}${C.off}\n\n`
|
|
156
192
|
);
|
|
193
|
+
out(remaining(config));
|
|
157
194
|
return { request: req, reason };
|
|
158
195
|
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The queue after a decision, numbered as it now is.
|
|
199
|
+
*
|
|
200
|
+
* Numbers are positions: settling #1 makes the old #2 the new #1, and the next
|
|
201
|
+
* `grant 2` — typed from the listing still on screen — lands on what used to
|
|
202
|
+
* be #3. One line with the new numbering, so the screen is right again.
|
|
203
|
+
*/
|
|
204
|
+
export function remaining(config) {
|
|
205
|
+
const queue = queueOf(config);
|
|
206
|
+
if (!queue.length) return ` ${C.dim}no requests left${C.off}\n\n`;
|
|
207
|
+
const shown = queue.slice(0, 5).map((r, i) => `#${i + 1} ${safe(r.role)} ${r.action} ${safe(r.grant)}`);
|
|
208
|
+
const more = queue.length > 5 ? ` · +${queue.length - 5} more` : "";
|
|
209
|
+
return ` ${C.dim}left, renumbered: ${shown.join(" · ")}${more}${C.off}\n\n`;
|
|
210
|
+
}
|
package/src/commands/review.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* would do; this reads what actually happened and tells you where the config
|
|
6
6
|
* was wrong about it. Both run nothing.
|
|
7
7
|
*
|
|
8
|
-
* Exits 1
|
|
9
|
-
* is a thing to fix, and a CI job that says so beats a
|
|
8
|
+
* Exits 1 on repeated denials, so it composes — a role denied over and over
|
|
9
|
+
* is a thing to fix, and a CI job that says so beats a person noticing in a
|
|
10
10
|
* month. Unused grants and unowned paths exit 0: they are worth reading, not
|
|
11
11
|
* worth failing a build over.
|
|
12
12
|
*
|
|
@@ -15,11 +15,22 @@
|
|
|
15
15
|
* would mean a correct policy cannot go green.
|
|
16
16
|
*/
|
|
17
17
|
import { review } from "../review.js";
|
|
18
|
-
import {
|
|
18
|
+
import { parseSince } from "../views.js";
|
|
19
|
+
import { C, out, safe } from "../render.js";
|
|
20
|
+
import { wired } from "./wire.js";
|
|
19
21
|
|
|
20
22
|
export function reviewCommand(config, argv = []) {
|
|
21
23
|
const i = argv.indexOf("--min");
|
|
22
|
-
|
|
24
|
+
/**
|
|
25
|
+
* `--since 2026-10-01` or `--since 7d`. The MCP ignores a filter it cannot
|
|
26
|
+
* read; a person typed this one, and a review of the whole log presented as
|
|
27
|
+
* the last week is the wrong answer said confidently — so it is refused.
|
|
28
|
+
*/
|
|
29
|
+
const s = argv.indexOf("--since");
|
|
30
|
+
const since = s === -1 ? null : parseSince(argv[s + 1]);
|
|
31
|
+
if (s !== -1 && !since)
|
|
32
|
+
throw new Error(`--since takes a date (2026-10-01) or a duration back from now (7d, 12h, 2w), not "${safe(argv[s + 1] ?? "")}"`);
|
|
33
|
+
const r = review(config, { minDenials: i === -1 ? 3 : Number(argv[i + 1]), since });
|
|
23
34
|
// Ten rows per section unless --all: on a real 32-role log the full report
|
|
24
35
|
// was 1,557 lines, which nobody reads.
|
|
25
36
|
const all = argv.includes("--all");
|
|
@@ -30,17 +41,20 @@ export function reviewCommand(config, argv = []) {
|
|
|
30
41
|
};
|
|
31
42
|
const who = (owners) => owners.length > 3 ? `${owners.slice(0, 3).join(", ")} +${owners.length - 3}` : owners.join(", ");
|
|
32
43
|
|
|
44
|
+
const day = (s) => (s ?? "").slice(0, 10);
|
|
33
45
|
if (!r.entries) {
|
|
34
|
-
out(
|
|
46
|
+
out(since
|
|
47
|
+
? `\n ${C.dim}nothing recorded since ${day(since)}.${C.off}\n\n`
|
|
48
|
+
: `\n ${C.dim}nothing recorded yet. Run an agent through seisin first.${C.off}\n\n`);
|
|
35
49
|
return r;
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
out(`\n ${C.dim}${r.entries} entries, ${day(r.window.from)} to ${day(r.window.to)}` +
|
|
53
|
+
`${since ? ` (since ${day(since)})` : ""}${C.off}\n`);
|
|
40
54
|
|
|
41
55
|
if (r.friction.length) {
|
|
42
|
-
out(`\n ${C.b}
|
|
43
|
-
out(` ${C.dim}a role
|
|
56
|
+
out(`\n ${C.b}Denied repeatedly${C.off}\n`);
|
|
57
|
+
out(` ${C.dim}a role denied on the same place over and over is a policy that is wrong,${C.off}\n`);
|
|
44
58
|
out(` ${C.dim}not an agent that is misbehaving${C.off}\n\n`);
|
|
45
59
|
for (const f of cap(r.friction)) {
|
|
46
60
|
const whose = f.owners.length ? ` ${C.dim}— belongs to ${who(f.owners)}${C.off}` : "";
|
|
@@ -50,6 +64,20 @@ export function reviewCommand(config, argv = []) {
|
|
|
50
64
|
out(`\n ${C.dim}seisin grant, or move the territory. Either way it is a decision, not noise.${C.off}\n`);
|
|
51
65
|
}
|
|
52
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Repeated, refused correctly, and nothing to decide: Claude Code updating
|
|
69
|
+
* its plugins from inside the sandbox (kinds.js expectedOf). Under the
|
|
70
|
+
* heading above it read as a policy that is wrong, and the only grant that
|
|
71
|
+
* would quiet it is a write into what runs outside the sandbox.
|
|
72
|
+
*/
|
|
73
|
+
if (r.expected?.length) {
|
|
74
|
+
out(`\n ${C.b}Denied as expected${C.off}\n`);
|
|
75
|
+
out(` ${C.dim}${r.expected[0].expected.hint}${C.off}\n\n`);
|
|
76
|
+
for (const f of cap(r.expected))
|
|
77
|
+
out(` ${C.dim}${String(f.times).padStart(4)}×${C.off} ${C.b}${f.role}${C.off} ${f.action} ${f.where}\n`);
|
|
78
|
+
more(r.expected);
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
/**
|
|
54
82
|
* The same repetition, and the opposite conclusion.
|
|
55
83
|
*
|
|
@@ -65,7 +93,7 @@ export function reviewCommand(config, argv = []) {
|
|
|
65
93
|
* working either when the service is one the role was meant to reach.
|
|
66
94
|
*/
|
|
67
95
|
if (r.connects?.length) {
|
|
68
|
-
out(`\n ${C.b}
|
|
96
|
+
out(`\n ${C.b}Denied connections${C.off}\n`);
|
|
69
97
|
out(` ${C.dim}a local port or socket the role kept dialling. Not a grant: a port goes in${C.off}\n`);
|
|
70
98
|
out(` ${C.dim}local_ports, and a socket stays closed${C.off}\n\n`);
|
|
71
99
|
for (const c of cap(r.connects))
|
|
@@ -75,7 +103,7 @@ export function reviewCommand(config, argv = []) {
|
|
|
75
103
|
}
|
|
76
104
|
|
|
77
105
|
if (r.guarded.length) {
|
|
78
|
-
out(`\n ${C.b}
|
|
106
|
+
out(`\n ${C.b}Denied at the key directories${C.off}\n`);
|
|
79
107
|
out(` ${C.dim}a [keys] dir is closed to every role, so this is the policy working${C.off}\n`);
|
|
80
108
|
out(` ${C.dim}— not a territory drawn wrong, and never a thing to grant${C.off}\n\n`);
|
|
81
109
|
for (const g of cap(r.guarded))
|
|
@@ -90,32 +118,56 @@ export function reviewCommand(config, argv = []) {
|
|
|
90
118
|
*
|
|
91
119
|
* Without the hook the log holds denials and nothing else, so every grant
|
|
92
120
|
* looks dead. The old text called itself the only evidence anyone will ever
|
|
93
|
-
* have for making a
|
|
121
|
+
* have for making a policy smaller, printed over the whole policy.
|
|
94
122
|
*/
|
|
95
123
|
if (!r.unusedKnowable) {
|
|
96
124
|
out(`\n ${C.b}Granted, never used${C.off} ${C.dim}— cannot be answered from this log${C.off}\n`);
|
|
97
|
-
out(` ${C.dim}nothing here records what was ALLOWED, only what was
|
|
98
|
-
|
|
125
|
+
out(` ${C.dim}nothing here records what was ALLOWED, only what was denied. Every grant${C.off}\n`);
|
|
126
|
+
// Wired already: telling someone to run `seisin wire` again sends them
|
|
127
|
+
// looking for a fault in a setup that is fine and simply has not run.
|
|
128
|
+
if (wired(config.root))
|
|
129
|
+
out(` ${C.dim}would look dead, so nothing is listed. wired, but no allowed action logged yet —${C.off}\n` +
|
|
130
|
+
` ${C.dim}run an agent (claude, codex…) through seisin run and come back.${C.off}\n`);
|
|
131
|
+
else
|
|
132
|
+
out(` ${C.dim}would look dead, so nothing is listed. Run \`seisin wire\` and come back.${C.off}\n`);
|
|
99
133
|
}
|
|
100
134
|
|
|
101
135
|
if (r.unused.length) {
|
|
102
136
|
out(`\n ${C.b}Granted, never used${C.off}\n`);
|
|
103
137
|
out(` ${C.dim}nothing was written here in the window above. This is the only evidence${C.off}\n`);
|
|
104
|
-
out(` ${C.dim}anyone will ever have for making a
|
|
138
|
+
out(` ${C.dim}anyone will ever have for making a policy smaller${C.off}\n\n`);
|
|
105
139
|
for (const u of cap(r.unused)) out(` ${C.b}${u.role}${C.off} ${u.glob}\n`);
|
|
106
140
|
more(r.unused);
|
|
107
141
|
out(`\n ${C.dim}A short window proves nothing. Check the dates before you delete a line.${C.off}\n`);
|
|
108
142
|
}
|
|
109
143
|
|
|
110
144
|
if (r.unowned.length) {
|
|
111
|
-
out(`\n ${C.b}
|
|
112
|
-
out(` ${C.dim}touched by an agent, claimed by no role
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
out(`\n ${C.b}Unowned${C.off}\n`);
|
|
146
|
+
out(` ${C.dim}touched by an agent, claimed by no role. Only an ownable path (kind \`territory\`)${C.off}\n`);
|
|
147
|
+
out(` ${C.dim}is a hole in the map; the rest is a kind of thing with its own answer, and none${C.off}\n`);
|
|
148
|
+
out(` ${C.dim}of them is an owner${C.off}\n\n`);
|
|
149
|
+
// Territory first: it is the part that is a decision, and on a real log it
|
|
150
|
+
// was 22 paths under 279 of a test's scratch — past the ten-row cap.
|
|
151
|
+
const ordered = [...r.unowned.filter((u) => u.kind === "territory"), ...r.unowned.filter((u) => u.kind !== "territory")];
|
|
152
|
+
const shown = cap(ordered);
|
|
153
|
+
const width = Math.max(...shown.map((u) => (u.kind ?? "").length));
|
|
154
|
+
for (const u of shown)
|
|
155
|
+
out(` ${String(u.times).padStart(4)}× ${u.kind ? `${C.dim}${u.kind.padEnd(width)}${C.off} ` : ""}${u.where}\n`);
|
|
156
|
+
more(ordered);
|
|
157
|
+
// The advice once per kind shown, not once per row: a hundred rows of
|
|
158
|
+
// `.git` do not need the same sentence a hundred times.
|
|
159
|
+
const hints = new Map(shown.filter((u) => u.hint).map((u) => [u.kind, u.hint]));
|
|
160
|
+
if (hints.size) out("\n");
|
|
161
|
+
for (const [kind, hint] of hints) out(` ${C.dim}${kind}: ${hint}${C.off}\n`);
|
|
162
|
+
// All of it by kind, so the part past the cap is still said in one line.
|
|
163
|
+
const tally = new Map();
|
|
164
|
+
for (const u of r.unowned) if (u.kind) tally.set(u.kind, (tally.get(u.kind) ?? 0) + u.times);
|
|
165
|
+
if (tally.size > 1)
|
|
166
|
+
out(` ${C.dim}by kind: ${[...tally].sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k} ${n.toLocaleString("en")}×`).join(" · ")}${C.off}\n`);
|
|
115
167
|
}
|
|
116
168
|
|
|
117
|
-
if (!r.friction.length && !r.guarded.length && !r.unused.length && !r.unowned.length && r.unusedKnowable)
|
|
118
|
-
out(`\n ${C.green}nothing to report${C.off} ${C.dim}— no repeated
|
|
169
|
+
if (!r.friction.length && !r.expected?.length && !r.guarded.length && !r.unused.length && !r.unowned.length && r.unusedKnowable)
|
|
170
|
+
out(`\n ${C.green}nothing to report${C.off} ${C.dim}— no repeated denials, no unused grants, no unowned paths${C.off}\n`);
|
|
119
171
|
|
|
120
172
|
out("\n");
|
|
121
173
|
return r;
|