ticlawk 0.1.17-dev.17 → 0.1.17-dev.19
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/README.md +3 -17
- package/bin/ticlawk.mjs +21 -245
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +51 -327
- package/src/adapters/ticlawk/credentials.mjs +1 -41
- package/src/adapters/ticlawk/index.mjs +27 -249
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +22 -703
- package/src/core/agent-cli-handlers.mjs +18 -519
- package/src/core/agent-home.mjs +1 -64
- package/src/core/events/worker-events.mjs +36 -32
- package/src/core/http.mjs +0 -138
- package/src/core/runtime-contract.mjs +1 -0
- package/src/core/runtime-env.mjs +0 -7
- package/src/core/runtime-support.mjs +78 -130
- package/src/runtimes/_shared/incoming-message-prompt.mjs +232 -0
- package/src/runtimes/_shared/runtime-base-instructions.mjs +34 -0
- package/src/runtimes/claude-code/index.mjs +48 -21
- package/src/runtimes/claude-code/session.mjs +7 -2
- package/src/runtimes/codex/index.mjs +64 -116
- package/src/runtimes/codex/session.mjs +12 -2
- package/src/runtimes/openclaw/index.mjs +30 -17
- package/src/runtimes/opencode/index.mjs +64 -42
- package/src/runtimes/opencode/session.mjs +14 -14
- package/src/runtimes/pi/index.mjs +64 -42
- package/src/runtimes/pi/session.mjs +8 -11
- package/ticlawk.mjs +30 -0
- package/src/runtimes/_shared/agent-handbook.mjs +0 -38
- package/src/runtimes/_shared/brand.mjs +0 -2
- package/src/runtimes/_shared/goal-step-prompt.mjs +0 -133
- package/src/runtimes/_shared/goal-task-protocol.mjs +0 -50
- package/src/runtimes/_shared/standing-prompt.mjs +0 -331
- package/src/runtimes/_shared/wake-prompt.mjs +0 -296
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { basename } from 'node:path';
|
|
8
8
|
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
9
|
-
import {
|
|
10
|
-
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
9
|
+
import { buildRuntimeBaseInstructions } from '../_shared/runtime-base-instructions.mjs';
|
|
11
10
|
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
12
11
|
import {
|
|
13
12
|
buildPiImagesFromInbound,
|
|
@@ -19,18 +18,15 @@ import {
|
|
|
19
18
|
requirePiPath,
|
|
20
19
|
runPiPrompt,
|
|
21
20
|
} from './session.mjs';
|
|
22
|
-
import {
|
|
21
|
+
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
23
22
|
import {
|
|
24
23
|
shouldStreamRuntime,
|
|
24
|
+
createDeltaAggregator,
|
|
25
25
|
reportSubprocessFailure,
|
|
26
26
|
terminalRuntimeFailure,
|
|
27
27
|
updateBindingRuntimeMeta,
|
|
28
|
-
resolveRuntimeSessionScope,
|
|
29
|
-
buildRuntimeSessionMetaPatch,
|
|
30
28
|
} from '../../core/runtime-support.mjs';
|
|
31
29
|
|
|
32
|
-
const standingPromptSeen = new Set();
|
|
33
|
-
|
|
34
30
|
export const piRuntime = {
|
|
35
31
|
name: 'pi',
|
|
36
32
|
|
|
@@ -42,8 +38,7 @@ export const piRuntime = {
|
|
|
42
38
|
images: opts.images,
|
|
43
39
|
piPath,
|
|
44
40
|
agentEnv,
|
|
45
|
-
|
|
46
|
-
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
41
|
+
runtimeBaseInstructions: opts.runtimeBaseInstructions || null,
|
|
47
42
|
timeoutMs: opts.timeoutMs,
|
|
48
43
|
});
|
|
49
44
|
},
|
|
@@ -56,8 +51,7 @@ export const piRuntime = {
|
|
|
56
51
|
images: opts.images,
|
|
57
52
|
piPath,
|
|
58
53
|
agentEnv,
|
|
59
|
-
|
|
60
|
-
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
54
|
+
runtimeBaseInstructions: opts.runtimeBaseInstructions || null,
|
|
61
55
|
timeoutMs: opts.timeoutMs,
|
|
62
56
|
onEvent: opts.onEvent,
|
|
63
57
|
});
|
|
@@ -134,39 +128,46 @@ export const piRuntime = {
|
|
|
134
128
|
message = captionText || 'Please analyze the attached image(s).';
|
|
135
129
|
}
|
|
136
130
|
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
131
|
+
const shouldRotate = !meta.sessionId || meta.rotatePending;
|
|
132
|
+
const deltaAggregator = createDeltaAggregator({
|
|
133
|
+
flushDelta: async ({ text, sessionId, cwd }) => {
|
|
134
|
+
await emitWorkerEvent({
|
|
135
|
+
adapter,
|
|
136
|
+
binding,
|
|
137
|
+
agent: this.name,
|
|
138
|
+
sessionId: sessionId || meta.sessionId || binding.id,
|
|
139
|
+
cwd: cwd || agentHome,
|
|
140
|
+
replyToMessageId: inbound.messageId || null,
|
|
141
|
+
event: {
|
|
142
|
+
hook_event_name: 'worker.message.delta',
|
|
143
|
+
worker_event_name: 'worker.message.delta',
|
|
144
|
+
delta: text,
|
|
145
|
+
},
|
|
146
|
+
logger: ctx.logger,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
});
|
|
140
150
|
|
|
141
151
|
try {
|
|
142
152
|
const runtimePiPath = requirePiPath(meta.piPath || meta.runtimePath);
|
|
143
153
|
const runtimePiVersion = getPiRuntimeHealth(runtimePiPath).version || meta.piVersion || null;
|
|
144
154
|
const agentEnv = buildAgentRuntimeEnv({
|
|
145
155
|
agentId: binding.id,
|
|
146
|
-
sessionId:
|
|
156
|
+
sessionId: meta.sessionId,
|
|
147
157
|
hostId: binding.runtime_host_id,
|
|
148
|
-
conversationId: inbound.conversationId,
|
|
149
|
-
messageId: inbound.messageId,
|
|
150
|
-
target: inbound.envelopeTarget,
|
|
151
158
|
});
|
|
152
|
-
const
|
|
153
|
-
const protocolOverlayKey = getGoalTaskProtocolOverlayKey({ agentId: binding.id, inbound });
|
|
154
|
-
const standingPromptSeenKey = targetSessionId
|
|
155
|
-
? `${binding.id}|${targetSessionId}|${protocolOverlayKey}`
|
|
156
|
-
: null;
|
|
157
|
-
const forceStandingPrompt = Boolean(standingPromptSeenKey && !standingPromptSeen.has(standingPromptSeenKey));
|
|
159
|
+
const runtimeBaseInstructions = buildRuntimeBaseInstructions({ agentId: binding.id });
|
|
158
160
|
const result = shouldStreamRuntime(this.name, this)
|
|
159
|
-
? await this.runTurnStream({ sessionId:
|
|
160
|
-
|
|
161
|
-
forceStandingPrompt,
|
|
161
|
+
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, {
|
|
162
|
+
runtimeBaseInstructions,
|
|
162
163
|
images,
|
|
163
164
|
onEvent: async (event) => {
|
|
164
165
|
if (event?.type === 'turn.started') {
|
|
165
|
-
|
|
166
|
+
await emitWorkerEvent({
|
|
166
167
|
adapter,
|
|
167
168
|
binding,
|
|
168
169
|
agent: this.name,
|
|
169
|
-
sessionId: event.sessionId ||
|
|
170
|
+
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
170
171
|
cwd: agentHome,
|
|
171
172
|
replyToMessageId: inbound.messageId || null,
|
|
172
173
|
event: {
|
|
@@ -175,29 +176,31 @@ export const piRuntime = {
|
|
|
175
176
|
},
|
|
176
177
|
logger: ctx.logger,
|
|
177
178
|
});
|
|
179
|
+
} else if (event?.type === 'message.delta' && event.text) {
|
|
180
|
+
deltaAggregator.push(event.text, {
|
|
181
|
+
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
182
|
+
cwd: agentHome,
|
|
183
|
+
});
|
|
178
184
|
}
|
|
179
185
|
},
|
|
180
186
|
})
|
|
181
|
-
: await this.runTurn({ sessionId:
|
|
187
|
+
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, { images, runtimeBaseInstructions });
|
|
182
188
|
|
|
183
|
-
|
|
184
|
-
if (observedSessionId) {
|
|
185
|
-
standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
|
|
186
|
-
}
|
|
189
|
+
await deltaAggregator.flush();
|
|
187
190
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
path: result?.path,
|
|
191
|
-
}),
|
|
191
|
+
sessionId: result?.sessionId || meta.sessionId,
|
|
192
|
+
path: result?.path || meta.path || null,
|
|
192
193
|
runtimePath: runtimePiPath,
|
|
193
194
|
piPath: runtimePiPath,
|
|
194
195
|
piVersion: runtimePiVersion,
|
|
196
|
+
rotatePending: false,
|
|
197
|
+
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
195
198
|
}, { status: 'connected' });
|
|
196
|
-
|
|
199
|
+
await emitWorkerEvent({
|
|
197
200
|
adapter,
|
|
198
201
|
binding: nextBinding,
|
|
199
202
|
agent: this.name,
|
|
200
|
-
sessionId: result?.sessionId ||
|
|
203
|
+
sessionId: result?.sessionId || meta.sessionId || binding.id,
|
|
201
204
|
cwd: result?.cwd || agentHome,
|
|
202
205
|
replyToMessageId: inbound.messageId || null,
|
|
203
206
|
event: {
|
|
@@ -208,11 +211,12 @@ export const piRuntime = {
|
|
|
208
211
|
});
|
|
209
212
|
return true;
|
|
210
213
|
} catch (err) {
|
|
211
|
-
|
|
214
|
+
await deltaAggregator.flush().catch(() => {});
|
|
215
|
+
await emitWorkerEvent({
|
|
212
216
|
adapter,
|
|
213
217
|
binding,
|
|
214
218
|
agent: this.name,
|
|
215
|
-
sessionId:
|
|
219
|
+
sessionId: meta.sessionId || binding.id,
|
|
216
220
|
cwd: agentHome,
|
|
217
221
|
replyToMessageId: inbound.messageId || null,
|
|
218
222
|
event: {
|
|
@@ -238,6 +242,24 @@ export const piRuntime = {
|
|
|
238
242
|
}
|
|
239
243
|
},
|
|
240
244
|
|
|
245
|
+
async reconcileAfterRestart(binding, ctx) {
|
|
246
|
+
const meta = binding.runtimeMeta || {};
|
|
247
|
+
await emitWorkerEvent({
|
|
248
|
+
adapter: ctx.adapter,
|
|
249
|
+
binding,
|
|
250
|
+
agent: this.name,
|
|
251
|
+
sessionId: meta.sessionId || binding.id,
|
|
252
|
+
cwd: ensureAgentHome(binding.id) || '',
|
|
253
|
+
event: {
|
|
254
|
+
hook_event_name: 'Stop',
|
|
255
|
+
worker_event_name: 'worker.turn.complete',
|
|
256
|
+
reason: 'connector.restart.reconcile',
|
|
257
|
+
},
|
|
258
|
+
logger: ctx.logger,
|
|
259
|
+
});
|
|
260
|
+
return 1;
|
|
261
|
+
},
|
|
262
|
+
|
|
241
263
|
sessionsDir: PI_SESSIONS_DIR,
|
|
242
264
|
maxAgeMs: PI_MAX_AGE_MS,
|
|
243
265
|
};
|
|
@@ -196,16 +196,13 @@ export function runPiPrompt({
|
|
|
196
196
|
images = [],
|
|
197
197
|
piPath = null,
|
|
198
198
|
agentEnv = null,
|
|
199
|
-
|
|
200
|
-
forceStandingPrompt = false,
|
|
199
|
+
runtimeBaseInstructions = null,
|
|
201
200
|
timeoutMs = Number(process.env.PI_RUN_TIMEOUT_MS || DEFAULT_PI_RUN_TIMEOUT_MS),
|
|
202
201
|
onEvent,
|
|
203
202
|
}) {
|
|
204
|
-
// pi has no documented system-prompt flag
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const finalMessage = standingPrompt && (!sessionId || forceStandingPrompt)
|
|
208
|
-
? `${standingPrompt}\n\n---\n\n${message}`
|
|
203
|
+
// pi has no documented system-prompt flag — prepend on first turn.
|
|
204
|
+
const finalMessage = runtimeBaseInstructions && !sessionId
|
|
205
|
+
? `${runtimeBaseInstructions}\n\n---\n\n${message}`
|
|
209
206
|
: message;
|
|
210
207
|
return new Promise((resolve, reject) => {
|
|
211
208
|
const startedAt = Date.now();
|
|
@@ -223,13 +220,12 @@ export function runPiPrompt({
|
|
|
223
220
|
let activeSessionFile = null;
|
|
224
221
|
let finalText = '';
|
|
225
222
|
let settled = false;
|
|
223
|
+
let eventChain = Promise.resolve();
|
|
226
224
|
const pending = new Map();
|
|
227
225
|
|
|
228
226
|
const emit = (event) => {
|
|
229
227
|
if (typeof onEvent !== 'function') return;
|
|
230
|
-
|
|
231
|
-
.then(() => onEvent(event))
|
|
232
|
-
.catch(() => {});
|
|
228
|
+
eventChain = eventChain.then(() => onEvent(event)).catch(() => {});
|
|
233
229
|
};
|
|
234
230
|
|
|
235
231
|
const settle = (fn, value) => {
|
|
@@ -241,7 +237,7 @@ export function runPiPrompt({
|
|
|
241
237
|
}
|
|
242
238
|
pending.clear();
|
|
243
239
|
try { child.kill('SIGTERM'); } catch {}
|
|
244
|
-
fn(value);
|
|
240
|
+
eventChain.catch(() => {}).finally(() => fn(value));
|
|
245
241
|
};
|
|
246
242
|
|
|
247
243
|
const sendRaw = (payload) => {
|
|
@@ -309,6 +305,7 @@ export function runPiPrompt({
|
|
|
309
305
|
const delta = extractDeltaFromEvent(event);
|
|
310
306
|
if (delta) {
|
|
311
307
|
finalText += delta;
|
|
308
|
+
emit({ type: 'message.delta', sessionId: activeSessionId, text: delta });
|
|
312
309
|
}
|
|
313
310
|
return;
|
|
314
311
|
}
|
package/ticlawk.mjs
CHANGED
|
@@ -211,6 +211,35 @@ async function recoverAllRuntimes(runtimeList, adapter) {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
async function reconcileBindingsAfterRestart(runtimes, adapter) {
|
|
215
|
+
const hostId = getHostId();
|
|
216
|
+
for (const binding of listBindings({ adapter: adapter.id })) {
|
|
217
|
+
if (!belongsToRuntimeHost(binding, hostId)) {
|
|
218
|
+
logger.debugError('core', 'binding.reconcile-host-mismatch', {
|
|
219
|
+
bindingId: binding.id,
|
|
220
|
+
adapter: binding.adapter,
|
|
221
|
+
hostId,
|
|
222
|
+
runtime_host_id: getBindingRuntimeHostId(binding),
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const runtime = binding.runtime ? runtimes[binding.runtime] : null;
|
|
227
|
+
if (typeof runtime?.reconcileAfterRestart !== 'function') continue;
|
|
228
|
+
try {
|
|
229
|
+
await runtime.reconcileAfterRestart(binding, {
|
|
230
|
+
adapter,
|
|
231
|
+
logger,
|
|
232
|
+
});
|
|
233
|
+
} catch (err) {
|
|
234
|
+
logger.debugError('startup', 'reconcileAfterRestart.failed', {
|
|
235
|
+
runtime: binding.runtime,
|
|
236
|
+
bindingId: binding.id,
|
|
237
|
+
error: err?.message || String(err),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
214
243
|
export async function startTiclawk() {
|
|
215
244
|
if (started) return;
|
|
216
245
|
started = true;
|
|
@@ -249,6 +278,7 @@ export async function startTiclawk() {
|
|
|
249
278
|
});
|
|
250
279
|
startReminderTicker();
|
|
251
280
|
await recoverAllRuntimes(runtimeList, adapter);
|
|
281
|
+
await reconcileBindingsAfterRestart(runtimes, adapter);
|
|
252
282
|
await adapter.start();
|
|
253
283
|
}
|
|
254
284
|
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Legacy handbook cleanup.
|
|
3
|
-
*
|
|
4
|
-
* As of the lane-separation refactor, all handbook content is inlined into
|
|
5
|
-
* the standing prompt (see standing-prompt.mjs). The daemon no longer
|
|
6
|
-
* syncs handbook .md files into per-agent workspaces. This module now
|
|
7
|
-
* exists only to enumerate the historical filenames so `agent-home.mjs`
|
|
8
|
-
* can remove them from workspaces that still have them.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export const LEGACY_HANDBOOK_FILE_NAMES = Object.freeze([
|
|
12
|
-
// Previously active set (inlined into standing-prompt.mjs).
|
|
13
|
-
'BASICS.md',
|
|
14
|
-
'COMMUNICATION.md',
|
|
15
|
-
'COLLABORATION.md',
|
|
16
|
-
'GOAL_TASK_CORE.md',
|
|
17
|
-
'GOAL_AUTHORITY.md',
|
|
18
|
-
'TASK_WORKER.md',
|
|
19
|
-
'SURFACES.md',
|
|
20
|
-
// Older names from prior reorganizations.
|
|
21
|
-
'TICLAWK.md',
|
|
22
|
-
'TICLAWK_CLI.md',
|
|
23
|
-
'TICLAWK_COLLABORATION.md',
|
|
24
|
-
'TICLAWK_GOAL_TASK_PROTOCOL.md',
|
|
25
|
-
'TICLAWK_GOAL_TASK_CORE.md',
|
|
26
|
-
'TICLAWK_GOAL_AUTHORITY.md',
|
|
27
|
-
'TICLAWK_TASK_WORKER.md',
|
|
28
|
-
'TICLAWK_DM_SCOPE.md',
|
|
29
|
-
'TICLAWK_GROUP_ADMIN_SCOPE.md',
|
|
30
|
-
'TICLAWK_GROUP_MEMBER_SCOPE.md',
|
|
31
|
-
'TICLAWK_SURFACES.md',
|
|
32
|
-
'TICLAWK_WORKSPACE.md',
|
|
33
|
-
'WORKSPACE.md',
|
|
34
|
-
'GOAL_TASK_PROTOCOL.md',
|
|
35
|
-
'DM_SCOPE.md',
|
|
36
|
-
'GROUP_ADMIN_SCOPE.md',
|
|
37
|
-
'GROUP_MEMBER_SCOPE.md',
|
|
38
|
-
]);
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-step goal-lane (FSM) prompt builder.
|
|
3
|
-
*
|
|
4
|
-
* A transition delivery is an FSM event, not a chat message: it has no
|
|
5
|
-
* backing message, only a payload carrying { transition_id, kind,
|
|
6
|
-
* goal_version, step }. The goal lane runs exactly the step named in the
|
|
7
|
-
* payload, then reports the outcome with `ticlawk goal report`, which
|
|
8
|
-
* advances the state machine and (for running states) schedules the next
|
|
9
|
-
* step as a fresh transition. This is the goal-lane analogue of
|
|
10
|
-
* buildInboundWakePrompt — same {header, target, text, rawText} contract.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { buildEnvelopeTarget, buildCharterBlock } from './wake-prompt.mjs';
|
|
14
|
-
|
|
15
|
-
const STEP_GUIDES = {
|
|
16
|
-
gap_analysis: {
|
|
17
|
-
title: 'GAP ANALYSIS',
|
|
18
|
-
body: `Compare the current state against the goal and success criteria. The [goal_context] block above gives you the open tasks, active reminders, and dashboard state — judge from it; read the charter/repo/prior messages only for what it doesn't cover. The dashboard is the owner's at-a-glance visualization of how far this goal has progressed — this step owns keeping it true to reality: create it if a durable goal has none, refresh it when progress moved materially (\`ticlawk dashboard set\`).
|
|
19
|
-
- Judge "due now" against the current owner-local time above. Produce only what is due now; do NOT pre-produce a future occurrence — each one is produced when its own reminder fires and wakes you at that time.
|
|
20
|
-
- If there is concrete work to do NOW, make sure the next unit exists as a task (\`ticlawk task ...\`), then report outcome=gap.
|
|
21
|
-
- If nothing needs doing this instant but the goal is ONGOING/STANDING — its job is to keep something maintained and work recurs (e.g. an active recurring reminder above already covers the next occurrence) — report outcome=wait. Do NOT report no_gap for a standing goal: it has no "done", and parking on no_gap would stop it from waking at the next occurrence. If nothing is scheduled to resume it yet, schedule a reminder first, then report wait.
|
|
22
|
-
- Report outcome=no_gap ONLY if the goal is genuinely, permanently met and will never need action again (an achievement goal that is finished). The completed result is something the owner is waiting on — surface it per the briefing rule below.
|
|
23
|
-
- Judge done-ness by the goal's real deliverable — the work itself and the dashboard — NOT by whether the task board is tidy. A task still showing open, or one you could not close, is not by itself remaining work: if the deliverable is met, report no_gap. Never keep the loop alive, or spend this turn, just to reconcile the board.`,
|
|
24
|
-
outcomes: ['gap', 'no_gap', 'wait'],
|
|
25
|
-
},
|
|
26
|
-
execute: {
|
|
27
|
-
title: 'EXECUTE',
|
|
28
|
-
body: `Do the next concrete unit of work toward the goal (or drive the current task to completion). Keep progress on the dashboard (\`ticlawk dashboard set\`); the goal lane reaches the owner only through the dashboard and briefings — never \`message send\`.
|
|
29
|
-
- When the unit of work is finished and ready to be checked, report outcome=task_completed.
|
|
30
|
-
- If you cannot proceed without something only the owner can grant — a decision, a permission, or approval to spend money or use a resource — ask for it with ONE approval briefing: \`ticlawk briefing publish --mode approval\` (short: what you need and why). That single call both asks the owner and suspends the loop on the approval. Then report outcome=needs_approval. Their answer (tapping approve, or a natural-language reply) resumes you automatically.
|
|
31
|
-
- If you are blocked by something an owner decision cannot fix — an external failure, or a missing input no decision of theirs unblocks — surface it with \`ticlawk briefing publish --mode info\` only if they would be wrong not to know, then report outcome=blocked.`,
|
|
32
|
-
outcomes: ['task_completed', 'needs_approval', 'blocked'],
|
|
33
|
-
},
|
|
34
|
-
review: {
|
|
35
|
-
title: 'REVIEW',
|
|
36
|
-
body: `Review the work that was just completed against the task and the goal.
|
|
37
|
-
- If it meets the bar, report outcome=accepted. Mark the task done if the board supports it (\`ticlawk task update ... --status done\`), but a close that fails or does not apply must NOT stop you from reporting accepted — the board is a coordination aid, not a gate.
|
|
38
|
-
- If it needs rework, return it with a clean, specific redo instruction, then report outcome=rejected.`,
|
|
39
|
-
outcomes: ['accepted', 'rejected'],
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function readPayload(msg) {
|
|
44
|
-
const payload = msg?.payload && typeof msg.payload === 'object' ? msg.payload : {};
|
|
45
|
-
return {
|
|
46
|
-
transitionId: String(payload.transition_id || '').trim(),
|
|
47
|
-
goalVersion: payload.goal_version != null ? String(payload.goal_version) : '',
|
|
48
|
-
kind: String(payload.kind || '').trim(),
|
|
49
|
-
step: String(payload.step || '').trim(),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Per-step context: the claim attaches msg.goal_context (open tasks, active
|
|
54
|
-
// reminders, current task, dashboard) for transition deliveries. Render the
|
|
55
|
-
// slice THIS step needs so each step decides on facts, not guesses.
|
|
56
|
-
function buildGoalContextBlock(msg, step) {
|
|
57
|
-
const gc = msg && msg.goal_context && typeof msg.goal_context === 'object' ? msg.goal_context : null;
|
|
58
|
-
if (!gc) return '';
|
|
59
|
-
const lines = ['[goal_context] Current state for this goal (given to you — use it, do not re-derive):'];
|
|
60
|
-
if (gc.now_local) {
|
|
61
|
-
lines.push(`- current time (owner local): ${gc.now_local}${gc.timezone ? ` [${gc.timezone}]` : ''}`);
|
|
62
|
-
}
|
|
63
|
-
if (step === 'gap_analysis' || !STEP_GUIDES[step]) {
|
|
64
|
-
const tasks = Array.isArray(gc.open_tasks) ? gc.open_tasks : [];
|
|
65
|
-
const rems = Array.isArray(gc.active_reminders) ? gc.active_reminders : [];
|
|
66
|
-
lines.push(`- open tasks (${tasks.length}): ${tasks.length
|
|
67
|
-
? tasks.map((t) => `#${t.number} ${t.title} [${t.status}]`).join('; ')
|
|
68
|
-
: 'none'}`);
|
|
69
|
-
lines.push(`- active reminders (${rems.length}): ${rems.length
|
|
70
|
-
? rems.map((r) => `"${r.title}" @ ${r.fire_at}${r.recurrence ? ' (recurring)' : ''}`).join('; ')
|
|
71
|
-
: 'none'}`);
|
|
72
|
-
lines.push(`- dashboard: ${gc.dashboard ? 'exists' : 'none yet'}`);
|
|
73
|
-
} else {
|
|
74
|
-
const ct = gc.current_task;
|
|
75
|
-
lines.push(ct ? `- current task: #${ct.number} ${ct.title} [${ct.status}]` : '- current task: (none set)');
|
|
76
|
-
}
|
|
77
|
-
lines.push('[/goal_context]');
|
|
78
|
-
return lines.join('\n');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function buildGoalStepHeader(msg, { step, transitionId, goalVersion, kind }) {
|
|
82
|
-
const target = buildEnvelopeTarget(msg);
|
|
83
|
-
const time = msg.created_at || new Date().toISOString();
|
|
84
|
-
return `[goal_lane target=${target} step=${step || 'unknown'} transition=${transitionId} goal_version=${goalVersion} kind=${kind} time=${time}]`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function buildGoalStepPrompt(msg) {
|
|
88
|
-
const { transitionId, goalVersion, kind, step } = readPayload(msg);
|
|
89
|
-
const target = buildEnvelopeTarget(msg);
|
|
90
|
-
const conversationId = msg.conversation_id || '';
|
|
91
|
-
const header = buildGoalStepHeader(msg, { step, transitionId, goalVersion, kind });
|
|
92
|
-
const charterBlock = buildCharterBlock(msg);
|
|
93
|
-
const guide = STEP_GUIDES[step] || null;
|
|
94
|
-
|
|
95
|
-
const reportCmd = `ticlawk goal report --conversation ${conversationId} --transition ${transitionId} --outcome <${guide ? guide.outcomes.join('|') : 'outcome'}>`;
|
|
96
|
-
|
|
97
|
-
const sections = [];
|
|
98
|
-
if (charterBlock) sections.push(charterBlock);
|
|
99
|
-
const goalContextBlock = buildGoalContextBlock(msg, step);
|
|
100
|
-
if (goalContextBlock) sections.push(goalContextBlock);
|
|
101
|
-
|
|
102
|
-
if (guide) {
|
|
103
|
-
sections.push([
|
|
104
|
-
`[goal_step] You are running one step of the goal loop for this conversation — a backend FSM step. Act on it and report the outcome; there is no message to reply to.`,
|
|
105
|
-
``,
|
|
106
|
-
`Current step: ${guide.title}`,
|
|
107
|
-
guide.body,
|
|
108
|
-
``,
|
|
109
|
-
`Briefings are scarce pushes that interrupt the owner; routine progress belongs on the dashboard (the owner pulls it). Send one (\`ticlawk briefing publish\`) only when the owner must act or decide (\`--mode approval\`), asked to be told, or would be wrong not to know now — goal done, blocked, off-track, or a result they are waiting on (\`--mode info\`). If unsure, it is not a briefing.`,
|
|
110
|
-
``,
|
|
111
|
-
`When the step is done, advance the state machine by running EXACTLY ONE report:`,
|
|
112
|
-
` ${reportCmd}`,
|
|
113
|
-
``,
|
|
114
|
-
`Reporting the outcome continues the loop: a running next state comes back as a fresh step; with no gap or a wait, the loop parks itself. Report exactly once — do not loop inside this turn.`,
|
|
115
|
-
`Reach the owner only through two surfaces: \`ticlawk dashboard set\` (the goal report — routine progress, the owner pulls it) and \`ticlawk briefing publish\` (a scarce push, per the rule above). The goal lane never uses \`ticlawk message send\` — chat is the chat lane's. Write owner-facing text for someone reading cold who has never seen your plan or task board: what changed, why it matters, and what (if anything) they must do — naming things by what they are, never exposing how the system works inside or how your work is organized behind the scenes (tracks, loops, internal states, codes, run names, task numbers).`,
|
|
116
|
-
`[/goal_step]`,
|
|
117
|
-
].join('\n'));
|
|
118
|
-
} else {
|
|
119
|
-
sections.push([
|
|
120
|
-
`[goal_step] Goal-loop FSM step with an unrecognized step "${step}". Re-evaluate the goal as a gap analysis: decide whether there is a gap, and report with:`,
|
|
121
|
-
` ${reportCmd}`,
|
|
122
|
-
`[/goal_step]`,
|
|
123
|
-
].join('\n'));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const text = sections.filter(Boolean).join('\n\n');
|
|
127
|
-
return {
|
|
128
|
-
header,
|
|
129
|
-
target,
|
|
130
|
-
text,
|
|
131
|
-
rawText: '',
|
|
132
|
-
};
|
|
133
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime goal/task branch selection.
|
|
3
|
-
*
|
|
4
|
-
* Prompt text lives in the managed handbook files. This module only derives
|
|
5
|
-
* the current conversation scope, role, and goal-authority branch for runtime
|
|
6
|
-
* prompt selection and cache keys.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
function getInboundRaw(ctx = {}) {
|
|
10
|
-
return ctx?.inbound?.raw && typeof ctx.inbound.raw === 'object'
|
|
11
|
-
? ctx.inbound.raw
|
|
12
|
-
: {};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function inferScope(ctx = {}) {
|
|
16
|
-
const raw = getInboundRaw(ctx);
|
|
17
|
-
const conversationType = String(raw.conversation_type || '').trim();
|
|
18
|
-
if (conversationType === 'group' || conversationType === 'thread') return 'group';
|
|
19
|
-
return 'dm';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getRecipientConversationRole(ctx = {}) {
|
|
23
|
-
const raw = getInboundRaw(ctx);
|
|
24
|
-
return String(raw.recipient_conversation_role || raw.recipient_role || '').trim().toLowerCase() || 'member';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function hasConversationAdminRole(ctx = {}) {
|
|
28
|
-
const raw = getInboundRaw(ctx);
|
|
29
|
-
if (raw.recipient_is_conversation_admin === true) return true;
|
|
30
|
-
const role = getRecipientConversationRole(ctx);
|
|
31
|
-
return role === 'admin' || role === 'owner';
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function hasGoalAuthority(ctx = {}) {
|
|
35
|
-
const scope = inferScope(ctx);
|
|
36
|
-
if (scope === 'dm') return true;
|
|
37
|
-
return hasConversationAdminRole(ctx);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function selectGoalTaskProtocolOverlays(ctx = {}) {
|
|
41
|
-
const scope = inferScope(ctx);
|
|
42
|
-
const recipientRole = getRecipientConversationRole(ctx);
|
|
43
|
-
const goalAuthority = hasGoalAuthority(ctx);
|
|
44
|
-
return { scope, recipientRole, goalAuthority };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function getGoalTaskProtocolOverlayKey(ctx = {}) {
|
|
48
|
-
const overlays = selectGoalTaskProtocolOverlays(ctx);
|
|
49
|
-
return `${overlays.scope}:${overlays.recipientRole}:${overlays.goalAuthority ? 'goal' : 'task'}`;
|
|
50
|
-
}
|