zelari-code 2.54.0 → 2.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/app.js +2 -2
  3. package/dist/cli/app.js.map +1 -1
  4. package/dist/cli/commands/evolve.js +247 -0
  5. package/dist/cli/commands/evolve.js.map +1 -0
  6. package/dist/cli/commands/replay.js +202 -0
  7. package/dist/cli/commands/replay.js.map +1 -0
  8. package/dist/cli/commands/session.js +264 -0
  9. package/dist/cli/commands/session.js.map +1 -0
  10. package/dist/cli/commands/spineSession.js +77 -0
  11. package/dist/cli/commands/spineSession.js.map +1 -0
  12. package/dist/cli/components/StatusBar.js +6 -2
  13. package/dist/cli/components/StatusBar.js.map +1 -1
  14. package/dist/cli/components/statusChips.js +43 -0
  15. package/dist/cli/components/statusChips.js.map +1 -1
  16. package/dist/cli/councilDispatcher.js +12 -0
  17. package/dist/cli/councilDispatcher.js.map +1 -1
  18. package/dist/cli/evolution/controller.js +107 -0
  19. package/dist/cli/evolution/controller.js.map +1 -0
  20. package/dist/cli/evolution/ledger.js +42 -0
  21. package/dist/cli/evolution/ledger.js.map +1 -1
  22. package/dist/cli/headless/runOneTurn.js +18 -0
  23. package/dist/cli/headless/runOneTurn.js.map +1 -1
  24. package/dist/cli/hooks/useChatTurn.js +39 -1
  25. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  26. package/dist/cli/hooks/useSlashDispatch.js +8 -0
  27. package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
  28. package/dist/cli/inbox.js +63 -10
  29. package/dist/cli/inbox.js.map +1 -1
  30. package/dist/cli/inboxSources.js +173 -0
  31. package/dist/cli/inboxSources.js.map +1 -0
  32. package/dist/cli/main.bundled.js +9650 -6423
  33. package/dist/cli/main.bundled.js.map +4 -4
  34. package/dist/cli/main.js +80 -0
  35. package/dist/cli/main.js.map +1 -1
  36. package/dist/cli/plugins/bundleCommand.js +238 -0
  37. package/dist/cli/plugins/bundleCommand.js.map +1 -0
  38. package/dist/cli/plugins/bundleDiscover.js +74 -0
  39. package/dist/cli/plugins/bundleDiscover.js.map +1 -0
  40. package/dist/cli/plugins/bundleFs.js +57 -0
  41. package/dist/cli/plugins/bundleFs.js.map +1 -0
  42. package/dist/cli/plugins/bundleHookDef.js +70 -0
  43. package/dist/cli/plugins/bundleHookDef.js.map +1 -0
  44. package/dist/cli/plugins/bundleLoad.js +180 -0
  45. package/dist/cli/plugins/bundleLoad.js.map +1 -0
  46. package/dist/cli/plugins/bundleManifest.js +233 -0
  47. package/dist/cli/plugins/bundleManifest.js.map +1 -0
  48. package/dist/cli/plugins/bundleMcp.js +41 -0
  49. package/dist/cli/plugins/bundleMcp.js.map +1 -0
  50. package/dist/cli/plugins/bundleState.js +142 -0
  51. package/dist/cli/plugins/bundleState.js.map +1 -0
  52. package/dist/cli/runHeadless.js +40 -0
  53. package/dist/cli/runHeadless.js.map +1 -1
  54. package/dist/cli/safety/decisionEmit.js +43 -0
  55. package/dist/cli/safety/decisionEmit.js.map +1 -0
  56. package/dist/cli/safety/permissionGate.js +239 -0
  57. package/dist/cli/safety/permissionGate.js.map +1 -0
  58. package/dist/cli/safety/permissionPolicy.js +279 -0
  59. package/dist/cli/safety/permissionPolicy.js.map +1 -0
  60. package/dist/cli/safety/permissionRules.js +145 -0
  61. package/dist/cli/safety/permissionRules.js.map +1 -0
  62. package/dist/cli/safety/sessionSink.js +169 -0
  63. package/dist/cli/safety/sessionSink.js.map +1 -0
  64. package/dist/cli/slashCommands.js +28 -2
  65. package/dist/cli/slashCommands.js.map +1 -1
  66. package/dist/cli/slashHandlers/evolve.js +41 -0
  67. package/dist/cli/slashHandlers/evolve.js.map +1 -0
  68. package/dist/cli/slashHandlers/permissions.js +170 -0
  69. package/dist/cli/slashHandlers/permissions.js.map +1 -0
  70. package/dist/cli/statusline/inboxFeed.js +76 -0
  71. package/dist/cli/statusline/inboxFeed.js.map +1 -0
  72. package/dist/cli/statusline/statuslineItems.js +15 -0
  73. package/dist/cli/statusline/statuslineItems.js.map +1 -1
  74. package/dist/cli/toolRegistry.js +161 -7
  75. package/dist/cli/toolRegistry.js.map +1 -1
  76. package/dist/cli/tools/askUser.js +17 -2
  77. package/dist/cli/tools/askUser.js.map +1 -1
  78. package/dist/cli/tools/execProcess.js +15 -1
  79. package/dist/cli/tools/execProcess.js.map +1 -1
  80. package/dist/cli/tools/krakenWorktree.js +334 -43
  81. package/dist/cli/tools/krakenWorktree.js.map +1 -1
  82. package/dist/cli/tools/taskTool.js +179 -43
  83. package/dist/cli/tools/taskTool.js.map +1 -1
  84. package/dist/cli/tools/verifyDebtSpine.js +41 -0
  85. package/dist/cli/tools/verifyDebtSpine.js.map +1 -1
  86. package/package.json +5 -2
