@raidiant/notifai 5.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +2 -2
- package/dist/atomic-file.js +13 -0
- package/dist/atomic-file.js.map +1 -1
- package/dist/client.d.ts +8 -3
- package/dist/client.js +24 -3
- package/dist/client.js.map +1 -1
- package/dist/commands-auth.d.ts +14 -0
- package/dist/commands-auth.js +154 -0
- package/dist/commands-auth.js.map +1 -0
- package/dist/commands-config.d.ts +31 -0
- package/dist/commands-config.js +246 -0
- package/dist/commands-config.js.map +1 -0
- package/dist/commands-core.d.ts +102 -0
- package/dist/commands-core.js +135 -0
- package/dist/commands-core.js.map +1 -0
- package/dist/commands-devices.d.ts +31 -0
- package/dist/commands-devices.js +124 -0
- package/dist/commands-devices.js.map +1 -0
- package/dist/commands-doctor.d.ts +24 -0
- package/dist/commands-doctor.js +1011 -0
- package/dist/commands-doctor.js.map +1 -0
- package/dist/commands-harness-context.d.ts +65 -0
- package/dist/commands-harness-context.js +97 -0
- package/dist/commands-harness-context.js.map +1 -0
- package/dist/commands-hook-shape.d.ts +17 -0
- package/dist/commands-hook-shape.js +37 -0
- package/dist/commands-hook-shape.js.map +1 -0
- package/dist/commands-hooks.d.ts +115 -0
- package/dist/commands-hooks.js +1225 -0
- package/dist/commands-hooks.js.map +1 -0
- package/dist/commands-init.d.ts +35 -0
- package/dist/commands-init.js +580 -0
- package/dist/commands-init.js.map +1 -0
- package/dist/commands-io.d.ts +2 -0
- package/dist/commands-io.js +110 -0
- package/dist/commands-io.js.map +1 -0
- package/dist/commands-logs.d.ts +30 -0
- package/dist/commands-logs.js +201 -0
- package/dist/commands-logs.js.map +1 -0
- package/dist/commands-send-support.d.ts +44 -0
- package/dist/commands-send-support.js +183 -0
- package/dist/commands-send-support.js.map +1 -0
- package/dist/commands-send.d.ts +33 -0
- package/dist/commands-send.js +519 -0
- package/dist/commands-send.js.map +1 -0
- package/dist/commands-setup-proof.d.ts +15 -0
- package/dist/commands-setup-proof.js +53 -0
- package/dist/commands-setup-proof.js.map +1 -0
- package/dist/commands-skill.d.ts +10 -0
- package/dist/commands-skill.js +71 -0
- package/dist/commands-skill.js.map +1 -0
- package/dist/commands.d.ts +12 -387
- package/dist/commands.js +12 -4536
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.js +1 -1
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/config.js +6 -4
- package/dist/config.js.map +1 -1
- package/dist/credentials.js +13 -3
- package/dist/credentials.js.map +1 -1
- package/dist/generated-session-label.d.ts +2 -0
- package/dist/generated-session-label.js +152 -0
- package/dist/generated-session-label.js.map +1 -0
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +127 -99
- package/dist/hooks.js.map +1 -1
- package/dist/interactive.js +4 -4
- package/dist/interactive.js.map +1 -1
- package/dist/invocation-context.d.ts +3 -0
- package/dist/invocation-context.js +23 -6
- package/dist/invocation-context.js.map +1 -1
- package/dist/logging.js +1 -0
- package/dist/logging.js.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +1 -1
- package/dist/opencode-plugin.js +18 -3
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/readiness.d.ts +3 -1
- package/dist/readiness.js.map +1 -1
- package/dist/session-labels.d.ts +30 -0
- package/dist/session-labels.js +278 -0
- package/dist/session-labels.js.map +1 -0
- package/package.json +2 -2
- package/src/atomic-file.ts +12 -0
- package/src/client.test.ts +90 -1
- package/src/client.ts +35 -2
- package/src/commands-auth.ts +174 -0
- package/src/commands-config.ts +307 -0
- package/src/commands-core.ts +240 -0
- package/src/commands-devices.ts +150 -0
- package/src/commands-doctor.ts +1209 -0
- package/src/commands-harness-context.ts +134 -0
- package/src/commands-hook-shape.ts +53 -0
- package/src/commands-hooks.ts +1586 -0
- package/src/commands-init.ts +721 -0
- package/src/commands-io.ts +108 -0
- package/src/commands-logs.ts +243 -0
- package/src/commands-send-support.ts +227 -0
- package/src/commands-send.ts +623 -0
- package/src/commands-setup-proof.ts +71 -0
- package/src/commands-skill.ts +84 -0
- package/src/commands.test.ts +567 -42
- package/src/commands.ts +58 -5614
- package/src/config-schema.ts +1 -1
- package/src/config.test.ts +9 -0
- package/src/config.ts +10 -5
- package/src/credentials.test.ts +25 -1
- package/src/credentials.ts +13 -3
- package/src/generated-session-label.ts +155 -0
- package/src/hooks.test.ts +355 -16
- package/src/hooks.ts +145 -97
- package/src/install-hooks.test.ts +84 -1
- package/src/interactive.test.ts +19 -1
- package/src/interactive.ts +4 -4
- package/src/invocation-context.test.ts +115 -16
- package/src/invocation-context.ts +32 -11
- package/src/logging.test.ts +15 -2
- package/src/logging.ts +1 -0
- package/src/main.ts +9 -3
- package/src/opencode-plugin.ts +18 -3
- package/src/readiness.ts +3 -1
- package/src/session-labels.test.ts +303 -0
- package/src/session-labels.ts +360 -0
- package/src/skill.test.ts +13 -0
package/src/hooks.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from 'node:child_process'
|
|
1
|
+
import { execFileSync, spawn, type ChildProcess } from 'node:child_process'
|
|
2
2
|
import {
|
|
3
3
|
existsSync,
|
|
4
4
|
mkdirSync,
|
|
@@ -15,7 +15,6 @@ import os from 'node:os'
|
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import { PassThrough } from 'node:stream'
|
|
17
17
|
import {
|
|
18
|
-
sessionLabelFromId,
|
|
19
18
|
type ListRepliesResponse,
|
|
20
19
|
type ReplyView,
|
|
21
20
|
type SubmissionReceipt,
|
|
@@ -167,7 +166,8 @@ function fakeClient(recorder: Recorder, replies: ReplyView[]): ApiClient {
|
|
|
167
166
|
platform: 'ios' as const,
|
|
168
167
|
permission_status: 'authorized',
|
|
169
168
|
registration_healthy: true,
|
|
170
|
-
|
|
169
|
+
capabilities: ['answer'],
|
|
170
|
+
derived_status: 'working',
|
|
171
171
|
last_seen_at: null,
|
|
172
172
|
},
|
|
173
173
|
{
|
|
@@ -176,7 +176,8 @@ function fakeClient(recorder: Recorder, replies: ReplyView[]): ApiClient {
|
|
|
176
176
|
platform: 'macos' as const,
|
|
177
177
|
permission_status: 'authorized',
|
|
178
178
|
registration_healthy: true,
|
|
179
|
-
|
|
179
|
+
capabilities: ['answer'],
|
|
180
|
+
derived_status: 'working',
|
|
180
181
|
last_seen_at: null,
|
|
181
182
|
},
|
|
182
183
|
],
|
|
@@ -337,6 +338,23 @@ function harness(replies: ReplyView[] = []): Harness {
|
|
|
337
338
|
}
|
|
338
339
|
}
|
|
339
340
|
|
|
341
|
+
function runFixtureGit(cwd: string, ...args: string[]): string {
|
|
342
|
+
return execFileSync('git', ['-C', cwd, ...args], {
|
|
343
|
+
encoding: 'utf8',
|
|
344
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
345
|
+
}).trim()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function initializeFixtureRepository(root: string, branch = 'branch-a'): void {
|
|
349
|
+
runFixtureGit(root, 'init')
|
|
350
|
+
runFixtureGit(root, 'config', 'user.email', 'notifai-tests@example.invalid')
|
|
351
|
+
runFixtureGit(root, 'config', 'user.name', 'Notifai Tests')
|
|
352
|
+
writeFileSync(path.join(root, 'tracked.txt'), 'fixture\n')
|
|
353
|
+
runFixtureGit(root, 'add', 'tracked.txt')
|
|
354
|
+
runFixtureGit(root, 'commit', '-m', 'fixture')
|
|
355
|
+
runFixtureGit(root, 'branch', '-M', branch)
|
|
356
|
+
}
|
|
357
|
+
|
|
340
358
|
function stdin(payload: unknown): () => Promise<string> {
|
|
341
359
|
return async () => JSON.stringify(payload)
|
|
342
360
|
}
|
|
@@ -494,6 +512,26 @@ describe('the waiter wall clock', () => {
|
|
|
494
512
|
})
|
|
495
513
|
|
|
496
514
|
describe('pushing a registered question', () => {
|
|
515
|
+
it('keeps unknown future session keys through an older read-modify-write', () => {
|
|
516
|
+
const h = harness([])
|
|
517
|
+
const sessionId = 'future-session-format'
|
|
518
|
+
const file = path.join(stateDir(h.env), 'sessions', `${sanitizeSessionId(sessionId)}.json`)
|
|
519
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
520
|
+
writeFileSync(
|
|
521
|
+
file,
|
|
522
|
+
`${JSON.stringify({
|
|
523
|
+
last_prompt_at: NOW,
|
|
524
|
+
future_session_key: { nested: ['kept'] },
|
|
525
|
+
})}\n`,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
registerQuestion(sessionId, h.env, { question: 'Still preserved?' }, NOW)
|
|
529
|
+
|
|
530
|
+
const rewritten = JSON.parse(readFileSync(file, 'utf8')) as Record<string, unknown>
|
|
531
|
+
expect(rewritten['future_session_key']).toEqual({ nested: ['kept'] })
|
|
532
|
+
expect(rewritten['pending']).toHaveLength(1)
|
|
533
|
+
})
|
|
534
|
+
|
|
497
535
|
// Found by a live Claude Code session: a spawned agent always has a
|
|
498
536
|
// just-set last_prompt_at, and the routing gate once read that as the user
|
|
499
537
|
// being present — so its FIRST question could never escalate, which is the
|
|
@@ -864,7 +902,7 @@ describe('terminal-first grace window', () => {
|
|
|
864
902
|
).toBeGreaterThanOrEqual(60)
|
|
865
903
|
})
|
|
866
904
|
|
|
867
|
-
it('never pushes a question to a Companion
|
|
905
|
+
it('never pushes a question to a Companion that did not advertise answer', async () => {
|
|
868
906
|
const h = harness([])
|
|
869
907
|
const factory = h.deps.clientFactory
|
|
870
908
|
h.deps.clientFactory = () => {
|
|
@@ -874,7 +912,7 @@ describe('terminal-first grace window', () => {
|
|
|
874
912
|
listDevices: async () => ({
|
|
875
913
|
devices: (await client.listDevices()).devices.map((device) => ({
|
|
876
914
|
...device,
|
|
877
|
-
|
|
915
|
+
capabilities: [],
|
|
878
916
|
})),
|
|
879
917
|
}),
|
|
880
918
|
} as ApiClient
|
|
@@ -2638,6 +2676,58 @@ describe('user-prompt-submit hook', () => {
|
|
|
2638
2676
|
clearSessionState('s14', h.env)
|
|
2639
2677
|
expect(readProjectSession(project, h.env, NOW)).toBeNull()
|
|
2640
2678
|
})
|
|
2679
|
+
|
|
2680
|
+
it('round-trips future project-pointer fields while publishing a session', () => {
|
|
2681
|
+
const h = harness()
|
|
2682
|
+
const pointer = projectSessionPointerPath(h.deps.cwd, h.env)
|
|
2683
|
+
const futureEntry = {
|
|
2684
|
+
format: 2,
|
|
2685
|
+
session: { id: 'future-session', runtime: 'future-harness' },
|
|
2686
|
+
}
|
|
2687
|
+
mkdirSync(path.dirname(pointer), { recursive: true })
|
|
2688
|
+
writeFileSync(
|
|
2689
|
+
pointer,
|
|
2690
|
+
`${JSON.stringify({
|
|
2691
|
+
future_root: { retained: true },
|
|
2692
|
+
sessions: [
|
|
2693
|
+
{
|
|
2694
|
+
session_id: 'still-running',
|
|
2695
|
+
updated_at: NOW - 1,
|
|
2696
|
+
harness: 'codex',
|
|
2697
|
+
future_entry: { retained: true },
|
|
2698
|
+
},
|
|
2699
|
+
{
|
|
2700
|
+
session_id: 'new-session',
|
|
2701
|
+
updated_at: NOW - 2,
|
|
2702
|
+
harness: 'claude-code',
|
|
2703
|
+
future_current_entry: { retained: true },
|
|
2704
|
+
},
|
|
2705
|
+
futureEntry,
|
|
2706
|
+
],
|
|
2707
|
+
})}\n`,
|
|
2708
|
+
)
|
|
2709
|
+
|
|
2710
|
+
writeProjectSession(h.deps.cwd, h.env, 'new-session', NOW, 'claude-code')
|
|
2711
|
+
|
|
2712
|
+
expect(JSON.parse(readFileSync(pointer, 'utf8'))).toEqual({
|
|
2713
|
+
future_root: { retained: true },
|
|
2714
|
+
sessions: [
|
|
2715
|
+
{
|
|
2716
|
+
session_id: 'still-running',
|
|
2717
|
+
updated_at: NOW - 1,
|
|
2718
|
+
harness: 'codex',
|
|
2719
|
+
future_entry: { retained: true },
|
|
2720
|
+
},
|
|
2721
|
+
futureEntry,
|
|
2722
|
+
{
|
|
2723
|
+
session_id: 'new-session',
|
|
2724
|
+
updated_at: NOW,
|
|
2725
|
+
harness: 'claude-code',
|
|
2726
|
+
future_current_entry: { retained: true },
|
|
2727
|
+
},
|
|
2728
|
+
],
|
|
2729
|
+
})
|
|
2730
|
+
})
|
|
2641
2731
|
})
|
|
2642
2732
|
|
|
2643
2733
|
describe('session-end hook', () => {
|
|
@@ -2656,6 +2746,36 @@ describe('session-end hook', () => {
|
|
|
2656
2746
|
expect(records.at(-1)?.data).toMatchObject({ outcome: 'cleaned', queued_retirements: 0 })
|
|
2657
2747
|
})
|
|
2658
2748
|
|
|
2749
|
+
it('round-trips future session-state fields when SessionEnd preserves an obligation', async () => {
|
|
2750
|
+
const h = harness()
|
|
2751
|
+
const sessionId = 'session-end-future-state'
|
|
2752
|
+
const stateFile = path.join(
|
|
2753
|
+
stateDir(h.env),
|
|
2754
|
+
'sessions',
|
|
2755
|
+
`${sanitizeSessionId(sessionId)}.json`,
|
|
2756
|
+
)
|
|
2757
|
+
const acknowledgement = {
|
|
2758
|
+
request_id: 'req_future_ack',
|
|
2759
|
+
recorded_at: NOW,
|
|
2760
|
+
future_entry: { retained: true },
|
|
2761
|
+
}
|
|
2762
|
+
mkdirSync(path.dirname(stateFile), { recursive: true })
|
|
2763
|
+
writeFileSync(
|
|
2764
|
+
stateFile,
|
|
2765
|
+
`${JSON.stringify({
|
|
2766
|
+
future_root: { retained: true },
|
|
2767
|
+
acknowledgement_due: [acknowledgement],
|
|
2768
|
+
})}\n`,
|
|
2769
|
+
)
|
|
2770
|
+
|
|
2771
|
+
await hookRunCommand(h.deps, 'session-end', stdin({ session_id: sessionId }))
|
|
2772
|
+
|
|
2773
|
+
expect(JSON.parse(readFileSync(stateFile, 'utf8'))).toEqual({
|
|
2774
|
+
future_root: { retained: true },
|
|
2775
|
+
acknowledgement_due: [acknowledgement],
|
|
2776
|
+
})
|
|
2777
|
+
})
|
|
2778
|
+
|
|
2659
2779
|
it('runs the real Codex SessionEnd cleanup before contended diagnostics and exits fail-open', async () => {
|
|
2660
2780
|
const h = harness()
|
|
2661
2781
|
const sessionId = 'session-end-budget'
|
|
@@ -2772,6 +2892,54 @@ describe('session-end hook', () => {
|
|
|
2772
2892
|
).toEqual({ sessionId: 'still-running', harness: 'codex' })
|
|
2773
2893
|
})
|
|
2774
2894
|
|
|
2895
|
+
it('keeps future project-pointer data when clearing the ending session', async () => {
|
|
2896
|
+
const h = harness()
|
|
2897
|
+
const pointer = projectSessionPointerPath(h.deps.cwd, h.env)
|
|
2898
|
+
const futureEntry = {
|
|
2899
|
+
format: 2,
|
|
2900
|
+
session: { id: 'future-session', runtime: 'future-harness' },
|
|
2901
|
+
}
|
|
2902
|
+
writeSessionState('still-running', h.env, { last_prompt_at: NOW })
|
|
2903
|
+
writeSessionState('ending', h.env, { last_prompt_at: NOW })
|
|
2904
|
+
mkdirSync(path.dirname(pointer), { recursive: true })
|
|
2905
|
+
writeFileSync(
|
|
2906
|
+
pointer,
|
|
2907
|
+
`${JSON.stringify({
|
|
2908
|
+
future_root: { retained: true },
|
|
2909
|
+
sessions: [
|
|
2910
|
+
{
|
|
2911
|
+
session_id: 'still-running',
|
|
2912
|
+
updated_at: NOW - 1,
|
|
2913
|
+
harness: 'codex',
|
|
2914
|
+
future_entry: { retained: true },
|
|
2915
|
+
},
|
|
2916
|
+
futureEntry,
|
|
2917
|
+
{ session_id: 'ending', updated_at: NOW, harness: 'claude-code' },
|
|
2918
|
+
],
|
|
2919
|
+
})}\n`,
|
|
2920
|
+
)
|
|
2921
|
+
|
|
2922
|
+
await hookRunCommand(
|
|
2923
|
+
h.deps,
|
|
2924
|
+
'session-end',
|
|
2925
|
+
stdin({ session_id: 'ending', cwd: h.deps.cwd }),
|
|
2926
|
+
'claude-code',
|
|
2927
|
+
)
|
|
2928
|
+
|
|
2929
|
+
expect(JSON.parse(readFileSync(pointer, 'utf8'))).toEqual({
|
|
2930
|
+
future_root: { retained: true },
|
|
2931
|
+
sessions: [
|
|
2932
|
+
{
|
|
2933
|
+
session_id: 'still-running',
|
|
2934
|
+
updated_at: NOW - 1,
|
|
2935
|
+
harness: 'codex',
|
|
2936
|
+
future_entry: { retained: true },
|
|
2937
|
+
},
|
|
2938
|
+
futureEntry,
|
|
2939
|
+
],
|
|
2940
|
+
})
|
|
2941
|
+
})
|
|
2942
|
+
|
|
2775
2943
|
it('preserves incomplete live state and reports why it cannot retire it', async () => {
|
|
2776
2944
|
const h = harness()
|
|
2777
2945
|
writeSessionState('s-incomplete', h.env, {
|
|
@@ -2803,6 +2971,164 @@ describe('malformed input', () => {
|
|
|
2803
2971
|
})
|
|
2804
2972
|
|
|
2805
2973
|
describe('telling concurrent agents apart', () => {
|
|
2974
|
+
it('uses each hook event branch while keeping one immutable session identity', async () => {
|
|
2975
|
+
const h = harness([reply({ text: 'Yes' })])
|
|
2976
|
+
initializeFixtureRepository(h.deps.cwd)
|
|
2977
|
+
registerQuestion(
|
|
2978
|
+
'session-branch-transition',
|
|
2979
|
+
h.env,
|
|
2980
|
+
{
|
|
2981
|
+
question: 'Ship it?',
|
|
2982
|
+
source: {
|
|
2983
|
+
session_id: 'session-branch-transition',
|
|
2984
|
+
session_label: 'Branch transition',
|
|
2985
|
+
harness: 'claude-code',
|
|
2986
|
+
branch: 'branch-a',
|
|
2987
|
+
},
|
|
2988
|
+
},
|
|
2989
|
+
NOW,
|
|
2990
|
+
)
|
|
2991
|
+
|
|
2992
|
+
runFixtureGit(h.deps.cwd, 'checkout', '-b', 'branch-b')
|
|
2993
|
+
await hookRunCommand(
|
|
2994
|
+
h.deps,
|
|
2995
|
+
'stop',
|
|
2996
|
+
stdin({ session_id: 'session-branch-transition', cwd: h.deps.cwd }),
|
|
2997
|
+
'claude-code',
|
|
2998
|
+
)
|
|
2999
|
+
|
|
3000
|
+
const question = h.recorder.submitted.find((entry) => entry.draft.event === 'agent_question')
|
|
3001
|
+
expect(question?.draft.source).toEqual({
|
|
3002
|
+
session_id: 'session-branch-transition',
|
|
3003
|
+
session_label: 'Branch transition',
|
|
3004
|
+
harness: 'claude-code',
|
|
3005
|
+
branch: 'branch-b',
|
|
3006
|
+
})
|
|
3007
|
+
|
|
3008
|
+
runFixtureGit(h.deps.cwd, 'checkout', 'branch-a')
|
|
3009
|
+
h.recorder.acknowledged ??= new Set()
|
|
3010
|
+
h.recorder.acknowledged.add(h.recorder.receipts[0]!)
|
|
3011
|
+
await hookRunCommand(
|
|
3012
|
+
h.deps,
|
|
3013
|
+
'stop',
|
|
3014
|
+
stdin({
|
|
3015
|
+
session_id: 'session-branch-transition',
|
|
3016
|
+
cwd: h.deps.cwd,
|
|
3017
|
+
stop_hook_active: true,
|
|
3018
|
+
}),
|
|
3019
|
+
'claude-code',
|
|
3020
|
+
)
|
|
3021
|
+
|
|
3022
|
+
const retirement = h.recorder.submitted.find(
|
|
3023
|
+
(entry) => entry.draft.event === 'question_retired',
|
|
3024
|
+
)
|
|
3025
|
+
expect(retirement?.draft.source).toEqual({
|
|
3026
|
+
session_id: 'session-branch-transition',
|
|
3027
|
+
session_label: 'Branch transition',
|
|
3028
|
+
harness: 'claude-code',
|
|
3029
|
+
branch: 'branch-a',
|
|
3030
|
+
})
|
|
3031
|
+
})
|
|
3032
|
+
|
|
3033
|
+
it('clears registration-time branch and worktree on a detached-HEAD hook event', async () => {
|
|
3034
|
+
const h = harness([])
|
|
3035
|
+
initializeFixtureRepository(h.deps.cwd)
|
|
3036
|
+
registerQuestion(
|
|
3037
|
+
'session-detached',
|
|
3038
|
+
h.env,
|
|
3039
|
+
{
|
|
3040
|
+
question: 'Ship it?',
|
|
3041
|
+
source: {
|
|
3042
|
+
session_id: 'session-detached',
|
|
3043
|
+
session_label: 'Detached review',
|
|
3044
|
+
harness: 'claude-code',
|
|
3045
|
+
branch: 'branch-a',
|
|
3046
|
+
worktree: 'stale-worktree',
|
|
3047
|
+
},
|
|
3048
|
+
},
|
|
3049
|
+
NOW,
|
|
3050
|
+
)
|
|
3051
|
+
runFixtureGit(h.deps.cwd, 'checkout', '--detach', 'HEAD')
|
|
3052
|
+
|
|
3053
|
+
await hookRunCommand(
|
|
3054
|
+
h.deps,
|
|
3055
|
+
'stop',
|
|
3056
|
+
stdin({ session_id: 'session-detached', cwd: h.deps.cwd }),
|
|
3057
|
+
'claude-code',
|
|
3058
|
+
)
|
|
3059
|
+
|
|
3060
|
+
const question = h.recorder.submitted.find((entry) => entry.draft.event === 'agent_question')
|
|
3061
|
+
expect(question?.draft.source).toEqual({
|
|
3062
|
+
session_id: 'session-detached',
|
|
3063
|
+
session_label: 'Detached review',
|
|
3064
|
+
harness: 'claude-code',
|
|
3065
|
+
})
|
|
3066
|
+
})
|
|
3067
|
+
|
|
3068
|
+
it('captures entry into and exit from a linked worktree on the same session', async () => {
|
|
3069
|
+
const h = harness([reply({ text: 'Yes' })])
|
|
3070
|
+
initializeFixtureRepository(h.deps.cwd, 'main')
|
|
3071
|
+
const linked = `${h.deps.cwd}-linked`
|
|
3072
|
+
runFixtureGit(h.deps.cwd, 'worktree', 'add', '-b', 'worktree-branch', linked)
|
|
3073
|
+
try {
|
|
3074
|
+
registerQuestion(
|
|
3075
|
+
'session-worktree-transition',
|
|
3076
|
+
h.env,
|
|
3077
|
+
{
|
|
3078
|
+
question: 'Ship it?',
|
|
3079
|
+
source: {
|
|
3080
|
+
session_id: 'session-worktree-transition',
|
|
3081
|
+
session_label: 'Worktree transition',
|
|
3082
|
+
harness: 'claude-code',
|
|
3083
|
+
branch: 'main',
|
|
3084
|
+
},
|
|
3085
|
+
},
|
|
3086
|
+
NOW,
|
|
3087
|
+
)
|
|
3088
|
+
|
|
3089
|
+
await hookRunCommand(
|
|
3090
|
+
h.deps,
|
|
3091
|
+
'stop',
|
|
3092
|
+
stdin({ session_id: 'session-worktree-transition', cwd: linked }),
|
|
3093
|
+
'claude-code',
|
|
3094
|
+
)
|
|
3095
|
+
const question = h.recorder.submitted.find(
|
|
3096
|
+
(entry) => entry.draft.event === 'agent_question',
|
|
3097
|
+
)
|
|
3098
|
+
expect(question?.draft.source).toEqual({
|
|
3099
|
+
session_id: 'session-worktree-transition',
|
|
3100
|
+
session_label: 'Worktree transition',
|
|
3101
|
+
harness: 'claude-code',
|
|
3102
|
+
branch: 'worktree-branch',
|
|
3103
|
+
worktree: path.basename(linked),
|
|
3104
|
+
})
|
|
3105
|
+
|
|
3106
|
+
h.recorder.acknowledged ??= new Set()
|
|
3107
|
+
h.recorder.acknowledged.add(h.recorder.receipts[0]!)
|
|
3108
|
+
await hookRunCommand(
|
|
3109
|
+
h.deps,
|
|
3110
|
+
'stop',
|
|
3111
|
+
stdin({
|
|
3112
|
+
session_id: 'session-worktree-transition',
|
|
3113
|
+
cwd: h.deps.cwd,
|
|
3114
|
+
stop_hook_active: true,
|
|
3115
|
+
}),
|
|
3116
|
+
'claude-code',
|
|
3117
|
+
)
|
|
3118
|
+
const retirement = h.recorder.submitted.find(
|
|
3119
|
+
(entry) => entry.draft.event === 'question_retired',
|
|
3120
|
+
)
|
|
3121
|
+
expect(retirement?.draft.source).toEqual({
|
|
3122
|
+
session_id: 'session-worktree-transition',
|
|
3123
|
+
session_label: 'Worktree transition',
|
|
3124
|
+
harness: 'claude-code',
|
|
3125
|
+
branch: 'main',
|
|
3126
|
+
})
|
|
3127
|
+
} finally {
|
|
3128
|
+
runFixtureGit(h.deps.cwd, 'worktree', 'remove', '--force', linked)
|
|
3129
|
+
}
|
|
3130
|
+
})
|
|
3131
|
+
|
|
2806
3132
|
it('stamps structured Source Context on the question it pushes', async () => {
|
|
2807
3133
|
const h = harness([])
|
|
2808
3134
|
registerQuestion(
|
|
@@ -2812,7 +3138,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
2812
3138
|
question: 'Ship it?',
|
|
2813
3139
|
source: {
|
|
2814
3140
|
session_id: 'sess-abc',
|
|
2815
|
-
session_label:
|
|
3141
|
+
session_label: 'Semantic session',
|
|
2816
3142
|
harness: 'claude-code',
|
|
2817
3143
|
},
|
|
2818
3144
|
},
|
|
@@ -2823,7 +3149,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
2823
3149
|
|
|
2824
3150
|
expect(h.recorder.submitted[0]?.draft.source).toEqual({
|
|
2825
3151
|
session_id: 'sess-abc',
|
|
2826
|
-
session_label:
|
|
3152
|
+
session_label: 'Semantic session',
|
|
2827
3153
|
harness: 'claude-code',
|
|
2828
3154
|
})
|
|
2829
3155
|
})
|
|
@@ -2837,7 +3163,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
2837
3163
|
question: 'Ship it?',
|
|
2838
3164
|
source: {
|
|
2839
3165
|
session_id: 'sess-abc',
|
|
2840
|
-
session_label:
|
|
3166
|
+
session_label: 'Semantic session',
|
|
2841
3167
|
harness: 'claude-code',
|
|
2842
3168
|
},
|
|
2843
3169
|
},
|
|
@@ -2856,7 +3182,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
2856
3182
|
const retirement = h.recorder.submitted.find((s) => s.draft.event === 'question_retired')
|
|
2857
3183
|
expect(retirement?.draft.source).toEqual({
|
|
2858
3184
|
session_id: 'sess-abc',
|
|
2859
|
-
session_label:
|
|
3185
|
+
session_label: 'Semantic session',
|
|
2860
3186
|
harness: 'claude-code',
|
|
2861
3187
|
})
|
|
2862
3188
|
})
|
|
@@ -3881,11 +4207,9 @@ describe('escalation waiter delivery seam', () => {
|
|
|
3881
4207
|
|
|
3882
4208
|
|
|
3883
4209
|
describe('session state across a prompt', () => {
|
|
3884
|
-
it('carries
|
|
3885
|
-
// The prompt
|
|
3886
|
-
//
|
|
3887
|
-
// acknowledgement obligation once vanished. This fails if a future field is
|
|
3888
|
-
// added to SessionState and not carried across.
|
|
4210
|
+
it('carries known and future session state across a typed prompt', async () => {
|
|
4211
|
+
// The prompt transition deliberately changes pending/retiring state and the
|
|
4212
|
+
// continuation count, but fields introduced by a newer CLI remain opaque.
|
|
3889
4213
|
const h = harness([])
|
|
3890
4214
|
const before = {
|
|
3891
4215
|
last_prompt_at: AWAY,
|
|
@@ -3903,7 +4227,11 @@ describe('session state across a prompt', () => {
|
|
|
3903
4227
|
acknowledgement_due: [{ request_id: 'req_carry_ack', recorded_at: NOW }],
|
|
3904
4228
|
acknowledgement_blocks: 2,
|
|
3905
4229
|
}
|
|
3906
|
-
|
|
4230
|
+
const futureSessionValue = { nested: ['kept'], revision: 2 }
|
|
4231
|
+
writeSessionState('carry-all', h.env, {
|
|
4232
|
+
...before,
|
|
4233
|
+
future_session_key: futureSessionValue,
|
|
4234
|
+
} as SessionState & { future_session_key: typeof futureSessionValue })
|
|
3907
4235
|
|
|
3908
4236
|
await hookRunCommand(
|
|
3909
4237
|
h.deps,
|
|
@@ -3915,6 +4243,17 @@ describe('session state across a prompt', () => {
|
|
|
3915
4243
|
for (const key of Object.keys(before) as (keyof typeof before)[]) {
|
|
3916
4244
|
expect(after[key], `${key} did not survive the prompt`).toBeDefined()
|
|
3917
4245
|
}
|
|
4246
|
+
const persisted = JSON.parse(
|
|
4247
|
+
readFileSync(
|
|
4248
|
+
path.join(
|
|
4249
|
+
stateDir(h.env),
|
|
4250
|
+
'sessions',
|
|
4251
|
+
`${sanitizeSessionId('carry-all')}.json`,
|
|
4252
|
+
),
|
|
4253
|
+
'utf8',
|
|
4254
|
+
),
|
|
4255
|
+
) as Record<string, unknown>
|
|
4256
|
+
expect(persisted['future_session_key']).toEqual(futureSessionValue)
|
|
3918
4257
|
// The one field a prompt resets rather than carries.
|
|
3919
4258
|
expect(after.continuation?.count).toBe(0)
|
|
3920
4259
|
expect(after.acknowledgement_blocks).toBe(2)
|