omp-conductor 0.15.10 → 0.15.12
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 +273 -2544
- package/REFERENCE.md +2680 -0
- package/package.json +3 -2
- package/schema/config.schema.json +11 -23
- package/src/arm-challenge.ts +112 -0
- package/src/ask.ts +434 -0
- package/src/board.ts +81 -15
- package/src/brief-upgrade.ts +114 -8
- package/src/briefs/orchestrator.md +113 -34
- package/src/briefs/policy.md +33 -8
- package/src/briefs/worker.md +18 -9
- package/src/chain-check.ts +1 -1
- package/src/check-trailing-newlines.ts +82 -0
- package/src/cli.ts +225 -1406
- package/src/commands/arm.ts +21 -0
- package/src/commands/board.ts +23 -0
- package/src/commands/brief-upgrade.ts +186 -0
- package/src/commands/context.ts +150 -0
- package/src/commands/daemon.ts +71 -0
- package/src/commands/dashboard.ts +74 -0
- package/src/commands/decision.ts +103 -0
- package/src/commands/disarm.ts +21 -0
- package/src/commands/doctor.ts +100 -0
- package/src/commands/event.ts +62 -0
- package/src/commands/extend.ts +64 -0
- package/src/commands/friction.ts +56 -0
- package/src/commands/help.ts +9 -0
- package/src/commands/hold.ts +26 -0
- package/src/commands/intake.ts +155 -0
- package/src/commands/ledger.ts +69 -0
- package/src/commands/message.ts +96 -0
- package/src/commands/report.ts +206 -0
- package/src/commands/restart.ts +76 -0
- package/src/commands/resume.ts +58 -0
- package/src/commands/setup.ts +143 -0
- package/src/commands/start.ts +23 -0
- package/src/commands/stats.ts +131 -0
- package/src/commands/status.ts +48 -0
- package/src/commands/stop.ts +51 -0
- package/src/commands/tail.ts +109 -0
- package/src/commands/unblock.ts +39 -0
- package/src/commands/upgrade-install.ts +31 -0
- package/src/commands/upgrade-rollback.ts +32 -0
- package/src/commands/upgrade.ts +25 -0
- package/src/commands/verb.ts +83 -0
- package/src/commands/version.ts +30 -0
- package/src/commands/worker.ts +100 -0
- package/src/config-schema.ts +47 -1
- package/src/config.ts +45 -4
- package/src/daemon.ts +972 -101
- package/src/dashboard/app.js +459 -0
- package/src/dashboard/index.html +61 -0
- package/src/dashboard/server.ts +481 -0
- package/src/dashboard/style.css +348 -0
- package/src/decisions.ts +39 -14
- package/src/diff-flags.ts +131 -241
- package/src/doctor.ts +932 -0
- package/src/escalate.ts +2 -2
- package/src/failure-class.ts +66 -3
- package/src/fleet.ts +58 -1
- package/src/gitops.ts +157 -0
- package/src/graph-health.ts +1 -1
- package/src/label-projection.ts +1 -1
- package/src/lifecycle.ts +198 -2
- package/src/model-fallback.ts +177 -0
- package/src/notices.ts +9 -0
- package/src/omp.ts +93 -13
- package/src/orchestrator-tick.ts +414 -20
- package/src/orchestrator.ts +4 -4
- package/src/privileged.ts +10 -0
- package/src/release-policy.ts +342 -30
- package/src/reports.ts +19 -5
- package/src/session-host.ts +11 -5
- package/src/setup-host.ts +663 -25
- package/src/setup-install.ts +292 -28
- package/src/setup-wizard.ts +255 -74
- package/src/setup.ts +156 -35
- package/src/stats.ts +331 -0
- package/src/store.ts +219 -22
- package/src/tracker/github.ts +74 -4
- package/src/types.ts +215 -31
- package/src/unblock.ts +55 -11
- package/src/upgrade-journal.ts +220 -0
- package/src/upgrade-verify.ts +506 -0
- package/src/upgrade.ts +385 -58
- package/src/verbs/actions.ts +73 -1
- package/src/verbs/protocol.ts +29 -4
- package/src/verbs/server.ts +183 -20
- package/src/worker.ts +3 -3
- package/systemd/omp-conductor-recover.sh +433 -0
- package/systemd/omp-conductor.service.example +14 -3
- package/systemd/recover-unit-test.sh +428 -0
package/src/orchestrator-tick.ts
CHANGED
|
@@ -63,22 +63,27 @@ import {
|
|
|
63
63
|
reportScopeFromPolicy,
|
|
64
64
|
refreshComposedBriefForProject,
|
|
65
65
|
} from "./setup.ts";
|
|
66
|
+
import { sharedPolicyPath } from "./brief-upgrade.ts";
|
|
66
67
|
import {
|
|
67
68
|
recordReleaseBlock,
|
|
68
69
|
redactReleaseArgs,
|
|
70
|
+
releaseBlockedInvocation,
|
|
69
71
|
releaseDriftDigestLine,
|
|
70
72
|
releaseRefusal,
|
|
71
|
-
|
|
73
|
+
releaseToolMatch,
|
|
72
74
|
type ReleaseDecision,
|
|
73
75
|
} from "./release-policy.ts";
|
|
74
76
|
import {
|
|
75
77
|
DEFAULT_REPORT_POLICY,
|
|
76
78
|
DEFAULT_REPORT_SCOPE,
|
|
77
79
|
DENIED_RELEASE_GRANTS,
|
|
80
|
+
INTERRUPT_CATEGORIES,
|
|
78
81
|
type DispatchSummary,
|
|
79
82
|
type DigestBacklog,
|
|
80
83
|
type FrictionSignal,
|
|
81
84
|
type HeldNotice,
|
|
85
|
+
type InterruptCategory,
|
|
86
|
+
type IntakeItem,
|
|
82
87
|
type MaterialEvent,
|
|
83
88
|
type ReportScopeChoice,
|
|
84
89
|
type ReportingPolicy,
|
|
@@ -86,9 +91,22 @@ import {
|
|
|
86
91
|
type Store,
|
|
87
92
|
} from "./types.ts";
|
|
88
93
|
import { formatDecisionDigest } from "./decisions.ts";
|
|
94
|
+
import {
|
|
95
|
+
ASK_TOOL,
|
|
96
|
+
askParameterSchema,
|
|
97
|
+
DEFAULT_ASK_TIMEOUT_SECONDS,
|
|
98
|
+
MAX_ASK_TIMEOUT_SECONDS,
|
|
99
|
+
MIN_ASK_TIMEOUT_SECONDS,
|
|
100
|
+
parseAskRequest,
|
|
101
|
+
performAsk,
|
|
102
|
+
type AskResult,
|
|
103
|
+
} from "./ask.ts";
|
|
104
|
+
import { deliverOperatorMessage } from "./reports.ts";
|
|
89
105
|
import type { RunRecord } from "./types.ts";
|
|
90
106
|
import { dbPath, openStore } from "./store.ts";
|
|
91
107
|
import { digestDue, localDayKey } from "./digest-schedule.ts";
|
|
108
|
+
import { heldNoticeId } from "./notices.ts";
|
|
109
|
+
import { isActiveArmProof } from "./arm-challenge.ts";
|
|
92
110
|
|
|
93
111
|
/** The activation file. Absent means "this is not an orchestrator session". */
|
|
94
112
|
export const TICK_CONFIG_FILE = ".conductor-tick.json";
|
|
@@ -96,6 +114,24 @@ export const TICK_CONFIG_FILE = ".conductor-tick.json";
|
|
|
96
114
|
/** Namespaced so a renderer or a session-log reader can pick ticks out. */
|
|
97
115
|
export const TICK_CUSTOM_TYPE = "omp-conductor.tick";
|
|
98
116
|
|
|
117
|
+
/**
|
|
118
|
+
* A deterministic machine-readable steer the availability gate sends when an
|
|
119
|
+
* inbound user turn is an active arming proof (conductor #415). It names the
|
|
120
|
+
* turn for what it is so the model acknowledges receipt and hands completion to
|
|
121
|
+
* the host-side `arm` instead of ad-libbing pairing-safety prose at the bare
|
|
122
|
+
* `FLEET-…` token — and it never quotes the code, which has no place outside
|
|
123
|
+
* the protected session transcript.
|
|
124
|
+
*/
|
|
125
|
+
export const ARM_PROOF_CUSTOM_TYPE = "omp-conductor.arm-proof";
|
|
126
|
+
|
|
127
|
+
/** The model-ready wording for {@link ARM_PROOF_CUSTOM_TYPE}. Kept fixed so the
|
|
128
|
+
* orchestrator's handling of an arming proof is deterministic, whichever
|
|
129
|
+
* challenge it was cut for. */
|
|
130
|
+
export const ARM_PROOF_ACK_TEXT =
|
|
131
|
+
"The message you just received was an active omp conductor arming confirmation. " +
|
|
132
|
+
"Acknowledge that the arming confirmation was received; the host-side `arm` command " +
|
|
133
|
+
"owns completion of the arming handshake. No pairing or access change is required.";
|
|
134
|
+
|
|
99
135
|
/**
|
|
100
136
|
* A tick costs a whole turn of a frontier model, and the orchestrator's loop is
|
|
101
137
|
* about minutes of latency, not seconds. Anything under a minute is a
|
|
@@ -268,6 +304,23 @@ interface TickApi {
|
|
|
268
304
|
* `@oh-my-pi/pi-coding-agent/src/extensibility/extensions/types.ts:1267-1268`.
|
|
269
305
|
*/
|
|
270
306
|
getActiveTools(): string[];
|
|
307
|
+
/**
|
|
308
|
+
* Register one tool on this session — the same shape the verb client uses
|
|
309
|
+
* (`verbs/client.ts`). The bounded ask surface ({@link ASK_TOOL}) is mounted
|
|
310
|
+
* with it, so the orchestrator's only waiting ask can never outlive its
|
|
311
|
+
* ceiling.
|
|
312
|
+
*/
|
|
313
|
+
registerTool(tool: {
|
|
314
|
+
name: string;
|
|
315
|
+
label: string;
|
|
316
|
+
description: string;
|
|
317
|
+
parameters: unknown;
|
|
318
|
+
approval?: "read" | "write" | "exec";
|
|
319
|
+
execute(
|
|
320
|
+
toolCallId: string,
|
|
321
|
+
params: Record<string, unknown>,
|
|
322
|
+
): Promise<{ content: { type: "text"; text: string }[]; isError?: boolean }>;
|
|
323
|
+
}): void;
|
|
271
324
|
on(event: "session_start", handler: (event: { type: "session_start" }, ctx: TickContext) => void): void;
|
|
272
325
|
on(event: "turn_start", handler: (event: { type: "turn_start" }, ctx: TickContext) => void): void;
|
|
273
326
|
on(event: "turn_end", handler: (event: { type: "turn_end" }, ctx: TickContext) => void): void;
|
|
@@ -282,6 +335,11 @@ interface TickApi {
|
|
|
282
335
|
synthetic?: boolean;
|
|
283
336
|
attribution?: "user" | "agent";
|
|
284
337
|
timestamp?: number;
|
|
338
|
+
/** Full-text content parts, as the runtime's `AgentMessage` carries
|
|
339
|
+
* them. The arming-proof classifier hashes the reply's tokens
|
|
340
|
+
* against persisted pending-challenge state, so it needs the text
|
|
341
|
+
* here — content absent means "no reply text", which never matches. */
|
|
342
|
+
content?: readonly { type?: string; text?: string }[];
|
|
285
343
|
};
|
|
286
344
|
},
|
|
287
345
|
ctx: TickContext,
|
|
@@ -340,6 +398,15 @@ export interface TickConfig {
|
|
|
340
398
|
* before the next tick).
|
|
341
399
|
*/
|
|
342
400
|
budgetSeconds?: number;
|
|
401
|
+
/**
|
|
402
|
+
* Seconds one {@link ASK_TOOL} call waits for the operator before its
|
|
403
|
+
* declared timeout outcome fires (#438). Optional; defaults to
|
|
404
|
+
* {@link DEFAULT_ASK_TIMEOUT_SECONDS} (5 minutes) and is always capped at the
|
|
405
|
+
* turn budget, so an ask can never outlive the turn it runs in. The ceiling
|
|
406
|
+
* is the tool's, not the model's: an ask issued without a `timeoutSeconds`
|
|
407
|
+
* gets this value all the same.
|
|
408
|
+
*/
|
|
409
|
+
askTimeoutSeconds?: number;
|
|
343
410
|
/**
|
|
344
411
|
* The herdr agent name this fleet's orchestrator pane is registered under, and
|
|
345
412
|
* the whole of {@link resolveTickOwnership}'s identity test under herdr.
|
|
@@ -380,9 +447,12 @@ export function defaultTickMessage(
|
|
|
380
447
|
now: Date,
|
|
381
448
|
briefPath = "ORCHESTRATOR.md",
|
|
382
449
|
policyPath = "POLICY.md",
|
|
450
|
+
sharedPath?: string,
|
|
383
451
|
): string {
|
|
452
|
+
const sources =
|
|
453
|
+
sharedPath === undefined ? "package floor + policy" : "package floor + shared policy + policy";
|
|
384
454
|
return (
|
|
385
|
-
`Tick ${now.toISOString()}: re-read ${briefPath} (composed
|
|
455
|
+
`Tick ${now.toISOString()}: re-read ${briefPath} (composed ${sources}) and ` +
|
|
386
456
|
`${policyPath} (editable fleet policy) from disk, then run your standing loop from them. ` +
|
|
387
457
|
`Learning-loop amendments edit only ${policyPath} — never the package floor.`
|
|
388
458
|
);
|
|
@@ -476,7 +546,7 @@ export function formatDigestLedger(
|
|
|
476
546
|
return flat.length <= limit ? flat : `${flat.slice(0, limit - 1)}…`;
|
|
477
547
|
};
|
|
478
548
|
const lines = [
|
|
479
|
-
`Durable digest ledger — ${backlog.materialCount} material event(s), ${backlog.heldNoticeCount} held
|
|
549
|
+
`Durable digest ledger — ${backlog.materialCount} material event(s), ${backlog.heldNoticeCount} held notice(s); oldest first:`,
|
|
480
550
|
...events.map(
|
|
481
551
|
(event) =>
|
|
482
552
|
` event ${event.id} | ${new Date(event.occurredAt).toISOString()} | ${event.category} | ` +
|
|
@@ -492,7 +562,7 @@ export function formatDigestLedger(
|
|
|
492
562
|
lines.push(` … ${backlog.materialCount - events.length} newer material event(s) remain owed.`);
|
|
493
563
|
}
|
|
494
564
|
if (backlog.heldNoticeCount > notices.length) {
|
|
495
|
-
lines.push(` … ${backlog.heldNoticeCount - notices.length} additional held
|
|
565
|
+
lines.push(` … ${backlog.heldNoticeCount - notices.length} additional held notice(s) remain owed.`);
|
|
496
566
|
}
|
|
497
567
|
const eventIds = events.map((event) => event.id).join(",");
|
|
498
568
|
const noticeIds = notices.map((notice) => notice.id).join(",");
|
|
@@ -635,10 +705,69 @@ export { TELEGRAM_APPROVAL_TOOL };
|
|
|
635
705
|
*/
|
|
636
706
|
export const TICK_APPROVAL_UNAVAILABLE_RULE =
|
|
637
707
|
`The ${TELEGRAM_APPROVAL_TOOL} tool is NOT mounted on this tick, so the package floor's yes/no amendment approval cannot be asked here. ` +
|
|
638
|
-
`If you have an amendment to propose, deliver the question with \`omp-conductor message --text "
|
|
639
|
-
`
|
|
708
|
+
`If you have an amendment to propose, deliver the question with \`omp-conductor message --category decision-needed --text "<the question>"\` — ` +
|
|
709
|
+
`it resolves this project's own Telegram chat and topic, records the question as an open decision row, and applies the ` +
|
|
710
|
+
`same availability policy as the tick: an unanswered yes/no stays pending — re-surfaced in every tick until answered ` +
|
|
711
|
+
`or the seven-day expiry, never recorded as approved. ` +
|
|
712
|
+
`Wait for the operator's reply on a later turn, then resolve the row with \`omp-conductor decision resolve <id> --answer "..."\`. ` +
|
|
713
|
+
`A returned answer proves an answer, not Telegram delivery. ` +
|
|
640
714
|
`Never apply an amendment, or record one as approved, without an explicit answer you actually received.`;
|
|
641
715
|
|
|
716
|
+
/**
|
|
717
|
+
* Appended to every tick — the shipped prompt or the operator's own — because
|
|
718
|
+
* the bounded-ask surface is the mechanical contract of a local tick (#438).
|
|
719
|
+
*
|
|
720
|
+
* Even a *mounted* `telegram_ask` is not the ask surface for a locally injected
|
|
721
|
+
* tick: it would wait for the operator for as long as the answer takes, and on
|
|
722
|
+
* 2026-08-16 that was six stopped hours. The gate refuses it, so the tick names
|
|
723
|
+
* the bounded replacement up front instead of letting the model discover the
|
|
724
|
+
* refusal mid-ask.
|
|
725
|
+
*/
|
|
726
|
+
export const TICK_ASK_RULE =
|
|
727
|
+
`On this locally injected tick, questions to your operator go through ${ASK_TOOL}: ` +
|
|
728
|
+
`call it with "on-timeout": "auto-proceed" or "park" (what to do when nobody answers within the ceiling) ` +
|
|
729
|
+
`and optionally "timeoutSeconds" — an ask issued without one still gets the default ceiling, capped at the turn budget. ` +
|
|
730
|
+
`The ${TELEGRAM_APPROVAL_TOOL} tool is refused here: it would wait for your operator for as long as the answer ` +
|
|
731
|
+
`takes, and an unanswered question must never hold the loop.`;
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* What a tick says when the bounded ask surface did not actually mount on this
|
|
735
|
+
* session (#520). {@link TICK_ASK_RULE} is appended only to a tick whose live
|
|
736
|
+
* mounted set carries {@link ASK_TOOL}; this is the other half of that
|
|
737
|
+
* agreement, and it has to carry the ask's semantics rather than just its text:
|
|
738
|
+
* the escalation category is declared from the vocabulary that already exists
|
|
739
|
+
* (the `conductor_ask` category is the same one), and the fallback command
|
|
740
|
+
* records the question as an open decision row, so "nobody answered" is a
|
|
741
|
+
* durable pending row re-surfaced in every tick — never "asked once, no reply,
|
|
742
|
+
* dropped". It also names the #524 consequence of a plain send: delivery
|
|
743
|
+
* follows the fleet's reporting policy, so a category the policy defers waits
|
|
744
|
+
* for the scheduled digest, and a *blocking* question must declare a category
|
|
745
|
+
* the fleet interrupts on.
|
|
746
|
+
*/
|
|
747
|
+
export const TICK_ASK_UNAVAILABLE_RULE =
|
|
748
|
+
`The ${ASK_TOOL} tool is NOT mounted on this tick, so the bounded ask surface is unavailable on this session. ` +
|
|
749
|
+
`Ask through the CLI fallback instead: run \`omp-conductor message --category <category> --text "<the question>"\` ` +
|
|
750
|
+
`with the escalation category declared from the policy vocabulary — fleet-stopped, tier2 or decision-needed — ` +
|
|
751
|
+
`never smuggled through a "QUESTION:" text prefix. The command records the question as an open decision row before ` +
|
|
752
|
+
`delivering, parked on silence: nobody answering keeps the row open and pending, re-surfaced in every tick until ` +
|
|
753
|
+
`answered or the seven-day expiry, never an approval. Delivery follows the same reporting policy as the tick — a ` +
|
|
754
|
+
`category the fleet's interruptOn list defers waits for the next digest or working-hours catch-up and prints a ` +
|
|
755
|
+
`held-notice id, so a blocking question must declare a category the fleet interrupts on (tier2 or fleet-stopped). ` +
|
|
756
|
+
`Wait for the operator's reply on a later turn, then resolve the row with \`omp-conductor decision resolve <id> --answer "..."\`. ` +
|
|
757
|
+
`A returned answer proves an answer, not Telegram delivery.`;
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* The gate's refusal for a raw {@link TELEGRAM_APPROVAL_TOOL} / `write
|
|
761
|
+
* xd://telegram_ask` call on a locally injected tick (#438). Fixed wording the
|
|
762
|
+
* way {@link TICK_ASK_RULE} is: a model that must act on a refusal reads it the
|
|
763
|
+
* same way every time.
|
|
764
|
+
*/
|
|
765
|
+
export const ASK_DIVERSION_REASON =
|
|
766
|
+
`Blocked: on this locally injected tick, ${TELEGRAM_APPROVAL_TOOL} would wait for your operator for as long as ` +
|
|
767
|
+
`the answer takes, and an unanswered question must never stop the loop (#438). Nothing was sent or recorded ` +
|
|
768
|
+
`by this call. Ask instead with ${ASK_TOOL}, declaring "on-timeout" ("auto-proceed" or "park") — it bounds the ` +
|
|
769
|
+
`wait, records the question durably, and returns one of those outcomes when nobody answers.`;
|
|
770
|
+
|
|
642
771
|
/**
|
|
643
772
|
* Appended to every tick — the shipped prompt or the operator's own — composed
|
|
644
773
|
* on a bridge that is not in omp-telegram's `profile: "daemon"`.
|
|
@@ -700,6 +829,49 @@ export function formatFrictionDigest(signals: readonly FrictionSignal[]): string
|
|
|
700
829
|
return lines.join("\n");
|
|
701
830
|
}
|
|
702
831
|
|
|
832
|
+
/** Bounds one pending idea's text in the tick prompt, like the digest ledger. */
|
|
833
|
+
const INTAKE_TEXT_LIMIT = 160;
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Pending intake items, each waiting to become an issue (#300).
|
|
837
|
+
*
|
|
838
|
+
* The one appended block that is a *duty* rather than a read-out: every pending
|
|
839
|
+
* item is listed with its id and text (oldest first, as the store returns
|
|
840
|
+
* them), followed by the standing instruction for grooming one into an issue —
|
|
841
|
+
* file it without the queue label, mark provenance with `intake groomed`, and
|
|
842
|
+
* name the grooming in the digest ledger. Absent when nothing is pending: an
|
|
843
|
+
* empty intake is the common state of a healthy fleet, and a block that said
|
|
844
|
+
* "0 pending" every tick would be permanent noise — the same convention as
|
|
845
|
+
* {@link queueDigestLine}. Project-specific grooming taste (priority scales,
|
|
846
|
+
* template wording) stays in POLICY.md; this block carries only the floor duty.
|
|
847
|
+
*/
|
|
848
|
+
export function formatPendingIntake(
|
|
849
|
+
items: readonly IntakeItem[],
|
|
850
|
+
instruction: { tracker: string; queueLabel: string; labelPrefix: string },
|
|
851
|
+
): string | undefined {
|
|
852
|
+
if (items.length === 0) return undefined;
|
|
853
|
+
const oneLine = (text: string): string => {
|
|
854
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
855
|
+
return flat.length <= INTAKE_TEXT_LIMIT ? flat : `${flat.slice(0, INTAKE_TEXT_LIMIT - 1)}…`;
|
|
856
|
+
};
|
|
857
|
+
const lines = [
|
|
858
|
+
`Pending intake — ${items.length} idea(s) captured, waiting to be groomed into issues, oldest first:`,
|
|
859
|
+
...items.map((item) => `- ${item.id} — ${oneLine(item.text)}`),
|
|
860
|
+
"",
|
|
861
|
+
`Groom each into exactly one issue on ${instruction.tracker}: a title stating the problem; a body ` +
|
|
862
|
+
`carrying the product rationale and the acceptance criteria as a checklist; the routing ` +
|
|
863
|
+
`"${instruction.labelPrefix}<repo>" label and a priority per POLICY.md. File it WITHOUT the ` +
|
|
864
|
+
`"${instruction.queueLabel}" queue label — filing is not promoting, and never add ` +
|
|
865
|
+
`${instruction.queueLabel} to an issue you groomed from intake. Then mark provenance with ` +
|
|
866
|
+
"`omp-conductor intake groomed <id> --issue <url>` — an already-groomed id is a no-op, and an " +
|
|
867
|
+
"idea is filed exactly once — and record the outcome with `omp-conductor event record --category " +
|
|
868
|
+
'intake --summary "groomed intake <id> → #<issue-number>" --evidence <url>` so the digest names ' +
|
|
869
|
+
"the grooming. An item that is malformed or empty is dismissed with `omp-conductor intake dismiss " +
|
|
870
|
+
"<id>` and noted in the digest rather than filed.",
|
|
871
|
+
];
|
|
872
|
+
return lines.join("\n");
|
|
873
|
+
}
|
|
874
|
+
|
|
703
875
|
/**
|
|
704
876
|
* The scope this tick carries, where the brief actually lives, and — when the
|
|
705
877
|
* config could not answer — why.
|
|
@@ -934,6 +1106,19 @@ export function readTickConfig(cwd: string): TickConfigResult {
|
|
|
934
1106
|
? budgetRaw
|
|
935
1107
|
: undefined;
|
|
936
1108
|
|
|
1109
|
+
// Same tolerance, same reason. The ask ceiling is bounded on both ends by
|
|
1110
|
+
// construction: MIN..MAX here, and the resolved per-ask ceiling is capped at
|
|
1111
|
+
// the turn budget by the ask tool itself, so a config cannot smuggle an
|
|
1112
|
+
// unbounded wait in through this key.
|
|
1113
|
+
const askRaw = raw["askTimeoutSeconds"];
|
|
1114
|
+
const askTimeoutSeconds =
|
|
1115
|
+
typeof askRaw === "number" &&
|
|
1116
|
+
Number.isInteger(askRaw) &&
|
|
1117
|
+
askRaw >= MIN_ASK_TIMEOUT_SECONDS &&
|
|
1118
|
+
askRaw <= MAX_ASK_TIMEOUT_SECONDS
|
|
1119
|
+
? askRaw
|
|
1120
|
+
: undefined;
|
|
1121
|
+
|
|
937
1122
|
// Relative paths resolve against the session cwd, so the files can sit beside
|
|
938
1123
|
// the config that names them (`state/armed`) without hard-coding a deploy path.
|
|
939
1124
|
const armedFile = optionalPath(raw["armedFile"], "armedFile", cwd, problems);
|
|
@@ -978,6 +1163,7 @@ export function readTickConfig(cwd: string): TickConfigResult {
|
|
|
978
1163
|
intervalSeconds,
|
|
979
1164
|
...(project === undefined ? {} : { project }),
|
|
980
1165
|
...(budgetSeconds === undefined ? {} : { budgetSeconds }),
|
|
1166
|
+
...(askTimeoutSeconds === undefined ? {} : { askTimeoutSeconds }),
|
|
981
1167
|
...(armedFile === undefined ? {} : { armedFile }),
|
|
982
1168
|
...(accessFile === undefined ? {} : { accessFile }),
|
|
983
1169
|
...(message === undefined ? {} : { message }),
|
|
@@ -1611,6 +1797,12 @@ interface PendingLocalTick {
|
|
|
1611
1797
|
interface ActiveLocalTick extends PendingLocalTick {
|
|
1612
1798
|
/** A person who writes during an autonomous run is awake by construction. */
|
|
1613
1799
|
humanWaiting: boolean;
|
|
1800
|
+
/** The most recent inbound turn was an *active arming proof* (conductor #415):
|
|
1801
|
+
* a reply matching a persisted pending challenge for this project. Set when
|
|
1802
|
+
* `humanWaiting` is, without flipping the human-present exemption — the
|
|
1803
|
+
* operator answered a challenge, so they are awake, and the model must not
|
|
1804
|
+
* treat the matching token as license to mutate pairing or access. */
|
|
1805
|
+
armingProof?: boolean;
|
|
1614
1806
|
}
|
|
1615
1807
|
|
|
1616
1808
|
interface TelegramInterrupt {
|
|
@@ -1718,6 +1910,32 @@ export function telegramTopicRoutingRefusal(payload: Record<string, unknown>): s
|
|
|
1718
1910
|
);
|
|
1719
1911
|
}
|
|
1720
1912
|
|
|
1913
|
+
/**
|
|
1914
|
+
* The caller's declared interrupt category for one Telegram call, when it names
|
|
1915
|
+
* a real one (#347). The tick prompt tells an autonomous turn which categories
|
|
1916
|
+
* may interrupt under this scope — a condition that stops the fleet is
|
|
1917
|
+
* `fleet-stopped`, a decision the operator must make is `tier2` — and the
|
|
1918
|
+
* caller tags the call with `category` so the admission gate below runs that
|
|
1919
|
+
* category through the same policy as a daemon escalation. Anything that is
|
|
1920
|
+
* not a known category falls back to the historical mapping in
|
|
1921
|
+
* {@link telegramInterruptFromTool}: a question is `decision-needed`, an
|
|
1922
|
+
* ordinary send is `material`.
|
|
1923
|
+
*/
|
|
1924
|
+
function declaredTelegramCategory(payload: Record<string, unknown>): InterruptCategory | undefined {
|
|
1925
|
+
const value = payload["category"];
|
|
1926
|
+
if (typeof value !== "string") return undefined;
|
|
1927
|
+
return (INTERRUPT_CATEGORIES as readonly string[]).includes(value)
|
|
1928
|
+
? (value as InterruptCategory)
|
|
1929
|
+
: undefined;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* One Telegram call, whatever surface it arrived on, classified enough for the
|
|
1934
|
+
* availability gate: its kind (a question that wants an answer, or a message)
|
|
1935
|
+
* plus the interrupt category the caller declared — defaulting to
|
|
1936
|
+
* `decision-needed` for questions and `material` for messages when the caller
|
|
1937
|
+
* declares none.
|
|
1938
|
+
*/
|
|
1721
1939
|
function telegramInterruptFromTool(
|
|
1722
1940
|
toolName: string,
|
|
1723
1941
|
input: Record<string, unknown>,
|
|
@@ -1769,7 +1987,7 @@ function telegramInterruptFromTool(
|
|
|
1769
1987
|
const canonical = serialiseTelegramPayload({ kind, payload });
|
|
1770
1988
|
return {
|
|
1771
1989
|
kind,
|
|
1772
|
-
category: kind === "question" ? "decision-needed" : "material",
|
|
1990
|
+
category: declaredTelegramCategory(payload) ?? (kind === "question" ? "decision-needed" : "material"),
|
|
1773
1991
|
summary,
|
|
1774
1992
|
hasFiles: Array.isArray(payload["files"]) && payload["files"].length > 0,
|
|
1775
1993
|
detail,
|
|
@@ -1822,6 +2040,20 @@ function autonomousTelegramInterruptBlock(
|
|
|
1822
2040
|
if (active === undefined || active.humanWaiting) return undefined;
|
|
1823
2041
|
const interrupt = telegramInterruptFromTool(event.toolName, event.input);
|
|
1824
2042
|
if (interrupt === undefined) return undefined;
|
|
2043
|
+
// The raw ask tool would wait for the operator for as long as the answer
|
|
2044
|
+
// takes; on an autonomous tick that wait must be bounded and its timeout
|
|
2045
|
+
// outcome declared (#438). Divert before the policy read, because the
|
|
2046
|
+
// diversion does not depend on availability — the incident was an ask the
|
|
2047
|
+
// policy *admitted*: the gate let telegram_ask through at 01:01Z and the loop
|
|
2048
|
+
// stopped for six hours. Nothing is persisted here: the bounded surface
|
|
2049
|
+
// records the row and the delivery when the model actually asks, and a
|
|
2050
|
+
// gate-side duplicate would record questions that were never asked.
|
|
2051
|
+
const directAsk =
|
|
2052
|
+
event.toolName === TELEGRAM_APPROVAL_TOOL ||
|
|
2053
|
+
(event.toolName === "write" && event.input["path"] === `xd://${TELEGRAM_APPROVAL_TOOL}`);
|
|
2054
|
+
if (directAsk) {
|
|
2055
|
+
return { block: true, reason: ASK_DIVERSION_REASON };
|
|
2056
|
+
}
|
|
1825
2057
|
if (active.projectName === undefined) {
|
|
1826
2058
|
return {
|
|
1827
2059
|
block: true,
|
|
@@ -1869,7 +2101,7 @@ function autonomousTelegramInterruptBlock(
|
|
|
1869
2101
|
try {
|
|
1870
2102
|
if (!store.wasNotified(key)) {
|
|
1871
2103
|
store.addHeldNotice({
|
|
1872
|
-
id:
|
|
2104
|
+
id: heldNoticeId(key),
|
|
1873
2105
|
project: active.projectName,
|
|
1874
2106
|
category: interrupt.category,
|
|
1875
2107
|
summary: interrupt.summary,
|
|
@@ -2085,13 +2317,28 @@ function tick(pi: TickApi, ctx: TickContext, config: TickConfig, session: TickSe
|
|
|
2085
2317
|
store.close();
|
|
2086
2318
|
}
|
|
2087
2319
|
}
|
|
2088
|
-
content = `${defaultTickMessage(
|
|
2320
|
+
content = `${defaultTickMessage(
|
|
2321
|
+
new Date(),
|
|
2322
|
+
scope.briefPath,
|
|
2323
|
+
scope.policyPath,
|
|
2324
|
+
existsSync(sharedPolicyPath()) ? sharedPolicyPath() : undefined,
|
|
2325
|
+
)}\n${reportingConstraint}\n${bridged ? TICK_DELIVERY_RULE_BRIDGED : TICK_DELIVERY_RULE}`;
|
|
2089
2326
|
}
|
|
2090
2327
|
if (scope.projectName !== undefined) {
|
|
2091
2328
|
// Unlike ordinary reporting prose, this is a mechanical clock reading and
|
|
2092
2329
|
// survives a custom heartbeat message. The model never infers whether the
|
|
2093
|
-
// operator can be interrupted.
|
|
2330
|
+
// operator can be interrupted. The bounded-ask rule is the same class
|
|
2331
|
+
// (#438): even a mounted telegram_ask is refused on a locally injected
|
|
2332
|
+
// tick — it would wait unbounded — so the tick names the replacement
|
|
2333
|
+
// surface up front, custom message included.
|
|
2094
2334
|
content = `${content}\n${availabilityPrompt(scope.policy, Date.now())}`;
|
|
2335
|
+
// The ask rule names the surface this session can actually call (#520):
|
|
2336
|
+
// the bounded ask is registered at `session_start`, so the live mounted
|
|
2337
|
+
// set read here — between turns, in the heartbeat timer — is the honest
|
|
2338
|
+
// source for whether the registration took. A tick that mandates a tool
|
|
2339
|
+
// the session does not carry is #114 all over again, so a missing surface
|
|
2340
|
+
// degrades to the CLI path that exists and carries the ask's semantics.
|
|
2341
|
+
content = `${content}\n${pi.getActiveTools().includes(ASK_TOOL) ? TICK_ASK_RULE : TICK_ASK_UNAVAILABLE_RULE}`;
|
|
2095
2342
|
}
|
|
2096
2343
|
let frictionStore: Store | undefined;
|
|
2097
2344
|
let frictionSignals: FrictionSignal[] = [];
|
|
@@ -2137,8 +2384,17 @@ function tick(pi: TickApi, ctx: TickContext, config: TickConfig, session: TickSe
|
|
|
2137
2384
|
project.groomBelow ?? DEFAULT_GROOM_BELOW,
|
|
2138
2385
|
);
|
|
2139
2386
|
if (queue !== undefined) content = `${content}\n${queue}`;
|
|
2387
|
+
// Pending intake is the same class of standing block as the friction
|
|
2388
|
+
// and decisions read-outs: a store-backed duty the orchestrator must
|
|
2389
|
+
// not derive from memory. The store answers, the prompt instructs.
|
|
2390
|
+
const pendingIntake = formatPendingIntake(frictionStore.pendingIntake(scope.projectName), {
|
|
2391
|
+
tracker: project.tracker.repo,
|
|
2392
|
+
queueLabel: project.queueLabel,
|
|
2393
|
+
labelPrefix: project.routing.labelPrefix,
|
|
2394
|
+
});
|
|
2395
|
+
if (pendingIntake !== undefined) content = `${content}\n${pendingIntake}`;
|
|
2140
2396
|
} catch {
|
|
2141
|
-
// unreadable config: no queue digest this tick
|
|
2397
|
+
// unreadable config: no queue digest or pending-intake block this tick
|
|
2142
2398
|
}
|
|
2143
2399
|
} catch (err) {
|
|
2144
2400
|
frictionStore?.close();
|
|
@@ -2398,6 +2654,7 @@ export default function orchestratorTickExtension(pi: TickApi): void {
|
|
|
2398
2654
|
};
|
|
2399
2655
|
let releaseGateArmed = false;
|
|
2400
2656
|
let availabilityGateArmed = false;
|
|
2657
|
+
let askToolArmed = false;
|
|
2401
2658
|
let guardArmed = false;
|
|
2402
2659
|
// An activation file makes this a fleet directory before Herdr can prove
|
|
2403
2660
|
// which pane owns it. The gate therefore starts closed and only honours a
|
|
@@ -2424,8 +2681,9 @@ export default function orchestratorTickExtension(pi: TickApi): void {
|
|
|
2424
2681
|
}).on("tool_call", (event) => {
|
|
2425
2682
|
// Most tool calls are ordinary tracker/file work. Classify first so they
|
|
2426
2683
|
// do not re-read config or emit policy diagnostics.
|
|
2427
|
-
const
|
|
2428
|
-
if (
|
|
2684
|
+
const match = releaseToolMatch(event.toolName, event.input);
|
|
2685
|
+
if (match === undefined) return undefined;
|
|
2686
|
+
const shape = match.shape;
|
|
2429
2687
|
let projectName: string | undefined;
|
|
2430
2688
|
let grants: ResolvedGrants = DENIED_RELEASE_GRANTS;
|
|
2431
2689
|
let external = true;
|
|
@@ -2457,9 +2715,12 @@ export default function orchestratorTickExtension(pi: TickApi): void {
|
|
|
2457
2715
|
if (decision === undefined) return undefined;
|
|
2458
2716
|
if (projectName !== undefined) {
|
|
2459
2717
|
try {
|
|
2718
|
+
const invocation = releaseBlockedInvocation(event.toolName, event.input);
|
|
2460
2719
|
recordReleaseBlock(projectName, "orchestrator", shape, {
|
|
2461
2720
|
tool: event.toolName,
|
|
2462
2721
|
reason: decision.reason,
|
|
2722
|
+
...(invocation === undefined ? {} : { invocation }),
|
|
2723
|
+
matched: match.matched,
|
|
2463
2724
|
args: redactReleaseArgs(event.input),
|
|
2464
2725
|
});
|
|
2465
2726
|
} catch (err) {
|
|
@@ -2509,13 +2770,35 @@ export default function orchestratorTickExtension(pi: TickApi): void {
|
|
|
2509
2770
|
}
|
|
2510
2771
|
return;
|
|
2511
2772
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
)
|
|
2518
|
-
|
|
2773
|
+
// The reply to an arming challenge lands here as an ordinary user turn. It
|
|
2774
|
+
// must reach the transcript exactly as sent — the host's transcript scan
|
|
2775
|
+
// (`armTicks`) is the backend proof, so it is left untouched — while the
|
|
2776
|
+
// model gets a trusted, machine-readable steer about it, so it stops
|
|
2777
|
+
// ad-libbing pairing-safety prose at a bare `FLEET-…` token (conductor
|
|
2778
|
+
// #415). The classification derives from persisted authenticated
|
|
2779
|
+
// pending-challenge state (hash + expiry, keyed by this project), never
|
|
2780
|
+
// from the `FLEET-` prefix, so an unsolicited lookalike that matches no
|
|
2781
|
+
// active challenge stays inert.
|
|
2782
|
+
if (message.role === "user" && message.synthetic !== true && message.attribution !== "agent") {
|
|
2783
|
+
const replyText =
|
|
2784
|
+
message.content
|
|
2785
|
+
?.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
2786
|
+
.map((part) => part.text as string)
|
|
2787
|
+
.join(" ") ?? "";
|
|
2788
|
+
const proof = isActiveArmProof(configuredProject, replyText, Date.now());
|
|
2789
|
+
if (session.activeLocalTick !== undefined) {
|
|
2790
|
+
session.activeLocalTick.humanWaiting = true;
|
|
2791
|
+
if (proof) session.activeLocalTick.armingProof = true;
|
|
2792
|
+
}
|
|
2793
|
+
// Acknowledged whether or not a local tick is running: an inactivity-
|
|
2794
|
+
// window challenge reply still deserves deterministic handling, and the
|
|
2795
|
+
// host-side `arm` owns completion either way.
|
|
2796
|
+
if (proof) {
|
|
2797
|
+
pi.sendMessage(
|
|
2798
|
+
{ customType: ARM_PROOF_CUSTOM_TYPE, content: ARM_PROOF_ACK_TEXT, display: true, attribution: "agent" },
|
|
2799
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2519
2802
|
}
|
|
2520
2803
|
});
|
|
2521
2804
|
pi.on("agent_end", () => {
|
|
@@ -2539,6 +2822,116 @@ export default function orchestratorTickExtension(pi: TickApi): void {
|
|
|
2539
2822
|
);
|
|
2540
2823
|
};
|
|
2541
2824
|
|
|
2825
|
+
/**
|
|
2826
|
+
* Mount the bounded ask surface (#438). One registration per session, next to
|
|
2827
|
+
* the gates it complements: raw {@link TELEGRAM_APPROVAL_TOOL} is refused on
|
|
2828
|
+
* autonomous ticks, so the session must own the tool that asks instead. The
|
|
2829
|
+
* ceiling comes from the tick config the heartbeat started with — the same
|
|
2830
|
+
* startup-only contract `budgetSeconds` already has — and is capped at the
|
|
2831
|
+
* turn budget by the tool itself, so no combination of config and arguments
|
|
2832
|
+
* produces an unbounded wait.
|
|
2833
|
+
*
|
|
2834
|
+
* The tool is inert unless the config resolves this fleet's project: it needs
|
|
2835
|
+
* the project to file the decision row against and to resolve the delivery
|
|
2836
|
+
* target. An unreadable/ambiguous config makes the tool say so and record
|
|
2837
|
+
* nothing, which is the same fail-closed posture the autonomous gate takes.
|
|
2838
|
+
*/
|
|
2839
|
+
const armAskTool = (config: TickConfig, configuredProject: string | undefined): void => {
|
|
2840
|
+
if (askToolArmed) return;
|
|
2841
|
+
pi.registerTool({
|
|
2842
|
+
name: ASK_TOOL,
|
|
2843
|
+
label: ASK_TOOL,
|
|
2844
|
+
description:
|
|
2845
|
+
`Ask your operator one question and wait up to a bounded ceiling for the answer. ` +
|
|
2846
|
+
`Records the question durably (decision row + the same delivery path as \`omp-conductor message\`), ` +
|
|
2847
|
+
`delivers it to the operator per the reporting policy, and waits at most the ceiling ` +
|
|
2848
|
+
`(default ${DEFAULT_ASK_TIMEOUT_SECONDS}s, capped at the turn budget; pass "timeoutSeconds" to ` +
|
|
2849
|
+
`shorten or extend within ${MIN_ASK_TIMEOUT_SECONDS}–${MAX_ASK_TIMEOUT_SECONDS}s — an ask issued without ` +
|
|
2850
|
+
`one still gets the default). When nobody answers, the declared "on-timeout" decides: ` +
|
|
2851
|
+
`"auto-proceed" applies your recommended option and resolves the decision row naming the ` +
|
|
2852
|
+
`auto-application ("<option> (auto-applied on ask timeout)"); "park" leaves the row open and ` +
|
|
2853
|
+
`pending — re-surfaced in every tick prompt until answered or the seven-day expiry — and you then ` +
|
|
2854
|
+
`take the blocked work out of the claimable queue and record its state. A timeout is "nobody ` +
|
|
2855
|
+
`answered yet", never a cancellation, an error, or an operator "no".`,
|
|
2856
|
+
parameters: askParameterSchema(),
|
|
2857
|
+
approval: "write",
|
|
2858
|
+
execute: async (_toolCallId, params) => {
|
|
2859
|
+
const parsed = parseAskRequest(params);
|
|
2860
|
+
if (!parsed.ok) {
|
|
2861
|
+
return { content: [{ type: "text", text: parsed.problem }], isError: true };
|
|
2862
|
+
}
|
|
2863
|
+
let projectConfig;
|
|
2864
|
+
try {
|
|
2865
|
+
projectConfig = findProject(loadConfig(), configuredProject);
|
|
2866
|
+
} catch (err) {
|
|
2867
|
+
return {
|
|
2868
|
+
content: [
|
|
2869
|
+
{
|
|
2870
|
+
type: "text",
|
|
2871
|
+
text:
|
|
2872
|
+
`${ASK_TOOL}: conductor config unreadable (${err instanceof Error ? err.message : String(err)}); ` +
|
|
2873
|
+
"nothing was asked or recorded. Repair the config, do not ask through another path.",
|
|
2874
|
+
},
|
|
2875
|
+
],
|
|
2876
|
+
isError: true,
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
const store = openStore(dbPath());
|
|
2880
|
+
let result: AskResult;
|
|
2881
|
+
try {
|
|
2882
|
+
result = await performAsk(parsed.request, {
|
|
2883
|
+
store,
|
|
2884
|
+
project: projectConfig.name,
|
|
2885
|
+
configuredCeilingSeconds: config.askTimeoutSeconds,
|
|
2886
|
+
turnBudgetSeconds: config.budgetSeconds ?? DEFAULT_TICK_BUDGET_SECONDS,
|
|
2887
|
+
deliver: async (text, category) => {
|
|
2888
|
+
const at = Date.now();
|
|
2889
|
+
const noticeId = randomUUID();
|
|
2890
|
+
try {
|
|
2891
|
+
const delivered = await deliverOperatorMessage(projectConfig, text, {
|
|
2892
|
+
store,
|
|
2893
|
+
at,
|
|
2894
|
+
noticeId,
|
|
2895
|
+
category,
|
|
2896
|
+
});
|
|
2897
|
+
return delivered.kind === "sent"
|
|
2898
|
+
? { kind: "sent", category: delivered.category }
|
|
2899
|
+
: { kind: "held", category: delivered.category, noticeId: delivered.noticeId };
|
|
2900
|
+
} catch (err) {
|
|
2901
|
+
// A failed immediate send must not drop the question: fall back
|
|
2902
|
+
// to the durable hold exactly like the gate's own hold path, and
|
|
2903
|
+
// let the daemon retry with the digest.
|
|
2904
|
+
store.addHeldNotice({
|
|
2905
|
+
id: noticeId,
|
|
2906
|
+
project: projectConfig.name,
|
|
2907
|
+
category,
|
|
2908
|
+
summary: text.split("\n", 1)[0]!.slice(0, 240),
|
|
2909
|
+
detail: text,
|
|
2910
|
+
createdAt: at,
|
|
2911
|
+
});
|
|
2912
|
+
pi.logger.error(
|
|
2913
|
+
`[omp-conductor] ${ASK_TOOL} could not deliver the ask directly (${err instanceof Error ? err.message : String(err)}); held durably`,
|
|
2914
|
+
);
|
|
2915
|
+
return { kind: "held", category, noticeId };
|
|
2916
|
+
}
|
|
2917
|
+
},
|
|
2918
|
+
});
|
|
2919
|
+
} finally {
|
|
2920
|
+
store.close();
|
|
2921
|
+
}
|
|
2922
|
+
return { content: [{ type: "text", text: result.text }] };
|
|
2923
|
+
},
|
|
2924
|
+
});
|
|
2925
|
+
// Latched after the registration returns, not before: a throw inside
|
|
2926
|
+
// `registerTool` must not leave the surface permanently unmounted — the
|
|
2927
|
+
// latch would otherwise turn a transient registration failure into a
|
|
2928
|
+
// missing ask surface for the whole session (#520). On a real harness
|
|
2929
|
+
// `session_start` fires once, so this is a retry on the next session
|
|
2930
|
+
// rather than a loop, but it is the difference between a recovery and a
|
|
2931
|
+
// permanent divergence between the prompt and the mounted set.
|
|
2932
|
+
askToolArmed = true;
|
|
2933
|
+
};
|
|
2934
|
+
|
|
2542
2935
|
pi.on("session_start", (_event, ctx) => {
|
|
2543
2936
|
if (decided) return;
|
|
2544
2937
|
|
|
@@ -2571,6 +2964,7 @@ export default function orchestratorTickExtension(pi: TickApi): void {
|
|
|
2571
2964
|
const configuredProject = result.kind === "ok" ? result.config.project : undefined;
|
|
2572
2965
|
armReleaseGate(configuredProject);
|
|
2573
2966
|
armAvailabilityGate(configuredProject);
|
|
2967
|
+
if (result.kind === "ok") armAskTool(result.config, configuredProject);
|
|
2574
2968
|
|
|
2575
2969
|
if (result.kind === "invalid") {
|
|
2576
2970
|
const detail = `${result.path}: ${result.problem}`;
|
package/src/orchestrator.ts
CHANGED
|
@@ -33,8 +33,8 @@ import { stateDir } from "./config.ts";
|
|
|
33
33
|
import { formatEscalation } from "./escalate.ts";
|
|
34
34
|
import { createSession, disposeSession } from "./omp.ts";
|
|
35
35
|
import type { AgentSessionLike } from "./omp.ts";
|
|
36
|
-
import type { ReleaseBlockContext } from "./release-policy.ts";
|
|
37
|
-
import type { Escalation,
|
|
36
|
+
import type { GateShape, ReleaseBlockContext } from "./release-policy.ts";
|
|
37
|
+
import type { Escalation, ResolvedGrants, SessionRole } from "./types.ts";
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* The session factory {@link startOrchestrator} uses. Named so the test seam
|
|
@@ -47,7 +47,7 @@ export type CreateSessionFn = (opts: {
|
|
|
47
47
|
resume?: boolean;
|
|
48
48
|
role: SessionRole;
|
|
49
49
|
releaseGrants?: ResolvedGrants;
|
|
50
|
-
onReleaseBlocked?: (shape:
|
|
50
|
+
onReleaseBlocked?: (shape: GateShape, context: ReleaseBlockContext) => void;
|
|
51
51
|
onSpawn?: (pid: number) => void;
|
|
52
52
|
socketPath?: string;
|
|
53
53
|
verbSocketPath?: string;
|
|
@@ -85,7 +85,7 @@ export interface OrchestratorOpts {
|
|
|
85
85
|
sessionDir?: string;
|
|
86
86
|
model?: string;
|
|
87
87
|
releaseGrants?: ResolvedGrants;
|
|
88
|
-
onReleaseBlocked?: (shape:
|
|
88
|
+
onReleaseBlocked?: (shape: GateShape, context: ReleaseBlockContext) => void;
|
|
89
89
|
/** The child's pid, the instant it exists. See {@link VerbListener.bindPid}. */
|
|
90
90
|
onSpawn?: (pid: number) => void;
|
|
91
91
|
/** Control socket for the session child, beside its own working directory. */
|