@@ -0,0 +1,170 @@
1
+ import { appendSystem } from '../hooks/messageHelpers.js';
2
+ import { defaultPermissionPolicy } from '../safety/toolPermissions.js';
3
+ import { PERMISSION_RULE_CATEGORIES } from '../safety/permissionPolicy.js';
4
+ import { addSessionPermissionRule, listSessionPermissionRules, loadProjectPermissionRules, removeSessionPermissionRule, clearSessionPermissionRules, } from '../safety/permissionRules.js';
5
+ import { clearPermissionDenials, listRecentPermissionDenials, } from '../safety/permissionGate.js';
6
+ export const PERMISSIONS_USAGE = [
7
+ '/permissions — active rules (source: default | project | session) + recent denials',
8
+ '/permissions add <id> <allow|ask|deny> [tool=<name|glob>] [category=read|write|execute|network|ui] [pathPrefix=<p>] [host=<h>] [note=<text>]',
9
+ '/permissions remove <id> — drop a session rule',
10
+ '/permissions clear — drop every session rule',
11
+ '/permissions denials [--clear] — recent denials',
12
+ ].join('\n');
13
+ const MATCHER_KEYS = ['tool', 'category', 'pathPrefix', 'host', 'note'];
14
+ /** `id effect tool=bash pathPrefix=docs` → the raw rule object (zod validates it). */
15
+ function parseRuleArgs(args) {
16
+ const [id, effect, ...rest] = args;
17
+ if (!id || !effect)
18
+ return { ok: false, error: 'usage: /permissions add <id> <allow|ask|deny> [matchers…]' };
19
+ const raw = { id, effect };
20
+ for (const token of rest) {
21
+ const eq = token.indexOf('=');
22
+ if (eq <= 0)
23
+ return { ok: false, error: `unparseable argument '${token}' (expected key=value)` };
24
+ const key = token.slice(0, eq);
25
+ const value = token.slice(eq + 1);
26
+ if (!MATCHER_KEYS.includes(key)) {
27
+ return { ok: false, error: `unknown matcher '${key}' (expected ${MATCHER_KEYS.join('/')})` };
28
+ }
29
+ if (value === '')
30
+ return { ok: false, error: `empty value for '${key}'` };
31
+ raw[key] = value;
32
+ }
33
+ return { ok: true, raw };
34
+ }
35
+ function formatRuleLine(source, id, effect, matchers, note) {
36
+ return ` [${source}] ${id} → ${effect}${matchers ? ` (${matchers})` : ''}${note ? ` — ${note}` : ''}`;
37
+ }
38
+ function matchersOf(rule) {
39
+ return [
40
+ rule.tool !== undefined ? `tool=${rule.tool}` : '',
41
+ rule.category !== undefined ? `category=${rule.category}` : '',
42
+ rule.pathPrefix !== undefined ? `pathPrefix=${rule.pathPrefix}` : '',
43
+ rule.host !== undefined ? `host=${rule.host}` : '',
44
+ ]
45
+ .filter(Boolean)
46
+ .join(' ');
47
+ }
48
+ export function formatPermissionDenialLine(d) {
49
+ const when = new Date(d.ts).toISOString();
50
+ return ` ${when} ${d.tool} blocked by [${d.source}] '${d.matchedRuleId}'`;
51
+ }
52
+ /** The full `/permissions` report (pure — same input, same text). */
53
+ export function formatPermissionsReport(cwd) {
54
+ const policy = defaultPermissionPolicy();
55
+ const project = loadProjectPermissionRules(cwd);
56
+ const session = listSessionPermissionRules();
57
+ const denials = listRecentPermissionDenials(10);
58
+ const lines = [
59
+ '[permissions] local policy engine (WS1) — evaluated before every tool dispatch',
60
+ ' precedence: deny > ask > allow, then most specific rule, then session before project',
61
+ '',
62
+ `default (5 categories, unchanged): read=${policy.read} write=${policy.write} execute=${policy.execute} network=${policy.network} ui=${policy.ui}${policy.auto ? ' auto=on' : ''}`,
63
+ ` editable via ZELARI_PERMISSION_<CATEGORY> / --permissions <preset> — not here`,
64
+ '',
65
+ `project: ${project.path}`,
66
+ ];
67
+ if (project.error !== undefined) {
68
+ lines.push(` FAIL-CLOSED: ${project.error}`, ' (every unmatched tool call asks until the file is fixed)');
69
+ }
70
+ else if (project.rules.length === 0) {
71
+ lines.push(' (no rules — absent or empty file)');
72
+ }
73
+ else {
74
+ for (const { rule } of project.rules) {
75
+ lines.push(formatRuleLine('project', rule.id, rule.effect, matchersOf(rule), rule.note));
76
+ }
77
+ }
78
+ lines.push('', `session (${session.length} rule${session.length === 1 ? '' : 's'}, runtime-only)`);
79
+ if (session.length === 0)
80
+ lines.push(' (none — /permissions add <id> <effect> [matchers…])');
81
+ else {
82
+ for (const { rule } of session) {
83
+ lines.push(formatRuleLine('session', rule.id, rule.effect, matchersOf(rule), rule.note));
84
+ }
85
+ }
86
+ lines.push('', `recent denials (${denials.length})`);
87
+ if (denials.length === 0)
88
+ lines.push(' (none this session)');
89
+ else
90
+ for (const d of denials)
91
+ lines.push(formatPermissionDenialLine(d));
92
+ lines.push('', `categories: ${PERMISSION_RULE_CATEGORIES.join(', ')}`, '', PERMISSIONS_USAGE);
93
+ return lines.join('\n');
94
+ }
95
+ /**
96
+ * `/permissions [subcommand] …`. Mirrors the /trust handler shape: it renders
97
+ * a system message and returns the text it renders (handy for tests).
98
+ */
99
+ export function handlePermissions(ctx, subcommand, args = [], cwd = process.cwd()) {
100
+ const sub = (subcommand ?? '').toLowerCase();
101
+ switch (sub) {
102
+ case '':
103
+ case 'list':
104
+ case 'show': {
105
+ const text = formatPermissionsReport(cwd);
106
+ appendSystem(ctx.setMessages, text);
107
+ return text;
108
+ }
109
+ case 'add': {
110
+ const parsed = parseRuleArgs(args);
111
+ if (!parsed.ok) {
112
+ appendSystem(ctx.setMessages, `[permissions] ${parsed.error}\n\n${PERMISSIONS_USAGE}`);
113
+ return parsed.error;
114
+ }
115
+ const res = addSessionPermissionRule(parsed.raw);
116
+ if (!res.ok) {
117
+ // Return what is rendered: the caller-facing text must carry the
118
+ // REJECTION verdict, not just the bare schema error.
119
+ const rejected = `[permissions] rejected: ${res.error}`;
120
+ appendSystem(ctx.setMessages, rejected);
121
+ return rejected;
122
+ }
123
+ const text = `[permissions] session rule '${res.rule.id}' → ${res.rule.effect}${matchersOf(res.rule) ? ` (${matchersOf(res.rule)})` : ''} — effective on the next dispatch (this process only, never persisted).`;
124
+ appendSystem(ctx.setMessages, text);
125
+ return text;
126
+ }
127
+ case 'remove':
128
+ case 'rm':
129
+ case 'delete': {
130
+ const id = args[0];
131
+ if (!id) {
132
+ appendSystem(ctx.setMessages, '[permissions] usage: /permissions remove <id>');
133
+ return '';
134
+ }
135
+ const removed = removeSessionPermissionRule(id);
136
+ const text = removed
137
+ ? `[permissions] removed session rule '${id}'.`
138
+ : `[permissions] no session rule '${id}' (project rules are edited in ${loadProjectPermissionRules(cwd).path}).`;
139
+ appendSystem(ctx.setMessages, text);
140
+ return text;
141
+ }
142
+ case 'clear': {
143
+ clearSessionPermissionRules();
144
+ appendSystem(ctx.setMessages, '[permissions] all session rules cleared.');
145
+ return 'cleared';
146
+ }
147
+ case 'denials':
148
+ case 'denied': {
149
+ if (args[0] === '--clear') {
150
+ clearPermissionDenials();
151
+ appendSystem(ctx.setMessages, '[permissions] denial ledger cleared.');
152
+ return 'cleared';
153
+ }
154
+ const denials = listRecentPermissionDenials(20);
155
+ const text = denials.length === 0
156
+ ? '[permissions] no denials recorded this session.'
157
+ : ['[permissions] recent denials:', ...denials.map(formatPermissionDenialLine)].join('\n');
158
+ appendSystem(ctx.setMessages, text);
159
+ return text;
160
+ }
161
+ default: {
162
+ // Return the text that was rendered — it names the bogus subcommand —
163
+ // instead of the bare usage block, so a caller can SEE the rejection.
164
+ const text = `[permissions] unknown subcommand '${sub}'.\n\n${PERMISSIONS_USAGE}`;
165
+ appendSystem(ctx.setMessages, text);
166
+ return text;
167
+ }
168
+ }
169
+ }
170
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../../src/cli/slashHandlers/permissions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * inboxFeed — the `inbox` status-line item: "what is waiting on YOU", derived
3
+ * from the CURRENT session spine (derive-only, ADR-0016/0024).
4
+ *
5
+ * Same discipline as verdictFeed (t124): this module READS entries.jsonl, never
6
+ * writes and never feeds the model. The item is OPT-IN (`/statusline on inbox`),
7
+ * so the default bar is unchanged; the counts come from the very same
8
+ * projections `/inbox` prints:
9
+ * - `questions` — unanswered `ask_user` calls (inbox.ts, t125);
10
+ * - `needs` — open needs: un-cleared verify debt + permission denials;
11
+ * - `finished` — tentacle completions you have not spoken past yet.
12
+ *
13
+ * Kill switch: `ZELARI_INBOX=0` (the t125 switch, reused — one switch for the
14
+ * whole inbox surface, not a second knob) ⇒ `inboxFeedText()` returns null and
15
+ * the item paints nothing.
16
+ *
17
+ * HONESTY: nothing recorded (or an unreadable spine) ⇒ zero rows ⇒ no item
18
+ * text. A zero is never printed as if it were a measurement of "all clear".
19
+ */
20
+ import { existsSync, readFileSync } from 'node:fs';
21
+ import path from 'node:path';
22
+ import { parseSessionLogText, resolveSessionsDir } from '@zelari/core/session';
23
+ import { getCurrentSessionId } from '../sessionManager.js';
24
+ import { deriveInboxQuestions, inboxEnabled } from '../inbox.js';
25
+ import { deriveInboxSourceRows } from '../inboxSources.js';
26
+ /** The honest "nothing derived" feed. */
27
+ export function emptyInboxFeed() {
28
+ return { questions: 0, needs: 0, finished: 0, total: 0 };
29
+ }
30
+ /**
31
+ * Project ONE session's events into the feed. Pure: no I/O, no clock. The
32
+ * session id is a label here (the rows are counted, not reported), hence the
33
+ * neutral `current` handle.
34
+ */
35
+ export function deriveInboxFeed(events) {
36
+ const questions = deriveInboxQuestions(events, { sessionId: 'current' }).length;
37
+ const rows = deriveInboxSourceRows(events);
38
+ const needs = rows.filter((r) => r.source === 'needs-input').length;
39
+ return { questions, needs, finished: rows.length - needs, total: questions + rows.length };
40
+ }
41
+ /**
42
+ * Item text for the `inbox` statusline item, or null when there is nothing
43
+ * honest to say (kill switch off, or nothing waiting). Terse on purpose: the
44
+ * bar is one line. `inbox 3` / `inbox 3 (1 need)`.
45
+ */
46
+ export function inboxFeedText(feed, env = process.env) {
47
+ if (!inboxEnabled(env))
48
+ return null;
49
+ if (!feed || feed.total === 0)
50
+ return null;
51
+ return feed.needs > 0 ? `inbox ${feed.total} (${feed.needs} need)` : `inbox ${feed.total}`;
52
+ }
53
+ /**
54
+ * Read the inbox feed of the CURRENT run from its spine log. Tolerant by
55
+ * contract: a missing marker/file, a corrupt log or a kill-switched inbox all
56
+ * return the empty feed.
57
+ */
58
+ export function readInboxFeed(opts = {}) {
59
+ const env = opts.env ?? process.env;
60
+ if (!inboxEnabled(env))
61
+ return emptyInboxFeed();
62
+ const sessionId = opts.sessionId ?? getCurrentSessionId();
63
+ if (!sessionId)
64
+ return emptyInboxFeed();
65
+ const dir = opts.sessionsDir ?? resolveSessionsDir({ workspaceRoot: opts.cwd, env });
66
+ const file = path.join(dir, sessionId, 'events.jsonl');
67
+ try {
68
+ if (!existsSync(file))
69
+ return emptyInboxFeed();
70
+ return deriveInboxFeed(parseSessionLogText(file, readFileSync(file, 'utf-8')).events);
71
+ }
72
+ catch {
73
+ return emptyInboxFeed();
74
+ }
75
+ }
76
+ //# sourceMappingURL=inboxFeed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inboxFeed.js","sourceRoot":"","sources":["../../../src/cli/statusline/inboxFeed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAuB,MAAM,aAAa,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAa3D,yCAAyC;AACzC,MAAM,UAAU,cAAc;IAC5B,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;AAC3D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAiC;IAC/D,MAAM,SAAS,GAAG,oBAAoB,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC;IAChF,MAAM,IAAI,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC,MAAM,CAAC;IACpE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,GAAG,KAAK,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;AAC7F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAsB,EAAE,MAAyB,OAAO,CAAC,GAAG;IACxF,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3C,OAAO,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,KAAK,EAAE,CAAC;AAC7F,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,OAA6B,EAAE;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACpC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;QAAE,OAAO,cAAc,EAAE,CAAC;IAChD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,mBAAmB,EAAE,CAAC;IAC1D,IAAI,CAAC,SAAS;QAAE,OAAO,cAAc,EAAE,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,IAAI,kBAAkB,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;IACrF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;IACvD,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,cAAc,EAAE,CAAC;QAC/C,OAAO,eAAe,CAAC,mBAAmB,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACxF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,cAAc,EAAE,CAAC;IAC1B,CAAC;AACH,CAAC"}
@@ -50,11 +50,21 @@ export const STATUSLINE_CUSTOM_ID = 'custom';
50
50
  * that no renderer paints yet must not be reported as enabled.
