instar 1.3.596 → 1.3.598
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/core/LiveTestRunner.d.ts +66 -0
- package/dist/core/LiveTestRunner.d.ts.map +1 -0
- package/dist/core/LiveTestRunner.js +85 -0
- package/dist/core/LiveTestRunner.js.map +1 -0
- package/dist/core/PlacementResponderReader.d.ts +45 -0
- package/dist/core/PlacementResponderReader.d.ts.map +1 -0
- package/dist/core/PlacementResponderReader.js +46 -0
- package/dist/core/PlacementResponderReader.js.map +1 -0
- package/dist/core/SlackLiveSender.d.ts +65 -0
- package/dist/core/SlackLiveSender.d.ts.map +1 -0
- package/dist/core/SlackLiveSender.js +77 -0
- package/dist/core/SlackLiveSender.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.597.md +21 -0
- package/upgrades/1.3.598.md +24 -0
- package/upgrades/side-effects/live-test-runner.md +28 -0
- package/upgrades/side-effects/live-test-slack-sender.md +29 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiveTestRunner — the orchestrator that APPLIES the live-user-channel-proof standard
|
|
3
|
+
* to the cross-machine transfer (docs/specs/live-user-channel-proof-standard.md §6 —
|
|
4
|
+
* the first feature held to the bar). It encodes the capstone flow that a user-role
|
|
5
|
+
* session runs:
|
|
6
|
+
*
|
|
7
|
+
* 1. MOVE THE SEAT FIRST — transfer the (throwaway) topic to the target machine.
|
|
8
|
+
* If the transfer reports the seat did NOT move, the capstone premise is false and
|
|
9
|
+
* we stop loudly (that "ok:true but moved nothing" lie is the original bug — we
|
|
10
|
+
* refuse to paper over it).
|
|
11
|
+
* 2. Then run the LiveTestHarness over a scenario matrix that sends a real message
|
|
12
|
+
* through each channel and asserts the reply was served FROM the target machine
|
|
13
|
+
* (the `responderMachine` expectation — the deterministic cross-machine proof).
|
|
14
|
+
*
|
|
15
|
+
* This module is pure orchestration over the injected harness + an injected `transfer`
|
|
16
|
+
* action, so it is unit-testable with fakes. The server.ts route wires the real
|
|
17
|
+
* RealChannelDriver (real senders + placement reader) into the harness and the real
|
|
18
|
+
* `POST /pool/transfer` into `transfer`.
|
|
19
|
+
*
|
|
20
|
+
* Honesty contract: a non-moved seat THROWS (never records a misleading PASS); a moved
|
|
21
|
+
* seat that then fails to reply-from-target is a normal harness FAIL (recorded with the
|
|
22
|
+
* responder mismatch). The artifact only ever shows PASS when the seat moved AND the
|
|
23
|
+
* reply genuinely came from the target machine.
|
|
24
|
+
*/
|
|
25
|
+
import type { LiveTestHarness } from './LiveTestHarness.js';
|
|
26
|
+
export interface TransferResult {
|
|
27
|
+
/** Did the seat genuinely move (the honest signal, NOT a bare ok:true)? */
|
|
28
|
+
seatMoved: boolean;
|
|
29
|
+
detail?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface MultiMachineCapstoneOpts {
|
|
32
|
+
featureId?: string;
|
|
33
|
+
/** The machine the seat must move to (e.g. the Mini's machine id) — also the expected responder. */
|
|
34
|
+
targetMachine: string;
|
|
35
|
+
/** The throwaway Telegram topic id (also the placement key the responder reader uses). */
|
|
36
|
+
telegramTopicId: string;
|
|
37
|
+
/** Optional Slack channel id for the Slack half of the Telegram-AND-Slack bar. */
|
|
38
|
+
slackChannelId?: string;
|
|
39
|
+
/** The real transfer action (server.ts injects POST /pool/transfer). MUST report seatMoved honestly. */
|
|
40
|
+
transfer: (topicId: string, toMachine: string) => Promise<TransferResult>;
|
|
41
|
+
/** The user message to send (default a benign probe). */
|
|
42
|
+
message?: string;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
runId?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare class LiveTestRunnerError extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
export declare class LiveTestRunner {
|
|
50
|
+
private readonly deps;
|
|
51
|
+
constructor(deps: {
|
|
52
|
+
harness: LiveTestHarness;
|
|
53
|
+
logger?: (m: string) => void;
|
|
54
|
+
});
|
|
55
|
+
private log;
|
|
56
|
+
/**
|
|
57
|
+
* Run the multi-machine transfer capstone. Throws LiveTestRunnerError if the seat did
|
|
58
|
+
* not move (the capstone cannot meaningfully run). Otherwise returns the harness
|
|
59
|
+
* artifact (PASS only when the reply came FROM `targetMachine`).
|
|
60
|
+
*/
|
|
61
|
+
runMultiMachineTransferCapstone(opts: MultiMachineCapstoneOpts): Promise<{
|
|
62
|
+
artifact: import("./LiveTestArtifactStore.js").LiveTestArtifact;
|
|
63
|
+
entry: ReturnType<import("./LiveTestArtifactStore.js").LiveTestArtifactStore["write"]>;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=LiveTestRunner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveTestRunner.d.ts","sourceRoot":"","sources":["../../src/core/LiveTestRunner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkC,MAAM,sBAAsB,CAAC;AAG5F,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oGAAoG;IACpG,aAAa,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,eAAe,EAAE,MAAM,CAAC;IACxB,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wGAAwG;IACxG,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAC1E,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAC5B;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE;QAAE,OAAO,EAAE,eAAe,CAAC;QAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE;IAE7F,OAAO,CAAC,GAAG;IAEX;;;;OAIG;IACG,+BAA+B,CAAC,IAAI,EAAE,wBAAwB;;;;CAiDrE"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiveTestRunner — the orchestrator that APPLIES the live-user-channel-proof standard
|
|
3
|
+
* to the cross-machine transfer (docs/specs/live-user-channel-proof-standard.md §6 —
|
|
4
|
+
* the first feature held to the bar). It encodes the capstone flow that a user-role
|
|
5
|
+
* session runs:
|
|
6
|
+
*
|
|
7
|
+
* 1. MOVE THE SEAT FIRST — transfer the (throwaway) topic to the target machine.
|
|
8
|
+
* If the transfer reports the seat did NOT move, the capstone premise is false and
|
|
9
|
+
* we stop loudly (that "ok:true but moved nothing" lie is the original bug — we
|
|
10
|
+
* refuse to paper over it).
|
|
11
|
+
* 2. Then run the LiveTestHarness over a scenario matrix that sends a real message
|
|
12
|
+
* through each channel and asserts the reply was served FROM the target machine
|
|
13
|
+
* (the `responderMachine` expectation — the deterministic cross-machine proof).
|
|
14
|
+
*
|
|
15
|
+
* This module is pure orchestration over the injected harness + an injected `transfer`
|
|
16
|
+
* action, so it is unit-testable with fakes. The server.ts route wires the real
|
|
17
|
+
* RealChannelDriver (real senders + placement reader) into the harness and the real
|
|
18
|
+
* `POST /pool/transfer` into `transfer`.
|
|
19
|
+
*
|
|
20
|
+
* Honesty contract: a non-moved seat THROWS (never records a misleading PASS); a moved
|
|
21
|
+
* seat that then fails to reply-from-target is a normal harness FAIL (recorded with the
|
|
22
|
+
* responder mismatch). The artifact only ever shows PASS when the seat moved AND the
|
|
23
|
+
* reply genuinely came from the target machine.
|
|
24
|
+
*/
|
|
25
|
+
export class LiveTestRunnerError extends Error {
|
|
26
|
+
constructor(message) { super(message); this.name = 'LiveTestRunnerError'; }
|
|
27
|
+
}
|
|
28
|
+
export class LiveTestRunner {
|
|
29
|
+
deps;
|
|
30
|
+
constructor(deps) {
|
|
31
|
+
this.deps = deps;
|
|
32
|
+
}
|
|
33
|
+
log(m) { this.deps.logger?.(`[live-test-runner] ${m}`); }
|
|
34
|
+
/**
|
|
35
|
+
* Run the multi-machine transfer capstone. Throws LiveTestRunnerError if the seat did
|
|
36
|
+
* not move (the capstone cannot meaningfully run). Otherwise returns the harness
|
|
37
|
+
* artifact (PASS only when the reply came FROM `targetMachine`).
|
|
38
|
+
*/
|
|
39
|
+
async runMultiMachineTransferCapstone(opts) {
|
|
40
|
+
// 1. Move the seat FIRST — and demand the honest seatMoved signal.
|
|
41
|
+
const t = await opts.transfer(opts.telegramTopicId, opts.targetMachine);
|
|
42
|
+
if (!t.seatMoved) {
|
|
43
|
+
throw new LiveTestRunnerError(`transfer to ${opts.targetMachine} did not move the seat (${t.detail ?? 'seatMoved=false'}) — ` +
|
|
44
|
+
`capstone cannot run (refusing to record a misleading PASS over a non-move)`);
|
|
45
|
+
}
|
|
46
|
+
this.log(`seat moved to ${opts.targetMachine}; running channel scenarios`);
|
|
47
|
+
// 2. Build the matrix — each channel asserts the reply was served FROM targetMachine.
|
|
48
|
+
const message = (opts.message ?? `live-test probe ${opts.runId ?? ''}`).trim();
|
|
49
|
+
const surfaces = opts.slackChannelId ? ['telegram', 'slack'] : ['telegram'];
|
|
50
|
+
const scenarios = [
|
|
51
|
+
{
|
|
52
|
+
id: 'mm-transfer-telegram-reply-from-target',
|
|
53
|
+
description: `after transfer to ${opts.targetMachine}, the Telegram reply is served FROM it`,
|
|
54
|
+
surface: 'telegram',
|
|
55
|
+
riskCategory: 'happy-path',
|
|
56
|
+
volatility: 'safe',
|
|
57
|
+
channelId: opts.telegramTopicId,
|
|
58
|
+
input: message,
|
|
59
|
+
expect: { replyNotEmpty: true, responderMachine: opts.targetMachine },
|
|
60
|
+
...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
if (opts.slackChannelId) {
|
|
64
|
+
scenarios.push({
|
|
65
|
+
id: 'mm-transfer-slack-reply-from-target',
|
|
66
|
+
description: `after transfer to ${opts.targetMachine}, the Slack reply is served FROM it (channel parity)`,
|
|
67
|
+
surface: 'slack',
|
|
68
|
+
riskCategory: 'channel-parity',
|
|
69
|
+
volatility: 'safe',
|
|
70
|
+
channelId: opts.slackChannelId,
|
|
71
|
+
input: message,
|
|
72
|
+
expect: { replyNotEmpty: true, responderMachine: opts.targetMachine },
|
|
73
|
+
...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const matrix = {
|
|
77
|
+
featureId: opts.featureId ?? 'multi-machine-transfer',
|
|
78
|
+
surfaces,
|
|
79
|
+
riskCategories: opts.slackChannelId ? ['happy-path', 'channel-parity'] : ['happy-path'],
|
|
80
|
+
scenarios,
|
|
81
|
+
};
|
|
82
|
+
return this.deps.harness.run(matrix, opts.runId ? { runId: opts.runId } : {});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=LiveTestRunner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveTestRunner.js","sourceRoot":"","sources":["../../src/core/LiveTestRunner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlacementResponderReader — resolves WHICH machine served a reply, for the
|
|
3
|
+
* RealChannelDriver's injected `resolveResponderMachine` seam
|
|
4
|
+
* (docs/specs/live-user-channel-proof-standard.md §5.6 — the deterministic
|
|
5
|
+
* cross-machine proof). It maps a (surface, channelId) to the topic that
|
|
6
|
+
* `GET /pool/placement?topic=N` is keyed on, reads the authoritative owner, and
|
|
7
|
+
* returns that machine id.
|
|
8
|
+
*
|
|
9
|
+
* Why this is the right reader: `/pool/placement` is the authoritative placement
|
|
10
|
+
* surface (it proxies to the lease-holder, so any machine can answer), and its
|
|
11
|
+
* `owner` field is the machine currently serving the topic — exactly "who replied"
|
|
12
|
+
* after a transfer. We read `owner` (not `pinnedTo`): a pin is the INTENT to place;
|
|
13
|
+
* `owner` is who actually holds it. For the capstone that distinction is the whole
|
|
14
|
+
* point — a pin that didn't move the seat is the original bug.
|
|
15
|
+
*
|
|
16
|
+
* Parameterized for testability: the HTTP GET + the (surface, channelId)→topicId
|
|
17
|
+
* mapping are injected, so this is unit-testable with no live server. MUST NOT throw
|
|
18
|
+
* on a transient read error — it returns null so the RealChannelDriver degrades to an
|
|
19
|
+
* unattributed reply rather than failing the whole scenario.
|
|
20
|
+
*/
|
|
21
|
+
import type { Surface } from './LiveTestArtifactStore.js';
|
|
22
|
+
export interface PlacementResult {
|
|
23
|
+
owner: string | null;
|
|
24
|
+
ownerNickname?: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface PlacementResponderReaderDeps {
|
|
27
|
+
/**
|
|
28
|
+
* Map (surface, channelId) → the topic id `/pool/placement` is keyed on. For
|
|
29
|
+
* telegram, channelId IS the topic. For slack, the caller injects the
|
|
30
|
+
* channel→topic resolution (the SlackAdapter's session keying). Return null when
|
|
31
|
+
* the channel doesn't map to a placement-tracked topic.
|
|
32
|
+
*/
|
|
33
|
+
topicForChannel: (surface: Surface, channelId: string) => string | null;
|
|
34
|
+
/** GET /pool/placement?topic=<id> → the placement (owner). Throwing/transient errors are tolerated. */
|
|
35
|
+
fetchPlacement: (topicId: string) => Promise<PlacementResult | null>;
|
|
36
|
+
logger?: (m: string) => void;
|
|
37
|
+
}
|
|
38
|
+
export declare class PlacementResponderReader {
|
|
39
|
+
private readonly d;
|
|
40
|
+
constructor(deps: PlacementResponderReaderDeps);
|
|
41
|
+
private log;
|
|
42
|
+
/** The function shape RealChannelDriver injects: (surface, channelId) → machine id | null. */
|
|
43
|
+
resolve: (surface: Surface, channelId: string) => Promise<string | null>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=PlacementResponderReader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlacementResponderReader.d.ts","sourceRoot":"","sources":["../../src/core/PlacementResponderReader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,4BAA4B;IAC3C;;;;;OAKG;IACH,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACxE,uGAAuG;IACvG,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IACrE,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,qBAAa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAA+B;gBACrC,IAAI,EAAE,4BAA4B;IAE9C,OAAO,CAAC,GAAG;IAEX,8FAA8F;IAC9F,OAAO,GAAU,SAAS,OAAO,EAAE,WAAW,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiB3E;CACH"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlacementResponderReader — resolves WHICH machine served a reply, for the
|
|
3
|
+
* RealChannelDriver's injected `resolveResponderMachine` seam
|
|
4
|
+
* (docs/specs/live-user-channel-proof-standard.md §5.6 — the deterministic
|
|
5
|
+
* cross-machine proof). It maps a (surface, channelId) to the topic that
|
|
6
|
+
* `GET /pool/placement?topic=N` is keyed on, reads the authoritative owner, and
|
|
7
|
+
* returns that machine id.
|
|
8
|
+
*
|
|
9
|
+
* Why this is the right reader: `/pool/placement` is the authoritative placement
|
|
10
|
+
* surface (it proxies to the lease-holder, so any machine can answer), and its
|
|
11
|
+
* `owner` field is the machine currently serving the topic — exactly "who replied"
|
|
12
|
+
* after a transfer. We read `owner` (not `pinnedTo`): a pin is the INTENT to place;
|
|
13
|
+
* `owner` is who actually holds it. For the capstone that distinction is the whole
|
|
14
|
+
* point — a pin that didn't move the seat is the original bug.
|
|
15
|
+
*
|
|
16
|
+
* Parameterized for testability: the HTTP GET + the (surface, channelId)→topicId
|
|
17
|
+
* mapping are injected, so this is unit-testable with no live server. MUST NOT throw
|
|
18
|
+
* on a transient read error — it returns null so the RealChannelDriver degrades to an
|
|
19
|
+
* unattributed reply rather than failing the whole scenario.
|
|
20
|
+
*/
|
|
21
|
+
export class PlacementResponderReader {
|
|
22
|
+
d;
|
|
23
|
+
constructor(deps) { this.d = deps; }
|
|
24
|
+
log(m) { this.d.logger?.(`[placement-responder-reader] ${m}`); }
|
|
25
|
+
/** The function shape RealChannelDriver injects: (surface, channelId) → machine id | null. */
|
|
26
|
+
resolve = async (surface, channelId) => {
|
|
27
|
+
const topicId = this.d.topicForChannel(surface, channelId);
|
|
28
|
+
if (!topicId) {
|
|
29
|
+
this.log(`no placement-tracked topic for ${surface}:${channelId}`);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const p = await this.d.fetchPlacement(topicId);
|
|
34
|
+
return p?.owner ?? null;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
// @silent-fallback-ok: a transient /pool/placement read failure degrades this
|
|
38
|
+
// best-effort responder-machine lookup to null — the RealChannelDriver then
|
|
39
|
+
// proceeds with an unattributed reply rather than throwing the whole scenario.
|
|
40
|
+
// The failure is logged with topic context; it is not a gating/authority path.
|
|
41
|
+
this.log(`fetchPlacement failed for topic ${topicId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=PlacementResponderReader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlacementResponderReader.js","sourceRoot":"","sources":["../../src/core/PlacementResponderReader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA4BH,MAAM,OAAO,wBAAwB;IAClB,CAAC,CAA+B;IACjD,YAAY,IAAkC,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAE1D,GAAG,CAAC,CAAS,IAAU,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,gCAAgC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAEtF,8FAA8F;IAC9F,OAAO,GAAG,KAAK,EAAE,OAAgB,EAAE,SAAiB,EAA0B,EAAE;QAC9E,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QAC3D,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,kCAAkC,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC;YACnE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;YAC/C,OAAO,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;QAC1B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,8EAA8E;YAC9E,4EAA4E;YAC5E,+EAA+E;YAC/E,+EAA+E;YAC/E,IAAI,CAAC,GAAG,CAAC,mCAAmC,OAAO,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC5G,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC;CACH"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SlackLiveSender — the real Slack `SurfaceSender` for the live-test harness
|
|
3
|
+
* (docs/specs/live-user-channel-proof-standard.md §5.4). It posts a message into a
|
|
4
|
+
* Slack channel AS A NON-AGENT IDENTITY (a user/second-bot token, NOT Echo's own bot
|
|
5
|
+
* — Echo never replies to itself) and then waits for the AGENT's reply by polling the
|
|
6
|
+
* channel history for a message from the agent's bot user id.
|
|
7
|
+
*
|
|
8
|
+
* Parameterization is deliberate: the sender takes an already-constructed
|
|
9
|
+
* `SlackApiClient` (carrying the non-Echo sender token) + the agent's bot user id. So
|
|
10
|
+
* the CODE is complete and unit-testable here; the only thing that has to be provided
|
|
11
|
+
* at wiring time is the sender CREDENTIAL (a user token / second-bot token in the
|
|
12
|
+
* demo workspace). That credential is the one piece that may need provisioning — the
|
|
13
|
+
* logic does not.
|
|
14
|
+
*
|
|
15
|
+
* `awaitReply` identifies the agent's reply DETERMINISTICALLY (a message strictly
|
|
16
|
+
* after the sent ts whose author is the agent's bot user id), never a fuzzy guess, and
|
|
17
|
+
* resolves null on timeout (the harness records that as a FAIL with reason). The
|
|
18
|
+
* responder-MACHINE attribution (the cross-machine proof) is NOT done here — that is
|
|
19
|
+
* the RealChannelDriver's injected placement reader; this sender only returns the
|
|
20
|
+
* reply text + id.
|
|
21
|
+
*/
|
|
22
|
+
import type { SurfaceSender } from './RealChannelDriver.js';
|
|
23
|
+
import type { SendResult, ReplyResult } from './LiveTestHarness.js';
|
|
24
|
+
/** Minimal Slack client surface this sender needs (matches SlackApiClient.call). */
|
|
25
|
+
export interface SlackCaller {
|
|
26
|
+
call(method: string, params?: Record<string, unknown>): Promise<{
|
|
27
|
+
ok?: boolean;
|
|
28
|
+
ts?: string;
|
|
29
|
+
messages?: Array<{
|
|
30
|
+
ts: string;
|
|
31
|
+
user?: string;
|
|
32
|
+
bot_id?: string;
|
|
33
|
+
text?: string;
|
|
34
|
+
subtype?: string;
|
|
35
|
+
}>;
|
|
36
|
+
[k: string]: unknown;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export interface SlackLiveSenderDeps {
|
|
40
|
+
/** A SlackApiClient constructed with the NON-AGENT sender token (the user-role identity). */
|
|
41
|
+
api: SlackCaller;
|
|
42
|
+
/** The agent's (Echo's) Slack bot user id — awaitReply waits for a reply authored by THIS id. */
|
|
43
|
+
agentBotUserId: string;
|
|
44
|
+
/** Poll cadence while awaiting a reply. Default 2000ms. */
|
|
45
|
+
pollIntervalMs?: number;
|
|
46
|
+
/** Injected for tests; defaults to real timers / clock. */
|
|
47
|
+
sleep?: (ms: number) => Promise<void>;
|
|
48
|
+
now?: () => number;
|
|
49
|
+
logger?: (m: string) => void;
|
|
50
|
+
}
|
|
51
|
+
export declare class SlackLiveSender implements SurfaceSender {
|
|
52
|
+
private readonly d;
|
|
53
|
+
constructor(deps: SlackLiveSenderDeps);
|
|
54
|
+
private now;
|
|
55
|
+
private sleep;
|
|
56
|
+
private log;
|
|
57
|
+
send(channelId: string, text: string): Promise<SendResult>;
|
|
58
|
+
awaitReply(channelId: string, opts: {
|
|
59
|
+
timeoutMs: number;
|
|
60
|
+
afterMessageId?: string;
|
|
61
|
+
}): Promise<Omit<ReplyResult, 'responderMachineId'> | null>;
|
|
62
|
+
/** Find the FIRST agent-authored message strictly after `after` (oldest-first). */
|
|
63
|
+
private findAgentReply;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=SlackLiveSender.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SlackLiveSender.d.ts","sourceRoot":"","sources":["../../src/core/SlackLiveSender.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEpE,oFAAoF;AACpF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QAC9D,EAAE,CAAC,EAAE,OAAO,CAAC;QACb,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE,KAAK,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAClG,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;KACtB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,mBAAmB;IAClC,6FAA6F;IAC7F,GAAG,EAAE,WAAW,CAAC;IACjB,iGAAiG;IACjG,cAAc,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,qBAAa,eAAgB,YAAW,aAAa;IACnD,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAsB;gBAC5B,IAAI,EAAE,mBAAmB;IAErC,OAAO,CAAC,GAAG;YACG,KAAK;IAGnB,OAAO,CAAC,GAAG;IAEL,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAU1D,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,oBAAoB,CAAC,GAAG,IAAI,CAAC;IAelJ,mFAAmF;YACrE,cAAc;CAkB7B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SlackLiveSender — the real Slack `SurfaceSender` for the live-test harness
|
|
3
|
+
* (docs/specs/live-user-channel-proof-standard.md §5.4). It posts a message into a
|
|
4
|
+
* Slack channel AS A NON-AGENT IDENTITY (a user/second-bot token, NOT Echo's own bot
|
|
5
|
+
* — Echo never replies to itself) and then waits for the AGENT's reply by polling the
|
|
6
|
+
* channel history for a message from the agent's bot user id.
|
|
7
|
+
*
|
|
8
|
+
* Parameterization is deliberate: the sender takes an already-constructed
|
|
9
|
+
* `SlackApiClient` (carrying the non-Echo sender token) + the agent's bot user id. So
|
|
10
|
+
* the CODE is complete and unit-testable here; the only thing that has to be provided
|
|
11
|
+
* at wiring time is the sender CREDENTIAL (a user token / second-bot token in the
|
|
12
|
+
* demo workspace). That credential is the one piece that may need provisioning — the
|
|
13
|
+
* logic does not.
|
|
14
|
+
*
|
|
15
|
+
* `awaitReply` identifies the agent's reply DETERMINISTICALLY (a message strictly
|
|
16
|
+
* after the sent ts whose author is the agent's bot user id), never a fuzzy guess, and
|
|
17
|
+
* resolves null on timeout (the harness records that as a FAIL with reason). The
|
|
18
|
+
* responder-MACHINE attribution (the cross-machine proof) is NOT done here — that is
|
|
19
|
+
* the RealChannelDriver's injected placement reader; this sender only returns the
|
|
20
|
+
* reply text + id.
|
|
21
|
+
*/
|
|
22
|
+
export class SlackLiveSender {
|
|
23
|
+
d;
|
|
24
|
+
constructor(deps) { this.d = deps; }
|
|
25
|
+
now() { return (this.d.now ?? Date.now)(); }
|
|
26
|
+
async sleep(ms) {
|
|
27
|
+
return (this.d.sleep ?? ((m) => new Promise((r) => setTimeout(r, m))))(ms);
|
|
28
|
+
}
|
|
29
|
+
log(m) { this.d.logger?.(`[slack-live-sender] ${m}`); }
|
|
30
|
+
async send(channelId, text) {
|
|
31
|
+
const res = await this.d.api.call('chat.postMessage', { channel: channelId, text });
|
|
32
|
+
if (!res.ts) {
|
|
33
|
+
// A post with no ts is a real failure — surface it (the harness records a driver
|
|
34
|
+
// error FAIL), never fabricate a messageId.
|
|
35
|
+
throw new Error(`chat.postMessage returned no ts (ok=${res.ok}) — message not posted`);
|
|
36
|
+
}
|
|
37
|
+
return { messageId: res.ts };
|
|
38
|
+
}
|
|
39
|
+
async awaitReply(channelId, opts) {
|
|
40
|
+
const deadline = this.now() + opts.timeoutMs;
|
|
41
|
+
const pollMs = this.d.pollIntervalMs ?? 2000;
|
|
42
|
+
const after = opts.afterMessageId; // a Slack ts string; lexicographic compare works for ts
|
|
43
|
+
// Poll at least once even if timeoutMs is ~0.
|
|
44
|
+
for (let first = true; first || this.now() < deadline; first = false) {
|
|
45
|
+
const reply = await this.findAgentReply(channelId, after);
|
|
46
|
+
if (reply)
|
|
47
|
+
return reply;
|
|
48
|
+
if (this.now() >= deadline)
|
|
49
|
+
break;
|
|
50
|
+
await this.sleep(pollMs);
|
|
51
|
+
}
|
|
52
|
+
this.log(`no agent reply in ${channelId} within ${opts.timeoutMs}ms`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** Find the FIRST agent-authored message strictly after `after` (oldest-first). */
|
|
56
|
+
async findAgentReply(channelId, after) {
|
|
57
|
+
const res = await this.d.api.call('conversations.history', {
|
|
58
|
+
channel: channelId,
|
|
59
|
+
...(after ? { oldest: after, inclusive: false } : {}),
|
|
60
|
+
limit: 100,
|
|
61
|
+
});
|
|
62
|
+
const messages = res.messages ?? [];
|
|
63
|
+
// conversations.history returns newest-first; scan oldest-first so we return the
|
|
64
|
+
// EARLIEST agent reply after the prompt (deterministic).
|
|
65
|
+
const oldestFirst = [...messages].sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));
|
|
66
|
+
for (const m of oldestFirst) {
|
|
67
|
+
if (after && !(m.ts > after))
|
|
68
|
+
continue; // strictly-after guard (belt + suspenders vs `oldest`)
|
|
69
|
+
if (m.user !== this.d.agentBotUserId)
|
|
70
|
+
continue; // only the AGENT's reply
|
|
71
|
+
const text = m.text ?? '';
|
|
72
|
+
return { text, messageId: m.ts };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=SlackLiveSender.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SlackLiveSender.js","sourceRoot":"","sources":["../../src/core/SlackLiveSender.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-16T12:37:17.394Z",
|
|
5
|
+
"instarVersion": "1.3.598",
|
|
6
6
|
"entryCount": 201,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
`LiveTestRunner` (spec §6) — the orchestrator that applies the live-test standard to the cross-machine transfer, ships DARK (not wired). It transfers the throwaway topic to the target machine, demands the honest `seatMoved` signal (throws if the seat didn't move — no misleading PASS over the original "ok:true but moved nothing" bug), then runs the LiveTestHarness over a matrix asserting the reply was served FROM the target machine (the cross-machine proof). Pure orchestration over an injected harness + transfer action.
|
|
9
|
+
|
|
10
|
+
## What to Tell Your User
|
|
11
|
+
|
|
12
|
+
Nothing yet — internal harness infrastructure, ships dark (no runtime surface). The payoff is the capstone: proving a real Laptop↔Mini seat move with a reply served from the destination, through the real channels.
|
|
13
|
+
|
|
14
|
+
## Summary of New Capabilities
|
|
15
|
+
|
|
16
|
+
None user-facing. Internally: the orchestration that runs the multi-machine transfer capstone and records the signed PASS/FAIL artifact. No new routes, config, or flags (the route wrapper is the next increment).
|
|
17
|
+
|
|
18
|
+
## Evidence
|
|
19
|
+
|
|
20
|
+
- `tests/unit/LiveTestRunner.test.ts` — 5 tests: non-move throws (no harness run), moved+reply-from-target→PASS, moved+wrong-machine→FAIL, slack channel-parity scenario added, empty-reply→FAIL.
|
|
21
|
+
- `tsc --noEmit` clean. instar-dev gate green.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Two more pure building blocks for the live-test harness (spec live-user-channel-proof-standard §5.4/§5.6), ships DARK (not wired):
|
|
9
|
+
- `SlackLiveSender` — a real Slack `SurfaceSender`: posts as a non-agent identity via `chat.postMessage`, awaits the agent's reply by polling `conversations.history` (deterministic: a message strictly after the prompt, authored by the agent's bot user id). Parameterized on the sender token so only the credential needs provisioning.
|
|
10
|
+
- `PlacementResponderReader` — the injectable `resolveResponderMachine` for RealChannelDriver: maps (surface, channelId)→topic, reads `/pool/placement` `owner` (the machine that actually holds the seat — the cross-machine proof), tolerates read errors (returns null).
|
|
11
|
+
|
|
12
|
+
## What to Tell Your User
|
|
13
|
+
|
|
14
|
+
Nothing yet — internal infrastructure for the gold-standard live-testing harness, ships dark (no runtime surface, no behavior change). The payoff is when the harness drives the real Slack channel and proves which machine served the reply.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
None user-facing. Internally: the real Slack drive + the cross-machine "which machine answered" reader that the live-test harness needs. No new routes, no config, no flags.
|
|
19
|
+
|
|
20
|
+
## Evidence
|
|
21
|
+
|
|
22
|
+
- `tests/unit/SlackLiveSender.test.ts` — 7 tests: post→ts, no-ts throw, agent-reply-after-prompt, ignores non-agent + stale messages, poll-until-appears, timeout→null, no-afterId.
|
|
23
|
+
- `tests/unit/PlacementResponderReader.test.ts` — 7 tests: telegram channel==topic, owner reflects seat move, slack channel→topic mapping, no-topic→null (no fetch), owner-null→null, fetch-error→null, arrow-bound detached reference.
|
|
24
|
+
- `tsc --noEmit` clean. instar-dev gate green.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Side-Effects Review — LiveTestRunner (multi-machine transfer capstone orchestrator)
|
|
2
|
+
|
|
3
|
+
**Slug:** live-test-runner
|
|
4
|
+
**Spec:** docs/specs/live-user-channel-proof-standard.md §6 (apply the standard to multi-machine — the first feature)
|
|
5
|
+
**Files:** src/core/LiveTestRunner.ts, tests/unit/LiveTestRunner.test.ts
|
|
6
|
+
**Posture:** ships DARK — pure orchestration over an injected harness + transfer action. NOT wired into server.ts.
|
|
7
|
+
|
|
8
|
+
## What it is
|
|
9
|
+
LiveTestRunner encodes the capstone flow: (1) transfer the throwaway topic to the target machine and DEMAND the honest `seatMoved` signal (throw if it didn't move — refuse a misleading PASS over the original "ok:true but moved nothing" bug); (2) run the LiveTestHarness over a matrix that sends a real message per channel and asserts the reply was served FROM the target machine (the `responderMachine` expectation = the cross-machine proof).
|
|
10
|
+
|
|
11
|
+
## Phase 1 — Principle check (signal vs authority)
|
|
12
|
+
Not a decision gate over agent behavior — it's a test orchestrator. The one control-flow decision (throw on `seatMoved===false`) is a FAIL-LOUD on a test premise, not a runtime authority over messages/sessions. Compliant: it produces a PASS/FAIL artifact (a signal the completion gate already consumes), holds no blocking authority.
|
|
13
|
+
|
|
14
|
+
## Phase 4 — Side-effects answers
|
|
15
|
+
1. **Over-block** — n/a (no runtime block). Throwing on a non-moved seat is intended: it prevents recording a PASS when the premise failed. The alternative (running anyway) would manufacture a misleading verdict.
|
|
16
|
+
2. **Under-block** — the runner trusts the injected `transfer`'s `seatMoved`. If a transfer LIES (reports seatMoved=true but didn't move), the harness still catches it downstream: the reply's `responderMachine` won't equal target → FAIL. So a lying transfer can't produce a false PASS — the responder assertion is the real backstop.
|
|
17
|
+
3. **Level-of-abstraction fit** — correct: thin orchestration over the merged harness + an injected transfer. It does NOT reimplement transfer or the harness; it sequences them. The server.ts route injects the real `/pool/transfer` + RealChannelDriver.
|
|
18
|
+
4. **Signal vs authority** — compliant (produces an artifact; no authority).
|
|
19
|
+
5. **Interactions** — none yet (dark, unwired). When wired, it CALLS `/pool/transfer` (a real seat move on a THROWAWAY topic) then sends real messages — those surfaces are the server.ts route's review. The runner itself only sequences injected actions.
|
|
20
|
+
6. **External surfaces** — none in this increment (unwired). When wired, the real transfer + sends are external — but scoped to a throwaway topic/demo channel by the caller (the §5.3 isolation in DemoChannelRegistry + the throwaway-topic choice).
|
|
21
|
+
7. **Multi-machine posture** — this IS the multi-machine capstone: it proves a seat moves between machines and the reply is served from the destination. The `responderMachine` assertion (via the driver's placement reader, which reads the authoritative `/pool/placement`) is the cross-machine proof. No single-machine assumption — the whole point is cross-machine.
|
|
22
|
+
8. **Rollback cost** — trivial: dark, unwired. Revert the commit.
|
|
23
|
+
|
|
24
|
+
## No-deferrals
|
|
25
|
+
The server.ts route (`POST /live-test/run`) that wires the real driver + transfer, and the demo-channel sender-credential provisioning, are the NEXT tracked steps (CMT-1568, `.instar/plans/live-test-harness-drivers-BUILD.md` GATE 1/2), not deferrals of this orchestrator. This module is complete + unit-tested (5 tests: non-move throws, moved+reply-from-target PASS, wrong-machine FAIL, slack-parity scenario, empty-reply FAIL).
|
|
26
|
+
|
|
27
|
+
## Phase 5 — Second-pass review
|
|
28
|
+
Borderline (it has a control-flow throw), but it adds no runtime authority over messages/sessions/lifecycle — it orchestrates a test and records an artifact. The genuine cross-machine safety (no false PASS) rests on the harness's responder assertion (reviewed in #1195/#1196), which this can't bypass. Not required; noted.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Side-Effects Review — Live-test Slack sender + placement responder reader
|
|
2
|
+
|
|
3
|
+
**Slug:** live-test-slack-sender
|
|
4
|
+
**Spec:** docs/specs/live-user-channel-proof-standard.md §5.4 (Platform-Sanctioned Automation) + §5.6 (deterministic cross-machine proof)
|
|
5
|
+
**Files:** src/core/SlackLiveSender.ts, src/core/PlacementResponderReader.ts, tests/unit/SlackLiveSender.test.ts, tests/unit/PlacementResponderReader.test.ts
|
|
6
|
+
**Posture:** ships DARK — two pure/injectable building blocks, NOT wired into server.ts. No route, no runtime construction. Stacked on the driver core (PR #1195).
|
|
7
|
+
|
|
8
|
+
## What it is
|
|
9
|
+
- **SlackLiveSender** — a real Slack `SurfaceSender`: posts a message AS A NON-AGENT identity (a user/second-bot token, injected) via `chat.postMessage`, and `awaitReply` polls `conversations.history` for the AGENT's reply (a message strictly after the sent ts authored by the agent's bot user id). Deterministic match; null on timeout; throws (never fabricates an id) if a post returns no ts.
|
|
10
|
+
- **PlacementResponderReader** — the injectable `resolveResponderMachine` for RealChannelDriver: maps (surface, channelId)→topic, reads `GET /pool/placement` `owner`, returns the serving machine id. Tolerates read errors (returns null). Reads `owner` (who holds the seat), NOT `pinnedTo` (the intent) — the distinction IS the capstone.
|
|
11
|
+
|
|
12
|
+
## Phase 1 — Principle check (signal vs authority)
|
|
13
|
+
Neither module is a decision point. SlackLiveSender is transport (post + poll); PlacementResponderReader is a read that returns a machine id. Neither blocks, filters, or gates anything — they produce data the harness asserts on. No brittle blocking authority added. Compliant.
|
|
14
|
+
|
|
15
|
+
## Phase 4 — Side-effects answers
|
|
16
|
+
1. **Over-block** — n/a (no block surface). Worst case: SlackLiveSender's reply-match is too strict and misses a genuine agent reply → the scenario records a FAIL/no-reply. Mitigation: it matches on the agent's bot user id (the deterministic author), and polls the full window.
|
|
17
|
+
2. **Under-block** — n/a. A risk: matching a STALE agent message as the reply. Mitigated by the strictly-after-ts guard (both via `oldest`+`inclusive:false` AND an explicit `m.ts > after` belt-and-suspenders) and oldest-first scan returning the EARLIEST post-prompt agent message.
|
|
18
|
+
3. **Level-of-abstraction fit** — correct: both are the concrete adapters the harness's injected seams (`SurfaceSender`, `resolveResponderMachine`) were designed for. They wrap existing surfaces (`SlackApiClient`, `/pool/placement`) rather than reinventing them.
|
|
19
|
+
4. **Signal vs authority** — compliant (transport + read, no authority). See Phase 1.
|
|
20
|
+
5. **Interactions** — none yet (dark, unwired). SlackLiveSender uses a SEPARATE sender token from Echo's bot, so it can't be confused with Echo's own outbound; it reads history read-only. PlacementResponderReader only GETs placement. No shadowing/double-fire.
|
|
21
|
+
6. **External surfaces** — when wired, SlackLiveSender WILL post a real message into a Slack channel (the demo workspace) and PlacementResponderReader WILL GET the local placement route. In THIS increment: no wiring, so no external surface. The sender token/identity is the one piece that needs provisioning (a user/second-bot token in the demo workspace) — the code is parameterized on it.
|
|
22
|
+
7. **Multi-machine posture** — PlacementResponderReader IS the multi-machine attribution: it reads the authoritative `/pool/placement` (proxied to the lease-holder), so it answers "which machine served this" correctly regardless of which machine runs the harness. SlackLiveSender is machine-agnostic (talks to Slack's API). No single-machine assumption.
|
|
23
|
+
8. **Rollback cost** — trivial: dark, unwired. Revert the commit.
|
|
24
|
+
|
|
25
|
+
## No-deferrals
|
|
26
|
+
The Telegram live sender + the runner route are the NEXT tracked increment (CMT-1568, `.instar/plans/live-test-harness-drivers-BUILD.md`), not a deferral of this one. This increment is complete + fully unit-tested (14 tests). The Slack/Telegram SENDER-IDENTITY provisioning (a user token / demo bot) is an external-credential dependency tracked in the plan, explicitly NOT a code deferral — the code is parameterized so only the credential is missing.
|
|
27
|
+
|
|
28
|
+
## Phase 5 — Second-pass review
|
|
29
|
+
Not required: neither module adds a block/allow decision, a session-lifecycle action, a sentinel/guard/gate/watchdog, or a coherence/trust surface (the Phase-5 triggers). Both are transport/read adapters. The driver core that DOES touch the §5.3 block (DemoChannelRegistry) had its second-pass in PR #1195.
|