harnery 0.34.0 → 0.35.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/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/agents.d.ts +15 -4
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +91 -162
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +26 -2
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +2 -2
- package/dist/commands/events.d.ts.map +1 -1
- package/dist/commands/events.js +18 -1
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +2 -2
- package/dist/commands/semantic.d.ts +4 -0
- package/dist/commands/semantic.d.ts.map +1 -0
- package/dist/commands/semantic.js +147 -0
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +17 -0
- package/dist/core/agents/canonical-emit.d.ts +3 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +12 -0
- package/dist/core/agents/claim-path.d.ts +11 -0
- package/dist/core/agents/claim-path.d.ts.map +1 -0
- package/dist/core/agents/claim-path.js +22 -0
- package/dist/core/agents/coord-client.d.ts +29 -3
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +88 -9
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +1 -3
- package/dist/core/agents/live-lifecycle-v3.js +3 -3
- package/dist/core/agents/live-observation-v3.d.ts.map +1 -1
- package/dist/core/agents/live-observation-v3.js +2 -2
- package/dist/core/agents/render/prompt-context.js +12 -10
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +23 -11
- package/dist/core/agents/session-name-display.d.ts +2 -1
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +10 -3
- package/dist/core/agents/state/heartbeat-reader.d.ts +46 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-reader.js +25 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts +3 -48
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +16 -34
- package/dist/core/agents/state/live-coordination-view.d.ts +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +25 -1
- package/dist/core/agents/state/names.d.ts +9 -1
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +190 -182
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +23 -5
- package/dist/core/artifacts/constants.d.ts +5 -0
- package/dist/core/artifacts/constants.d.ts.map +1 -0
- package/dist/core/artifacts/constants.js +4 -0
- package/dist/core/artifacts/index.d.ts +3 -3
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +50 -7
- package/dist/core/events/v3/base-contract.d.ts +795 -0
- package/dist/core/events/v3/base-contract.d.ts.map +1 -1
- package/dist/core/events/v3/base-contract.js +33 -0
- package/dist/core/events/v3/capabilities.d.ts +11 -0
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +23 -0
- package/dist/core/events/v3/generated.d.ts +1 -1
- package/dist/core/events/v3/generated.js +1 -1
- package/dist/core/events/v3/index.d.ts +4 -3
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +4 -3
- package/dist/core/events/v3/latency.d.ts +31 -1
- package/dist/core/events/v3/latency.d.ts.map +1 -1
- package/dist/core/events/v3/latency.js +127 -3
- package/dist/core/events/v3/live-routing.d.ts +30 -0
- package/dist/core/events/v3/live-routing.d.ts.map +1 -1
- package/dist/core/events/v3/live-routing.js +109 -1
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/command-recorder.js +10 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +14 -3
- package/dist/core/events/v3/producers/hook-base.d.ts +17 -1
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +82 -1
- package/dist/core/events/v3/producers/intake.d.ts +6 -0
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +11 -0
- package/dist/core/events/v3/producers/recorder.d.ts +62 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +654 -18
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +51 -0
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -0
- package/dist/core/events/v3/runtime-telemetry-capabilities.js +54 -0
- package/dist/core/events/v3/turn-telemetry.d.ts +9 -0
- package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.d.ts +3 -0
- package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.js +14 -0
- package/dist/core/hooks/adapter/runtime-telemetry.d.ts +119 -0
- package/dist/core/hooks/adapter/runtime-telemetry.d.ts.map +1 -0
- package/dist/core/hooks/adapter/runtime-telemetry.js +568 -0
- package/dist/core/hooks/cli.js +114 -38
- package/dist/core/hooks/remediation-cap.d.ts +34 -0
- package/dist/core/hooks/remediation-cap.d.ts.map +1 -0
- package/dist/core/hooks/remediation-cap.js +71 -0
- package/dist/core/hooks/resolve/transcript.d.ts +25 -9
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +35 -17
- package/dist/core/semantic/adapters.d.ts +34 -0
- package/dist/core/semantic/adapters.d.ts.map +1 -0
- package/dist/core/semantic/adapters.js +222 -0
- package/dist/core/semantic/contract.d.ts +588 -0
- package/dist/core/semantic/contract.d.ts.map +1 -0
- package/dist/core/semantic/contract.js +230 -0
- package/dist/core/semantic/evidence.d.ts +11 -0
- package/dist/core/semantic/evidence.d.ts.map +1 -0
- package/dist/core/semantic/evidence.js +262 -0
- package/dist/core/semantic/index.d.ts +10 -0
- package/dist/core/semantic/index.d.ts.map +1 -0
- package/dist/core/semantic/index.js +9 -0
- package/dist/core/semantic/once.d.ts +33 -0
- package/dist/core/semantic/once.d.ts.map +1 -0
- package/dist/core/semantic/once.js +293 -0
- package/dist/core/semantic/prompt.d.ts +12 -0
- package/dist/core/semantic/prompt.d.ts.map +1 -0
- package/dist/core/semantic/prompt.js +70 -0
- package/dist/core/semantic/scheduler.d.ts +16 -0
- package/dist/core/semantic/scheduler.d.ts.map +1 -0
- package/dist/core/semantic/scheduler.js +67 -0
- package/dist/core/semantic/service-status.d.ts +31 -0
- package/dist/core/semantic/service-status.d.ts.map +1 -0
- package/dist/core/semantic/service-status.js +84 -0
- package/dist/core/semantic/service.d.ts +51 -0
- package/dist/core/semantic/service.d.ts.map +1 -0
- package/dist/core/semantic/service.js +470 -0
- package/dist/core/semantic/storage.d.ts +71 -0
- package/dist/core/semantic/storage.d.ts.map +1 -0
- package/dist/core/semantic/storage.js +179 -0
- package/dist/core/semantic/validate.d.ts +16 -0
- package/dist/core/semantic/validate.d.ts.map +1 -0
- package/dist/core/semantic/validate.js +174 -0
- package/dist/lib/browser/client.d.ts +11 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +13 -0
- package/dist/lib/browser/index.d.ts +1 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/standalone-html.d.ts +25 -0
- package/dist/lib/browser/standalone-html.d.ts.map +1 -0
- package/dist/lib/browser/standalone-html.js +284 -0
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +1 -0
- package/dist/lib/instructions/templates.d.ts +2 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +144 -4
- package/package.json +6 -1
- package/schemas/event-v3.schema.json +2758 -478
- package/src/commander.ts +2 -0
- package/src/commands/agents.ts +104 -152
- package/src/commands/browse.ts +25 -2
- package/src/commands/decision.ts +8 -2
- package/src/commands/events.ts +21 -1
- package/src/commands/presence.ts +7 -2
- package/src/commands/semantic.ts +179 -0
- package/src/commands/web.ts +15 -0
- package/src/core/agents/canonical-emit.ts +15 -0
- package/src/core/agents/claim-path.ts +23 -0
- package/src/core/agents/coord-client.ts +92 -8
- package/src/core/agents/live-authority-v3.ts +1 -4
- package/src/core/agents/live-lifecycle-v3.ts +8 -3
- package/src/core/agents/live-observation-v3.ts +7 -2
- package/src/core/agents/render/prompt-context.ts +13 -10
- package/src/core/agents/rules/stop-hook.ts +24 -11
- package/src/core/agents/session-name-display.ts +11 -3
- package/src/core/agents/state/heartbeat-reader.ts +69 -0
- package/src/core/agents/state/heartbeat-writer.ts +20 -84
- package/src/core/agents/state/live-coordination-view.ts +26 -1
- package/src/core/agents/state/names.ts +190 -182
- package/src/core/agents/state/stale-sweep.ts +25 -5
- package/src/core/artifacts/constants.ts +4 -0
- package/src/core/artifacts/index.ts +49 -7
- package/src/core/events/v3/base-contract.ts +39 -0
- package/src/core/events/v3/capabilities.ts +34 -0
- package/src/core/events/v3/generated.ts +1 -1
- package/src/core/events/v3/index.ts +21 -0
- package/src/core/events/v3/latency.ts +213 -2
- package/src/core/events/v3/live-routing.ts +120 -1
- package/src/core/events/v3/producers/command-recorder.ts +10 -1
- package/src/core/events/v3/producers/coordination-recorder.ts +17 -5
- package/src/core/events/v3/producers/hook-base.ts +115 -1
- package/src/core/events/v3/producers/intake.ts +18 -0
- package/src/core/events/v3/producers/recorder.ts +856 -21
- package/src/core/events/v3/runtime-telemetry-capabilities.ts +137 -0
- package/src/core/events/v3/turn-telemetry.ts +10 -0
- package/src/core/hooks/adapter/parse.ts +16 -0
- package/src/core/hooks/adapter/runtime-telemetry.ts +805 -0
- package/src/core/hooks/cli.ts +126 -41
- package/src/core/hooks/remediation-cap.ts +77 -0
- package/src/core/hooks/resolve/transcript.ts +49 -17
- package/src/core/semantic/adapters.ts +293 -0
- package/src/core/semantic/contract.ts +298 -0
- package/src/core/semantic/evidence.ts +336 -0
- package/src/core/semantic/index.ts +127 -0
- package/src/core/semantic/once.ts +410 -0
- package/src/core/semantic/prompt.ts +84 -0
- package/src/core/semantic/scheduler.ts +107 -0
- package/src/core/semantic/service-status.ts +118 -0
- package/src/core/semantic/service.ts +580 -0
- package/src/core/semantic/storage.ts +286 -0
- package/src/core/semantic/validate.ts +212 -0
- package/src/lib/browser/client.ts +21 -0
- package/src/lib/browser/index.ts +4 -0
- package/src/lib/browser/standalone-html.ts +312 -0
- package/src/lib/instructions/apply.ts +1 -0
- package/src/lib/instructions/templates.ts +149 -4
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
4
|
+
import {
|
|
5
|
+
createSemanticAdapters,
|
|
6
|
+
discoverSemanticReaders,
|
|
7
|
+
inspectSemanticDocument,
|
|
8
|
+
readSemanticManifest,
|
|
9
|
+
readSemanticServiceStatus,
|
|
10
|
+
requestSemanticServiceStop,
|
|
11
|
+
runSemanticOnce,
|
|
12
|
+
runSemanticServiceDaemon,
|
|
13
|
+
semanticPaths,
|
|
14
|
+
spawnSemanticService,
|
|
15
|
+
} from "../core/semantic/index.ts";
|
|
16
|
+
|
|
17
|
+
export function registerSemanticCommand(
|
|
18
|
+
program: Command,
|
|
19
|
+
emit: EmitContext,
|
|
20
|
+
context?: HarneryProgramContext,
|
|
21
|
+
): void {
|
|
22
|
+
const semantic = program
|
|
23
|
+
.command("semantic")
|
|
24
|
+
.description("Build and inspect evidence-cited semantic readings of active V3 generations");
|
|
25
|
+
|
|
26
|
+
semantic
|
|
27
|
+
.command("once")
|
|
28
|
+
.description("Run one bounded semantic pass using each generation's source harness")
|
|
29
|
+
.option("--root <path>", "Explicit coordination root")
|
|
30
|
+
.option("--calls-per-hour <count>", "Configured call ceiling below the hard limit", integer)
|
|
31
|
+
.action(async (options: { root?: string; callsPerHour?: number }) => {
|
|
32
|
+
try {
|
|
33
|
+
emit.data(
|
|
34
|
+
await runSemanticOnce({
|
|
35
|
+
coordRoot: resolve(options.root ?? coordRoot(context)),
|
|
36
|
+
callsPerHour: options.callsPerHour,
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
emitFailure(emit, "semantic_once_failed", error);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const service = semantic
|
|
45
|
+
.command("service")
|
|
46
|
+
.description("Run semantic reading as a singleton service");
|
|
47
|
+
|
|
48
|
+
service
|
|
49
|
+
.command("start")
|
|
50
|
+
.description("Start or resume the detached per-root semantic reader service")
|
|
51
|
+
.option("--root <path>", "Explicit coordination root")
|
|
52
|
+
.option("--calls-per-hour <count>", "Configured call ceiling below the hard limit", integer)
|
|
53
|
+
.action(async (options: { root?: string; callsPerHour?: number }) => {
|
|
54
|
+
try {
|
|
55
|
+
emit.data(
|
|
56
|
+
await spawnSemanticService(resolve(options.root ?? coordRoot(context)), {
|
|
57
|
+
callsPerHour: options.callsPerHour,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
emitFailure(emit, "semantic_service_start_failed", error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
service
|
|
66
|
+
.command("status")
|
|
67
|
+
.description("Show singleton liveness, pass metrics, and pending work")
|
|
68
|
+
.option("--root <path>", "Explicit coordination root")
|
|
69
|
+
.action((options: { root?: string }) => {
|
|
70
|
+
try {
|
|
71
|
+
emit.data(readSemanticServiceStatus(resolve(options.root ?? coordRoot(context))));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
emitFailure(emit, "semantic_service_status_failed", error);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
service
|
|
78
|
+
.command("stop")
|
|
79
|
+
.description("Request a graceful stop after the current model call")
|
|
80
|
+
.option("--root <path>", "Explicit coordination root")
|
|
81
|
+
.action(async (options: { root?: string }) => {
|
|
82
|
+
try {
|
|
83
|
+
const root = resolve(options.root ?? coordRoot(context));
|
|
84
|
+
let status = requestSemanticServiceStop(root);
|
|
85
|
+
const deadline = Date.now() + 5_000;
|
|
86
|
+
while (status.running && Date.now() < deadline) {
|
|
87
|
+
await new Promise((done) => setTimeout(done, 50));
|
|
88
|
+
status = readSemanticServiceStatus(root);
|
|
89
|
+
}
|
|
90
|
+
emit.data(status);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
emitFailure(emit, "semantic_service_stop_failed", error);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
service
|
|
97
|
+
.command("daemon", { hidden: true })
|
|
98
|
+
.description("Internal detached semantic service entrypoint")
|
|
99
|
+
.option("--root <path>", "Explicit coordination root")
|
|
100
|
+
.option("--calls-per-hour <count>", "Configured call ceiling below the hard limit", integer)
|
|
101
|
+
.action(async (options: { root?: string; callsPerHour?: number }) => {
|
|
102
|
+
try {
|
|
103
|
+
emit.data(
|
|
104
|
+
await runSemanticServiceDaemon({
|
|
105
|
+
coordRoot: resolve(options.root ?? coordRoot(context)),
|
|
106
|
+
callsPerHour: options.callsPerHour,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
emitFailure(emit, "semantic_service_daemon_failed", error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
semantic
|
|
115
|
+
.command("inspect")
|
|
116
|
+
.description("Inspect one generation or instance semantic document")
|
|
117
|
+
.argument("<instance-or-generation>")
|
|
118
|
+
.option("--root <path>", "Explicit coordination root")
|
|
119
|
+
.action((instanceOrGeneration: string, options: { root?: string }) => {
|
|
120
|
+
try {
|
|
121
|
+
const document = inspectSemanticDocument(
|
|
122
|
+
resolve(options.root ?? coordRoot(context)),
|
|
123
|
+
instanceOrGeneration,
|
|
124
|
+
);
|
|
125
|
+
if (!document) throw new Error(`no semantic document for ${instanceOrGeneration}`);
|
|
126
|
+
emit.data(document);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
emitFailure(emit, "semantic_inspect_failed", error);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
semantic
|
|
133
|
+
.command("doctor")
|
|
134
|
+
.description("Inspect semantic reader availability without making a model request")
|
|
135
|
+
.option("--root <path>", "Explicit coordination root")
|
|
136
|
+
.action((options: { root?: string }) => {
|
|
137
|
+
try {
|
|
138
|
+
const root = resolve(options.root ?? coordRoot(context));
|
|
139
|
+
emit.data({
|
|
140
|
+
schema_version: 1,
|
|
141
|
+
root,
|
|
142
|
+
storage: semanticPaths(root).root,
|
|
143
|
+
readers: discoverSemanticReaders(createSemanticAdapters()),
|
|
144
|
+
manifest: readManifestSafe(root),
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
emitFailure(emit, "semantic_doctor_failed", error);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function coordRoot(context: HarneryProgramContext | undefined): string {
|
|
153
|
+
return resolve(
|
|
154
|
+
context?.resolveCoordRoot?.() ??
|
|
155
|
+
context?.repoRoot ??
|
|
156
|
+
process.env.HARNERY_COORD_ROOT ??
|
|
157
|
+
process.cwd(),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function integer(value: string): number {
|
|
162
|
+
const parsed = Number(value);
|
|
163
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1)
|
|
164
|
+
throw new Error("count must be a positive integer");
|
|
165
|
+
return parsed;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function readManifestSafe(root: string) {
|
|
169
|
+
try {
|
|
170
|
+
return readSemanticManifest(root);
|
|
171
|
+
} catch {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function emitFailure(emit: EmitContext, code: string, error: unknown): never {
|
|
177
|
+
emit.error({ code, message: error instanceof Error ? error.message : String(error) });
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
package/src/commands/web.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import type { Command } from "commander";
|
|
7
7
|
import type { EmitContext } from "../commander.ts";
|
|
8
8
|
import { DEFAULT_WEB_PORT, resolveWebPort } from "../core/config.ts";
|
|
9
|
+
import { ensureSemanticServiceRunning } from "../core/semantic/service.ts";
|
|
9
10
|
import { lazyFetchWebRoot, webRunner } from "./web-fetch.ts";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -69,6 +70,17 @@ export function nodeOptionsWithHeapCap(mb: number): string | undefined {
|
|
|
69
70
|
return `${inherited} --max-old-space-size=${mb}`.trim();
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
async function startSemanticServiceWithWeb(coordRoot: string, emit: EmitContext): Promise<void> {
|
|
74
|
+
const result = await ensureSemanticServiceRunning(coordRoot);
|
|
75
|
+
if (result.state === "started") emit.log("semantic reader · started automatically", "info");
|
|
76
|
+
if (result.state === "running") emit.log("semantic reader · already running", "info");
|
|
77
|
+
if (result.state === "paused") emit.log("semantic reader · paused by operator", "info");
|
|
78
|
+
if (result.state === "inactive") emit.log("semantic reader · waiting for active V3", "info");
|
|
79
|
+
if (result.state === "unavailable") {
|
|
80
|
+
emit.log(`semantic reader · unavailable (${result.error ?? "startup failed"})`, "warn");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
/**
|
|
73
85
|
* Resolve the web app root. A local `web/` (source checkout / submodule) wins.
|
|
74
86
|
* Otherwise, on an npm install, lazy-fetch it (clone + install) unless the
|
|
@@ -193,6 +205,8 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
|
|
|
193
205
|
|
|
194
206
|
if (!(await requireAvailablePort(emit, port))) return;
|
|
195
207
|
|
|
208
|
+
await startSemanticServiceWithWeb(coordRoot, emit);
|
|
209
|
+
|
|
196
210
|
const heapMb = resolveMaxOldSpaceMb(opts.maxOldSpace);
|
|
197
211
|
emit.log(`harn web · http://localhost:${port} (${mode})`, "info");
|
|
198
212
|
emit.log(`files origin · http://harnery-files.localhost:${port}`, "info");
|
|
@@ -281,6 +295,7 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
|
|
|
281
295
|
return;
|
|
282
296
|
}
|
|
283
297
|
if (!(await requireAvailablePort(emit, port))) return;
|
|
298
|
+
await startSemanticServiceWithWeb(coordRoot, emit);
|
|
284
299
|
const heapMb = resolveMaxOldSpaceMb(opts.maxOldSpace);
|
|
285
300
|
emit.log(`harn web · http://localhost:${port} (start)`, "info");
|
|
286
301
|
emit.log(`reading .harnery/ from: ${coordRoot}`, "info");
|
|
@@ -14,6 +14,7 @@ import { spawnSync } from "node:child_process";
|
|
|
14
14
|
import type { LiveCoordinationObservationV3 } from "../events/v3/live-observation.ts";
|
|
15
15
|
import { coordBinPath } from "./coord-bin.ts";
|
|
16
16
|
import { resolveCoordRoot } from "./coord-client.ts";
|
|
17
|
+
import type { Heartbeat } from "./state/heartbeat-reader.ts";
|
|
17
18
|
|
|
18
19
|
export type EventV3EmitObservation =
|
|
19
20
|
| LiveCoordinationObservationV3
|
|
@@ -96,3 +97,17 @@ export function normalizeAdapter(platform: string | undefined): "claude-code" |
|
|
|
96
97
|
if (platform === "codex") return "codex";
|
|
97
98
|
return "claude-code";
|
|
98
99
|
}
|
|
100
|
+
|
|
101
|
+
/** Producer joins use the private native session ID, never its V3 fingerprint. */
|
|
102
|
+
export function nativeSessionIdentity(
|
|
103
|
+
row: Pick<Heartbeat, "native_session_id" | "session_id"> | null | undefined,
|
|
104
|
+
fallback: string,
|
|
105
|
+
): string {
|
|
106
|
+
if (row?.native_session_id) return row.native_session_id;
|
|
107
|
+
// A projection-only row carries the privacy-safe canonical session
|
|
108
|
+
// fingerprint, not the adapter's native session ID needed to join the hook
|
|
109
|
+
// producer. Until a disposable cache exists, the resolved owner is the only
|
|
110
|
+
// native identity available to command surfaces.
|
|
111
|
+
if (row?.session_id && !/^sid_[a-f0-9]{64}$/.test(row.session_id)) return row.session_id;
|
|
112
|
+
return fallback;
|
|
113
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical identity for an authority-bearing write claim.
|
|
5
|
+
*
|
|
6
|
+
* Workspace paths stay portable and repository-relative. Approved paths
|
|
7
|
+
* outside the coordination root become resolved absolute paths so a later
|
|
8
|
+
* release matches whether the caller supplies the stored dot-dot form or the
|
|
9
|
+
* absolute target. This is lexical identity only; finalization policy owns the
|
|
10
|
+
* physical containment and symlink checks before a claim is acquired.
|
|
11
|
+
*/
|
|
12
|
+
export function canonicalClaimPath(coordRoot: string, value: string): string {
|
|
13
|
+
const root = resolve(coordRoot);
|
|
14
|
+
const absolute = resolve(root, value);
|
|
15
|
+
const fromRoot = relative(root, absolute);
|
|
16
|
+
const contained = fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot);
|
|
17
|
+
if (contained) return portable(fromRoot || ".");
|
|
18
|
+
return portable(absolute);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function portable(value: string): string {
|
|
22
|
+
return value.split(sep).join("/");
|
|
23
|
+
}
|
|
@@ -347,9 +347,10 @@ function readPidmapRow(pidmapDir: string, pid: number): string | null {
|
|
|
347
347
|
* Prefer a row whose platform matches `HARNERY_AGENT_COORD_PLATFORM` (default
|
|
348
348
|
* `claude-code`); otherwise return the first owner seen on the walk.
|
|
349
349
|
*
|
|
350
|
-
* Subagents intentionally do not write pid-map entries; a Bash-tool
|
|
351
|
-
* from inside
|
|
352
|
-
*
|
|
350
|
+
* Subagents intentionally do not write pid-map entries; a bare Bash-tool
|
|
351
|
+
* ppid-walk from inside an unbridged child therefore resolves to the parent's
|
|
352
|
+
* pid-map entry. Native hook bridges supersede this fallback through the
|
|
353
|
+
* child session environment.
|
|
353
354
|
*/
|
|
354
355
|
export function resolveOwner(): string | null {
|
|
355
356
|
return resolveOwnerWithSource().owner;
|
|
@@ -381,9 +382,36 @@ export function resolveOwnerWithSource(): {
|
|
|
381
382
|
// Every supported adapter exports its native session id into subprocesses.
|
|
382
383
|
// Match it against the live V3 producer state, never a disposable cache.
|
|
383
384
|
if (shouldPreferSessionEnv()) {
|
|
384
|
-
const bySession =
|
|
385
|
+
const bySession = resolveOwnerBySessionEnvDetailed(root);
|
|
385
386
|
if (bySession) {
|
|
386
|
-
|
|
387
|
+
// Env vars cross process boundaries that session identity does not: a
|
|
388
|
+
// nested different-adapter child inherits its spawner's exported id (a
|
|
389
|
+
// Codex session's CODEX_THREAD_ID survives into a `claude -p` child's
|
|
390
|
+
// shells untouched), and that stale statement joins the spawner's live
|
|
391
|
+
// producer. The nearest token-verified pid-map anchor names the session
|
|
392
|
+
// whose tool environment this process actually runs in, so on a
|
|
393
|
+
// CROSS-adapter conflict the anchor wins — the anchoring adapter never
|
|
394
|
+
// stamps another adapter's variable, so a foreign-adapter id here is
|
|
395
|
+
// inherited context, while the anchor row is re-healed on every tool
|
|
396
|
+
// call. A SAME-adapter conflict keeps the env: the adapter stamps its
|
|
397
|
+
// own variable fresh per tool call, and the row may lag one heal (e.g.
|
|
398
|
+
// a session id change inside one process). Only per-session-process
|
|
399
|
+
// adapters may anchor (claude-code, codex) — Cursor sessions share one
|
|
400
|
+
// terminal process, so a verified Cursor row can belong to a sibling
|
|
401
|
+
// session. Unverified rows keep losing to the env too: without a start
|
|
402
|
+
// token the row is the guess the env ordering was introduced to beat.
|
|
403
|
+
// Never for bridge-marked children: a connector crosses a process-tree
|
|
404
|
+
// boundary, so an anchor in THIS tree is someone else's by definition.
|
|
405
|
+
const anchor = bridge ? null : nearestVerifiedSessionAnchor(root);
|
|
406
|
+
if (
|
|
407
|
+
anchor &&
|
|
408
|
+
anchor.owner !== bySession.owner &&
|
|
409
|
+
!bySession.adapters.includes(anchor.platform) &&
|
|
410
|
+
(anchor.platform === "claude-code" || anchor.platform === "codex")
|
|
411
|
+
) {
|
|
412
|
+
return { owner: anchor.owner, source: "pidmap" };
|
|
413
|
+
}
|
|
414
|
+
return { owner: bySession.owner, source: "session_env" };
|
|
387
415
|
}
|
|
388
416
|
}
|
|
389
417
|
|
|
@@ -410,6 +438,42 @@ export function resolveOwnerWithSource(): {
|
|
|
410
438
|
return { owner: null, source: "none" };
|
|
411
439
|
}
|
|
412
440
|
|
|
441
|
+
/**
|
|
442
|
+
* The nearest pid-map anchor on our own ppid chain whose start token PROVES the
|
|
443
|
+
* row still describes the process holding that pid, or null when the nearest
|
|
444
|
+
* row is unverified, recycled, or absent.
|
|
445
|
+
*
|
|
446
|
+
* Nearest matters: in a nested-agent tree (session A spawns a headless session
|
|
447
|
+
* B whose shell runs this code), both A's and B's adapter processes are
|
|
448
|
+
* ancestors, and only the innermost one owns this process's tool environment.
|
|
449
|
+
* Exported for unit testing with an injectable root.
|
|
450
|
+
*/
|
|
451
|
+
export function nearestVerifiedSessionAnchor(
|
|
452
|
+
root: string,
|
|
453
|
+
): { owner: string; platform: string } | null {
|
|
454
|
+
const pidmapDir = resolve(root, ".harnery", "pid-map");
|
|
455
|
+
if (!existsSync(pidmapDir)) return null;
|
|
456
|
+
let pid: number | null = process.pid;
|
|
457
|
+
for (let hop = 0; hop < 20; hop++) {
|
|
458
|
+
if (pid === null) break;
|
|
459
|
+
const row = readPidmapRow(pidmapDir, pid);
|
|
460
|
+
if (row) {
|
|
461
|
+
const owner = parsePidmapRowOwner(row);
|
|
462
|
+
if (owner && !pidWasRecycled(pid, row)) {
|
|
463
|
+
// A recycled row is about a dead process's tenure of this pid — walk
|
|
464
|
+
// past it like the owner walk does. A live row without a provable
|
|
465
|
+
// token match is the nearest anchor but not a VERIFIED one: stop and
|
|
466
|
+
// report none, so the session env keeps outranking guesses.
|
|
467
|
+
const token = parsePidmapRowStartToken(row);
|
|
468
|
+
if (!token || !pidStartToken(pid)) return null;
|
|
469
|
+
return { owner, platform: parsePidmapRowPlatform(row) };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
pid = readPpid(pid);
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
413
477
|
/**
|
|
414
478
|
* Walk our own ppid chain for a pid-map row in ONE given root.
|
|
415
479
|
*
|
|
@@ -524,17 +588,37 @@ function shouldPreferSessionEnv(): boolean {
|
|
|
524
588
|
* Exported for unit testing with an injectable root.
|
|
525
589
|
*/
|
|
526
590
|
export function resolveOwnerBySessionEnv(root: string): string | null {
|
|
591
|
+
return resolveOwnerBySessionEnvDetailed(root)?.owner ?? null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* `resolveOwnerBySessionEnv` plus WHICH adapter's producer state the session id
|
|
596
|
+
* joined. The caller uses the adapter to tell a same-adapter conflict (the env
|
|
597
|
+
* var is stamped fresh per tool call and outranks a possibly-lagging pid-map
|
|
598
|
+
* row) from a cross-adapter one (the var is inherited context from an outer
|
|
599
|
+
* session and the verified row wins).
|
|
600
|
+
*/
|
|
601
|
+
export function resolveOwnerBySessionEnvDetailed(
|
|
602
|
+
root: string,
|
|
603
|
+
): { owner: string; adapters: string[] } | null {
|
|
527
604
|
const sessionIds = sessionIdsFromEnv();
|
|
528
605
|
if (sessionIds.length === 0) return null;
|
|
529
606
|
const adapters = adapterCandidatesFromEnv();
|
|
530
|
-
const matches = new Set<string
|
|
607
|
+
const matches = new Map<string, Set<string>>();
|
|
531
608
|
for (const sessionId of sessionIds) {
|
|
532
609
|
for (const adapter of adapters) {
|
|
533
610
|
const state = readHookProducerStateV3(root, adapter, sessionId);
|
|
534
|
-
if (state && !state.terminal)
|
|
611
|
+
if (state && !state.terminal) {
|
|
612
|
+
const owner = nativeOwnerForV3Instance(root, state.instance_id);
|
|
613
|
+
const set = matches.get(owner) ?? new Set<string>();
|
|
614
|
+
set.add(adapter);
|
|
615
|
+
matches.set(owner, set);
|
|
616
|
+
}
|
|
535
617
|
}
|
|
536
618
|
}
|
|
537
|
-
|
|
619
|
+
if (matches.size !== 1) return null;
|
|
620
|
+
const [owner, adapterSet] = [...matches.entries()][0]!;
|
|
621
|
+
return { owner, adapters: [...adapterSet] };
|
|
538
622
|
}
|
|
539
623
|
|
|
540
624
|
/**
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
readTerminalHookProducerStateV3,
|
|
26
26
|
recordHookSignalV3,
|
|
27
27
|
} from "../events/v3/producers/recorder.ts";
|
|
28
|
+
import { canonicalClaimPath } from "./claim-path.ts";
|
|
28
29
|
import {
|
|
29
30
|
acquireClaim,
|
|
30
31
|
type Heartbeat,
|
|
@@ -369,7 +370,3 @@ function authorityStateDigest(heartbeat: Heartbeat): `sha256:${string}` {
|
|
|
369
370
|
}),
|
|
370
371
|
);
|
|
371
372
|
}
|
|
372
|
-
|
|
373
|
-
function canonicalClaimPath(coordRoot: string, path: string): string {
|
|
374
|
-
return path.startsWith(`${coordRoot}/`) ? path.slice(coordRoot.length + 1) : path;
|
|
375
|
-
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
livePlatformV3,
|
|
11
11
|
resolveLiveEventLedgerRouteV3,
|
|
12
12
|
} from "../events/v3/live-routing.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { readJoinableHookProducerStateV3 } from "../events/v3/producers/recorder.ts";
|
|
14
14
|
import { writeEventV3 } from "../events/v3/writer.ts";
|
|
15
15
|
import { LiveCoordinationAuthorityV3Error } from "./live-authority-v3.ts";
|
|
16
16
|
import { liveCoordinationAdapterV3 } from "./state/live-coordination-view.ts";
|
|
@@ -132,8 +132,13 @@ function withAttestedAdapter<T extends LiveLifecycleObservationBaseV3>(input: T)
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function requireHookState(input: LiveLifecycleObservationBaseV3) {
|
|
135
|
-
const hook =
|
|
136
|
-
|
|
135
|
+
const hook = readJoinableHookProducerStateV3(
|
|
136
|
+
input.coordRoot,
|
|
137
|
+
input.adapter,
|
|
138
|
+
input.nativeSessionId,
|
|
139
|
+
liveInstanceIdV3(input.owner),
|
|
140
|
+
);
|
|
141
|
+
if (!hook) {
|
|
137
142
|
throw new LiveCoordinationAuthorityV3Error("hook_generation_not_joinable");
|
|
138
143
|
}
|
|
139
144
|
return hook;
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
livePlatformV3,
|
|
18
18
|
resolveLiveEventLedgerRouteV3,
|
|
19
19
|
} from "../events/v3/live-routing.ts";
|
|
20
|
-
import {
|
|
20
|
+
import { readJoinableHookProducerStateV3 } from "../events/v3/producers/recorder.ts";
|
|
21
21
|
import { writeEventV3 } from "../events/v3/writer.ts";
|
|
22
22
|
import { LiveCoordinationAuthorityV3Error } from "./live-authority-v3.ts";
|
|
23
23
|
import { liveCoordinationAdapterV3 } from "./state/live-coordination-view.ts";
|
|
@@ -54,7 +54,12 @@ export function recordLiveCoordinationObservationV3(
|
|
|
54
54
|
throw new LiveCoordinationAuthorityV3Error("control_state_changed");
|
|
55
55
|
}
|
|
56
56
|
const adapter = liveCoordinationAdapterV3(input.coordRoot, input.owner) ?? input.adapter;
|
|
57
|
-
const hook =
|
|
57
|
+
const hook = readJoinableHookProducerStateV3(
|
|
58
|
+
input.coordRoot,
|
|
59
|
+
adapter,
|
|
60
|
+
input.nativeSessionId,
|
|
61
|
+
liveInstanceIdV3(input.owner),
|
|
62
|
+
);
|
|
58
63
|
if (
|
|
59
64
|
!hook ||
|
|
60
65
|
hook.terminal ||
|
|
@@ -224,7 +224,7 @@ function computeFocusNudgeIfChanged(
|
|
|
224
224
|
// Keyed on "a name was ever produced", not task_updated_at: a bare clear
|
|
225
225
|
// as the first declaration must not end the naming window.
|
|
226
226
|
needsNudge = true;
|
|
227
|
-
nudgeKind = "session-name";
|
|
227
|
+
nudgeKind = pendingSessionName ? "session-name-pending" : "session-name-unminted";
|
|
228
228
|
message = pendingSessionName
|
|
229
229
|
? `This session name is still pending display. Before any prose or another tool call, send this exact block as your next assistant text:\n\n${sessionNameDisplayBlock(pendingSessionName)}`
|
|
230
230
|
: `This session has no name yet: run \`${bin} agents set-task "<2-5 word session topic>"\` as your first tool call. ` +
|
|
@@ -254,19 +254,22 @@ function computeFocusNudgeIfChanged(
|
|
|
254
254
|
return "";
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
// is
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
if (nudgeKind === "session-name") {
|
|
257
|
+
// An unminted session still gets a fresh reminder on every prompt until its
|
|
258
|
+
// first focus declaration. Once a name exists, the exact pending-name prompt
|
|
259
|
+
// is delivered once per minted value. Repeating it on every prompt creates
|
|
260
|
+
// an unbounded loop on adapters that cannot verify assistant reply text.
|
|
261
|
+
if (nudgeKind === "session-name-unminted") {
|
|
263
262
|
clearHashFile(hashFile);
|
|
264
263
|
return message;
|
|
265
264
|
}
|
|
266
265
|
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
|
|
266
|
+
// A pending reminder is keyed only to the minted name: changing the task
|
|
267
|
+
// must not reconstruct or re-request that older title. Task reminders retain
|
|
268
|
+
// their own task-state key.
|
|
269
|
+
const state =
|
|
270
|
+
nudgeKind === "session-name-pending"
|
|
271
|
+
? `${nudgeKind}|name=${pendingSessionName ?? ""}`
|
|
272
|
+
: `${nudgeKind}|task=${taskValue}|updated=${hb.task_updated_at ?? ""}|threshold=${threshold}`;
|
|
270
273
|
const newHash = sha256Hex16(state);
|
|
271
274
|
const oldHash = safeRead(hashFile);
|
|
272
275
|
if (oldHash && oldHash === newHash) return "";
|
|
@@ -138,16 +138,18 @@ export function evaluateStopHookV3Events(
|
|
|
138
138
|
event.event_type === "turn.completed",
|
|
139
139
|
);
|
|
140
140
|
const latestTerminal = turnTerminals.at(-1);
|
|
141
|
-
const latestBox = observedBoolean(latestTerminal?.payload.ritual?.
|
|
141
|
+
const latestBox = observedBoolean(latestTerminal?.payload.ritual?.status_box_present_strict);
|
|
142
142
|
if (req.adapter !== "cursor" && latestBox === undefined) {
|
|
143
143
|
return evidenceUnavailable(
|
|
144
|
-
"turn.completed has no observed V3 status-box result; Stop remains fail-open",
|
|
144
|
+
"turn.completed has no observed V3 assistant-text status-box result; Stop remains fail-open",
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const ackPresent = req.adapter === "cursor" ? requiredStatusChecked : latestBox === true;
|
|
149
149
|
const ackBlock =
|
|
150
|
-
req.adapter === "cursor"
|
|
150
|
+
req.adapter === "cursor"
|
|
151
|
+
? () => rule13Block(coordRoot, req.session_id)
|
|
152
|
+
: () => rule23Block(coordRoot, req.session_id);
|
|
151
153
|
|
|
152
154
|
// Pure prose turns keep the human-visible acknowledgement but do not require
|
|
153
155
|
// task or final-status mutations. This preserves the pre-V3 policy exactly.
|
|
@@ -161,9 +163,9 @@ export function evaluateStopHookV3Events(
|
|
|
161
163
|
};
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
if (!requiredStatusChecked) return rule13Block(coordRoot);
|
|
166
|
+
if (!requiredStatusChecked) return rule13Block(coordRoot, req.session_id);
|
|
165
167
|
if (!ackPresent) return ackBlock();
|
|
166
|
-
if (!taskSet) return rule33Block(coordRoot);
|
|
168
|
+
if (!taskSet) return rule33Block(coordRoot, req.session_id);
|
|
167
169
|
|
|
168
170
|
// Claude Code can prove session-name presentation from assistant-only text.
|
|
169
171
|
// Cursor cannot expose that text and Codex returned observe-only above.
|
|
@@ -271,7 +273,18 @@ function evidenceUnavailable(reason: string): VerdictResult {
|
|
|
271
273
|
};
|
|
272
274
|
}
|
|
273
275
|
|
|
274
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Remediation commands carry `--session-id` when the session id is known: the
|
|
278
|
+
* remediating tool call may not descend from a pid-map-registered anchor
|
|
279
|
+
* (headless children, Cursor shells), and a ppid-walk that resolves to a
|
|
280
|
+
* DIFFERENT owner records evidence this verdict will never see — the block
|
|
281
|
+
* then repeats forever while the agent complies every time.
|
|
282
|
+
*/
|
|
283
|
+
function sessionIdSuffix(sessionId?: string): string {
|
|
284
|
+
return sessionId ? ` --session-id ${sessionId}` : "";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function rule13Block(coordRoot?: string, sessionId?: string): VerdictResult {
|
|
275
288
|
const missingEvidence = agentsRequireGitFinalization(coordRoot)
|
|
276
289
|
? "no end_turn_checked coord.status_observed event found in this turn"
|
|
277
290
|
: "no coord.status_observed event found in this turn";
|
|
@@ -279,16 +292,16 @@ function rule13Block(coordRoot?: string): VerdictResult {
|
|
|
279
292
|
allow: false,
|
|
280
293
|
exit_code: 2,
|
|
281
294
|
rule: "stop-hook.rule_1_3",
|
|
282
|
-
reason: `End-of-turn rule (1/3): ${missingEvidence}; run \`${endOfTurnStatusCommand(coordRoot)}\` as your last tool call.`,
|
|
295
|
+
reason: `End-of-turn rule (1/3): ${missingEvidence}; run \`${endOfTurnStatusCommand(coordRoot)}${sessionIdSuffix(sessionId)}\` as your last tool call.`,
|
|
283
296
|
};
|
|
284
297
|
}
|
|
285
298
|
|
|
286
|
-
function rule23Block(coordRoot?: string): VerdictResult {
|
|
299
|
+
function rule23Block(coordRoot?: string, sessionId?: string): VerdictResult {
|
|
287
300
|
return {
|
|
288
301
|
allow: false,
|
|
289
302
|
exit_code: 2,
|
|
290
303
|
rule: "stop-hook.rule_2_3",
|
|
291
|
-
reason: `End-of-turn rule (2/3): turn.completed did not observe the agent-status box in your reply text. Paste the \`${endOfTurnStatusCommand(coordRoot)}\` output verbatim as a fenced code block (the \`┌─ agent-\` prefix is the detection signal).`,
|
|
304
|
+
reason: `End-of-turn rule (2/3): turn.completed did not observe the agent-status box in your reply text. Paste the \`${endOfTurnStatusCommand(coordRoot)}${sessionIdSuffix(sessionId)}\` output verbatim as a fenced code block (the \`┌─ agent-\` prefix is the detection signal).`,
|
|
292
305
|
};
|
|
293
306
|
}
|
|
294
307
|
|
|
@@ -335,11 +348,11 @@ function sessionNameBlock(coordRoot: string, instanceId: string): VerdictResult
|
|
|
335
348
|
};
|
|
336
349
|
}
|
|
337
350
|
|
|
338
|
-
function rule33Block(coordRoot?: string): VerdictResult {
|
|
351
|
+
function rule33Block(coordRoot?: string, sessionId?: string): VerdictResult {
|
|
339
352
|
return {
|
|
340
353
|
allow: false,
|
|
341
354
|
exit_code: 2,
|
|
342
355
|
rule: "stop-hook.rule_3_3",
|
|
343
|
-
reason: `End-of-turn rule (3/3): no coord.task_changed event found in this turn; run \`${resolveBinName(coordRoot)} agents set-task "<short focus>"\` to declare what you're working on. Pass an empty string if the turn was purely conversational.`,
|
|
356
|
+
reason: `End-of-turn rule (3/3): no coord.task_changed event found in this turn; run \`${resolveBinName(coordRoot)} agents set-task${sessionIdSuffix(sessionId)} "<short focus>"\` to declare what you're working on. Pass an empty string if the turn was purely conversational.`,
|
|
344
357
|
};
|
|
345
358
|
}
|
|
@@ -36,9 +36,17 @@ export function sessionNameDisplayInstruction(name: string): string {
|
|
|
36
36
|
].join("\n");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function sessionNameDisplayRecoveryInstruction(name: string, binName: string): string {
|
|
40
|
+
return [
|
|
41
|
+
sessionNameDisplayInstruction(name),
|
|
42
|
+
"",
|
|
43
|
+
`If that exact block was already shown but the gate still rejects tools, run \`${binName} agents suggest-name --json\` as the only tool call. That read-only command is exempt and creates a fresh verification boundary.`,
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
/**
|
|
40
48
|
* A PostToolUse result should announce the display protocol only when that
|
|
41
|
-
* exact tool call minted the current suggestion. The coordination row keeps
|
|
49
|
+
* exact tool call minted or retried the current suggestion. The coordination row keeps
|
|
42
50
|
* the suggestion pending until transcript evidence catches up, so checking
|
|
43
51
|
* the row alone would re-announce the same name after every later tool.
|
|
44
52
|
*
|
|
@@ -89,7 +97,7 @@ function responseContainsSessionNameMint(
|
|
|
89
97
|
const row = value as Record<string, unknown>;
|
|
90
98
|
if (
|
|
91
99
|
row.suggested_session_name === name &&
|
|
92
|
-
(row.first_of_session === true || row.name_reminted === true)
|
|
100
|
+
(row.first_of_session === true || row.name_reminted === true || row.session_name_retry === true)
|
|
93
101
|
) {
|
|
94
102
|
return true;
|
|
95
103
|
}
|
|
@@ -143,7 +151,7 @@ export function isSessionNameRemediationCommand(
|
|
|
143
151
|
if (!body || /[\n;&|<>`$]/.test(body)) return false;
|
|
144
152
|
const escapedBin = binName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
145
153
|
const pattern = new RegExp(
|
|
146
|
-
`^(?:codex-wsl\\s+--\\s+)?${escapedBin}\\s+agents\\s+(?:set-task|status)(?:\\s+.*)?$`,
|
|
154
|
+
`^(?:codex-wsl\\s+--\\s+)?${escapedBin}\\s+agents\\s+(?:set-task|status|suggest-name)(?:\\s+.*)?$`,
|
|
147
155
|
);
|
|
148
156
|
return pattern.test(body);
|
|
149
157
|
}
|