51
51
  */
52
52
  export const STATUSLINE_VERDICT_ID = 'verdict';
53
+ /**
54
+ * `inbox` — what is waiting on YOU, derived from the current session spine
55
+ * (inboxFeed.ts: the same projections `/inbox` prints — unanswered ask_user,
56
+ * open needs, tentacle completions). OPT-IN for the same reason as `verdict`:
57
+ * the default list mirrors the chips StatusBar paints with no configuration.
58
+ *
59
+ * WS2 (notification bus): shares the t125 kill switch `ZELARI_INBOX=0`.
60
+ */
61
+ export const STATUSLINE_INBOX_ID = 'inbox';
53
62
  /** Any id a configuration may reference (built-ins + the opt-in items). */
54
63
  export const STATUSLINE_ITEM_IDS = [
55
64
  ...DEFAULT_STATUSLINE_ITEMS,
56
65
  STATUSLINE_CUSTOM_ID,
57
66
  STATUSLINE_VERDICT_ID,
67
+ STATUSLINE_INBOX_ID,
58
68
  ];
59
69
  /** Catalog for `/statusline` (order = the default order). */
60
70
  export const STATUSLINE_ITEMS = [
@@ -80,6 +90,11 @@ export const STATUSLINE_ITEMS = [
80
90
  label: 'verdict',
81
91
  description: 'verification progress from spine events (derive-only; opt-in)',
82
92
  },
93
+ {
94
+ id: STATUSLINE_INBOX_ID,
95
+ label: 'inbox',
96
+ description: 'what is waiting on you: ask_user, needs input, tentacle finished (derive-only; opt-in)',
97
+ },
83
98
  ];
