@treeseed/dsh-chapters 0.2.0 → 0.2.2-dev.2

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.
Files changed (70) hide show
  1. package/cordis.patch.yml +7 -0
  2. package/docs/backlog.md +1566 -0
  3. package/docs/contract.md +281 -0
  4. package/docs/development.md +122 -0
  5. package/docs/host-protected-head-ask.md +80 -0
  6. package/docs/project-state.md +266 -0
  7. package/docs/verify.md +61 -0
  8. package/lib/arrival.js +24 -0
  9. package/lib/arrival.js.map +1 -1
  10. package/lib/auto-save.d.ts +1 -1
  11. package/lib/auto-save.js +15 -2
  12. package/lib/auto-save.js.map +1 -1
  13. package/lib/aux-message.d.ts +17 -0
  14. package/lib/aux-message.js +20 -0
  15. package/lib/aux-message.js.map +1 -0
  16. package/lib/client.js +536 -227
  17. package/lib/client.js.map +1 -1
  18. package/lib/commands.js +107 -1
  19. package/lib/commands.js.map +1 -1
  20. package/lib/continue-core.d.ts +6 -0
  21. package/lib/continue-core.js +18 -13
  22. package/lib/continue-core.js.map +1 -1
  23. package/lib/diag.d.ts +16 -8
  24. package/lib/diag.js +46 -1
  25. package/lib/diag.js.map +1 -1
  26. package/lib/engine-core.d.ts +5 -0
  27. package/lib/engine-core.js +5 -3
  28. package/lib/engine-core.js.map +1 -1
  29. package/lib/engine.d.ts +15 -0
  30. package/lib/engine.js +79 -6
  31. package/lib/engine.js.map +1 -1
  32. package/lib/index.d.ts +6 -0
  33. package/lib/index.js +202 -9
  34. package/lib/index.js.map +1 -1
  35. package/lib/injections.js +10 -0
  36. package/lib/injections.js.map +1 -1
  37. package/lib/prompt-loop.d.ts +73 -0
  38. package/lib/prompt-loop.js +141 -0
  39. package/lib/prompt-loop.js.map +1 -0
  40. package/lib/prompt-runner.d.ts +44 -0
  41. package/lib/prompt-runner.js +85 -0
  42. package/lib/prompt-runner.js.map +1 -0
  43. package/lib/prompt-state.d.ts +124 -0
  44. package/lib/prompt-state.js +96 -0
  45. package/lib/prompt-state.js.map +1 -0
  46. package/lib/prompt.d.ts +32 -2
  47. package/lib/prompt.js +31 -9
  48. package/lib/prompt.js.map +1 -1
  49. package/lib/registry.d.ts +7 -0
  50. package/lib/registry.js +2 -0
  51. package/lib/registry.js.map +1 -1
  52. package/lib/render.d.ts +22 -1
  53. package/lib/render.js +40 -7
  54. package/lib/render.js.map +1 -1
  55. package/lib/repair-run.d.ts +27 -0
  56. package/lib/repair-run.js +85 -2
  57. package/lib/repair-run.js.map +1 -1
  58. package/lib/rules.js +8 -0
  59. package/lib/rules.js.map +1 -1
  60. package/lib/save-core.js +4 -4
  61. package/lib/save-core.js.map +1 -1
  62. package/lib/store.js +11 -0
  63. package/lib/store.js.map +1 -1
  64. package/lib/tools.d.ts +3 -0
  65. package/lib/tools.js +29 -6
  66. package/lib/tools.js.map +1 -1
  67. package/lib/version.d.ts +1 -1
  68. package/lib/version.js +1 -1
  69. package/lib/version.js.map +1 -1
  70. package/package.json +20 -16
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Autorun: the suggestion loop that starts the next real turn (#113).
3
+ *
4
+ * This is the piece that makes the feature a loop rather than a widget, and it is deliberately built out of
5
+ * the host goal driver's own observed semantics:
6
+ * - the flag is **process-local** and starts disarmed, so a restart cannot resurrect a loop
7
+ * (`dsh-goal-round-driver` disarms every agent on mount; a re-arm is an explicit human act);
8
+ * - the next turn is started with the agent's own `followup`, which is how the goal round chains turns —
9
+ * no timers, no private queue, nothing that can drift from the agent's real idle state;
10
+ * - a human message **disarms** it. The host's driver only defers in that case; here the human said
11
+ * "stop", because the maintainer's rule is that autorun runs "until the user stops it".
12
+ *
13
+ * The authored message carries our producer kind and its own form, so it is captured as conversation by
14
+ * `isSessionStateNarration` yet can never be mistaken for a human turn — the host's goal tool decides
15
+ * "direct human turn" by `source.kind === 'user'`, so authoring as `user` would hand this loop authority to
16
+ * pause and resume goals. It also never uses `source.kind: 'goal'`: an invariant companion deep-equals every
17
+ * goal-sourced message against the host's own template, so impersonating it is a loud failure by design.
18
+ */
19
+ import { assertKernelAcceptedUserMessage } from "./notice.js";
20
+ import { goalDriverOwnsTheTurn } from "./prompt-state.js";
21
+ export function makePromptLoop(deps) {
22
+ const slots = new Map();
23
+ const slot = (id) => {
24
+ let s = slots.get(id);
25
+ if (s === undefined) {
26
+ s = { armed: 'disarmed', fired: 0, firedFor: null };
27
+ slots.set(id, s);
28
+ }
29
+ return s;
30
+ };
31
+ const note = (m) => { deps.log?.(m); };
32
+ return {
33
+ statusOf: (id) => { const s = slot(id); return { autorun: s.armed, fired: s.fired }; },
34
+ setArmed(id, on) {
35
+ const s = slot(id);
36
+ s.armed = on ? 'armed' : 'disarmed';
37
+ // Displace, do not defer. One loop owns the next turn: whichever the human armed last.
38
+ let displaced = false;
39
+ if (on && deps.displaceGoal !== undefined) {
40
+ try {
41
+ displaced = deps.displaceGoal(id) === true;
42
+ }
43
+ catch (error) {
44
+ deps.warn?.(`prompt: could not disarm the goal driver for ${id}: ${String(error?.message ?? error).slice(0, 120)}`);
45
+ }
46
+ }
47
+ // Arming mid-offer is allowed to fire for the offer already on screen: the human said "keep going
48
+ // from here", and making them wait for the next turn boundary is the difference between a loop and a
49
+ // poller. Re-arming does NOT reset the budget — that is what stops it being an infinite bypass.
50
+ note(on
51
+ ? `prompt: autorun armed for ${id} — ${displaced
52
+ ? "goal continuation DISARMED (objective unchanged); the suggestion loop drives the worker"
53
+ : 'the goal keeps its own continuation; this loop yields the turn to it'}`
54
+ : `prompt: autorun disarmed for ${id} (${String(s.fired)} round(s) spent of ${String(deps.maxRounds())})`);
55
+ return { autorun: s.armed, fired: s.fired, displaced };
56
+ },
57
+ disarmForHuman(id) {
58
+ const s = slot(id);
59
+ if (s.armed === 'disarmed')
60
+ return s.armed;
61
+ s.armed = 'disarmed';
62
+ note(`prompt: autorun disarmed for ${id} — a human prompt took the wheel`);
63
+ return s.armed;
64
+ },
65
+ maybeFire(id, state, goal = null) {
66
+ const s = slot(id);
67
+ if (s.armed !== 'armed' || state.status !== 'ready' || state.text === '')
68
+ return false;
69
+ // #113, the authority boundary the host drew: `GOAL:` offers RE-CUT the standing objective, and
70
+ // dsh-tool-goal gates that on a direct human turn (`source.kind === 'user'` in the open turn). Our
71
+ // autorun message is plugin-authored, so running it would either be refused mid-turn or — if we
72
+ // faked the kind — escalate the loop into authority it was never given. Hold it for the keystroke
73
+ // and SAY so, rather than trying.
74
+ if (state.intent === 'goal-edit') {
75
+ deps.log?.(`prompt: NOT auto-run for ${id} — this step re-cuts the goal, which needs your keystroke (Run)`);
76
+ return false;
77
+ }
78
+ // One turn-starter per boundary. The host driver queues its own round when the goal is active AND
79
+ // armed; two loops following up at one idle edge is a race with the driver's reservation fence. We
80
+ // keep generating (the director's eye stays open) and run nothing until the driver is not owning it.
81
+ if (goalDriverOwnsTheTurn(goal)) {
82
+ deps.log?.(`prompt: yielding the next turn to the goal driver for ${id} (goal active and armed) — offers continue, autorun does not`);
83
+ return false;
84
+ }
85
+ // A goal edit invalidates the offer made against the previous revision: do not push last cycle's
86
+ // directive into an objective the human just re-cut.
87
+ if (goal !== null && state.goalRevision !== null && goal.revision !== state.goalRevision) {
88
+ deps.log?.(`prompt: dropping a stale offer for ${id} (generated against revision ${String(state.goalRevision)}, goal is now ${String(goal.revision)})`);
89
+ return false;
90
+ }
91
+ if (state.turnSeq === null)
92
+ return false;
93
+ if (s.firedFor === state.turnSeq)
94
+ return false; // one turn per offer, never a retry storm
95
+ const cap = deps.maxRounds();
96
+ if (s.fired >= cap) {
97
+ s.armed = 'disarmed';
98
+ deps.warn?.(`prompt: autorun stopped for ${id} after ${String(cap)} rounds (promptMaxRounds) — re-arm to continue`);
99
+ return false;
100
+ }
101
+ s.fired += 1;
102
+ s.firedFor = state.turnSeq;
103
+ deps.followup(id, state.text);
104
+ note(`prompt: autorun fired round ${String(s.fired)} of ${String(cap)} for ${id} (turn ${String(state.turnSeq)})`);
105
+ return true;
106
+ },
107
+ goalSignal: (id) => (deps.goalSignal === undefined ? null : deps.goalSignal(id)),
108
+ reset(id) {
109
+ slots.set(id, { armed: 'disarmed', fired: 0, firedFor: null });
110
+ },
111
+ };
112
+ }
113
+ /**
114
+ * The message the loop puts into the conversation, as kernel-admitted `user/message` data: our producer kind,
115
+ * a published form, and an id so the row survives the host's own admission checks.
116
+ *
117
+ * `form: 'notice'` is a deliberate pick from the kernel's published `ContextForm` union
118
+ * (`instructions|catalog|snapshot|notice|relay|recall`, `dsh-llm/lib/types/message.d.ts:46`), not an invented
119
+ * word: my first version used `prompt-suggestion` and the shared assertion refused it at the authorship
120
+ * boundary — which is exactly what that guard exists for. `snapshot` was unavailable in practice: it is the
121
+ * TOC notice's own kind+form pair, and reusing it would have made the archive screen read suggestions as
122
+ * notices of the archive. Reuses `notice.ts`'s
123
+ * assertion rather than re-deriving the rules — one predicate for one decision (#59's lesson), and the
124
+ * `host-seed-contract` row already drift-gates that transcription against the kernel itself.
125
+ *
126
+ * Deliberately NOT `kind: 'user'`: the host's goal tool decides "a direct human turn exists" by exactly that
127
+ * (`dsh-tool-goal:49-65`), so a user-kinded suggestion would hand this loop the authority to pause and
128
+ * resume goals. And never `kind: 'goal'`: an invariant companion deep-equals every goal-sourced message
129
+ * against the host's own `<goal_round>` template, so impersonation fails loudly by design.
130
+ */
131
+ export function suggestPromptMessage(text, id) {
132
+ const data = {
133
+ id: `suggested-${id}`,
134
+ role: 'user',
135
+ source: { kind: 'plugin:dsh-chapters', form: 'notice' },
136
+ content: [{ type: 'text', text }],
137
+ };
138
+ assertKernelAcceptedUserMessage({ type: 'user/message', seq: 0, data });
139
+ return data;
140
+ }
141
+ //# sourceMappingURL=prompt-loop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-loop.js","sourceRoot":"","sources":["../src/prompt-loop.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import { type PromptSuggestionState } from './prompt-state.ts';
2
+ import type { SessionState } from './registry.ts';
3
+ export interface GenerateOutcome {
4
+ ok: boolean;
5
+ text?: string;
6
+ /** #113: the goal revision this offer was generated against (null = no goal). A later edit bumps it, and
7
+ * an outstanding offer made under the OLD revision must not run — that is how a re-cut objective interrupts
8
+ * the loop instead of being bulldozed by last cycle's directive. */
9
+ goalRevision?: number | null;
10
+ /** Present when !ok && !quiet: what actually went wrong. */
11
+ reason?: string;
12
+ /** True for "there is nothing to recommend yet" — stored as `off`, never as an error. */
13
+ quiet?: boolean;
14
+ }
15
+ export interface PromptRunnerDeps {
16
+ /** The model IS the switch: false ⇒ the door is inert and writes nothing at all. */
17
+ enabled: () => boolean;
18
+ /** Session eligibility: root, interactive, preset-mounted. Headless children never pay the call. */
19
+ eligible: (session: {
20
+ id: string;
21
+ header?: Record<string, unknown>;
22
+ }) => boolean;
23
+ store: {
24
+ mutate(sessionId: string, fn: (state: SessionState) => SessionState): Promise<unknown>;
25
+ };
26
+ generate: (session: {
27
+ id: string;
28
+ header?: Record<string, unknown>;
29
+ }, turnSeq: number) => Promise<GenerateOutcome>;
30
+ /**
31
+ * Called after a `ready` offer is durably written, with the state as written. The autorun loop rides this
32
+ * rather than polling the row: firing on a persisted offer is what makes "the offer the human would have
33
+ * seen" and "the offer that ran" the same fact, and a loop that fires from its own memory can outrun the
34
+ * row it is supposed to be showing.
35
+ */
36
+ onReady?: (sessionId: string, state: PromptSuggestionState) => void;
37
+ now: () => string;
38
+ log?: (message: string) => void;
39
+ warn?: (message: string) => void;
40
+ }
41
+ export declare function makePromptRoundRunner(deps: PromptRunnerDeps): (session: {
42
+ id: string;
43
+ header?: Record<string, unknown>;
44
+ }, event: unknown) => void;
@@ -0,0 +1,85 @@
1
+ import { classifyOffer, noSuggestion } from "./prompt-state.js";
2
+ export function makePromptRoundRunner(deps) {
3
+ const chains = new Map();
4
+ const section = (id, job) => {
5
+ const prev = chains.get(id) ?? Promise.resolve();
6
+ const next = prev.then(job, job);
7
+ chains.set(id, next.then(() => undefined, () => undefined));
8
+ };
9
+ const write = async (id, next) => {
10
+ await deps.store.mutate(id, (row) => ({ ...row, promptSuggestion: next }));
11
+ };
12
+ return (session, event) => {
13
+ try {
14
+ const ev = event;
15
+ if (ev?.type !== 'turn/end' || typeof ev.seq !== 'number')
16
+ return;
17
+ // Each early return NAMED, because "the bar never updates" has three different causes and only one of
18
+ // them is a switch the human set. Silent guards are how tonight took a day.
19
+ if (!deps.enabled()) {
20
+ deps.log?.('prompt: not enabled (no defining model) — round skipped');
21
+ return;
22
+ }
23
+ if (!deps.eligible(session)) {
24
+ deps.log?.(`prompt: session ${String(session.id)} not eligible — round skipped`);
25
+ return;
26
+ }
27
+ const id = session.id;
28
+ const turnSeq = ev.seq;
29
+ section(id, async () => {
30
+ // Claim the turn inside the mutate: the row is the only place "have I already done this turn?" can
31
+ // be answered after a restart, and the decision must see the LIVE row, not a snapshot.
32
+ let claimed = false;
33
+ await deps.store.mutate(id, (row) => {
34
+ const cur = row.promptSuggestion ?? noSuggestion;
35
+ const inFlightForThisTurn = cur.turnSeq === turnSeq && (cur.status === 'analyzing' || cur.status === 'ready' || cur.status === 'failed');
36
+ if (inFlightForThisTurn) {
37
+ claimed = false;
38
+ return row;
39
+ }
40
+ claimed = true;
41
+ return { ...row, promptSuggestion: { ...noSuggestion, status: 'analyzing', turnSeq, at: deps.now() } };
42
+ });
43
+ if (!claimed) {
44
+ deps.log?.(`prompt: turn ${String(turnSeq)} already claimed for ${id}`);
45
+ return;
46
+ }
47
+ try {
48
+ const out = await deps.generate(session, turnSeq);
49
+ if (out.ok && typeof out.text === 'string' && out.text !== '') {
50
+ // Classified HERE, at the moment of authorship: intent is a property of the durable offer, so the
51
+ // bar, the `state` verb and the loop read the same fact instead of re-parsing the text three ways
52
+ // (one predicate per decision, #59). The `GOAL:` prefix is stripped — the worker must never be
53
+ // handed protocol scaffolding as if it were the human's own words.
54
+ const offer = classifyOffer(out.text);
55
+ const ready = {
56
+ status: 'ready', text: offer.text, turnSeq, error: '', at: deps.now(),
57
+ intent: offer.intent, goalRevision: out.goalRevision ?? null,
58
+ };
59
+ await write(id, ready);
60
+ deps.log?.(`prompt: recommended next step ready for ${id} (turn ${String(turnSeq)}, ${offer.intent})`);
61
+ deps.onReady?.(id, ready);
62
+ return;
63
+ }
64
+ if (out.quiet === true) {
65
+ await write(id, { ...noSuggestion, turnSeq: null });
66
+ deps.log?.(`prompt: nothing to recommend for ${id} — bar stays silent`);
67
+ return;
68
+ }
69
+ await write(id, { ...noSuggestion, status: 'failed', turnSeq, error: out.reason ?? 'the suggestion call returned nothing', at: deps.now() });
70
+ deps.warn?.(`prompt: generation failed for ${id}: ${out.reason ?? 'unknown'}`);
71
+ }
72
+ catch (error) {
73
+ // A failed generation is a DELIVERED failure (#61): the bar shows it, it does not vanish.
74
+ const message = String(error?.message ?? error).slice(0, 200);
75
+ await write(id, { ...noSuggestion, status: 'failed', turnSeq, error: message, at: deps.now() }).catch(() => undefined);
76
+ deps.warn?.(`prompt: generation threw for ${id}: ${message}`);
77
+ }
78
+ });
79
+ }
80
+ catch (error) {
81
+ deps.warn?.(`prompt: turn listener failed: ${String(error?.message ?? error).slice(0, 160)}`);
82
+ }
83
+ };
84
+ }
85
+ //# sourceMappingURL=prompt-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-runner.js","sourceRoot":"","sources":["../src/prompt-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Reading the session's goal, so a suggestion can serve it (#113).
3
+ *
4
+ * The durable-log shape is measured, not assumed — from a live session's own `goal/change` events:
5
+ * { kind:'goal/change', version:1, operation:'create'|'resume'|'pause'|…,
6
+ * goal:{ id, revision, objective, phase:'active'|'paused'|…, maxGoalRounds } }
7
+ * and goal rounds themselves arrive as `user/message` with `source.kind:'goal'` carrying
8
+ * `{goalId, revision, round}`. So the goal is readable from the log with no service dependency, which is how
9
+ * this plugin works everywhere else (durable reads, never a guessed API).
10
+ *
11
+ * Why it matters: a suggested next step that ignores an active goal is noise. The maintainer's rule is that
12
+ * the two compose — the suggestion runs alongside the goal and the loop yields when the purpose ends.
13
+ */
14
+ import type { SessionEventLike } from './types.ts';
15
+ export type GoalPhaseView = 'none' | 'active' | 'paused' | 'blocked' | 'complete';
16
+ export interface ActiveGoal {
17
+ id: string;
18
+ revision: number;
19
+ objective: string;
20
+ phase: GoalPhaseView;
21
+ maxGoalRounds: number | null;
22
+ }
23
+ /**
24
+ * The newest goal declaration in the events, or null when the session has no goal.
25
+ *
26
+ * A malformed or partial row is SKIPPED rather than thrown on: this is host-authored data crossing a plugin
27
+ * boundary, and one unexpected row must not take the bar (or the archive) down with it.
28
+ *
29
+ * The operation vocabulary is now established from the shipped types — `GoalOperation =
30
+ * 'create'|'edit'|'pause'|'resume'|'complete'|'block'|'clear'` (`dsh-goal/lib/types/domain.d.ts:12`) — so
31
+ * `clear` (which carries `cleared: GoalRef`, not `goal`) is the real terminal removal. `delete`/`remove` are
32
+ * still accepted defensively: when I first wrote this I could not see the type, and a reader should not have
33
+ * to trust that a host rename would not silently resurrect a finished goal.
34
+ *
35
+ * LIVE reads should prefer the host's own accessors over this fold — `ctx.get('goals').get(agent)` for a
36
+ * `GoalView` (which also carries `roundsStarted` and the process-local `activation`), or
37
+ * `ctx.sessionProjections.stateOf(session,'goal')` the way `tools.ts` already reads `agentPreset`. This
38
+ * function exists for the paths that have only the log: repair, distill, and tests.
39
+ */
40
+ export declare function readActiveGoal(events: readonly SessionEventLike[]): ActiveGoal | null;
41
+ /** Whether the autorun loop still has a purpose to serve: armed means run on, anything else means stand down. */
42
+ export declare function goalAuthorizesAutorun(goal: ActiveGoal | null): boolean;
43
+ /**
44
+ * The goal block folded into the suggestion input. Written as a constraint on the NEXT step, not a restated
45
+ * objective: the model already has the conversation, and what it cannot see is that a goal governs this one.
46
+ */
47
+ export declare function goalDirectiveOf(goal: ActiveGoal | null): string;
48
+ /**
49
+ * The bar's durable facts, as stored on the session row (#113). `turnSeq` is the identity of an offer:
50
+ * it is the `turn/end` seq the suggestion was generated FOR, which is what makes "one generation per
51
+ * turn" idempotent across a re-delivered event, a restart, or a second pull by the client.
52
+ *
53
+ * Deliberately absent: `autorun` and `expanded`. Those are the client's process-local intent — the goal
54
+ * bar's `activation` in everything but name — and persisting them would let a loop resume itself across a
55
+ * restart, which is the property the host's goal driver explicitly does NOT have.
56
+ */
57
+ export interface PromptSuggestionState {
58
+ status: 'off' | 'analyzing' | 'ready' | 'failed';
59
+ text: string;
60
+ turnSeq: number | null;
61
+ error: string;
62
+ at: string;
63
+ /** #113: what kind of step this is. `goal-edit` re-cuts the standing objective, which the host reserves for
64
+ * a human turn, so autorun must never act on it. Measured: `dsh-tool-goal`'s `requireDirectHuman` gates
65
+ * create/edit/pause/resume on `source.kind === 'user'` in the open turn. */
66
+ intent: 'worker-step' | 'goal-edit';
67
+ /** The goal `revision` this offer was generated against, or null when no goal was active. A revision bump
68
+ * invalidates an outstanding offer rather than pushing last cycle's directive into a re-cut objective. */
69
+ goalRevision: number | null;
70
+ }
71
+ export declare const noSuggestion: PromptSuggestionState;
72
+ /** What the loop needs to know about the goal RIGHT NOW, from the service (activation is process-local and
73
+ * is not in the log, so this is the only honest source). */
74
+ export interface GoalSignal {
75
+ phase: GoalPhaseView;
76
+ activation: 'armed' | 'disarmed';
77
+ revision: number;
78
+ }
79
+ /**
80
+ * Split the protocol prefix off a suggestion. `GOAL:` means "this step changes the objective" and the human
81
+ * must be the one to send it; anything else is worker execution and may auto-run.
82
+ *
83
+ * Deliberately a PREFIX rule on the model's own one-line answer rather than a second structured call: the
84
+ * suggestion surface already validates one bounded string, and a protocol the model can see in the rules
85
+ * (see `prompt.ts`) costs 5 characters of `promptMaxChars` instead of another auxiliary request.
86
+ */
87
+ export declare function classifyOffer(raw: string): {
88
+ intent: 'worker-step' | 'goal-edit';
89
+ text: string;
90
+ };
91
+ /**
92
+ * Does the host goal driver intend to start the next turn itself? Measured rule (`dsh-goal-round-driver`):
93
+ * it queues a round on idle when `phase === 'active' && activation === 'armed'`. Two loops starting turns at
94
+ * one boundary is a race against the driver's own reservation fence, so when this says yes we yield.
95
+ */
96
+ export declare function goalDriverOwnsTheTurn(goal: GoalSignal | null): boolean;
97
+ /**
98
+ * What the Suggested Next Step bar can ask the server for (#113), and the single shape the command plane
99
+ * exposes to the client. Kept next to `PromptSuggestionState` rather than in the loop module because the bar
100
+ * and the row are the same contract — a client that can read it can render it.
101
+ */
102
+ export interface PromptBarAccess {
103
+ read(sessionId: string): Promise<PromptSuggestionState & {
104
+ autorun: 'armed' | 'disarmed';
105
+ fired: number;
106
+ }>;
107
+ /** `displaced` is the answer to "who starts the next turn": true when arming this loop also disarmed the
108
+ * goal driver's continuation, so the human is told which engine they just opted into. */
109
+ setAutorun(sessionId: string, on: boolean): {
110
+ autorun: 'armed' | 'disarmed';
111
+ fired: number;
112
+ displaced: boolean;
113
+ };
114
+ /** #113: the bar's Run. Submits the offer on screen as a HUMAN-authored user turn, through the same
115
+ * follow-up path autorun uses — one behavior for "run it", whether a keystroke or the loop came first. */
116
+ runNow(sessionId: string): Promise<{
117
+ ok: boolean;
118
+ text?: string;
119
+ reason?: string;
120
+ }>;
121
+ disarmHuman(sessionId: string): void;
122
+ setOff(sessionId: string): Promise<void>;
123
+ setOn(sessionId: string): Promise<void>;
124
+ }
@@ -0,0 +1,96 @@
1
+ const isStr = (v) => typeof v === 'string' && v.length > 0;
2
+ /**
3
+ * The newest goal declaration in the events, or null when the session has no goal.
4
+ *
5
+ * A malformed or partial row is SKIPPED rather than thrown on: this is host-authored data crossing a plugin
6
+ * boundary, and one unexpected row must not take the bar (or the archive) down with it.
7
+ *
8
+ * The operation vocabulary is now established from the shipped types — `GoalOperation =
9
+ * 'create'|'edit'|'pause'|'resume'|'complete'|'block'|'clear'` (`dsh-goal/lib/types/domain.d.ts:12`) — so
10
+ * `clear` (which carries `cleared: GoalRef`, not `goal`) is the real terminal removal. `delete`/`remove` are
11
+ * still accepted defensively: when I first wrote this I could not see the type, and a reader should not have
12
+ * to trust that a host rename would not silently resurrect a finished goal.
13
+ *
14
+ * LIVE reads should prefer the host's own accessors over this fold — `ctx.get('goals').get(agent)` for a
15
+ * `GoalView` (which also carries `roundsStarted` and the process-local `activation`), or
16
+ * `ctx.sessionProjections.stateOf(session,'goal')` the way `tools.ts` already reads `agentPreset`. This
17
+ * function exists for the paths that have only the log: repair, distill, and tests.
18
+ */
19
+ export function readActiveGoal(events) {
20
+ let current = null;
21
+ for (const ev of events) {
22
+ if (ev.type !== 'goal/change')
23
+ continue;
24
+ const data = (ev.data ?? {});
25
+ const operation = isStr(data.operation) ? data.operation : '';
26
+ if (operation === 'clear' || operation === 'delete' || operation === 'remove') {
27
+ current = null;
28
+ continue;
29
+ }
30
+ const goal = (data.goal ?? {});
31
+ if (!isStr(goal.id) || !isStr(goal.objective))
32
+ continue;
33
+ const rawPhase = isStr(goal.phase) ? goal.phase : 'active';
34
+ const phase = rawPhase === 'paused' ? 'paused'
35
+ : rawPhase === 'blocked' ? 'blocked'
36
+ : rawPhase === 'complete' ? 'complete'
37
+ : 'active';
38
+ const merged = {
39
+ id: goal.id,
40
+ revision: Number.isFinite(goal.revision) ? goal.revision : (current?.revision ?? 0),
41
+ objective: goal.objective,
42
+ // A later terminal phase must not be resurrected by an unrelated row: complete is sticky per id.
43
+ phase: current !== null && current.id === goal.id && current.phase === 'complete' ? 'complete' : phase,
44
+ maxGoalRounds: Number.isFinite(goal.maxGoalRounds) ? goal.maxGoalRounds : null,
45
+ };
46
+ current = operation === 'complete' ? { ...merged, phase: 'complete' } : merged;
47
+ }
48
+ return current === null || current.phase === 'complete' ? null : current;
49
+ }
50
+ /** Whether the autorun loop still has a purpose to serve: armed means run on, anything else means stand down. */
51
+ export function goalAuthorizesAutorun(goal) {
52
+ return goal !== null && goal.phase === 'active';
53
+ }
54
+ /**
55
+ * The goal block folded into the suggestion input. Written as a constraint on the NEXT step, not a restated
56
+ * objective: the model already has the conversation, and what it cannot see is that a goal governs this one.
57
+ */
58
+ export function goalDirectiveOf(goal) {
59
+ if (goal === null)
60
+ return 'No goal is active for this session. Recommend the next step the work itself calls for.';
61
+ return [
62
+ `An ACTIVE GOAL governs this session (revision ${String(goal.revision)}): ${goal.objective.slice(0, 600)}`,
63
+ 'The recommended next step MUST advance that goal — a step that merely continues current work does not count.',
64
+ 'If the goal is already accomplished, recommend the step that closes it out (mark it complete) rather than another round.',
65
+ ].join('\n');
66
+ }
67
+ export const noSuggestion = { status: 'off', text: '', turnSeq: null, error: '', at: '', intent: 'worker-step', goalRevision: null };
68
+ /**
69
+ * Split the protocol prefix off a suggestion. `GOAL:` means "this step changes the objective" and the human
70
+ * must be the one to send it; anything else is worker execution and may auto-run.
71
+ *
72
+ * Deliberately a PREFIX rule on the model's own one-line answer rather than a second structured call: the
73
+ * suggestion surface already validates one bounded string, and a protocol the model can see in the rules
74
+ * (see `prompt.ts`) costs 5 characters of `promptMaxChars` instead of another auxiliary request.
75
+ */
76
+ export function classifyOffer(raw) {
77
+ const t = raw.trim();
78
+ const m = /^GOAL\s*:\s*/i.exec(t);
79
+ if (m === null)
80
+ return { intent: 'worker-step', text: t };
81
+ const rest = t.slice(m[0].length).trim();
82
+ // `GOAL:` with nothing after it is not a re-cut; treat it as a worker step so an empty protocol never
83
+ // blocks the loop with a phantom "needs you" offer.
84
+ if (rest === '')
85
+ return { intent: 'worker-step', text: t };
86
+ return { intent: 'goal-edit', text: rest };
87
+ }
88
+ /**
89
+ * Does the host goal driver intend to start the next turn itself? Measured rule (`dsh-goal-round-driver`):
90
+ * it queues a round on idle when `phase === 'active' && activation === 'armed'`. Two loops starting turns at
91
+ * one boundary is a race against the driver's own reservation fence, so when this says yes we yield.
92
+ */
93
+ export function goalDriverOwnsTheTurn(goal) {
94
+ return goal !== null && goal.phase === 'active' && goal.activation === 'armed';
95
+ }
96
+ //# sourceMappingURL=prompt-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-state.js","sourceRoot":"","sources":["../src/prompt-state.ts"],"names":[],"mappings":"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"}
package/lib/prompt.d.ts CHANGED
@@ -6,9 +6,32 @@ export interface PromptRoute {
6
6
  provider: string;
7
7
  model: string;
8
8
  }
9
+ /**
10
+ * An offer, or a refusal. `quiet: true` marks the refusals that are NOT news — "there is nothing to recommend
11
+ * yet" and "suggestions are off" — so a caller can tell silence from failure (#113: the first thing the
12
+ * feature did on a fresh thread was shout). Genuine failures (no route, assembly, the model call) leave
13
+ * `quiet` falsy so they stay visible.
14
+ *
15
+ * WHY THIS IS NOT WIRED INTO `/chapters-prompt suggest` yet: the composer ghost obtains its suggestion by
16
+ * EXECUTING that same command over the client remote (`src/client/prompt-ghost.ts:61-65`) and treats
17
+ * `kind === 'success'` with non-empty text as AN OFFER. So softening the command's `kind` would render
18
+ * "no suggestion — nothing to recommend yet" as ghost text: a new bug, worse than the one it fixes. The
19
+ * command result carries no payload slot for this distinction, and the ghost's own guard
20
+ * (`shouldShowGhost`) has nothing to key on. Fixing the silence properly needs one of: a structured field on
21
+ * the command result the client may read, the ghost calling the surface through its own seam instead of a
22
+ * human-facing command, or the refusal text being machine-tagged. Decided deliberately, not by prose-prefix
23
+ * matching — see #113.
24
+ */
9
25
  export interface PromptSuggestionResult {
10
26
  ok: boolean;
11
- text: string;
27
+ text: string; /** 'nothing to recommend' when quiet; otherwise what went wrong — the bar shows it (#61). */
28
+ quiet?: boolean;
29
+ reason?: string;
30
+ }
31
+ /** What the surface will and won't accept, for a caller that must REPORT it (the command block, #113). */
32
+ export interface PromptLimits {
33
+ maxChars: number;
34
+ contextChars: number;
12
35
  }
13
36
  export interface PromptConfig {
14
37
  /** config default for the model; '' ⇒ off unless a stored override says otherwise. */
@@ -42,7 +65,8 @@ export interface PromptDeps {
42
65
  export interface SuggestSession {
43
66
  id: string;
44
67
  title?: string | undefined;
45
- events: SessionEventLike[];
68
+ events: SessionEventLike[]; /** #113: when a goal governs the session, the constraint on the next step — see `goalDirectiveOf`. Text, not a goal object: goal semantics stay in one module. */
69
+ goalDirective?: string | undefined;
46
70
  }
47
71
  /** The surface the host command and `/chapters-status` speak to. */
48
72
  export interface PromptSurface {
@@ -53,6 +77,10 @@ export interface PromptSurface {
53
77
  };
54
78
  setModel(spec: string | null): Promise<void>;
55
79
  suggest(session: SuggestSession): Promise<PromptSuggestionResult>;
80
+ /** #113: the Suggested Next Step bar's server-side access — the row plus the process-local loop flags. */
81
+ bar?: import('./prompt-state.ts').PromptBarAccess;
82
+ /** the configured caps, so an output can state the number it validated against */
83
+ limits(): PromptLimits;
56
84
  /** One line for /chapters-status — ON names the model, OFF names the command. */
57
85
  statusLine(): string;
58
86
  }
@@ -83,6 +111,8 @@ export declare function tailFromEvents(events: SessionEventLike[]): {
83
111
  text: string;
84
112
  }[];
85
113
  export interface SuggestFacts {
114
+ /** #113: the active goal's constraint on the next step, already worded. Absent ⇒ no goal block. */
115
+ goalDirective?: string | undefined;
86
116
  title?: string | undefined;
87
117
  chapters: {
88
118
  number: number;