instar 1.3.1637 → 1.3.1639
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 +22 -8
- package/dist/commands/server.js.map +1 -1
- package/dist/core/DesktopHost.d.ts +4 -2
- package/dist/core/DesktopHost.d.ts.map +1 -1
- package/dist/core/DesktopHost.js.map +1 -1
- package/dist/core/DesktopProbeReadiness.d.ts +1 -0
- package/dist/core/DesktopProbeReadiness.d.ts.map +1 -1
- package/dist/core/DesktopProbeReadiness.js +11 -2
- package/dist/core/DesktopProbeReadiness.js.map +1 -1
- package/dist/core/HyprlandDesktopHost.d.ts +150 -0
- package/dist/core/HyprlandDesktopHost.d.ts.map +1 -0
- package/dist/core/HyprlandDesktopHost.js +569 -0
- package/dist/core/HyprlandDesktopHost.js.map +1 -0
- package/dist/core/LinuxDesktopHost.d.ts +1 -0
- package/dist/core/LinuxDesktopHost.d.ts.map +1 -1
- package/dist/core/LinuxDesktopHost.js +7 -4
- package/dist/core/LinuxDesktopHost.js.map +1 -1
- package/dist/core/SessionDrainRunner.d.ts +4 -2
- package/dist/core/SessionDrainRunner.d.ts.map +1 -1
- package/dist/core/SessionDrainRunner.js +2 -2
- package/dist/core/SessionDrainRunner.js.map +1 -1
- package/dist/core/TopicProfileOrchestrator.d.ts.map +1 -1
- package/dist/core/TopicProfileOrchestrator.js +2 -1
- package/dist/core/TopicProfileOrchestrator.js.map +1 -1
- package/dist/core/WaylandVirtualPointer.d.ts +31 -0
- package/dist/core/WaylandVirtualPointer.d.ts.map +1 -0
- package/dist/core/WaylandVirtualPointer.js +171 -0
- package/dist/core/WaylandVirtualPointer.js.map +1 -0
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/messaging/plainNoticeCheck.d.ts +9 -0
- package/dist/messaging/plainNoticeCheck.d.ts.map +1 -0
- package/dist/messaging/plainNoticeCheck.js +68 -0
- package/dist/messaging/plainNoticeCheck.js.map +1 -0
- package/dist/messaging/shared/isSystemOrProxyMessage.d.ts +0 -24
- package/dist/messaging/shared/isSystemOrProxyMessage.d.ts.map +1 -1
- package/dist/messaging/shared/isSystemOrProxyMessage.js +6 -3
- package/dist/messaging/shared/isSystemOrProxyMessage.js.map +1 -1
- package/dist/messaging/userNotices.d.ts +47 -0
- package/dist/messaging/userNotices.d.ts.map +1 -0
- package/dist/messaging/userNotices.js +79 -0
- package/dist/messaging/userNotices.js.map +1 -0
- package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
- package/dist/native/instar-desktop-host +0 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.js +4 -4
- package/dist/server/AgentServer.js.map +1 -1
- package/package.json +4 -3
- package/scripts/lint-plain-user-notices.mjs +191 -0
- package/scripts/plain-user-notices-baseline.json +6 -0
- package/src/data/builtin-manifest.json +3 -3
- package/src/data/plain-notice-terms.json +38 -0
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/src/scaffold/templates.ts +2 -1
- package/upgrades/1.3.1638.md +21 -0
- package/upgrades/1.3.1639.md +24 -0
- package/upgrades/side-effects/hyprland-remote-desktop.eli16.md +39 -0
- package/upgrades/side-effects/hyprland-remote-desktop.md +91 -0
- package/upgrades/side-effects/plain-user-notices.eli16.md +15 -0
- package/upgrades/side-effects/plain-user-notices.md +76 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instar",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1639",
|
|
4
4
|
"description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"test:contract": "node scripts/run-contract-tests.js",
|
|
31
31
|
"test:contract:raw": "vitest run --config vitest.contract.config.ts",
|
|
32
32
|
"test:all": "vitest run && vitest run --config vitest.integration.config.ts && vitest run --config vitest.e2e.config.ts",
|
|
33
|
-
"lint": "node node_modules/typescript-native/bin/tsc --noEmit && node scripts/lint-no-direct-destructive.js && node scripts/lint-no-direct-llm-http.js && node scripts/lint-no-direct-url-log.js && node scripts/lint-no-unfunneled-topic-creation.js && node scripts/lint-no-unfunneled-headless-launch.js && node scripts/lint-no-unfunneled-tmux-literal-send.js && node scripts/lint-no-undefined-erasing-default-merge.js && node scripts/lint-no-unbounded-llm-spawn.js && node scripts/lint-no-unregistered-self-action.js && node scripts/lint-no-unfunneled-credential-write.js && node scripts/lint-state-registry.js && node scripts/lint-store-retention-declared.js && node scripts/lint-no-wholefile-sync-read.js && node scripts/lint-cas-emit-placement.js && node scripts/lint-journal-actuation-ban.js && node scripts/lint-no-blocking-process-scans.js && node scripts/lint-sync-subprocess-chokepoint.js && node scripts/lint-dev-agent-dark-gate.js && node scripts/lint-guard-manifest.js && node scripts/lint-llm-attribution.js && node scripts/lint-no-mainthread-cartographer-walk.js && node scripts/lint-scrape-fixture-realness.js && node scripts/check-codex-rule1-drift.js && node scripts/lint-routing-registry-freshness.js && node scripts/lint-no-opus-claude-cli-gating.js && node scripts/lint-model-registry-freshness.mjs && node scripts/lint-no-unreachable-messaging-gate.js && node scripts/lint-telegram-egress-boundary.mjs && node scripts/lint-nature-chains.mjs && node scripts/lint-emit-without-admit.js && node scripts/lint-migration-consumer-completeness.js && node scripts/lint-canonical-pipeline-completeness.mjs && node scripts/lint-expected-capacity-degradations.js && node scripts/lint-rollout-evidence-resolvable.js && node scripts/lint-no-direct-standards-registry-path.mjs && node scripts/lint-blocking-decisions-declared.mjs && node scripts/lint-no-duplicate-definitions.mjs && node scripts/lint-dispatch-withholds-answer.mjs && node scripts/lint-recall-surface-names-match-mechanism.mjs && node scripts/lint-registry-insertion-placement.mjs && node scripts/lint-standard-code-backreference.mjs && node scripts/lint-bounded-judgment-input.mjs && node scripts/lint-registry-tree-parentage.mjs && node scripts/lint-single-governing-obligation.mjs && node scripts/lint-documented-only-countdown.mjs && node scripts/lint-deferral-carrier-resolvable.mjs --staged --enforce && node scripts/generate-standards-hierarchy.mjs --check && node scripts/lint-framework-list-completeness.mjs && node scripts/lint-ecosystem-adoption.mjs",
|
|
33
|
+
"lint": "node node_modules/typescript-native/bin/tsc --noEmit && node scripts/lint-no-direct-destructive.js && node scripts/lint-no-direct-llm-http.js && node scripts/lint-no-direct-url-log.js && node scripts/lint-no-unfunneled-topic-creation.js && node scripts/lint-no-unfunneled-headless-launch.js && node scripts/lint-no-unfunneled-tmux-literal-send.js && node scripts/lint-no-undefined-erasing-default-merge.js && node scripts/lint-no-unbounded-llm-spawn.js && node scripts/lint-no-unregistered-self-action.js && node scripts/lint-no-unfunneled-credential-write.js && node scripts/lint-state-registry.js && node scripts/lint-store-retention-declared.js && node scripts/lint-no-wholefile-sync-read.js && node scripts/lint-cas-emit-placement.js && node scripts/lint-journal-actuation-ban.js && node scripts/lint-no-blocking-process-scans.js && node scripts/lint-sync-subprocess-chokepoint.js && node scripts/lint-dev-agent-dark-gate.js && node scripts/lint-guard-manifest.js && node scripts/lint-llm-attribution.js && node scripts/lint-no-mainthread-cartographer-walk.js && node scripts/lint-scrape-fixture-realness.js && node scripts/check-codex-rule1-drift.js && node scripts/lint-routing-registry-freshness.js && node scripts/lint-no-opus-claude-cli-gating.js && node scripts/lint-model-registry-freshness.mjs && node scripts/lint-no-unreachable-messaging-gate.js && node scripts/lint-telegram-egress-boundary.mjs && node scripts/lint-nature-chains.mjs && node scripts/lint-emit-without-admit.js && node scripts/lint-migration-consumer-completeness.js && node scripts/lint-canonical-pipeline-completeness.mjs && node scripts/lint-expected-capacity-degradations.js && node scripts/lint-rollout-evidence-resolvable.js && node scripts/lint-no-direct-standards-registry-path.mjs && node scripts/lint-blocking-decisions-declared.mjs && node scripts/lint-no-duplicate-definitions.mjs && node scripts/lint-dispatch-withholds-answer.mjs && node scripts/lint-recall-surface-names-match-mechanism.mjs && node scripts/lint-registry-insertion-placement.mjs && node scripts/lint-standard-code-backreference.mjs && node scripts/lint-bounded-judgment-input.mjs && node scripts/lint-registry-tree-parentage.mjs && node scripts/lint-single-governing-obligation.mjs && node scripts/lint-documented-only-countdown.mjs && node scripts/lint-deferral-carrier-resolvable.mjs --staged --enforce && node scripts/generate-standards-hierarchy.mjs --check && node scripts/lint-framework-list-completeness.mjs && node scripts/lint-ecosystem-adoption.mjs && node scripts/lint-plain-user-notices.mjs",
|
|
34
34
|
"lint:parallel": "node scripts/run-lint-parallel.mjs",
|
|
35
35
|
"lint:canonical-pipeline-completeness": "node scripts/lint-canonical-pipeline-completeness.mjs",
|
|
36
36
|
"test:canonical-pipeline-runtime": "vitest run --config vitest.e2e.config.ts tests/e2e/feedback-factory-drain-lifecycle.test.ts && vitest run tests/unit/canonical-pipeline-completeness-lint.test.ts tests/unit/canonical-pipeline-runtime-evidence.test.ts",
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"postinstall": "node scripts/fix-better-sqlite3.cjs",
|
|
67
67
|
"prepare": "husky",
|
|
68
68
|
"lint:standards-registry-path": "node scripts/lint-no-direct-standards-registry-path.mjs",
|
|
69
|
-
"lint:framework-list": "node scripts/lint-framework-list-completeness.mjs"
|
|
69
|
+
"lint:framework-list": "node scripts/lint-framework-list-completeness.mjs",
|
|
70
|
+
"lint:plain-user-notices": "node scripts/lint-plain-user-notices.mjs"
|
|
70
71
|
},
|
|
71
72
|
"keywords": [
|
|
72
73
|
"instar",
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-plain-user-notices.mjs — Plain and Complete (docs/UX-AND-AGENT-AGENCY-STANDARD.md Rule 8).
|
|
4
|
+
*
|
|
5
|
+
* Notices the server writes itself (telegram.sendToTopic, slack sendToChannel,
|
|
6
|
+
* sendDeterministicTelegramNotice, …) go out on deterministic paths that skip
|
|
7
|
+
* the LLM tone gate on purpose: an alarm must arrive even when the model is
|
|
8
|
+
* down. That is why their wording has to be held at BUILD time instead. This
|
|
9
|
+
* lint finds the send call sites in src/ and flags, inside the text argument:
|
|
10
|
+
*
|
|
11
|
+
* RAW_REASON — an internal value pasted into user text: `${detail}`,
|
|
12
|
+
* `${reason}`, `${err.message}`, `${String(err)}` …
|
|
13
|
+
* (the 2026-11-03 "drain bound (30000ms) reached" notice).
|
|
14
|
+
* INTERNAL_TERM — a word from src/data/plain-notice-terms.json in a string
|
|
15
|
+
* literal of the text argument ("respawned", "final turn" …).
|
|
16
|
+
*
|
|
17
|
+
* Ratchet: a frozen per-file baseline (scripts/plain-user-notices-baseline.json)
|
|
18
|
+
* grandfathers today's hits; a file whose count GROWS fails. Shrinking a count
|
|
19
|
+
* is reported so the baseline can be lowered (--update rewrites it).
|
|
20
|
+
* A deliberate exception carries `@plain-notice-ok: <why>` on the call line or
|
|
21
|
+
* the line above (e.g. a reply that echoes an id the person asked for).
|
|
22
|
+
*
|
|
23
|
+
* COVERAGE HONESTY: static and approximate. A notice whose text is built in a
|
|
24
|
+
* variable away from the send call is not seen here; those belong in the
|
|
25
|
+
* catalog (src/messaging/userNotices.ts), whose test runs the full check on
|
|
26
|
+
* rendered text. Exit codes: 0 pass · 1 new violation · 2 cannot-read.
|
|
27
|
+
*/
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
function argVal(flag) {
|
|
34
|
+
const i = process.argv.indexOf(flag);
|
|
35
|
+
return i >= 0 ? process.argv[i + 1] : undefined;
|
|
36
|
+
}
|
|
37
|
+
const ROOT = path.resolve(argVal('--root') ?? path.join(__dirname, '..'));
|
|
38
|
+
const BASELINE = path.resolve(argVal('--baseline') ?? path.join(ROOT, 'scripts', 'plain-user-notices-baseline.json'));
|
|
39
|
+
const TERMS_PATH = path.join(ROOT, 'src', 'data', 'plain-notice-terms.json');
|
|
40
|
+
const UPDATE = process.argv.includes('--update');
|
|
41
|
+
|
|
42
|
+
const SEND_CALL_RE = /\b(?:sendToTopic|sendToChannel|sendDeterministicTelegramNotice|sendToLifeline|postToChannel)\s*\(/g;
|
|
43
|
+
const WAIVER = '@plain-notice-ok';
|
|
44
|
+
// A bare internal value (identifier / member chain, no call) named like a reason.
|
|
45
|
+
const RAW_EXPR_RE = /^[\w$?.!]*\b(?:detail|details|reason|err|error|cause|stderr|stdout)\b[\w$?.!]*$/;
|
|
46
|
+
// Any expression that reads an error's message or stringifies an error, in any form:
|
|
47
|
+
// `err.message`, `(err as Error).message`, `err instanceof Error ? err.message : String(err)`.
|
|
48
|
+
const RAW_CALL_RE = /(?:\b(?:e|err|error)\b\)?\??\.message\b|\bString\(\s*(?:e|err|error)\s*\)|^JSON\.stringify\()/i;
|
|
49
|
+
|
|
50
|
+
function readTerms() {
|
|
51
|
+
const raw = JSON.parse(fs.readFileSync(TERMS_PATH, 'utf-8'));
|
|
52
|
+
return raw.internalTerms.map((t) => ({
|
|
53
|
+
term: t,
|
|
54
|
+
re: new RegExp(`(^|[^A-Za-z0-9_-])${t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_-])`,
|
|
55
|
+
t === t.toUpperCase() && t.length <= 4 ? '' : 'i'),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** From the char after "(", return the index of the matching ")" (strings/templates aware). */
|
|
60
|
+
function argSpanEnd(src, start) {
|
|
61
|
+
let depth = 1;
|
|
62
|
+
const stack = []; // template-literal nesting
|
|
63
|
+
for (let i = start; i < src.length; i++) {
|
|
64
|
+
const c = src[i];
|
|
65
|
+
if (c === '"' || c === "'") {
|
|
66
|
+
for (i++; i < src.length && src[i] !== c; i++) if (src[i] === '\\') i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === '`') {
|
|
70
|
+
// walk the template; recurse on ${ … }
|
|
71
|
+
for (i++; i < src.length && src[i] !== '`'; i++) {
|
|
72
|
+
if (src[i] === '\\') { i++; continue; }
|
|
73
|
+
if (src[i] === '$' && src[i + 1] === '{') {
|
|
74
|
+
let d = 1; i += 2;
|
|
75
|
+
for (; i < src.length && d > 0; i++) {
|
|
76
|
+
if (src[i] === '{') d++;
|
|
77
|
+
else if (src[i] === '}') d--;
|
|
78
|
+
else if (src[i] === '`') { for (i++; i < src.length && src[i] !== '`'; i++) if (src[i] === '\\') i++; }
|
|
79
|
+
}
|
|
80
|
+
i--;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (c === '/' && src[i + 1] === '/') { while (i < src.length && src[i] !== '\n') i++; continue; }
|
|
86
|
+
if (c === '/' && src[i + 1] === '*') { i = src.indexOf('*/', i + 2); if (i < 0) return src.length; i++; continue; }
|
|
87
|
+
if (c === '(') depth++;
|
|
88
|
+
else if (c === ')') { depth--; if (depth === 0) return i; }
|
|
89
|
+
}
|
|
90
|
+
void stack;
|
|
91
|
+
return src.length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Template interpolation expressions + plain string literal bodies inside a span. */
|
|
95
|
+
function scanSpan(span) {
|
|
96
|
+
const exprs = [];
|
|
97
|
+
const literals = [];
|
|
98
|
+
const tpl = /`((?:\\.|[^`\\])*)`/gs;
|
|
99
|
+
for (const m of span.matchAll(tpl)) {
|
|
100
|
+
const body = m[1];
|
|
101
|
+
let lit = '';
|
|
102
|
+
for (let i = 0; i < body.length; i++) {
|
|
103
|
+
if (body[i] === '$' && body[i + 1] === '{') {
|
|
104
|
+
let d = 1; let j = i + 2;
|
|
105
|
+
for (; j < body.length && d > 0; j++) { if (body[j] === '{') d++; else if (body[j] === '}') d--; }
|
|
106
|
+
exprs.push(body.slice(i + 2, j - 1).trim());
|
|
107
|
+
lit += ' ';
|
|
108
|
+
i = j - 1;
|
|
109
|
+
} else lit += body[i];
|
|
110
|
+
}
|
|
111
|
+
literals.push(lit);
|
|
112
|
+
}
|
|
113
|
+
for (const m of span.matchAll(/'((?:\\.|[^'\\])*)'|"((?:\\.|[^"\\])*)"/g)) literals.push(m[1] ?? m[2] ?? '');
|
|
114
|
+
return { exprs, literals };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function walk(dir, out = []) {
|
|
118
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
119
|
+
const p = path.join(dir, e.name);
|
|
120
|
+
if (e.isDirectory()) { if (e.name !== 'node_modules') walk(p, out); }
|
|
121
|
+
else if (/\.(ts|js|mjs)$/.test(e.name) && !/\.d\.ts$/.test(e.name)) out.push(p);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function lintSource(src, terms) {
|
|
127
|
+
const hits = [];
|
|
128
|
+
for (const m of src.matchAll(SEND_CALL_RE)) {
|
|
129
|
+
const open = m.index + m[0].length;
|
|
130
|
+
const end = argSpanEnd(src, open);
|
|
131
|
+
const span = src.slice(open, end);
|
|
132
|
+
const line = src.slice(0, m.index).split('\n').length;
|
|
133
|
+
const lines = src.split('\n');
|
|
134
|
+
const context = `${lines[line - 2] ?? ''}\n${lines[line - 1] ?? ''}`;
|
|
135
|
+
if (context.includes(WAIVER) || span.includes(WAIVER)) continue;
|
|
136
|
+
const { exprs, literals } = scanSpan(span);
|
|
137
|
+
for (const x of exprs) {
|
|
138
|
+
if (RAW_EXPR_RE.test(x) || RAW_CALL_RE.test(x)) hits.push({ line, code: 'RAW_REASON', match: `\${${x}}` });
|
|
139
|
+
}
|
|
140
|
+
for (const lit of literals) {
|
|
141
|
+
for (const { term, re } of terms) if (re.test(lit)) hits.push({ line, code: 'INTERNAL_TERM', match: term });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return hits;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function main() {
|
|
148
|
+
let terms;
|
|
149
|
+
try { terms = readTerms(); } catch (e) { console.error(`lint-plain-user-notices: cannot read ${TERMS_PATH}: ${e.message}`); process.exit(2); }
|
|
150
|
+
const files = walk(path.join(ROOT, 'src'));
|
|
151
|
+
const counts = {};
|
|
152
|
+
const detail = {};
|
|
153
|
+
for (const f of files) {
|
|
154
|
+
const rel = path.relative(ROOT, f).split(path.sep).join('/');
|
|
155
|
+
const hits = lintSource(fs.readFileSync(f, 'utf-8'), terms);
|
|
156
|
+
if (hits.length) { counts[rel] = hits.length; detail[rel] = hits; }
|
|
157
|
+
}
|
|
158
|
+
if (UPDATE) {
|
|
159
|
+
fs.writeFileSync(BASELINE, JSON.stringify(Object.fromEntries(Object.entries(counts).sort()), null, 2) + '\n');
|
|
160
|
+
console.log(`lint-plain-user-notices: baseline written (${Object.keys(counts).length} files).`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let baseline = {};
|
|
164
|
+
try { baseline = JSON.parse(fs.readFileSync(BASELINE, 'utf-8')); } catch { /* no baseline = zero allowance */ }
|
|
165
|
+
const failures = [];
|
|
166
|
+
const lowered = [];
|
|
167
|
+
for (const [rel, n] of Object.entries(counts)) {
|
|
168
|
+
const allowed = baseline[rel] ?? 0;
|
|
169
|
+
if (n > allowed) failures.push({ rel, n, allowed });
|
|
170
|
+
else if (n < allowed) lowered.push({ rel, n, allowed });
|
|
171
|
+
}
|
|
172
|
+
for (const [rel, allowed] of Object.entries(baseline)) if (!(rel in counts) && allowed > 0) lowered.push({ rel, n: 0, allowed });
|
|
173
|
+
if (lowered.length) {
|
|
174
|
+
console.log(`lint-plain-user-notices: ${lowered.length} file(s) improved — lower the baseline with --update:`);
|
|
175
|
+
for (const l of lowered) console.log(` ${l.rel}: ${l.allowed} → ${l.n}`);
|
|
176
|
+
}
|
|
177
|
+
if (failures.length) {
|
|
178
|
+
console.error('lint-plain-user-notices: new jargon or raw internal detail in a notice sent to the user.');
|
|
179
|
+
console.error('Standard: docs/UX-AND-AGENT-AGENCY-STANDARD.md Rule 8 (Plain and Complete).');
|
|
180
|
+
for (const f of failures) {
|
|
181
|
+
console.error(`\n ${f.rel}: ${f.n} hit(s), baseline allows ${f.allowed}`);
|
|
182
|
+
for (const h of detail[f.rel]) console.error(` line ${h.line}: ${h.code} ${h.match}`);
|
|
183
|
+
}
|
|
184
|
+
console.error(`\nFix: say what happened, what it means and what to do, in plain words; keep reasons/ids in logs.`);
|
|
185
|
+
console.error(`Fixed wording belongs in src/messaging/userNotices.ts (tested). Deliberate exception: "// ${WAIVER}: <why>".`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
console.log(`lint-plain-user-notices: ok (${files.length} files scanned).`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-11-04T00:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-11-04T00:37:44.365Z",
|
|
5
|
+
"instarVersion": "1.3.1639",
|
|
6
6
|
"entryCount": 206,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -1563,7 +1563,7 @@
|
|
|
1563
1563
|
"type": "subsystem",
|
|
1564
1564
|
"domain": "server",
|
|
1565
1565
|
"sourcePath": "src/server/AgentServer.ts",
|
|
1566
|
-
"contentHash": "
|
|
1566
|
+
"contentHash": "fd8b7c323d5ab1bfcff04c0e32ae0aee9385cda315c174874d1e36d9dabbcd28",
|
|
1567
1567
|
"since": "2025-01-01"
|
|
1568
1568
|
},
|
|
1569
1569
|
"subsystem:session-manager": {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Shared by src/messaging/plainNoticeCheck.ts (test-time check of catalogued notices) and scripts/lint-plain-user-notices.mjs (build-time check of send sites). Standard: docs/UX-AND-AGENT-AGENCY-STANDARD.md Rule 8. Internal words the person cannot act on. Matched case-insensitively on word boundaries.",
|
|
3
|
+
"internalTerms": [
|
|
4
|
+
"drain bound",
|
|
5
|
+
"drained",
|
|
6
|
+
"final turn",
|
|
7
|
+
"respawn",
|
|
8
|
+
"respawned",
|
|
9
|
+
"respawning",
|
|
10
|
+
"epoch",
|
|
11
|
+
"CAS",
|
|
12
|
+
"tmux",
|
|
13
|
+
"reap-log",
|
|
14
|
+
"reaper",
|
|
15
|
+
"payload",
|
|
16
|
+
"stderr",
|
|
17
|
+
"stdout",
|
|
18
|
+
"exit code",
|
|
19
|
+
"pid",
|
|
20
|
+
"subprocess",
|
|
21
|
+
"launchd",
|
|
22
|
+
"systemd",
|
|
23
|
+
"pinned profile",
|
|
24
|
+
"boot sweep",
|
|
25
|
+
"undefined",
|
|
26
|
+
"NaN",
|
|
27
|
+
"null"
|
|
28
|
+
],
|
|
29
|
+
"codePatterns": [
|
|
30
|
+
{ "code": "DURATION_IN_MS", "re": "\\b\\d+\\s?ms\\b", "why": "a millisecond count is a setting, not news; say the time in words if it matters" },
|
|
31
|
+
{ "code": "SNAKE_CASE", "re": "\\b[a-z]+_[a-z0-9_]+\\b", "why": "a snake_case name is a code identifier" },
|
|
32
|
+
{ "code": "CAMEL_CASE", "re": "\\b[a-z]+[A-Z][A-Za-z0-9]*\\b", "why": "a camelCase name is a code identifier" },
|
|
33
|
+
{ "code": "REASON_CODE", "re": "\\(reason: [a-z0-9-]+\\)", "why": "a raw reason code; translate it into what happened" },
|
|
34
|
+
{ "code": "BRACKET_COUNTER", "re": "\\[#\\d+\\]", "why": "an audit counter means nothing to the person" },
|
|
35
|
+
{ "code": "ERRNO", "re": "\\b(ENOENT|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EPIPE|EACCES|EPERM|EADDRINUSE)\\b", "why": "an operating-system error code" }
|
|
36
|
+
],
|
|
37
|
+
"camelCaseAllow": ["iPhone", "iPad", "iMessage", "macOS", "iOS", "iCloud", "eBay", "gMail", "youTube"]
|
|
38
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1639",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "c31cb53ab5cc37a07500f2de39cc6ec13bade8afb52f76074876e04ca68a2d6b",
|
|
3
3
|
"registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1639"
|
|
5
5
|
}
|
|
@@ -363,6 +363,7 @@ I am the user's interface to the system. They talk to me in natural language; I
|
|
|
363
363
|
- **DO** suggest /commands that work within our chat (like /restart, /new)
|
|
364
364
|
- **DO** explain what happened in conversational terms, not technical ones
|
|
365
365
|
- When something goes wrong, say what went wrong and what I'm doing about it — not the stack trace
|
|
366
|
+
- **Every message is plain and complete**: what happened, what it means for them, and what they need to do (if anything) — said once, in plain words. Too vague ("The session has stopped.") is useless; too technical (reason codes, timeouts, ids, "drain bound (30000ms)") is useless too. Internal detail stays in logs.
|
|
366
367
|
|
|
367
368
|
## Agent Infrastructure
|
|
368
369
|
|
|
@@ -2706,7 +2707,7 @@ export const DASHBOARD_PREVIEW_BULLET = `- **${DASHBOARD_PREVIEW_MARKER}**: \`{t
|
|
|
2706
2707
|
export const CONSOLE_BULLET = `- **${CONSOLE_MARKER}**: \`/dashboard/console/\` (same PIN, same tunnel) is the new home screen: one sentence on whether anything needs the operator, then what needs them, held replies, open promises, live sessions on every machine, machine states and safety checks, each saying "Can't tell" with the reason when its source does not answer. It is read-only; every tab still opens in the legacy \`/dashboard\`, which stays the default until the operator calls the console MVP. Proactive: the operator asks "what needs me?" or "is everything OK?" from a phone → send them \`{tunnelUrl}/dashboard/console/\` and the PIN.`;
|
|
2707
2708
|
|
|
2708
2709
|
export const REMOTE_DESKTOP_MARKER = 'Native remote desktop is a separate operator action';
|
|
2709
|
-
export const remoteDesktopBullet = (port: number): string => `- **${REMOTE_DESKTOP_MARKER}**: the designated dashboard front's Desktop page shows every registered pool machine by name. The operator enters the existing dashboard PIN once, chooses a ready machine, and can Stop at any time; no Telegram approval is required. Each target stays OFF unless its own \`remoteDesktop.enabled: true\` and \`remoteDesktop.frontMachineId\` name the same designated front and its live host probe passes. Other machine dashboards stay dark for desktop admission. macOS uses a packaged native helper, and macOS checks its permissions against the program it holds responsible: without the helper's own grant that is whatever started this agent's process tree, which can change at any reboot. \`screenGrant\` and \`inputGrant\` in the diagnostics say which applies (\`own\`, \`inherited\` or \`none\`); only \`own\` survives a reboot and updates. When a person is at that Mac, \`curl -X POST -H "Authorization: Bearer $AUTH" -H "X-Instar-Request: 1" http://localhost:${port}/desktop/request-permission\` shows macOS's prompts there (at most once a minute); it grants nothing by itself. A Windows host such as Mama PC requires the separate interactive Windows companion paired with its WSL agent. A native Linux machine shares its own X11 desktop (a desktop login, or a virtual one such as TigerVNC or Xvfb run as the agent's user) through the standard X tools ImageMagick and xdotool; no VNC or other port is exposed, and \`remoteDesktop.linuxDisplay\` (e.g. \`\":1\"\`) picks the display when there are several. A Linux server with no
|
|
2710
|
+
export const remoteDesktopBullet = (port: number): string => `- **${REMOTE_DESKTOP_MARKER}**: the designated dashboard front's Desktop page shows every registered pool machine by name. The operator enters the existing dashboard PIN once, chooses a ready machine, and can Stop at any time; no Telegram approval is required. Each target stays OFF unless its own \`remoteDesktop.enabled: true\` and \`remoteDesktop.frontMachineId\` name the same designated front and its live host probe passes. Other machine dashboards stay dark for desktop admission. macOS uses a packaged native helper, and macOS checks its permissions against the program it holds responsible: without the helper's own grant that is whatever started this agent's process tree, which can change at any reboot. \`screenGrant\` and \`inputGrant\` in the diagnostics say which applies (\`own\`, \`inherited\` or \`none\`); only \`own\` survives a reboot and updates. When a person is at that Mac, \`curl -X POST -H "Authorization: Bearer $AUTH" -H "X-Instar-Request: 1" http://localhost:${port}/desktop/request-permission\` shows macOS's prompts there (at most once a minute); it grants nothing by itself. A Windows host such as Mama PC requires the separate interactive Windows companion paired with its WSL agent. A native Linux machine shares its own X11 desktop (a desktop login, or a virtual one such as TigerVNC or Xvfb run as the agent's user) through the standard X tools ImageMagick and xdotool; no VNC or other port is exposed, and \`remoteDesktop.linuxDisplay\` (e.g. \`\":1\"\`) picks the display when there are several. On a machine running Hyprland (Omarchy's Wayland desktop) it shares that session instead, through grim, hyprctl, wtype and a virtual pointer, and needs someone signed in at the machine or automatic sign-in; Hyprland's own Super-key shortcuts do not work from a viewer, so use the mouse for those. A Linux server with no desktop, or on another Wayland desktop, reports not ready with that reason. Registry First: \`curl -H "Authorization: Bearer $AUTH" http://localhost:${port}/desktop/diagnostics\` checks this target's own screen, input, display, and GUI-session signals without granting control. On WSL it also reports a fixed Windows companion boot failure phase and code if installation failed; the agent remains reachable while desktop control stays off. The designated front's \`GET /desktop/availability\` checks its local desktop, and its PIN-scoped \`GET /desktop/targets\` checks the pool. After patching the two config fields, the agent activates them with \`curl -X POST -H "Authorization: Bearer $AUTH" -H "X-Instar-Request: 1" http://localhost:${port}/desktop/activate-config\`, which requests a supervised restart. During a session the viewer lists a machine's screens by name, resolution and main screen from \`GET /desktop/requests/:id/displays\` and switches between them without reconnecting; a three-finger swipe sends the machine's standard shortcuts (Mission Control, App Exposé and Spaces on a Mac, Task View and Ctrl+Win+arrows on Windows). A Mac can have those shortcuts turned off, and the viewer cannot tell; \`supported: false\` means that machine needs an update. The target owns the signed ten-minute grant, controller lock, screen and input permissions, and Stop/revocation. A signed, one-use operator assertion binds the dashboard session to one target and browser key. The front only relays bounded encrypted stream bytes, admitting at most three remote grants plus its own desktop. An unexpected transport loss can resume within the original deadlines when the target authorizes it; Stop cannot resume. The PIN proves possession of the dashboard credential, not a separately verified human identity. Proactive: when the operator asks to control a machine, check live readiness and give the designated front's dashboard entry only for a verified ready target. Never describe source code or a mocked test as a usable desktop, and never claim Mama PC ready before a live Windows screen/input/Stop test. **Guided setup is the first thing to offer:** on the Desktop page, after the PIN, each machine that is not ready shows one next step from its live check (turn it on, restart, wake or unlock, allow a pop-up, open the exact settings page, or the written path), and the app shows the same steps. Send the operator there instead of patching config by hand. Ready means the live check passed, never what the System Settings list shows, which can be empty or show several entries. Registry First for an agent without a PIN session: GET /desktop/diagnostics on that machine; the audit is logs/desktop-setup.jsonl. On a Mac every agent shares one kept helper at \`~/.instar/desktop-helper/instar-desktop-host\`, replaced only when the helper's source changes, so an update with unchanged source keeps its macOS permission. When the source does change, macOS drops the permission and only a person at that Mac can allow it again: diagnostics carry \`helperChange\` and the steps, and one Attention item says so. Say that plainly; never describe it as a readiness flicker. Off switch: \`remoteDesktop.retainedHelper: false\`. Through the ordinary lock: built and off on every machine. No Mac can enable it until its macOS version has recorded passes of the real crash and Stop tests. \`GET http://localhost:${port}/desktop/locked/status\` reads it; a 409 \`unsupported-macos\` from the settings route means that version is not proven yet. The owner types the password live, normally in the Instar app, and no agent ever handles it: asked for one, refuse. The Mac relocks at the end and is watched for 2 minutes; the promise is "relocks, and tells you if it couldn't". Never tell the operator this works until that status says active.`;
|
|
2710
2711
|
|
|
2711
2712
|
export const STANDING_SESSIONS_MARKER = 'Standing sessions are never closed by cleanup';
|
|
2712
2713
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Remote desktop now works on Hyprland, the Wayland desktop that Omarchy uses. When the agent's user has a Hyprland session, the Desktop page shows and controls it with tools Omarchy already ships, with nothing to install and no root. Other Linux machines keep using X11 as before, and a configured X display always means X11. If nobody is signed in to the desktop, the page now says that instead of suggesting a virtual X server. Hyprland's own Super-key shortcuts do not work from a viewer, so those need the mouse.
|
|
9
|
+
|
|
10
|
+
## What to Tell Your User
|
|
11
|
+
|
|
12
|
+
If I run on Omarchy, you can now see and control my desktop from the Desktop page, once someone is signed in there or automatic sign-in is on.
|
|
13
|
+
|
|
14
|
+
## Summary of New Capabilities
|
|
15
|
+
|
|
16
|
+
- Remote desktop on Hyprland and Omarchy, with mouse, typing and screen choice.
|
|
17
|
+
- Clear wording when nobody is signed in to the desktop.
|
|
18
|
+
|
|
19
|
+
## Evidence
|
|
20
|
+
|
|
21
|
+
On Omarchy 4.0.6 (Hyprland 0.56.2), limited to the size of a Hetzner cx23, through the real Desktop page with the PIN: the live desktop streamed, a click opened Omarchy's menu, typing and Ctrl+L reached a terminal, the wheel scrolled, a drag selected text, Stop ended the session cleanly and reconnect worked, with 0 failed captures out of 1,397. A display turned off before or during a session came back on its own. Typing through this path unlocked the real lock screen. Multiple monitors are covered by unit tests only.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The notices Instar's server writes to you on its own now say, in plain words, what happened, what it means for you and what to do. The worst one, sent when a conversation moved to another machine before the last step finished, used to read "This conversation moved machines mid-task (drain bound (30000ms) reached — final turn may be partial)…". It now reads: "I moved this conversation to the Mac Studio before I'd finished my last step, so that step may be incomplete. The conversation carries on there. If that step matters, ask me to check it." It names the machine and never shows a machine id. The restart notices and the "pinned profile" note were rewritten the same way. The "Session respawned." line no longer follows a restart I have already told you about, because my reply confirms it.
|
|
9
|
+
|
|
10
|
+
The standard behind this is written down as Rule 8, "Plain and Complete", in the UX and Agent Agency standard. A message fails if it is too vague to act on or too technical to digest. It has to give the most distinct and critical information in plain language. Two checks hold it. The fixed server notices live in one catalog that a test reads for internal words, ids, timeouts and reason codes. A build-time lint refuses any new server notice that pastes a raw internal reason (`${detail}`, `${err.message}`) or an internal word into your message. Neither check runs while a message is being sent, so an alarm is never held up when the model is down.
|
|
11
|
+
|
|
12
|
+
## What to Tell Your User
|
|
13
|
+
|
|
14
|
+
When I tell you something on my own (a restart, or a move to another machine), I now say what happened, what it means for you and whether you need to do anything, with no internal codes. If one of my automatic messages ever reads as jargon or too vague to use, tell me. It is now a defect against a written standard.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
- Rule 8 "Plain and Complete" in docs/UX-AND-AGENT-AGENCY-STANDARD.md, with real before/after examples.
|
|
19
|
+
- `src/messaging/userNotices.ts`: the tested catalog of fixed server notices.
|
|
20
|
+
- `npm run lint` includes `lint-plain-user-notices`, a ratchet against raw internal detail in server notices (Telegram and Slack send sites).
|
|
21
|
+
|
|
22
|
+
## Evidence
|
|
23
|
+
|
|
24
|
+
Measured from the agent's sent-message log before choosing what to rewrite: in the four days to 2026-11-03, "Session respawned." and "Session restarting — message queued." went out 44 times and the pinned-profile note 29 times. The new tests fail on the old code: the drain notice received the raw reason, and the lint flags `${detail}` and "final turn" at that send site. They pass on the new code. Live proof is a real move between two updated machines (see the PR).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Remote desktop on Hyprland (Omarchy) — plain-English overview
|
|
2
|
+
|
|
3
|
+
> The one-line version: Instar's remote desktop now works on Hyprland, the Wayland desktop Omarchy uses, with nothing to install and no root access.
|
|
4
|
+
|
|
5
|
+
## The problem in one breath
|
|
6
|
+
|
|
7
|
+
Instar's Linux remote desktop reads and drives the screen with X11 tools. Omarchy runs Hyprland, which uses Wayland, where those tools see a black or partial screen. So on Omarchy the Desktop page told the person to choose an X11 session at sign-in, which Omarchy does not offer. People had assumed Wayland simply forbids this. On Hyprland it does not: the desktop offers these abilities to its own user's programs.
|
|
8
|
+
|
|
9
|
+
## What already exists
|
|
10
|
+
|
|
11
|
+
- The remote desktop itself: the dashboard's Desktop page, the PIN step, the encrypted stream, and the Stop button. None of that changes.
|
|
12
|
+
- The X11 host, which keeps working exactly as before.
|
|
13
|
+
|
|
14
|
+
## What this adds
|
|
15
|
+
|
|
16
|
+
A second Linux host for Hyprland, chosen automatically when the agent's user has a Hyprland session running:
|
|
17
|
+
- The picture comes from `grim`, about 60 to 160 milliseconds per frame on a small cloud server.
|
|
18
|
+
- Screens and the cursor position come from `hyprctl`.
|
|
19
|
+
- Typing goes through `wtype`, fed on standard input so a typed password never appears in the list of running programs.
|
|
20
|
+
- The mouse is a small client that speaks Hyprland's virtual-pointer protocol directly over its socket. That avoids ydotool, which would need root setup.
|
|
21
|
+
|
|
22
|
+
Omarchy already ships all of these tools. A plain Hyprland machine without `grim` or `wtype` is told exactly what to install.
|
|
23
|
+
|
|
24
|
+
## What I learned from the real machine
|
|
25
|
+
|
|
26
|
+
- When Omarchy's idle timer turns the display off, `grim` hangs. The host now wakes the screen by nudging the pointer when someone connects and whenever the screen goes off during a session.
|
|
27
|
+
- Omarchy's lock screen does not tell the system it is locked, so Instar cannot detect it. The viewer simply sees the password box and can type into it; I unlocked a real session that way.
|
|
28
|
+
- Hyprland's own Super-key shortcuts (like Super+Return for a terminal) do not fire from `wtype`. Shortcuts inside applications do work. The Omarchy menu icon works by mouse, and it reaches most of the same things. Holding a key while clicking (Ctrl-click) does not carry over either.
|
|
29
|
+
|
|
30
|
+
## The safeguards
|
|
31
|
+
|
|
32
|
+
- Nothing changes unless remote desktop is already turned on for this machine, with the same PIN and stream.
|
|
33
|
+
- A configured X display (`remoteDesktop.linuxDisplay`) always means X11.
|
|
34
|
+
- The host opens no network port and needs no root.
|
|
35
|
+
- It does not switch hosts while a viewer is connected, and if Hyprland goes away the viewer's session ends and says so.
|
|
36
|
+
|
|
37
|
+
## What the reader needs to decide
|
|
38
|
+
|
|
39
|
+
Nothing. It is off unless remote desktop is enabled, and a revert removes it.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Side-Effects Review — remote desktop on Hyprland (Omarchy)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `hyprland-remote-desktop`
|
|
4
|
+
**Date:** `2026-11-03`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Tier:** `1 declared; the gate's risk floor is 2 for a new capability. Reasoning: off unless remote desktop is enabled, no state, a revert restores everything. Two independent review rounds and live proof took the place of a converged spec. Recorded as belowFloor.`
|
|
7
|
+
**Second-pass reviewer:** `independent subagent, two rounds. First: concern raised (two high, two medium, five low; Stop safety and races), all fixed. Second: the fixes held; concern raised (capitals lost from a physical keyboard, confirmed live; two low restart races; weak tests), all fixed`
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Topic 136848 asked what Wayland remote desktop would take on Omarchy (Hyprland). This builds it for Hyprland, and proves it on Omarchy 4.0.6 on a Hetzner server.
|
|
12
|
+
|
|
13
|
+
- `src/core/HyprlandDesktopHost.ts` is a new `DesktopHost`: `grim` for frames, `hyprctl -j` for screens and cursor, `wtype` for keys (text on stdin), and a virtual pointer for the mouse.
|
|
14
|
+
- `src/core/WaylandVirtualPointer.ts` speaks `zwlr_virtual_pointer_v1` directly over the Wayland socket. No package and no root.
|
|
15
|
+
- `LinuxAutoDesktopHost`, in the same file as the Hyprland host, picks Hyprland or the existing X11 host at each probe, stays put while a viewer is connected, and always uses X11 when `remoteDesktop.linuxDisplay` is set.
|
|
16
|
+
- `DesktopProbeReadiness` gains wording for Hyprland with nobody signed in, with tools missing, and not answering. The generic Wayland wording no longer claims Wayland cannot be supported.
|
|
17
|
+
- `LinuxRunner` gains an optional `input` written to the child's stdin.
|
|
18
|
+
- The remote desktop awareness bullet mentions Hyprland. `migrateClaudeMd` already replaces that line in place for existing agents.
|
|
19
|
+
|
|
20
|
+
## Class-Closure Declaration
|
|
21
|
+
|
|
22
|
+
- `unbounded-self-action`: closure `n/a`. The only timer is the 2 s heartbeat of an operator-started remote desktop session, the same as the X11 host. It stops on Stop, on a lost desktop, and at the session's ten-minute limit, and it nudges a display only when it has gone off.
|
|
23
|
+
|
|
24
|
+
## Decision-point inventory
|
|
25
|
+
|
|
26
|
+
- Which host serves a Linux machine: add; Hyprland when this user has a live session and no X display is configured, otherwise X11, re-decided at each probe unless a viewer is connected.
|
|
27
|
+
- Whether a Hyprland desktop counts as ready: add; tools present, session live, at least one monitor.
|
|
28
|
+
|
|
29
|
+
## 1. Over-block
|
|
30
|
+
|
|
31
|
+
A Wayland session on Hyprland used to refuse and now works, unless `grim` or `wtype` is missing, which reports what to install. Other Wayland desktops refuse as before.
|
|
32
|
+
|
|
33
|
+
## 2. Under-block
|
|
34
|
+
|
|
35
|
+
Remote desktop reports `screenLocked: false` on Hyprland because Omarchy's lock screen leaves logind's `LockedHint` at no, so the X11 host's locked refusal has no equivalent. A viewer who has passed the dashboard PIN sees the lock screen and can type at it, but still needs the machine's password; the frames show only the lock screen. Measured: typing the password through this path unlocked a real session.
|
|
36
|
+
|
|
37
|
+
The virtual pointer and `wtype` run as the agent's user, the same trust as `xdotool` on X11.
|
|
38
|
+
|
|
39
|
+
## 3. Level-of-abstraction fit
|
|
40
|
+
|
|
41
|
+
The same `DesktopHost` interface as the other hosts, so admission, the PIN, the audit log and the stream are unchanged. Selection sits in a small wrapper instead of inside the X11 host, so the X11 host's code path is untouched apart from the optional stdin field.
|
|
42
|
+
|
|
43
|
+
## 4. Signal vs authority compliance
|
|
44
|
+
|
|
45
|
+
No message or action is gated by this. `probe()` already feeds the existing admission decision and this reports the same fields honestly.
|
|
46
|
+
|
|
47
|
+
## 4b. Judgment-point check
|
|
48
|
+
|
|
49
|
+
None.
|
|
50
|
+
|
|
51
|
+
## 5. Interactions
|
|
52
|
+
|
|
53
|
+
- **X11 host:** still used on every machine without a Hyprland session. A Hyprland login also owns an Xwayland socket, which the X11 host deliberately refuses; the wrapper now selects Hyprland before that arises.
|
|
54
|
+
- **`linuxDisplay` set to `:0` on a Hyprland machine:** still X11, as asked, even though it shows a partial screen.
|
|
55
|
+
- **Display idle:** the heartbeat nudges the pointer when a screen goes off during a session, so the display stays on while something off-screen turned it off. It does not reset idle continuously, so Omarchy can still lock during a long quiet session, and the viewer sees the lock.
|
|
56
|
+
- **Machine with a stale `hypr` folder:** ignored unless its recorded pid is running and its socket exists.
|
|
57
|
+
- **Hyprland restart while connected:** the heartbeat sees the signature change and ends the session through the existing lost-indicator path; the viewer can reconnect.
|
|
58
|
+
- **`hyprctl dispatch`:** deliberately unused; it became Lua in Hyprland 0.56.
|
|
59
|
+
- **Older grim or wtype:** the flags used are long-standing, and `wtype -` for stdin was checked on this machine only.
|
|
60
|
+
- **Page errors on Linux:** the Desktop page's calls to `/desktop/locked/status` and `/desktop/setup` answer 503 on this host, as those belong to the macOS locked-screen and setup features. The page carries on, and they appear in the browser console only. One `cursor` call answered 403 in the moment before the session was admitted.
|
|
61
|
+
|
|
62
|
+
## 6. External surfaces
|
|
63
|
+
|
|
64
|
+
A notification on the machine's own screen when a viewer connects, the same as X11. Nothing new leaves the machine and no port opens.
|
|
65
|
+
|
|
66
|
+
## 7. Multi-machine posture
|
|
67
|
+
|
|
68
|
+
Machine-local by design: each host describes its own screen, and the existing pool Desktop page lists machines as before.
|
|
69
|
+
|
|
70
|
+
## 8. Rollback cost
|
|
71
|
+
|
|
72
|
+
A revert restores the X11-only behaviour. Nothing is stored.
|
|
73
|
+
|
|
74
|
+
## Known limits
|
|
75
|
+
|
|
76
|
+
- Hyprland's own Super-key shortcuts do not fire from `wtype`, tried four ways; shortcuts inside applications reach them (Ctrl+L, measured). Queued as an improvement.
|
|
77
|
+
- Modifiers do not reach the mouse: Ctrl-click, Shift-click and Super-drag lose the modifier, because the virtual pointer and `wtype` are separate clients. X11 supports these.
|
|
78
|
+
- A press-and-hold of a non-modifier key sends one key, because `wtype` presses and releases within one run. A modifier tapped alone (Super by itself) sends nothing.
|
|
79
|
+
- Shift plus a key is sent as the character it makes on a US keyboard, because `wtype` chords lose capitals and shifted symbols (measured). Another layout's shifted digits will differ; letters are fine. The viewer's keyboard field, which sends text, is not affected.
|
|
80
|
+
- Stop blocks new input at once but does not kill a `wtype` already running, the same as `xdotool` on X11.
|
|
81
|
+
- A Hyprland restart during a session is covered by unit tests, not live.
|
|
82
|
+
- Single keys sent as key presses appear in the process list briefly (`wtype -k a`), as they do with `xdotool` on X11. Text sent through the viewer's keyboard field and pasted text go over stdin.
|
|
83
|
+
- A crash of the agent while a button is held relies on the compositor to release it, which was not tested. Normal Stop sends the release first and closes gently so it arrives.
|
|
84
|
+
- More than one monitor is covered by unit tests only; the test server has one screen.
|
|
85
|
+
- This has unit tests and live proof, not integration or e2e tier tests. Those tiers need a running Hyprland, and CI has none; the wiring is pinned by a source test, and the existing desktop route tests use the same host interface with a stub.
|
|
86
|
+
|
|
87
|
+
## Tests
|
|
88
|
+
|
|
89
|
+
- `tests/unit/hyprland-desktop-host.test.ts` (37 tests): session discovery (live, dead, junk); monitor parsing (scale, sideways, disabled, idled off); probe readiness and its three wordings; capture and screen choice; pointer mapping; clicks, drags, Stop releasing, including in a click's gap; scroll remainders; typing on stdin; modifier chords; waking a display; losing the desktop; Stop during connecting; a late connect; a failed start; nothing reopening the pointer after Stop; shared connection; a stale heartbeat not ending the next session; host selection and its X11 override; and the wire bytes against a fake compositor on a real Unix socket.
|
|
90
|
+
- `tests/unit/linux-desktop-host.test.ts` and `tests/unit/remote-desktop-delivery-parity.test.ts` unchanged and passing.
|
|
91
|
+
- Live, Omarchy 4.0.6, Hyprland 0.56.2, agent limited to 2 vCPUs and 3.9 GB (the size of a cx23), through the real dashboard Desktop page over Tailscale with the PIN: diagnostics `status: ready`; the live desktop streamed; a click opened Omarchy's menu; typing through the viewer's keyboard field and as direct keys both arrived in order, including spaces (Shift keys through the host class on the final build typed `xA!By` as sent); Ctrl+L cleared a terminal; wheel scrolled its history; a held drag selected text; Stop reported stopped and left no pointer socket; reconnect worked. Over 1,397 captures, 0 failed. A display turned off before connecting came back on connect, and one turned off mid-session came back within 4 s. `wtype` on stdin unlocked the real lock screen.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Plain and Complete notices — the short version
|
|
2
|
+
|
|
3
|
+
**What this is.** When Instar's server sends you a message on its own, for example after restarting a session or moving a conversation to another machine, that message is written in code by a developer. It doesn't go through the AI check that reviews messages a session writes. That is deliberate: if the AI is down, an alarm still has to reach you. The side effect is that nothing checked whether those messages made sense to a person. On 2026-11-03 Justin got "This conversation moved machines mid-task (drain bound (30000ms) reached — final turn may be partial). Work resumes on the new machine — the final turn before the move may be partial." It pasted an internal reason straight in, said the same thing twice and didn't name the machine. He asked for a standard: messages must be easy to digest and carry just enough information to be useful.
|
|
4
|
+
|
|
5
|
+
**What already existed.** The UX standard already said names must never be replaced by ids (Rule 7). The constitution already said routine self-narration ("I restarted") should not be sent at all (Near-Silent Notifications). Session-written messages already pass a tone gate, and scheduled jobs get a jargon and file-path advisory. None of that covered how the server's own fixed notices are worded.
|
|
6
|
+
|
|
7
|
+
**What is new.**
|
|
8
|
+
1. A written rule, Rule 8 "Plain and Complete", in Justin's words. Every message says what happened, what it means for the person and what they need to do, once and in plain words. It can fail by being too vague or too technical. It comes with real before/after examples.
|
|
9
|
+
2. One catalog file holds the server's fixed notices. A test reads every one for internal words, ids, timeouts, reason codes and file paths, and for messages too short to say anything.
|
|
10
|
+
3. A build check looks at every place the server sends a message (Telegram and Slack) and refuses new code that pastes a raw internal reason, such as an error message or a reason code, into your text. Today's remaining cases are frozen in a list that may only shrink. They are tracked for rewriting as feedback item fb-d1c2f259-243.
|
|
11
|
+
4. Four real notices rewritten: the moved-machines one (now names the machine, says the step may be incomplete, and tells you to ask for a check, since nothing re-checks it automatically), both restart notices, and the pinned-settings note.
|
|
12
|
+
|
|
13
|
+
**Safeguards.** Nothing here runs when a message is sent, so no message can be delayed or blocked by it. Code that recognises the server's notices by their wording (so they are never mistaken for a real reply) now reads the same catalog, so a reworded notice is still recognised. Old wordings stay recognised because they are still in message history.
|
|
14
|
+
|
|
15
|
+
**What you need to decide.** Nothing. The standard could not go into the constitution registry itself, because registry changes need a signed approval and no approval key is installed yet. It lives in the UX standard, which the registry already points to. Installing the key is Echo's job.
|