84
99
  /** True when `id` is a known item (built-in, `custom` or `verdict`). */
85
100
  export function isStatusLineItemId(id) {
@@ -1 +1 @@
1
- {"version":3,"file":"statuslineItems.js","sourceRoot":"","sources":["../../../src/cli/statusline/statuslineItems.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"statuslineItems.js","sourceRoot":"","sources":["../../../src/cli/statusline/statuslineItems.ts"],"names":[],"mappings":"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"}
@@ -57,6 +57,11 @@ import { PROVIDERS } from './keyStore.js';
57
57
  import { activePermissionPreset, defaultPermissionPolicy, intersectPermissionPolicy, isSessionGranted, resolveToolPermission, } from './safety/toolPermissions.js';
58
58
  // v2.32 (S5): destructive-shape escalation at the same choke-point.
59
59
  import { destructiveCommandHit } from './safety/destructiveCommands.js';
60
+ // WS1 (t133): local permission policy (allow/ask/deny rules) — evaluated
61
+ // BEFORE dispatch, with the structured `permission.denied` spine event.
62
+ import { applyAllowRule, emitAutoApproveGranted, emitPermissionAsked, emitPermissionDenied, emitPermissionObserverHooks, evaluateToolDispatch, permissionDenialMessage, autoApproveOrigin, } from './safety/permissionGate.js';
63
+ // WS7 slice 4 (t139): best-effort writer for the remaining decision kinds.
64
+ import { emitJailBlocked } from './safety/decisionEmit.js';
60
65
  import { provenanceAppliesTo, provenanceMatchIn, recordResultForProvenance, } from './safety/provenance.js';
61
66
  import { createDefaultLifecycleHooks, HOOKS_FAILURE_ENV, resolveHookFailureMode } from './safety/lifecycleHooks.js';
62
67
  // ADR-0033 (t76): post-write AST gate with auto-revert on the write path.
@@ -77,6 +82,9 @@ import { wrapWithBashWriteDetection } from './tools/bashWriteWatch.js';
77
82
  import { appendKrakenRadio } from './tools/krakenRadio.js';
78
83
  import { withResultCache } from './toolResultCache.js';
79
84
  import { inspectCommand } from './safety/selfKillGuard.js';
85
+ // WS7 slice 4b (t139): the ONE binding that makes ToolContext.emitSessionEvent
86
+ // live on a real host path (see safety/sessionSink.ts for the full WHY).
87
+ import { withSessionEventSink } from './safety/sessionSink.js';
80
88
  import { typedErr } from '@zelari/core/harness/tools/toolTypes';
81
89
  import { cliToolToEnhanced, registerCustomTool } from '@zelari/core/skills';
82
90
  /**
@@ -243,7 +251,10 @@ export function createBuiltinToolRegistry(options = {}) {
243
251
  }
244
252
  })();
245
253
  const agentPolicyLayers = agentLayersFor(agentPolicySet, options.policyAgent ?? 'lead');
246
- const withPerm = (t) => wrapWithPermissions(t, permPolicy, options.onPermissionAsk, agentPolicyLayers, agentPolicySet.precedence, root);
254
+ const withPerm = (t) => wrapWithPermissions(t, permPolicy, options.onPermissionAsk, agentPolicyLayers, agentPolicySet.precedence, root,
255
+ // WS5 (t137): the SAME runner the registry gates Pre/PostToolUse with —
256
+ // one runner per registry, no second hook universe.
257
+ hooks);
247
258
  // Observe tools — always registered.
248
259
  registry.register(withPerm(safeReadFile));
249
260
  registry.register(withPerm(safeGrepContent));
@@ -476,12 +487,21 @@ export function createBuiltinToolRegistry(options = {}) {
476
487
  ...(options.onPermissionAsk ? { onPermissionAsk: options.onPermissionAsk } : {}),
477
488
  ...(options.subAgentProvider ? { provider: options.subAgentProvider } : {}),
478
489
  ...(options.subAgentModel ? { model: options.subAgentModel } : {}),
490
+ // WS7 slice 4b (t139): a tentacle's own registry is built here, so it
491
+ // needs the parent turn's sink too — otherwise a tentacle's file.applied
492
+ // / file.rejected (and its permission refusals) would stay invisible on
493
+ // the very spine that recorded its `task` spawn.
494
+ ...(options.sessionEventSink ? { sessionEventSink: options.sessionEventSink } : {}),
479
495
  }),
480
496
  ...(options.memoryService ? { memoryService: options.memoryService } : {}),
481
497
  ...(options.memoryAutoWrite !== undefined
482
498
  ? { memoryAutoWrite: options.memoryAutoWrite }
483
499
  : {}),
484
500
  ...(options.onTentacleEvent ? { onTentacleEvent: options.onTentacleEvent } : {}),
501
+ // WS5 (t137): the registry's own hook runner — SubagentStart/End (and
502
+ // the finished-tentacle Notification) ride the SAME runner that gates
503
+ // Pre/PostToolUse, so one config location covers every hook.
504
+ lifecycleHooks: hooks,
485
505
  }, options.planMode === true ? { allowedAgents: ['explore'] } : undefined);
486
506
  registry.register(withPerm(taskTool));
487
507
  tools.push({
@@ -570,6 +590,25 @@ export function createBuiltinToolRegistry(options = {}) {
570
590
  }
571
591
  }
572
592
  }
593
+ // WS7 slice 4b (t139): populate `ToolContext.emitSessionEvent` for EVERY
594
+ // registered tool, applied LAST (outermost) so the sink reaches the
595
+ // permission gate, the sandbox, the jail preflight and the tool body alike.
596
+ // WHY the host-side injection exists at all: both hosts hand this registry to
597
+ // `AgentHarness`, whose dispatch calls
598
+ // `registry.invoke(name, args, { cwd, sessionId, signal })` — the harness
599
+ // forwards NO host sink into the invoke options (AgentHarness.ts:839/1848).
600
+ // That one missing hop is why the whole tool-side telemetry surface (ADR-0033
601
+ // file.*, WS1 permission.denied, the WS7 slice-4 decision events) was dormant
602
+ // in production while its tests passed (they hand-build a ToolContext).
603
+ // No sink ⇒ no wrap: a registry built without one is byte-identical to before.
604
+ if (options.sessionEventSink) {
605
+ const sessionSink = options.sessionEventSink;
606
+ for (const name of registry.list()) {
607
+ const def = registry.get(name);
608
+ if (def)
609
+ registry.register(withSessionEventSink(def, sessionSink));
610
+ }
611
+ }
573
612
  return { registry, tools };
574
613
  }
575
614
  /**
@@ -720,7 +759,7 @@ export function resolveTentacleThinkingInput(agent, taskArg, env = process.env)
720
759
  * (F6) — can build a `TaskToolDeps` without duplicating this wiring.
721
760
  */
722
761
  export function createKrakenSubAgentContextFactory(opts) {
723
- const { root, audit, sessionId, provider: providerOverride, model: modelOverride, parentPolicy, onPermissionAsk } = opts;
762
+ const { root, audit, sessionId, provider: providerOverride, model: modelOverride, parentPolicy, onPermissionAsk, sessionEventSink } = opts;
724
763
  return async ({ agent, cwd: subCwd, thinkingEffort }) => {
725
764
  const cfg = providerOverride
726
765
  ? await providerConfigFor(providerOverride)
@@ -792,6 +831,9 @@ export function createKrakenSubAgentContextFactory(opts) {
792
831
  ...(onPermissionAsk ? { onPermissionAsk } : {}),
793
832
  // P0.5: the tentacle's agent identity drives per-agent policy rules.
794
833
  policyAgent: agent,
834
+ // WS7 slice 4b (t139): inherit the parent turn's spine sink (absent ⇒
835
+ // the tentacle's tools emit nothing, i.e. today's behavior).
836
+ ...(sessionEventSink ? { sessionEventSink } : {}),
795
837
  });
796
838
  const wrapTentacleStream = (primary, primaryProviderId) => wrapWithHeadlessFailover({
797
839
  primary,
@@ -834,8 +876,13 @@ export function createKrakenSubAgentContextFactory(opts) {
834
876
  }
835
877
  /**
836
878
  * Gate tool execution with allow/ask/deny policy (OpenCode-style permissions).
879
+ *
880
+ * WS5 (t137): `hooks` is the lifecycle-hook runner used ONLY for the
881
+ * observation-only `PermissionRequest` (+ denied-`Notification`) events. It is
882
+ * called fire-and-forget and its verdict is discarded by construction, so
883
+ * passing it can never change the verdict computed below.
837
884
  */
838
- function wrapWithPermissions(original, policy, onAsk, agentLayers, precedence = 'restrict-only', root) {
885
+ function wrapWithPermissions(original, policy, onAsk, agentLayers, precedence = 'restrict-only', root, hooks) {
839
886
  const required = (original.permissions ?? []);
840
887
  // Pure read tools under allow policy — no wrap overhead.
841
888
  const decisionProbe = resolveToolPermission(original.name, required, policy);
@@ -872,14 +919,36 @@ function wrapWithPermissions(original, policy, onAsk, agentLayers, precedence =
872
919
  // other layer or the category decision (same deny>ask>allow lattice).
873
920
  // No scoped contract ⇒ null ⇒ this slot is inert.
874
921
  const contractRule = matchContractCapabilityRule(requiredNow, (input ?? {}), root ?? process.cwd());
875
- let action = intersectEffects(mergeRuleEffect(decision.action, rule), claims?.effect, contractRule?.effect);
922
+ // WS1 (t133): the local permission policy (`.zelari/permissions.json` +
923
+ // runtime session rules) is evaluated BEFORE the call is dispatched.
924
+ // null = no rule configured, or no rule matched → today's category
925
+ // decision stands, byte-identical. An 'allow' rule promotes only a
926
+ // CATEGORY ask (skip the prompt) — a category deny and every other
927
+ // layer's ask/deny stay restrict-only. An 'ask'/'deny' rule intersects
928
+ // like any other restriction.
929
+ const policyVerdict = evaluateToolDispatch({
930
+ toolName: original.name,
931
+ required: requiredNow,
932
+ args: input,
933
+ root: root ?? process.cwd(),
934
+ });
935
+ const categoryAction = policyVerdict?.decision === 'allow'
936
+ ? applyAllowRule(decision.action)
937
+ : policyVerdict?.decision === 'ask' || policyVerdict?.decision === 'deny'
938
+ ? mergeRuleEffect(decision.action, {
939
+ match: policyVerdict.matchedRuleId ?? 'permissions',
940
+ effect: policyVerdict.decision,
941
+ reason: policyVerdict.reason,
942
+ })
943
+ : decision.action;
944
+ let action = intersectEffects(mergeRuleEffect(categoryAction, rule), claims?.effect, contractRule?.effect);
876
945
  // W3.1 (t46): provenance escalation at the choke-point. Write/execute
877
946
  // args that EMBED fingerprinted non-user content (web fetch, MCP output,
878
947
  // file reads — safety/provenance.ts) escalate an "allow" to "ask":
879
948
  // injected instructions can no longer sail through on category
880
949
  // defaults. Deterministic substring match, zero LLM (P2); ask/deny
881
950
  // already gated pass through; ZELARI_PROVENANCE=0 opts out entirely.
882
- let actionReason = decision.reason;
951
+ let actionReason = policyVerdict?.decision === 'ask' ? policyVerdict.reason : decision.reason;
883
952
  if (action !== 'deny' && (requiredNow.includes('write') || requiredNow.includes('execute'))) {
884
953
  const provHit = provenanceMatchIn(JSON.stringify(input ?? {}));
885
954
  if (provHit && provenanceAppliesTo(provHit.source, requiredNow)) {
@@ -921,19 +990,88 @@ function wrapWithPermissions(original, policy, onAsk, agentLayers, precedence =
921
990
  : claimHit
922
991
  ? `[policy] claim '${claimHit.match}'${claimHit.reason ? ` — ${claimHit.reason}` : ''}`
923
992
  : '';
993
+ // WS7 slice 4 (t139): did THIS dispatch's ask get auto-answered by yolo?
994
+ // `autoApproveOrigin` needs the fact, not a re-derivation.
995
+ let yoloPromoted = false;
924
996
  // t65: yolo = full access — every residual ask (policy rules, claims,
925
997
  // contract, provenance) auto-approves instead of hitting the phone with
926
998
  // a permission.request. Explicit deny still denies ("deny is never
927
999
  // promoted", toolPermissions.ts contract).
928
1000
  if (action === 'ask' && activePermissionPreset() === 'yolo') {
929
1001
  action = 'allow';
1002
+ yoloPromoted = true;
1003
+ }
1004
+ // WS5 (t137): `action` is FINAL here — every layer merged, provenance
1005
+ // applied, yolo promoted — which is exactly the moment a permission is
1006
+ // requested or refused. Observation-only hooks fire from here:
1007
+ // fire-and-forget, and `emitPermissionObserverHooks` swallows internal
1008
+ // failures, so a hook can never block, slow or alter this dispatch.
1009
+ if (action === 'ask' || action === 'deny') {
1010
+ void emitPermissionObserverHooks(hooks, {
1011
+ tool: original.name,
1012
+ categories: requiredNow,
1013
+ effect: action,
1014
+ verdict: policyVerdict,
1015
+ args: input,
1016
+ }, { sessionId: ctx.sessionId, cwd: ctx.cwd });
1017
+ }
1018
+ // WS7 slice 4 (t139): the DECISION, on the spine — `permission.asked` when
1019
+ // the gate resolved this dispatch to a PROMPT (the fail-closed branch
1020
+ // below included: the decision WAS "ask", there was simply nobody to ask),
1021
+ // `auto_approve.granted` when it resolved to ALLOW with no prompt. Both go
1022
+ // through the SAME sink `permission.denied` and the file.* telemetry use;
1023
+ // both are best-effort (an absent or throwing sink changes nothing about
1024
+ // the dispatch) and both are validated against their slice-2 contract
1025
+ // before a single byte is written. The allow side is deliberately
1026
+ // selective — see `autoApproveOrigin`: rule match, yolo promotion, and
1027
+ // execute/network category grants only, never the read/write flood.
1028
+ if (action === 'ask') {
1029
+ await emitPermissionAsked(ctx.emitSessionEvent, {
1030
+ tool: original.name,
1031
+ categories: requiredNow,
1032
+ verdict: policyVerdict,
1033
+ args: input,
1034
+ reason: policyVerdict?.decision === 'ask' ? policyVerdict.reason : decision.reason,
1035
+ });
1036
+ }
1037
+ else if (action === 'allow') {
1038
+ const origin = autoApproveOrigin({
1039
+ effect: action,
1040
+ categories: requiredNow,
1041
+ verdict: policyVerdict,
1042
+ yoloPromoted,
1043
+ });
1044
+ if (origin) {
1045
+ await emitAutoApproveGranted(ctx.emitSessionEvent, {
1046
+ tool: original.name,
1047
+ categories: requiredNow,
1048
+ origin,
1049
+ });
1050
+ }
930
1051
  }
931
1052
  if (action === 'deny') {
1053
+ // WS1 (t133): a RULE deny is structured — the message names the
1054
+ // matched rule id and the denial lands on the spine
1055
+ // (`permission.denied`) through the same sink the file.* telemetry
1056
+ // uses, so a replayed session exposes it. yolo never promotes a deny.
1057
+ if (policyVerdict?.decision === 'deny') {
1058
+ await emitPermissionDenied(ctx.emitSessionEvent, {
1059
+ tool: original.name,
1060
+ verdict: policyVerdict,
1061
+ sessionId: ctx.sessionId,
1062
+ });
1063
+ return typedErr(`[permission] ${permissionDenialMessage(original.name, policyVerdict)}`);
1064
+ }
932
1065
  return typedErr(`[permission] ${rulePrefix || decision.reason}`);
933
1066
  }
934
1067
  if (action === 'ask') {
935
1068
  if (!onAsk) {
936
- return typedErr(`[permission] ${rulePrefix ? `${rulePrefix} ` : ''}${decision.reason} No interactive approval available ` +
1069
+ // WS1 (t133): when the RULE layer forced (or failed closed on) this
1070
+ // ask, its reason is the one that names the matched rule id and the
1071
+ // offending `.zelari/permissions.json` — surface THAT, not the bare
1072
+ // category reason, or a malformed config reads as a category ask.
1073
+ const askReason = policyVerdict?.decision === 'ask' ? policyVerdict.reason : decision.reason;
1074
+ return typedErr(`[permission] ${rulePrefix ? `${rulePrefix} ` : ''}${askReason} No interactive approval available ` +
937
1075
  `(set ZELARI_AUTO=1 to auto-allow, or configure onPermissionAsk).`);
938
1076
  }
939
1077
  try {
@@ -1386,6 +1524,11 @@ function wrapWithShellSafety(original, audit, sessionId, root, jail) {
1386
1524
  if (mode !== 'off' && !probe.available) {
1387
1525
  if (mode === 'required') {
1388
1526
  const reason = jailDenyReason(probe, mode);
1527
+ // WS7 slice 4 (t139): the deny this seam SURFACES is the typed
1528
+ // `[jail]` one, and the sibling spawn seam records exactly that
1529
+ // (spawnJailed already prefixed it) — so the spine carries the same
1530
+ // text the caller (and the model) got, never a twin spelling.
1531
+ const jailError = `[jail] ${reason}`;
1389
1532
  await audit.append({
1390
1533
  ts: new Date().toISOString(),
1391
1534
  sessionId,
@@ -1396,7 +1539,18 @@ function wrapWithShellSafety(original, audit, sessionId, root, jail) {
1396
1539
  durationMs: 0,
1397
1540
  error: 'jail_denied',
1398
1541
  });
1399
- return { ok: false, error: `[jail] ${reason}` };
1542
+ // WS7 slice 4 (t139): `jail.blocked` — the jail REFUSED this spawn
1543
+ // (t28 golden rule: required + no honest backend ⇒ deny, never a
1544
+ // silent unjailed run). The backend + mode that failed it ride the
1545
+ // payload, so a replay can tell WHICH containment was missing.
1546
+ // Best-effort: same sink as every other decision event.
1547
+ await emitJailBlocked(ctx.emitSessionEvent, {
1548
+ backend: probe.backend,
1549
+ mode,
1550
+ reason: jailError,
1551
+ tool: original.name,
1552
+ });
1553
+ return { ok: false, error: jailError };
1400
1554
  }
1401
1555
  await audit.append({
1402
1556
  ts: new Date().toISOString(),