@rubytech/create-maxy-code 0.1.492 → 0.1.494
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/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-23-task-1930-adherence-enforcement.md +422 -0
- package/payload/platform/docs/superpowers/specs/2026-07-23-task-1930-adherence-enforcement-design.md +81 -0
- package/payload/platform/plugins/admin/PLUGIN.md +3 -0
- package/payload/platform/plugins/admin/hooks/__tests__/preference-consult-directive.test.sh +15 -0
- package/payload/platform/plugins/admin/hooks/__tests__/preference-consult-gate.test.sh +59 -0
- package/payload/platform/plugins/admin/hooks/__tests__/quote-render-pdf-conformance.test.sh +99 -0
- package/payload/platform/plugins/admin/hooks/preference-consult-directive.sh +25 -0
- package/payload/platform/plugins/admin/hooks/preference-consult-gate.sh +120 -0
- package/payload/platform/plugins/admin/hooks/quote-render-pdf-conformance.sh +108 -0
- package/payload/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md +18 -6
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -1
- package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +11 -1
- package/payload/platform/plugins/cloudflare/bin/__tests__/portal-brand-css.test.sh +1 -1
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +22 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +111 -3
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +13 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +12 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +8 -0
- package/payload/platform/plugins/docs/references/internals.md +2 -0
- package/payload/platform/scripts/__tests__/preference-hooks-registered.test.sh +13 -0
- package/payload/platform/scripts/lib/__tests__/account-settings-pdf-conformance.test.sh +151 -0
- package/payload/platform/scripts/lib/account-settings-pdf-conformance.sh +98 -0
- package/payload/platform/scripts/lib/provision-account-dir.sh +20 -1
- package/payload/platform/scripts/setup-account.sh +13 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +35 -6
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +62 -1
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +7 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +9 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.d.ts +80 -0
- package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.js +216 -0
- package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.js.map +1 -0
- package/payload/platform/templates/account-schema/SCHEMA.md +5 -0
- package/payload/platform/templates/agents/admin/IDENTITY.md +5 -1
- package/payload/platform/templates/agents/passive/IDENTITY.md +2 -0
- package/payload/platform/templates/agents/public/IDENTITY.md +2 -0
- package/payload/platform/templates/specialists/agents/citation-auditor.md +1 -0
- package/payload/platform/templates/specialists/agents/coding-assistant.md +1 -0
- package/payload/platform/templates/specialists/agents/compiled-truth-rewriter.md +3 -0
- package/payload/platform/templates/specialists/agents/content-producer.md +1 -0
- package/payload/platform/templates/specialists/agents/data-manager.md +3 -2
- package/payload/platform/templates/specialists/agents/database-operator.md +1 -0
- package/payload/platform/templates/specialists/agents/librarian.md +1 -0
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -0
- package/payload/platform/templates/specialists/agents/project-manager.md +1 -0
- package/payload/platform/templates/specialists/agents/public-session-reviewer.md +1 -0
- package/payload/platform/templates/specialists/agents/research-assistant.md +3 -0
- package/payload/platform/templates/specialists/agents/typed-edge-classifier.md +1 -0
- package/payload/premium-plugins/writer-craft/agents/writer-craft--manuscript-reviewer.md +3 -0
- package/payload/server/public/assets/{chat-DWvYv9jq.js → chat-1SSmFsjd.js} +1 -1
- package/payload/server/public/assets/{operator-CldXXilj.js → operator-Cxq9TxaH.js} +1 -1
- package/payload/server/public/assets/{page-BIwdWXnv.js → page-DBHzC59_.js} +1 -1
- package/payload/server/public/assets/{public-BKQEQGPE.js → public-jIzvdjt_.js} +1 -1
- package/payload/server/public/chat.html +2 -2
- package/payload/server/public/operator.html +2 -2
- package/payload/server/public/public.html +2 -2
- package/payload/server/server.js +35 -26
|
@@ -309,13 +309,85 @@ function generationClient() {
|
|
|
309
309
|
return []
|
|
310
310
|
}
|
|
311
311
|
if (/DELETE FROM directory\b/i.test(sql)) {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
312
|
+
const g = Number(params[1])
|
|
313
|
+
// Two DELETE shapes reach this client: the post-flip sweep removes
|
|
314
|
+
// every OTHER generation (`generation != ?`); the pre-stage clear
|
|
315
|
+
// (Task 1923) removes exactly THIS generation (`generation = ?`).
|
|
316
|
+
if (/generation\s*!=\s*\?/i.test(sql)) {
|
|
317
|
+
for (let i = rows.length - 1; i >= 0; i--) if (rows[i].generation !== g) rows.splice(i, 1)
|
|
318
|
+
} else {
|
|
319
|
+
for (let i = rows.length - 1; i >= 0; i--) if (rows[i].generation === g) rows.splice(i, 1)
|
|
315
320
|
}
|
|
316
321
|
return []
|
|
317
322
|
}
|
|
318
323
|
if (/SELECT COUNT/i.test(sql)) {
|
|
324
|
+
// The pre-stage orphan count (Task 1923) filters `generation = ?`; the
|
|
325
|
+
// verify count joins the pointer. Only the former binds a generation.
|
|
326
|
+
if (/generation\s*=\s*\?/i.test(sql)) {
|
|
327
|
+
const g = Number(params[1])
|
|
328
|
+
return [{ n: rows.filter((r) => r.generation === g).length }]
|
|
329
|
+
}
|
|
330
|
+
return [{ n: rows.filter((r) => r.generation === pointer).length }]
|
|
331
|
+
}
|
|
332
|
+
return []
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// The wedge Task 1923 fixes: a stage that half-completed on a D1 500 leaves
|
|
338
|
+
// orphan rows at the NEXT generation while the pointer stays at current. This
|
|
339
|
+
// client models the one property that turns that into a permanent freeze — the
|
|
340
|
+
// D1 UNIQUE(accountId, relPath, generation) constraint — by throwing on a
|
|
341
|
+
// colliding INSERT, exactly as every later cycle's first re-stage did.
|
|
342
|
+
function wedgeableClient(seed: { pointer: number; current?: string[]; orphans: string[] }) {
|
|
343
|
+
const calls: { sql: string; params: unknown[] }[] = []
|
|
344
|
+
const rows: { accountId: string; relPath: string; generation: number }[] = []
|
|
345
|
+
let pointer: number | null = seed.pointer
|
|
346
|
+
const orphanGen = seed.pointer + 1
|
|
347
|
+
// The last good tree the pointer names, present in a real wedge alongside the
|
|
348
|
+
// orphans. Seeding it proves the pre-stage clear scopes to `next` and leaves
|
|
349
|
+
// the reader-visible current generation untouched.
|
|
350
|
+
for (const relPath of seed.current ?? []) rows.push({ accountId: 'acc-1', relPath, generation: seed.pointer })
|
|
351
|
+
for (const relPath of seed.orphans) rows.push({ accountId: 'acc-1', relPath, generation: orphanGen })
|
|
352
|
+
return {
|
|
353
|
+
calls,
|
|
354
|
+
rows,
|
|
355
|
+
pointerValue: () => pointer,
|
|
356
|
+
async query(sql: string, params: unknown[] = []) {
|
|
357
|
+
calls.push({ sql, params })
|
|
358
|
+
if (/SELECT\s+currentGeneration/i.test(sql)) {
|
|
359
|
+
return pointer === null ? [] : [{ currentGeneration: pointer }]
|
|
360
|
+
}
|
|
361
|
+
if (/INSERT INTO directory_state|UPDATE directory_state/i.test(sql)) {
|
|
362
|
+
pointer = Number(params[1] ?? params[0])
|
|
363
|
+
return []
|
|
364
|
+
}
|
|
365
|
+
if (/INSERT INTO directory \(/i.test(sql)) {
|
|
366
|
+
const accountId = String(params[0])
|
|
367
|
+
const relPath = String(params[1])
|
|
368
|
+
const generation = Number(params[6])
|
|
369
|
+
if (rows.some((r) => r.accountId === accountId && r.relPath === relPath && r.generation === generation)) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
'd1 query failed: HTTP 500 — UNIQUE constraint failed: directory.accountId, directory.relPath, directory.generation',
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
rows.push({ accountId, relPath, generation })
|
|
375
|
+
return []
|
|
376
|
+
}
|
|
377
|
+
if (/DELETE FROM directory\b/i.test(sql)) {
|
|
378
|
+
const g = Number(params[1])
|
|
379
|
+
if (/generation\s*!=\s*\?/i.test(sql)) {
|
|
380
|
+
for (let i = rows.length - 1; i >= 0; i--) if (rows[i].generation !== g) rows.splice(i, 1)
|
|
381
|
+
} else {
|
|
382
|
+
for (let i = rows.length - 1; i >= 0; i--) if (rows[i].generation === g) rows.splice(i, 1)
|
|
383
|
+
}
|
|
384
|
+
return []
|
|
385
|
+
}
|
|
386
|
+
if (/SELECT COUNT/i.test(sql)) {
|
|
387
|
+
if (/generation\s*=\s*\?/i.test(sql)) {
|
|
388
|
+
const g = Number(params[1])
|
|
389
|
+
return [{ n: rows.filter((r) => r.generation === g).length }]
|
|
390
|
+
}
|
|
319
391
|
return [{ n: rows.filter((r) => r.generation === pointer).length }]
|
|
320
392
|
}
|
|
321
393
|
return []
|
|
@@ -323,6 +395,42 @@ function generationClient() {
|
|
|
323
395
|
}
|
|
324
396
|
}
|
|
325
397
|
|
|
398
|
+
describe('a partial stage self-heals (Task 1923)', () => {
|
|
399
|
+
it('clears orphans at the next generation, flips onto it, and logs stage-clear', async () => {
|
|
400
|
+
// Pointer at 5 naming the last good tree; three orphan rows already at
|
|
401
|
+
// generation 6 for relPaths the current walk re-stages ('output' is the
|
|
402
|
+
// walk's first row, so the current code's first INSERT is the one that
|
|
403
|
+
// collides). Without the pre-stage clear that INSERT throws on the UNIQUE
|
|
404
|
+
// constraint and the push fails, which is what froze gls-data for ~6 h on
|
|
405
|
+
// 2026-07-22.
|
|
406
|
+
const c = wedgeableClient({
|
|
407
|
+
pointer: 5,
|
|
408
|
+
current: ['output', 'output/report.pdf', 'quotes', 'quotes/CR2969'],
|
|
409
|
+
orphans: ['output', 'output/report.pdf', 'quotes'],
|
|
410
|
+
})
|
|
411
|
+
const lines: string[] = []
|
|
412
|
+
const r = await push(c, { log: (l: string) => lines.push(l) })
|
|
413
|
+
|
|
414
|
+
expect(r.rows).toBe(7)
|
|
415
|
+
expect(c.pointerValue()).toBe(6)
|
|
416
|
+
// Nothing survives at a generation the pointer does not name.
|
|
417
|
+
expect(c.rows.every((row) => row.generation === 6)).toBe(true)
|
|
418
|
+
const clear = lines.find((l) => l.includes('op=stage-clear'))!
|
|
419
|
+
expect(clear).toContain('account=acc-1')
|
|
420
|
+
expect(clear).toContain('generation=6')
|
|
421
|
+
// Exactly the 3 orphans at generation 6, not the 4 current-tree rows the
|
|
422
|
+
// pointer names: the clear scopes to `next`, never to a reader-visible row.
|
|
423
|
+
expect(clear).toContain('cleared=3')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('emits no stage-clear line on a clean cycle with no orphans', async () => {
|
|
427
|
+
const c = wedgeableClient({ pointer: 5, orphans: [] })
|
|
428
|
+
const lines: string[] = []
|
|
429
|
+
await push(c, { log: (l: string) => lines.push(l) })
|
|
430
|
+
expect(lines.some((l) => l.includes('op=stage-clear'))).toBe(false)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
326
434
|
describe('the index replace is atomic', () => {
|
|
327
435
|
it('never issues a delete before the pointer flip', async () => {
|
|
328
436
|
// The delete-then-insert window this replaces was Task 1842: a client
|
|
@@ -60,6 +60,7 @@ const deadHead = async () => {
|
|
|
60
60
|
throw new Error('ECONNREFUSED')
|
|
61
61
|
}
|
|
62
62
|
const errHead = async () => ({ ok: false, status: 502 })
|
|
63
|
+
const missingHead = async () => ({ ok: false, status: 404 })
|
|
63
64
|
|
|
64
65
|
describe('processIndexedDownload', () => {
|
|
65
66
|
it('returns a signed url for an indexed file when the device answers', async () => {
|
|
@@ -87,6 +88,18 @@ describe('processIndexedDownload', () => {
|
|
|
87
88
|
expect(r.payload.error).toBe('device-offline')
|
|
88
89
|
})
|
|
89
90
|
|
|
91
|
+
// A live device that answers 404 means the indexed row is stale: the file was
|
|
92
|
+
// renamed or removed while the index was frozen. That is "refresh the list",
|
|
93
|
+
// not "the device is down", so it must not collapse into device-offline.
|
|
94
|
+
it('reports file-missing when a reachable device 404s the path', async () => {
|
|
95
|
+
const lines: string[] = []
|
|
96
|
+
const r = await processIndexedDownload('sess-a', PATH, makeEnv(), (l) => lines.push(l), 1000, missingHead as never)
|
|
97
|
+
expect(r.status).toBe(404)
|
|
98
|
+
expect(r.payload.error).toBe('file-missing')
|
|
99
|
+
expect(r.url).toBeUndefined()
|
|
100
|
+
expect(lines.some((l) => l.includes('result=file-missing'))).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
90
103
|
it('404s a path that is not in the index, without contacting the device', async () => {
|
|
91
104
|
let touched = false
|
|
92
105
|
const spy = async () => {
|
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts
CHANGED
|
@@ -171,6 +171,18 @@ export async function processIndexedDownload(
|
|
|
171
171
|
try {
|
|
172
172
|
const head = await fetchFn(url, { method: 'HEAD' })
|
|
173
173
|
if (!head.ok) {
|
|
174
|
+
if (head.status === 404) {
|
|
175
|
+
// The device answered, so it is not down: this row is stale. The file
|
|
176
|
+
// was renamed or removed while the index was frozen, and the device
|
|
177
|
+
// returns the same 404 for a path that is missing and one that is not
|
|
178
|
+
// exposed — so this cannot and must not try to tell those apart. The
|
|
179
|
+
// client's remedy is "refresh the list", not "wait for the device".
|
|
180
|
+
log(
|
|
181
|
+
`[data-portal] op=fetch-through owner=${q(session.ownerId)} path=${q(relPath)} ` +
|
|
182
|
+
`status=404 ms=${Date.now() - started} result=file-missing`,
|
|
183
|
+
)
|
|
184
|
+
return { status: 404, payload: { ok: false, error: 'file-missing' } }
|
|
185
|
+
}
|
|
174
186
|
log(
|
|
175
187
|
`[data-portal] op=fetch-through owner=${q(session.ownerId)} path=${q(relPath)} ` +
|
|
176
188
|
`status=${head.status} ms=${Date.now() - started} result=device-error`,
|
|
@@ -292,6 +292,14 @@
|
|
|
292
292
|
say('That file is on the device, which is not reachable right now. Try again shortly.');
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
|
+
/* A reachable device that 404s a stale row sends error:'file-missing'.
|
|
296
|
+
The message says refresh, not wait. Keyed on the string, so the
|
|
297
|
+
edge's own not-found 404s (not-granted, absent, is-directory) carry
|
|
298
|
+
error:'not found' and keep the generic message below. */
|
|
299
|
+
if (res.d && res.d.error === 'file-missing') {
|
|
300
|
+
say('That file is no longer available under this name. Refresh the list and try again.');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
295
303
|
if (!res.d.ok || !res.d.url) {
|
|
296
304
|
say('That file could not be opened.');
|
|
297
305
|
return;
|
|
@@ -408,6 +408,8 @@ Standing rules the operator gives over a channel are `Preference` nodes. Two mec
|
|
|
408
408
|
|
|
409
409
|
**Observability.** `[preference-write] op=reconcile … mode=<reinforce|update|contradict|new> similarity=…` — a stream of `mode=new` for subjects that already exist is the duplicate-minting signature. `[preference-inject] op=inject channel=<wa|tg|web> injected=<N> owner=<8> source=<owner|no-owner|owner-error|fetch-error>` — injection selects only the account owner's admin preferences, so `source=owner injected=0` is a genuinely empty owner, `source=no-owner` is an account with no seeded owner (injects nothing, a missed owner seeding), and `injected=0 source=owner` on an account whose owner holds rules is the write-only regression re-appearing. `[preference-audit] op=drift accountId=… storedActive=… injectedActive=… duplicateClusters=…` — the standing reconciliation audit (claude-session-manager, 5-minute tick) flags any account whose store holds more active rows than injection would surface, the no-event detector for drift.
|
|
410
410
|
|
|
411
|
+
**Consult-before-send gate.** Injection (above) puts the owner's active rules in front of every channel turn, but the growing layer-2 `Preference` set is read on demand with `profile-read`, a per-turn judgement the agent can skip, the miss that dropped a customer signature on a delivered document. `hooks/preference-consult-gate.sh` (admin plugin) is the deliverable-boundary enforcement: a PreToolUse gate on customer-facing document deliverables (`browser-pdf-save`/`SendUserFile` to `memory/users/<phone>/documents/`, and email/Outlook sends carrying attachments) that blocks (exit 2) when no `profile-read` ran after the last user message in the transcript. It fires on documents only, never on casual replies. **Observability:** `[preference-gate] op=allow tool=<name> consulted=true` on an allowed document deliverable, `op=bypass tool=<name> detail=no profile-read this turn` on a block; a stream of `op=bypass` is the un-consulted-send signature. The advisory companion `hooks/preference-consult-directive.sh` injects the two-layer architecture reminder every turn (`[pref-wrapper] op=inject`). The placement half, promoting finished deliverables out of `output/` scratch, is counted by the `data-manager` reconcile audit (`scratch-refs`, `stranded`); making that audit run on a standing periodic cadence is a filed follow-up.
|
|
412
|
+
|
|
411
413
|
---
|
|
412
414
|
|
|
413
415
|
## Output Formatting and Budget
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
+
LIB="$DIR/../lib/provision-account-dir.sh"
|
|
5
|
+
PASS=0; FAIL=0; FAILED=()
|
|
6
|
+
c(){ if grep -qF "$1" "$LIB"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$2"); fi; }
|
|
7
|
+
c 'preference-consult-gate.sh' "gate registered"
|
|
8
|
+
c 'preference-consult-directive.sh' "directive registered"
|
|
9
|
+
c 'mcp__plugin_email_email__email-send' "email matcher present"
|
|
10
|
+
c 'mcp__plugin_outlook_outlook__outlook-mail-send' "outlook matcher present"
|
|
11
|
+
echo "----- $PASS passed, $FAIL failed -----"
|
|
12
|
+
for f in "${FAILED[@]:-}"; do [ -n "$f" ] && echo " $f"; done
|
|
13
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Regression test for account-settings-pdf-conformance.sh (Task 1929).
|
|
3
|
+
#
|
|
4
|
+
# The lib retrofits the quote-render PDF-conformance PostToolUse matcher onto
|
|
5
|
+
# existing accounts' .claude/settings.json on every install/upgrade. Task 1922
|
|
6
|
+
# added the matcher to provision-account-dir.sh, but that writer only runs at
|
|
7
|
+
# account-provision time and the upgrade path re-provisions only the house
|
|
8
|
+
# account -- so an account provisioned before 1922 keeps a settings.json without
|
|
9
|
+
# the browser-pdf-save PostToolUse matcher and never runs the PDF gate (live
|
|
10
|
+
# glsmith is such an account).
|
|
11
|
+
#
|
|
12
|
+
# Covers:
|
|
13
|
+
# 1. Stale matcher (browser-pdf-save present, conformance cmd absent) -> updated, appended, idempotent
|
|
14
|
+
# 2. Correct matcher (conformance cmd present) -> already-set, byte-identical
|
|
15
|
+
# 3. browser-pdf-save matcher absent (broad mcp__.* present) -> updated, matcher added, wildcard untouched
|
|
16
|
+
# 4. No .hooks.PostToolUse -> no-hooks, untouched
|
|
17
|
+
# 5. File absent -> absent
|
|
18
|
+
# 6. Malformed JSON -> rewrite-failed, untouched, no temp
|
|
19
|
+
# 7. reconcile_all_accounts_pdf_conformance -> patches every account.json dir, skips others
|
|
20
|
+
#
|
|
21
|
+
# All cases assert the [backfill-1929] status token and the post-state of the file.
|
|
22
|
+
|
|
23
|
+
set -u
|
|
24
|
+
|
|
25
|
+
LIB="$(cd "$(dirname "$0")/.." && pwd)/account-settings-pdf-conformance.sh"
|
|
26
|
+
if [ ! -f "$LIB" ]; then
|
|
27
|
+
echo "FAIL: $LIB not found" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
# shellcheck source=/dev/null
|
|
31
|
+
. "$LIB"
|
|
32
|
+
|
|
33
|
+
MATCHER='mcp__plugin_browser_browser__browser-pdf-save'
|
|
34
|
+
CONFORMANCE='bash $PLATFORM_ROOT/plugins/admin/hooks/quote-render-pdf-conformance.sh'
|
|
35
|
+
|
|
36
|
+
PASS=0
|
|
37
|
+
FAIL=0
|
|
38
|
+
TMP="$(mktemp -d)"
|
|
39
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
40
|
+
|
|
41
|
+
ok() { PASS=$((PASS+1)); }
|
|
42
|
+
bad() { FAIL=$((FAIL+1)); echo "FAIL: $1" >&2; }
|
|
43
|
+
|
|
44
|
+
# settings.json with a PostToolUse browser-pdf-save matcher carrying the given
|
|
45
|
+
# command list. Args after $1 are command strings for that matcher.
|
|
46
|
+
write_settings() {
|
|
47
|
+
local file="$1"; shift
|
|
48
|
+
local cmds="" c
|
|
49
|
+
for c in "$@"; do
|
|
50
|
+
[ -n "$cmds" ] && cmds="$cmds,"
|
|
51
|
+
cmds="$cmds{\"type\":\"command\",\"command\":\"$c\"}"
|
|
52
|
+
done
|
|
53
|
+
mkdir -p "$(dirname "$file")"
|
|
54
|
+
cat > "$file" <<EOF
|
|
55
|
+
{
|
|
56
|
+
"permissions": { "defaultMode": "bypassPermissions", "allow": [], "deny": [] },
|
|
57
|
+
"hooks": {
|
|
58
|
+
"PostToolUse": [
|
|
59
|
+
{ "matcher": "Bash", "hooks": [ { "type": "command", "command": "bash \$P/b.sh" } ] },
|
|
60
|
+
{ "matcher": "$MATCHER", "hooks": [ $cmds ] },
|
|
61
|
+
{ "matcher": "mcp__.*", "hooks": [ { "type": "command", "command": "bash \$P/missing.sh" } ] }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
EOF
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
conformance_count() { grep -cF 'quote-render-pdf-conformance.sh' "$1" 2>/dev/null || true; }
|
|
69
|
+
pdfmatcher_present() {
|
|
70
|
+
jq -e --arg m "$MATCHER" '.hooks.PostToolUse | any(.[]; .matcher == $m)' "$1" >/dev/null 2>&1
|
|
71
|
+
}
|
|
72
|
+
wildcard_present() {
|
|
73
|
+
jq -e '.hooks.PostToolUse | any(.[]; .matcher == "mcp__.*")' "$1" >/dev/null 2>&1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# --- 1. stale matcher -> updated, then idempotent ---------------------------
|
|
77
|
+
F="$TMP/stale/.claude/settings.json"
|
|
78
|
+
write_settings "$F"
|
|
79
|
+
OUT="$(ensure_pdf_conformance_matcher "$F")"
|
|
80
|
+
case "$OUT" in *status=updated*) ok;; *) bad "1a stale should be updated, got: $OUT";; esac
|
|
81
|
+
[ "$(conformance_count "$F")" -eq 1 ] && ok || bad "1b conformance not appended (count=$(conformance_count "$F"))"
|
|
82
|
+
OUT2="$(ensure_pdf_conformance_matcher "$F")"
|
|
83
|
+
case "$OUT2" in *status=already-set*) ok;; *) bad "1c re-run should be already-set, got: $OUT2";; esac
|
|
84
|
+
[ "$(conformance_count "$F")" -eq 1 ] && ok || bad "1d conformance duplicated on re-run (count=$(conformance_count "$F"))"
|
|
85
|
+
|
|
86
|
+
# --- 2. correct matcher -> already-set, byte-identical ----------------------
|
|
87
|
+
F="$TMP/good/.claude/settings.json"
|
|
88
|
+
write_settings "$F" "$CONFORMANCE"
|
|
89
|
+
BEFORE="$(cat "$F")"
|
|
90
|
+
OUT="$(ensure_pdf_conformance_matcher "$F")"
|
|
91
|
+
case "$OUT" in *status=already-set*) ok;; *) bad "2a good should be already-set, got: $OUT";; esac
|
|
92
|
+
[ "$BEFORE" = "$(cat "$F")" ] && ok || bad "2b already-set file was rewritten"
|
|
93
|
+
|
|
94
|
+
# --- 3. browser-pdf-save matcher absent (wildcard present) -> updated, added --
|
|
95
|
+
F="$TMP/nomatcher/.claude/settings.json"
|
|
96
|
+
mkdir -p "$(dirname "$F")"
|
|
97
|
+
cat > "$F" <<'EOF'
|
|
98
|
+
{ "permissions": {}, "hooks": { "PostToolUse": [
|
|
99
|
+
{ "matcher": "Bash", "hooks": [] },
|
|
100
|
+
{ "matcher": "mcp__.*", "hooks": [ { "type": "command", "command": "bash $P/missing.sh" } ] }
|
|
101
|
+
] } }
|
|
102
|
+
EOF
|
|
103
|
+
OUT="$(ensure_pdf_conformance_matcher "$F")"
|
|
104
|
+
case "$OUT" in *status=updated*) ok;; *) bad "3a nomatcher should be updated, got: $OUT";; esac
|
|
105
|
+
pdfmatcher_present "$F" && ok || bad "3b browser-pdf-save matcher not added"
|
|
106
|
+
[ "$(conformance_count "$F")" -eq 1 ] && ok || bad "3c conformance missing after add"
|
|
107
|
+
wildcard_present "$F" && ok || bad "3d wildcard matcher was disturbed"
|
|
108
|
+
# added matcher carries exactly the one conformance command
|
|
109
|
+
if jq -e --arg m "$MATCHER" '.hooks.PostToolUse[] | select(.matcher==$m) | .hooks | length == 1' "$F" >/dev/null 2>&1; then ok; else bad "3e added matcher should carry exactly one command"; fi
|
|
110
|
+
|
|
111
|
+
# --- 4. no PostToolUse -> no-hooks, untouched -------------------------------
|
|
112
|
+
F="$TMP/nohooks/.claude/settings.json"
|
|
113
|
+
mkdir -p "$(dirname "$F")"
|
|
114
|
+
echo '{ "permissions": {}, "hooks": {} }' > "$F"
|
|
115
|
+
BEFORE="$(cat "$F")"
|
|
116
|
+
OUT="$(ensure_pdf_conformance_matcher "$F")"
|
|
117
|
+
case "$OUT" in *status=no-hooks*) ok;; *) bad "4a should be no-hooks, got: $OUT";; esac
|
|
118
|
+
[ "$BEFORE" = "$(cat "$F")" ] && ok || bad "4b no-hooks file was rewritten"
|
|
119
|
+
|
|
120
|
+
# --- 5. file absent -> absent ------------------------------------------------
|
|
121
|
+
OUT="$(ensure_pdf_conformance_matcher "$TMP/does-not-exist/.claude/settings.json")"
|
|
122
|
+
case "$OUT" in *status=absent*) ok;; *) bad "5 should be absent, got: $OUT";; esac
|
|
123
|
+
|
|
124
|
+
# --- 6. malformed JSON -> rewrite-failed, untouched -------------------------
|
|
125
|
+
F="$TMP/bad/.claude/settings.json"
|
|
126
|
+
mkdir -p "$(dirname "$F")"
|
|
127
|
+
printf '{ this is not json ' > "$F"
|
|
128
|
+
BEFORE="$(cat "$F")"
|
|
129
|
+
OUT="$(ensure_pdf_conformance_matcher "$F")"
|
|
130
|
+
case "$OUT" in *status=rewrite-failed*) ok;; *) bad "6a malformed should be rewrite-failed, got: $OUT";; esac
|
|
131
|
+
[ "$BEFORE" = "$(cat "$F")" ] && ok || bad "6b malformed file was mutated"
|
|
132
|
+
[ ! -e "$F.1929.tmp" ] && ok || bad "6c temp file left behind"
|
|
133
|
+
|
|
134
|
+
# --- 7. reconcile_all_accounts_pdf_conformance ------------------------------
|
|
135
|
+
ROOT="$TMP/accts"
|
|
136
|
+
mkdir -p "$ROOT/acctA/.claude" "$ROOT/acctB/.claude" "$ROOT/notanaccount/.claude"
|
|
137
|
+
echo '{"role":"house"}' > "$ROOT/acctA/account.json"
|
|
138
|
+
echo '{"role":"client"}' > "$ROOT/acctB/account.json"
|
|
139
|
+
# notanaccount has no account.json -> must be skipped
|
|
140
|
+
write_settings "$ROOT/acctA/.claude/settings.json"
|
|
141
|
+
write_settings "$ROOT/acctB/.claude/settings.json" "$CONFORMANCE"
|
|
142
|
+
write_settings "$ROOT/notanaccount/.claude/settings.json"
|
|
143
|
+
RECOUT="$(reconcile_all_accounts_pdf_conformance "$ROOT")"
|
|
144
|
+
[ "$(conformance_count "$ROOT/acctA/.claude/settings.json")" -eq 1 ] && ok || bad "7a acctA not patched"
|
|
145
|
+
[ "$(conformance_count "$ROOT/acctB/.claude/settings.json")" -eq 1 ] && ok || bad "7b acctB disturbed"
|
|
146
|
+
[ "$(conformance_count "$ROOT/notanaccount/.claude/settings.json")" -eq 0 ] && ok || bad "7c non-account dir was patched"
|
|
147
|
+
case "$RECOUT" in *acctA*status=updated*) ok;; *) bad "7d reconcile did not log acctA updated";; esac
|
|
148
|
+
|
|
149
|
+
echo "----------------------------------------"
|
|
150
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
151
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# account-settings-pdf-conformance.sh — retrofit the quote-render PDF-conformance
|
|
2
|
+
# PostToolUse matcher onto existing accounts' .claude/settings.json. Task 1929.
|
|
3
|
+
#
|
|
4
|
+
# Task 1922 added a PostToolUse matcher on the browser-pdf-save tool running
|
|
5
|
+
# quote-render-pdf-conformance.sh to provision-account-dir.sh. That writer only
|
|
6
|
+
# runs at account-provision time; the upgrade path re-provisions the house
|
|
7
|
+
# account but NOT existing client sub-accounts, so any account provisioned before
|
|
8
|
+
# 1922 keeps a settings.json without the matcher and never runs the PDF gate
|
|
9
|
+
# (live glsmith is such an account). This lib is the standing backfill:
|
|
10
|
+
# setup-account.sh reconciles every account on every install so the gate reaches
|
|
11
|
+
# pre-1922 accounts. Mirrors account-settings-askgate.sh (Task 1683).
|
|
12
|
+
#
|
|
13
|
+
# Requires jq. Idempotent. Never echoes file contents. Fail-open (nudge, not
|
|
14
|
+
# brick): any unusable file logs a status and is left untouched.
|
|
15
|
+
#
|
|
16
|
+
# Sourced by setup-account.sh; also runnable standalone for a manual repair.
|
|
17
|
+
|
|
18
|
+
# The exact command string provision-account-dir.sh writes. $PLATFORM_ROOT is a
|
|
19
|
+
# literal in the settings file, resolved by Claude Code at hook-run time.
|
|
20
|
+
_PDF_CONFORMANCE_MATCHER='mcp__plugin_browser_browser__browser-pdf-save'
|
|
21
|
+
_PDF_CONFORMANCE_CMD='bash $PLATFORM_ROOT/plugins/admin/hooks/quote-render-pdf-conformance.sh'
|
|
22
|
+
|
|
23
|
+
# ensure_pdf_conformance_matcher <settings_file>
|
|
24
|
+
# Guarantees the PostToolUse browser-pdf-save matcher carries the conformance
|
|
25
|
+
# command: appends it if the matcher exists without it, or adds the whole matcher
|
|
26
|
+
# (single conformance command) if the matcher is absent. Logs exactly one
|
|
27
|
+
# `[backfill-1929] file=<f> status=<absent|no-hooks|already-set|updated|rewrite-failed>`
|
|
28
|
+
# to stdout. Always returns 0.
|
|
29
|
+
ensure_pdf_conformance_matcher() {
|
|
30
|
+
local f="$1"
|
|
31
|
+
if [ ! -f "$f" ]; then
|
|
32
|
+
echo "[backfill-1929] file=$f status=absent"
|
|
33
|
+
return 0
|
|
34
|
+
fi
|
|
35
|
+
# Patchable only when the file is valid JSON with a PostToolUse array.
|
|
36
|
+
if ! jq -e '.hooks.PostToolUse | type == "array"' "$f" >/dev/null 2>&1; then
|
|
37
|
+
if jq -e . "$f" >/dev/null 2>&1; then
|
|
38
|
+
echo "[backfill-1929] file=$f status=no-hooks"
|
|
39
|
+
else
|
|
40
|
+
echo "[backfill-1929] file=$f status=rewrite-failed"
|
|
41
|
+
fi
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
local tmp="$f.1929.tmp"
|
|
45
|
+
if ! jq \
|
|
46
|
+
--arg matcher "$_PDF_CONFORMANCE_MATCHER" \
|
|
47
|
+
--arg cmd "$_PDF_CONFORMANCE_CMD" '
|
|
48
|
+
.hooks.PostToolUse |= (
|
|
49
|
+
if any(.[]; .matcher == $matcher)
|
|
50
|
+
then map(
|
|
51
|
+
if .matcher == $matcher
|
|
52
|
+
then .hooks = (
|
|
53
|
+
if any(.hooks[]; .command == $cmd)
|
|
54
|
+
then .hooks
|
|
55
|
+
else .hooks + [{type: "command", command: $cmd}]
|
|
56
|
+
end)
|
|
57
|
+
else . end)
|
|
58
|
+
else . + [{matcher: $matcher, hooks: [{type: "command", command: $cmd}]}]
|
|
59
|
+
end)
|
|
60
|
+
' "$f" > "$tmp" 2>/dev/null; then
|
|
61
|
+
rm -f "$tmp"
|
|
62
|
+
echo "[backfill-1929] file=$f status=rewrite-failed"
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
# Semantic no-op? Compare canonical forms; leave the file byte-identical if so.
|
|
66
|
+
# `|| true` keeps a jq failure here (e.g. $f truncated by a concurrent writer
|
|
67
|
+
# between the type-check above and this read) from tripping a caller's `set -e`
|
|
68
|
+
# and aborting the whole install. This lib is fail-open by contract.
|
|
69
|
+
local before after
|
|
70
|
+
before="$(jq -S . "$f" 2>/dev/null || true)"
|
|
71
|
+
after="$(jq -S . "$tmp" 2>/dev/null || true)"
|
|
72
|
+
if [ "$before" = "$after" ]; then
|
|
73
|
+
rm -f "$tmp"
|
|
74
|
+
echo "[backfill-1929] file=$f status=already-set"
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
if mv "$tmp" "$f"; then
|
|
78
|
+
echo "[backfill-1929] file=$f status=updated"
|
|
79
|
+
else
|
|
80
|
+
rm -f "$tmp"
|
|
81
|
+
echo "[backfill-1929] file=$f status=rewrite-failed"
|
|
82
|
+
fi
|
|
83
|
+
return 0
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# reconcile_all_accounts_pdf_conformance <accounts_dir>
|
|
87
|
+
# Standing check + backfill: every account dir with an account.json gets its
|
|
88
|
+
# PostToolUse browser-pdf-save matcher's conformance command ensured. Mirrors
|
|
89
|
+
# reconcile_all_accounts_askgate.
|
|
90
|
+
reconcile_all_accounts_pdf_conformance() {
|
|
91
|
+
local accts="$1"
|
|
92
|
+
[ -d "$accts" ] || return 0
|
|
93
|
+
local acct
|
|
94
|
+
for acct in "$accts"/*/; do
|
|
95
|
+
[ -f "$acct/account.json" ] || continue
|
|
96
|
+
ensure_pdf_conformance_matcher "$acct/.claude/settings.json"
|
|
97
|
+
done
|
|
98
|
+
}
|
|
@@ -113,6 +113,18 @@ provision_account_dir() {
|
|
|
113
113
|
"hooks": [
|
|
114
114
|
{ "type": "command", "command": "bash $HOOKS_PATH/quote-render-gate.sh" }
|
|
115
115
|
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"matcher": "mcp__plugin_browser_browser__browser-pdf-save",
|
|
119
|
+
"hooks": [
|
|
120
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/preference-consult-gate.sh" }
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"matcher": "mcp__plugin_email_email__email-send|mcp__plugin_email_email__email-reply|mcp__plugin_email_email__email-draft-send|mcp__plugin_outlook_outlook__outlook-mail-send|mcp__plugin_outlook_outlook__outlook-mail-reply|mcp__plugin_outlook_outlook__outlook-draft-send|SendUserFile",
|
|
125
|
+
"hooks": [
|
|
126
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/preference-consult-gate.sh" }
|
|
127
|
+
]
|
|
116
128
|
}
|
|
117
129
|
],
|
|
118
130
|
"PostToolUse": [
|
|
@@ -128,6 +140,12 @@ provision_account_dir() {
|
|
|
128
140
|
{ "type": "command", "command": "bash $HOOKS_PATH/post-tool-use-agent.sh" }
|
|
129
141
|
]
|
|
130
142
|
},
|
|
143
|
+
{
|
|
144
|
+
"matcher": "mcp__plugin_browser_browser__browser-pdf-save",
|
|
145
|
+
"hooks": [
|
|
146
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/quote-render-pdf-conformance.sh" }
|
|
147
|
+
]
|
|
148
|
+
},
|
|
131
149
|
{
|
|
132
150
|
"matcher": "mcp__.*__.*-(export|import)-parse$",
|
|
133
151
|
"hooks": [
|
|
@@ -158,7 +176,8 @@ provision_account_dir() {
|
|
|
158
176
|
"hooks": [
|
|
159
177
|
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" },
|
|
160
178
|
{ "type": "command", "command": "bash $HOOKS_PATH/prompt-optimiser-directive.sh" },
|
|
161
|
-
{ "type": "command", "command": "bash $HOOKS_PATH/datetime-inject.sh" }
|
|
179
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/datetime-inject.sh" },
|
|
180
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/preference-consult-directive.sh" }
|
|
162
181
|
]
|
|
163
182
|
}
|
|
164
183
|
],
|
|
@@ -36,6 +36,8 @@ ACCOUNTS_DIR="$INSTALL_DIR/data/accounts"
|
|
|
36
36
|
. "$SCRIPT_DIR/lib/provision-account-dir.sh"
|
|
37
37
|
# shellcheck source=lib/account-settings-askgate.sh
|
|
38
38
|
. "$SCRIPT_DIR/lib/account-settings-askgate.sh"
|
|
39
|
+
# shellcheck source=lib/account-settings-pdf-conformance.sh
|
|
40
|
+
. "$SCRIPT_DIR/lib/account-settings-pdf-conformance.sh"
|
|
39
41
|
# shellcheck source=lib/read-brand-json.sh
|
|
40
42
|
. "$SCRIPT_DIR/lib/read-brand-json.sh"
|
|
41
43
|
# Resolve brand-aware persistent users.json path before the resolver runs.
|
|
@@ -77,3 +79,14 @@ reconcile_all_accounts_owned_dirs "$ACCOUNTS_DIR"
|
|
|
77
79
|
# so the gate reaches pre-1552 accounts. Idempotent; logs one [backfill-1683] line
|
|
78
80
|
# per account.
|
|
79
81
|
reconcile_all_accounts_askgate "$ACCOUNTS_DIR"
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# 5. Retrofit the quote-render PDF-conformance matcher onto every account
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
# Standing check + durable backfill (Task 1929): provision_account_dir writes the
|
|
87
|
+
# PostToolUse browser-pdf-save matcher (quote-render-pdf-conformance.sh) only at
|
|
88
|
+
# provision time, and this script re-provisions only the house account. An
|
|
89
|
+
# account created before Task 1922 keeps a settings.json without the matcher and
|
|
90
|
+
# never runs the PDF gate. Reconcile every account on every install so the gate
|
|
91
|
+
# reaches pre-1922 accounts. Idempotent; logs one [backfill-1929] line per account.
|
|
92
|
+
reconcile_all_accounts_pdf_conformance "$ACCOUNTS_DIR"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -2071,6 +2071,10 @@ export function buildHttpApp(deps) {
|
|
|
2071
2071
|
}
|
|
2072
2072
|
const byte = body.byte;
|
|
2073
2073
|
const trigger = typeof body.trigger === 'string' ? body.trigger : 'manual';
|
|
2074
|
+
// Task 1920 — the stop button sets escalate:true so that when ESC proves
|
|
2075
|
+
// ineffective (turn blocked in a tool/subagent call) the manager escalates
|
|
2076
|
+
// once to Ctrl-C. Absent/false keeps the ESC-only re-send path.
|
|
2077
|
+
const escalate = body.escalate === true;
|
|
2074
2078
|
const intId = `${sessionId.slice(0, 8)}-${start}`;
|
|
2075
2079
|
deps.logger(`[webchat-interrupt] op=interrupt-requested intId=${intId} sessionId=${sessionId.slice(0, 8)} trigger=${trigger}`);
|
|
2076
2080
|
const result = writeInterruptToPty(sessionId, byte, deps.logger);
|
|
@@ -2115,6 +2119,11 @@ export function buildHttpApp(deps) {
|
|
|
2115
2119
|
let lastSendAt = sentAt;
|
|
2116
2120
|
let escSends = 1; // the initial ESC already written above
|
|
2117
2121
|
let canResend = true;
|
|
2122
|
+
// Task 1920 — the one-shot escalation guard. Seed it from the initial byte:
|
|
2123
|
+
// if the pre-loop write above already sent Ctrl-C (byte='ctrl-c'), the loop
|
|
2124
|
+
// must not send a second one. The invariant is "exactly one Ctrl-C ever per
|
|
2125
|
+
// call" because a second SIGINT can exit the CLI and kill the session.
|
|
2126
|
+
let ctrlCSent = byte === 'ctrl-c';
|
|
2118
2127
|
while (Date.now() - sentAt < stopMs) {
|
|
2119
2128
|
const st = deps.watcher.getBySessionId(sessionId)?.status;
|
|
2120
2129
|
if (st !== 'busy') {
|
|
@@ -2122,14 +2131,34 @@ export function buildHttpApp(deps) {
|
|
|
2122
2131
|
break;
|
|
2123
2132
|
}
|
|
2124
2133
|
if (canResend && Date.now() - lastSendAt >= reescalateMs) {
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2134
|
+
if (escalate) {
|
|
2135
|
+
// Task 1920 — ESC ran for a full sub-window while still busy, so it is
|
|
2136
|
+
// ineffective (turn blocked in a tool/subagent call). Escalate once to
|
|
2137
|
+
// Ctrl-C (SIGINT reaches the harness mid-tool-call); never a second
|
|
2138
|
+
// time — a second SIGINT can exit the CLI and kill the session. After
|
|
2139
|
+
// the one Ctrl-C, stop sending and just poll for busy-clear.
|
|
2140
|
+
if (!ctrlCSent) {
|
|
2141
|
+
const esc = writeInterruptToPty(sessionId, 'ctrl-c', deps.logger);
|
|
2142
|
+
lastSendAt = Date.now();
|
|
2143
|
+
if (esc.ok) {
|
|
2144
|
+
ctrlCSent = true;
|
|
2145
|
+
deps.logger(`[webchat-interrupt] op=interrupt-escalated intId=${intId} mechanism=ctrl-c sends=${escSends + 1}`);
|
|
2146
|
+
}
|
|
2147
|
+
else {
|
|
2148
|
+
canResend = false; // child vanished mid-interrupt; the next status read exits the loop
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2130
2151
|
}
|
|
2131
2152
|
else {
|
|
2132
|
-
|
|
2153
|
+
const re = writeInterruptToPty(sessionId, byte, deps.logger);
|
|
2154
|
+
lastSendAt = Date.now();
|
|
2155
|
+
if (re.ok) {
|
|
2156
|
+
escSends += 1;
|
|
2157
|
+
deps.logger(`[webchat-interrupt] op=interrupt-reescalated intId=${intId} sends=${escSends}`);
|
|
2158
|
+
}
|
|
2159
|
+
else {
|
|
2160
|
+
canResend = false; // child vanished mid-interrupt; the next status read exits the loop
|
|
2161
|
+
}
|
|
2133
2162
|
}
|
|
2134
2163
|
}
|
|
2135
2164
|
await new Promise((r) => setTimeout(r, 120));
|