@mstar-harness/dsh 3.8.1 → 3.8.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 (36) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +16 -6
  3. package/README.zh.md +16 -6
  4. package/dist/client/panel/engine-status-client.d.ts +84 -6
  5. package/dist/client/panel/graph/project-graph.d.ts +26 -13
  6. package/dist/client/panel/guards.d.ts +41 -1
  7. package/dist/client/panel/locale.d.ts +1 -1
  8. package/dist/client/panel/pages/AgentListPage.d.ts +1 -1
  9. package/dist/client/panel/sidebar.d.ts +3 -2
  10. package/dist/client/panel/state-section.d.ts +25 -3
  11. package/dist/client/panel/use-mstar-engine-status.d.ts +28 -4
  12. package/dist/client.js +346 -48
  13. package/dist/engine-status-endpoint.d.ts +85 -8
  14. package/dist/engine-status-store.d.ts +91 -1
  15. package/dist/engine-status-wire.d.ts +9 -0
  16. package/dist/gates/_shared.d.ts +61 -9
  17. package/dist/gates/adapter.d.ts +32 -2
  18. package/dist/gates/agent-flow.d.ts +312 -60
  19. package/dist/gates/catalog.d.ts +58 -37
  20. package/dist/gates/dispatch.d.ts +11 -2
  21. package/dist/gates/goal-bridge.d.ts +10 -130
  22. package/dist/gates/plan-mode-bridge.d.ts +20 -11
  23. package/dist/gates/role-persona.d.ts +16 -0
  24. package/dist/gates/steering.d.ts +41 -0
  25. package/dist/gates/workflow-ledger.d.ts +31 -4
  26. package/dist/gates/workflow-selection.d.ts +41 -20
  27. package/dist/index.js +1206 -392
  28. package/dist/types.d.ts +36 -11
  29. package/harness-commands/amazing-pr-review.md +2 -0
  30. package/harness-commands/codebase-audit.md +2 -0
  31. package/harness-skills/mstar-host/SKILL.md +3 -1
  32. package/harness-skills/mstar-host/references/dsh-workflow-scripts.md +424 -0
  33. package/harness-skills/mstar-host/references/dsh.md +170 -43
  34. package/harness-skills/mstar-roles/references/project-manager.md +2 -0
  35. package/harness-skills/mstar-sdd/SKILL.md +2 -0
  36. package/package.json +2 -2
@@ -3,49 +3,67 @@ import { type UserMessage } from '@deepseek-ai/dsh-llm';
3
3
  import type { PreStepDecision } from '@deepseek-ai/dsh-agent';
4
4
  import type { MstarEngineStatusPayload } from '../types.ts';
5
5
  import { HarnessResolver } from './_shared.ts';
6
+ import { type SessionHint } from './workflow-selection.ts';
6
7
  /** Default catalog cache refresh interval (ms) — see Config `catalogTtlMs`. */
7
8
  export declare const DEFAULT_CATALOG_TTL_MS = 60000;
8
- /** Catalog cache key for the explicit-`harnessDir` app-wide entry (one entry for every session). */
9
- export declare const EXPLICIT_CACHE_KEY = "\0explicit";
10
9
  /** One TTL cache entry: the unified payload plus the build timestamp. */
11
10
  export interface CatalogCacheEntry {
12
11
  payload: MstarEngineStatusPayload;
13
12
  builtAt: number;
14
13
  }
15
14
  /**
16
- * The apply-scoped `harnessDir → cache key` reverse map + invalidation
17
- * closure : the
18
- * catalog cache is keyed by {@link EXPLICIT_CACHE_KEY} or the session cwd,
19
- * while ledger records (dispatch/settle) identify the affected workspace by
20
- * `{HARNESS_DIR}` — the reverse map bridges the two so a ledger change
21
- * deletes EXACTLY the affected workspace's entry (no global clear; a
22
- * multi-workspace deployment does not rebuild every workspace). BOTH the map
23
- * and the closure are created per-apply by the entry (same lifetime as the
24
- * cache): module-level state would survive an HMR fiber restart and point at
25
- * a destroyed cache.
15
+ * The apply-scoped catalog invalidation (D4): the `harnessDir → { session →
16
+ * cache key }` reverse map + the digest set, created per-apply by the entry
17
+ * (same lifetime as the cache — module-level state would survive an HMR fiber
18
+ * restart and point at a destroyed cache).
19
+ *
20
+ * WHY a set per harness: two sessions in one workspace key their OWN catalog
21
+ * payloads, so a ledger record for that harness must drop EVERY current
22
+ * session key (never a single shared one) — and a picker commit must drop
23
+ * exactly ONE session's key without touching its neighbours.
26
24
  */
27
25
  export interface CatalogInvalidation {
28
26
  /**
29
- * Register `key` as the cache key of `harnessDir` — called by
30
- * `catalogPayloadFor` on cache hit AND build, and pre-registered by the
31
- * entry at apply for the explicit-config boot entry (a ledger record
32
- * between apply and the first pre-step must still invalidate the
33
- * pre-seeded entry). A null harness dir (no `{HARNESS_DIR}` resolved) has
34
- * no cache entry to invalidate → no-op.
27
+ * Register `key` as one session's cache key for `harnessDir` — called by
28
+ * `catalogPayloadFor` on hit AND build. A session whose hint changed
29
+ * (a picker commit) has its OLD key dropped here, so the reverse map never
30
+ * grows a key history. A null harness dir or a session with no stable id
31
+ * has no cached entry to invalidate → no-op.
35
32
  */
36
- register(harnessDir: string | null, key: string): void;
33
+ register(harnessDir: string | null, sessionId: string | undefined, key: string): void;
37
34
  /**
38
- * Delete the cache entry of `harnessDir` (D3 — the ledger record path
39
- * invokes this through the apply-bound hook). A missing mapping is a safe
40
- * no-op, and a missing entry after a previous invalidation is a no-op too
41
- * (the next pre-step rebuilds regardless). A throwing invalidation is
42
- * contained by the record path (log-only — it never blocks the ledger
43
- * record).
35
+ * Delete every current session's cache entry for `harnessDir` (the ledger
36
+ * record path invokes this through the apply-bound hook). A missing mapping
37
+ * is a safe no-op, and a missing entry after a previous invalidation is a
38
+ * no-op too (the next pre-step rebuilds regardless). A throwing
39
+ * invalidation is contained by the record path (log-only — it never blocks
40
+ * the ledger record).
44
41
  */
45
42
  invalidate(harnessDir: string): void;
43
+ /**
44
+ * Delete ONE session's cache entry (and its re-emission digest) — the
45
+ * picker commit path: the session's durable hint changed, so the next
46
+ * pre-step rebuilds from the acknowledged binding and re-emits the row
47
+ * within the same turn instead of serving the pre-pick payload until the
48
+ * TTL expires.
49
+ */
50
+ invalidateSession(harnessDir: string | null, sessionId: string | undefined): void;
46
51
  }
47
- /** Create the apply-scoped invalidation bound to ONE catalog cache (entry-internal wiring — see {@link CatalogInvalidation}). */
48
- export declare function createCatalogInvalidation(cache: Map<string, CatalogCacheEntry>): CatalogInvalidation;
52
+ /** Create the apply-scoped invalidation bound to ONE catalog cache + its digests. */
53
+ export declare function createCatalogInvalidation(cache: Map<string, CatalogCacheEntry>, digests: Map<string, TurnDigest>): CatalogInvalidation;
54
+ /**
55
+ * The cache key of ONE session's catalog payload (D4): the resolved harness
56
+ * dir + the durable `session.header.id` + the session cwd + the EFFECTIVE
57
+ * hint fields that can change the selection (the opaque lease holder and the
58
+ * durable pick — cwd and session id are already in the tuple). Two sessions in
59
+ * the same cwd therefore never share a payload, and a picker commit is a
60
+ * different key rather than a mutated entry.
61
+ *
62
+ * Encoded as a JSON string array so a delimiter (NUL / quote / comma) inside
63
+ * an opaque field cannot fuse two distinct tuples — `\u0000`-join was not
64
+ * collision-free for those values.
65
+ */
66
+ export declare function catalogCacheKey(harnessDir: string | null, sessionId: string, cwd: string, hint: SessionHint | undefined): string;
49
67
  /**
50
68
  * Build the unified catalog payload for one harness dir (boot for the
51
69
  * explicit config, first-use per workspace otherwise, then TTL-refreshed —
@@ -54,8 +72,11 @@ export declare function createCatalogInvalidation(cache: Map<string, CatalogCach
54
72
  * fallback is never silent.
55
73
  * @param ctx - registrant context (logger for the manifest fallback).
56
74
  * @param harnessDir - the resolved `{HARNESS_DIR}` (null when none found).
75
+ * @param hint - the carrying session's structural identity + durable pick
76
+ * (omitted ⇒ the automatic rungs miss and only a unique active lifecycle —
77
+ * or the picker error — resolves).
57
78
  */
58
- export declare function buildCatalogPayload(ctx: Context, harnessDir: string | null): MstarEngineStatusPayload;
79
+ export declare function buildCatalogPayload(ctx: Context, harnessDir: string | null, hint?: SessionHint): MstarEngineStatusPayload;
59
80
  /**
60
81
  * Advisory `agent/pre-step` waterfall listener (agent
61
82
  * catalog): delegates through `next()` (never `reject` — that would block the
@@ -89,26 +110,26 @@ export declare function buildCatalogPayload(ctx: Context, harnessDir: string | n
89
110
  * @param ctx - registrant context (logger for the containment path).
90
111
  * @param resolver - the per-workspace `{HARNESS_DIR}` resolver (the probe
91
112
  * never starts from the process cwd).
92
- * @param explicitKey - the app-wide cache key when an explicit
93
- * `harnessDir` config is set (undefined → per-session-cwd keys).
94
- * @param cache - per-workspace TTL catalog sources cache (boot pre-seeded
95
- * for the explicit-config case; otherwise built on first use of each
96
- * workspace root and TTL-refreshed — Config `catalogTtlMs`).
113
+ * @param cache - per-session TTL catalog cache: each session's payload is
114
+ * keyed by harness dir + `session.header.id` + cwd + effective hint, so two
115
+ * sessions in one workspace never share a selection; a session with no stable
116
+ * id is built uncached.
97
117
  * @param ttlMs - catalog refresh interval in milliseconds.
98
- * @param register - the apply-scoped `harnessDir → cache key` reverse-map
99
- * registration. * @param digests - per agent+workspace turn digests (last rendered text)
118
+ * @param invalidation - the apply-scoped session-keyed invalidation (ledger
119
+ * records evict every session of a harness; a picker commit evicts one).
120
+ * @param digests - per session+workspace turn digests (last rendered text)
100
121
  * for the digest-gated re-emission.
101
122
  * @param payload - the proposed step the loop is about to enter.
102
123
  * @param next - the remaining pre-step chain; its value is the delegated decision.
103
124
  */
104
- export declare function preStepCatalogListener(ctx: Context, resolver: HarnessResolver, explicitKey: string | undefined, cache: Map<string, CatalogCacheEntry>, ttlMs: number, register: (harnessDir: string | null, key: string) => void, digests: Map<string, TurnDigest>, payload: {
125
+ export declare function preStepCatalogListener(ctx: Context, resolver: HarnessResolver, cache: Map<string, CatalogCacheEntry>, ttlMs: number, invalidation: CatalogInvalidation, digests: Map<string, TurnDigest>, payload: {
105
126
  agent: unknown;
106
127
  messages: UserMessage[];
107
128
  turn: number;
108
129
  step: number;
109
130
  signal: AbortSignal;
110
131
  }, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>;
111
- /** Per agent+workspace turn digest: the rendered catalog text as of the last injection. */
132
+ /** Per session+workspace turn digest: the rendered catalog text as of the last injection. */
112
133
  export interface TurnDigest {
113
134
  turn: number;
114
135
  text: string;
@@ -3,6 +3,7 @@ import type { AssignmentFields, GateResult, ValidationResult } from '@mstar-harn
3
3
  import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools';
4
4
  import { HarnessResolver } from './_shared.ts';
5
5
  import type { Config } from './_shared.ts';
6
+ import type { SessionHint } from './workflow-selection.ts';
6
7
  import type { DshHostAdapter } from './adapter.ts';
7
8
  /** Logger label for the dispatch gate (dsh logger naming: `<scope>/<subject>`). */
8
9
  export declare const DISPATCH_LOGGER = "mstar/dispatch-gate";
@@ -116,8 +117,13 @@ export declare function sessionIdOf(exec: ToolExecution): string | undefined;
116
117
  * claim-before-InProgress red line needs the plan's execution_lease, and a
117
118
  * missing root/snapshot cannot confirm it — `lease.dispatch.unverifiable`
118
119
  * fires (advisory in warn, deny under hard).
120
+ * @param hint - the carrying session's selection hint: it decides WHICH
121
+ * active lifecycle's snapshot the lease is re-verified against, so a
122
+ * session that is not bound to the lifecycle holding the plan row stops
123
+ * with `lease.dispatch.plan-not-found` instead of verifying a lease it
124
+ * does not own.
119
125
  */
120
- export declare function leaseGateViolations(harnessDir: string | null, exec: ToolExecution, writable: boolean | undefined, prompt: string): ValidationResult[];
126
+ export declare function leaseGateViolations(harnessDir: string | null, exec: ToolExecution, writable: boolean | undefined, prompt: string, hint?: SessionHint): ValidationResult[];
121
127
  /**
122
128
  * The dispatch-gate validation core — the engine's SINGLE dispatch-gate
123
129
  * composition (`dispatch.composeDispatchGate`, opencode/omp/CLI parity — the
@@ -142,8 +148,11 @@ export declare function leaseGateViolations(harnessDir: string | null, exec: Too
142
148
  *
143
149
  * @returns the violations plus the writable flag (false for read-only
144
150
  * roles — the listener feeds it to the lease gate).
151
+ * @param hint - the carrying session's selection hint (the adapter derives
152
+ * it once per dispatch and shares it with the lease gate, the ledger
153
+ * record and every selection read below).
145
154
  */
146
- export declare function dispatchGateCore(config: Config, harnessDir: string | null, prompt: string): {
155
+ export declare function dispatchGateCore(config: Config, harnessDir: string | null, prompt: string, hint?: SessionHint): {
147
156
  violations: ValidationResult[];
148
157
  writable: boolean | undefined;
149
158
  };
@@ -1,13 +1,7 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
- import type { Config, HarnessResolver } from './_shared.ts';
2
+ import type { HarnessResolver } from './_shared.ts';
3
3
  /** Logger label for the goal bridge (dsh logger naming: `<scope>/<subject>`). */
4
4
  export declare const GOAL_BRIDGE_LOGGER = "mstar/goal-bridge";
5
- /**
6
- * Flat `maxGoalRounds` config fallback (architect decision — plan
7
- * ): 256, aligned with the GoalService default
8
- * (`goal/src/index.ts:187`) and ralph `maxRounds` (`tool-ralph/src/index.ts:37`).
9
- */
10
- export declare const DEFAULT_MAX_GOAL_ROUNDS = 256;
11
5
  /** Consumer log levels the module sink understands. */
12
6
  export type GoalBridgeLogLevel = 'debug' | 'warn';
13
7
  /** Module-level consumer log sink — bound by `apply` to `ctx.logger(GOAL_BRIDGE_LOGGER)` (agent-flow ledger precedent). */
@@ -19,130 +13,16 @@ export type GoalBridgeLogSink = (level: GoalBridgeLogLevel, message: string) =>
19
13
  */
20
14
  export declare function setGoalBridgeLogger(sink: GoalBridgeLogSink): GoalBridgeLogSink;
21
15
  /**
22
- * Root-agent discriminator (T1-verified; shared with the planMode bridge via
23
- * explicit no-barrel import): `header.parentSession === undefined` ⇒
24
- * root-like. Conversation forks also carry `parentSession` (seed lineage) →
25
- * conservatively excluded from the goal mirror (accepted boundary).
26
- */
27
- export declare function isRootLikeAgent(agent: unknown): boolean;
28
- /** CAS identity for one exact goal revision (upstream `GoalRef`). */
29
- export interface GoalRefView {
30
- readonly id: string;
31
- readonly revision: number;
32
- }
33
- /** The one goal surface the bridge reads (`GoalSnapshot` fields used by the mirror). */
34
- export interface GoalView extends GoalRefView {
35
- readonly objective: string;
36
- readonly phase: string;
37
- readonly maxGoalRounds: number;
38
- }
39
- /**
40
- * Minimal structural view of the goals service the bridge consumes
41
- * (`@deepseek-ai/dsh-goal` `GoalService` — every method is agent-scoped;
42
- * the runtime read is `ctx.get('goals')` without the inject requirement,
43
- * same pattern as the probe's service view). `create` throws
44
- * `GOAL_ALREADY_EXISTS` on a live non-complete goal and REPLACES a
45
- * completed one ("A completed goal may be replaced" — `goal/src/
46
- * index.ts:244-257`); `complete` is a CAS by `{ id, revision }`
47
- * (`GOAL_STALE_REVISION` on stale). The drift path uses complete+create
48
- * (never `edit`) so each new iteration gets a FRESH goal with a clean
49
- * round budget. */
50
- export interface GoalsServiceView {
51
- get(agent: unknown): GoalView | undefined;
52
- create(agent: unknown, request: {
53
- objective: string;
54
- maxGoalRounds?: number;
55
- }): unknown;
56
- complete(agent: unknown, ref: GoalRefView): unknown;
57
- }
58
- /** The `agents` service surface the `subagent/start` root walk reads. */
59
- interface AgentsView {
60
- get(id: string): unknown;
61
- }
62
- /**
63
- * The mirrored goal objective: the COMPLETE iteration flow with the exit
64
- * definition (mstar-host `/goal` rule — advancing an iteration means the
65
- * entire flow, never a sub-stage). Session-level text only — `status.json`
66
- * stays the harness SSOT.
67
- */
68
- export declare function iterationGoalObjective(iterationId: string): string;
69
- /**
70
- * Locate the steering iteration compass (mirror of the engine's
71
- * `resolveCompassEnforcement` scan + the catalog's `steeringCompassPath`):
72
- * the FIRST `{ITERATION_DIR}/<id>/delivery-compass.md` whose frontmatter
73
- * `status` is `active` or `locked` — the directory name IS the iteration id
74
- * (plan-conventions `{ITERATION_DIR}/<id>/`). Completed/status-less/archived
75
- * compasses do not steer. Silent on any read failure (advisory degrade).
76
- * Shared with the planMode bridge via explicit no-barrel import (the same "is an active iteration steering" read).
77
- * @param harnessDir - the resolved `{HARNESS_DIR}`.
78
- */
79
- export declare function steeringCompass(harnessDir: string): {
80
- iterationId: string;
81
- } | undefined;
82
- /** Inputs of the mirror: the structural goals view, the per-workspace resolver, and the resolved round cap. */
83
- export interface MirrorIterationGoalInput {
84
- resolver: HarnessResolver;
85
- /** The structural goals view (`ctx.get('goals')`); absent → the mirror is inert. */
86
- goals?: GoalsServiceView;
87
- /** The resolved `maxGoalRounds` (flat config key, absent → {@link DEFAULT_MAX_GOAL_ROUNDS}). */
88
- maxGoalRounds: number;
89
- }
90
- /**
91
- * Mirror the steering iteration objective into the goal of ONE agent.
92
- * Root-like agent (`header.parentSession === undefined`) + active iteration
93
- * (compass `status: active|locked`): `get` → absent → `create` (get-先行 —
94
- * no `GOAL_ALREADY_EXISTS`); present with drifted objective → REPLACE with
95
- * a fresh goal for the new iteration (see {@link replaceDriftedGoal} — a
96
- * completed goal is `create`d directly, a live goal is `complete`d first;
97
- * the new goal starts with a CLEAN round budget); present with the matching
98
- * objective → no-op (idempotent decision-point re-evaluation). ONE stale
99
- * re-read retry; a second stale failure is warned and abandoned —
100
- * goal-service-side concurrency is rare.
101
- *
102
- * @returns `true` when the mirror is ensured for this agent (created,
103
- * replaced, or already in place); `false` when not applicable or a
104
- * contained failure occurred. Never throws — the caller's listener stays
105
- * contained.
106
- */
107
- export declare function mirrorIterationGoal(agent: unknown, input: MirrorIterationGoalInput): boolean;
108
- /**
109
- * Resolve the ROOT agent of a published child via the `parentSession` walk
110
- * (upstream `subagent/src/continuation.ts:819-831` precedent): in-process
111
- * subagent children stamp `header.parentSession` = the parent SESSION id,
112
- * which IS the parent agent id (a session per agent); the walk stops at the
113
- * first root-like ancestor. `undefined` when unresolvable (fork lineage,
114
- * non-in-process provider, registry gap, or a cycle) — the decision point
115
- * then silently skips. Cycle guard: a `seen` set over visited session ids
116
- * (the upstream `liveLineage` guard) breaks on ANY revisited id — a 1-hop self-loop, a 2+ hop cycle
117
- * (A→B→A), or a longer malformed lineage — instead of spinning forever on
118
- * the synchronous `subagent/start` decision-point listeners (reachable via
119
- * HMR remounts, resumed/forked sessions with stale headers, or a future
120
- * host change). Shared with the planMode bridge via explicit no-barrel
121
- * import (the same `subagent/start` decision-point root walk).
122
- */
123
- export declare function rootAgentOf(agent: unknown, agents: AgentsView): unknown | undefined;
124
- /**
125
- * Register the goal bridge: an `agent/session-start` listener (root filter
126
- * inside the mirror — root and children alike fire, `runtime-types.ts:217`)
127
- * plus a decision-point re-evaluation on `subagent/start` (the existing
128
- * decision point — index.ts advisory slot), resolving the delegating ROOT
129
- * via the `parentSession` walk — the two mirror edges are idempotent (get +
130
- * compare when the mirror is in place — no churn) — plus a THIRD, advisory
131
- * listener on the `session/event` firehose : a `goal/change`
132
- * envelope whose goal is blocked logs ONE warn (code + objective summary +
133
- * project-register residual pointer) with ZERO harness writes
134
- * (the one-way mirror; see {@link warnBlockedGoal}). The goals service is an
135
- * OPTIONAL seam (`ctx.get('goals')` structural read): absent → ONE debug log
136
- * + the mirror stays inert, never a boot failure — the blocked advisory is
137
- * independent of it (it only needs the firehose + resolver). Every listener
138
- * body is try/catch-contained.
16
+ * Register the goal bridge: ONE `session/event` firehose listener
17
+ * (workflow-ledger consumer precedent) that structurally filters the durable
18
+ * `goal/change` envelopes and logs the blocked advisory (see
19
+ * {@link warnBlockedGoal}). It resolves no service — the envelope carries
20
+ * the facts, and the workspace attribution comes from the goal-owning
21
+ * session's `header.cwd`; an unresolvable harness → silent skip. Observe-only:
22
+ * no goal mutation of any kind (`create` / `edit` / `complete` / `pause` /
23
+ * `resume`), and the goal-owning agent is never read.
139
24
  *
140
25
  * @param ctx - the plugin's registrant context (the app composition root).
141
26
  * @param resolver - the shared per-workspace `{HARNESS_DIR}` resolver.
142
- * @param config - validated plugin configuration (flat `maxGoalRounds`,
143
- * absent → {@link DEFAULT_MAX_GOAL_ROUNDS}; a non-positive / non-integer
144
- * value warns once and falls back to the default — see
145
- * {@link resolveGoalRounds}).
146
27
  */
147
- export declare function registerGoalBridge(ctx: Context, resolver: HarnessResolver, config: Config): void;
148
- export {};
28
+ export declare function registerGoalBridge(ctx: Context, resolver: HarnessResolver): void;
@@ -1,5 +1,6 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { HarnessResolver } from './_shared.ts';
3
+ import type { SessionHint } from './workflow-selection.ts';
3
4
  /** Logger label for the planMode bridge (dsh logger naming: `<scope>/<subject>`). */
4
5
  export declare const PLAN_MODE_BRIDGE_LOGGER = "mstar/plan-mode-bridge";
5
6
  /** Consumer log levels the module sink understands. */
@@ -27,13 +28,20 @@ export interface PlanModeServiceView {
27
28
  set(agent: unknown, active: boolean): unknown;
28
29
  }
29
30
  /**
30
- * The planMode target for one harness: `true` iff an active iteration steers
31
- * (compass `status: active|locked`) AND a Prepare window exists (≥1 plan
32
- * row `Todo`). No active iteration / no Prepare window → `false` (plan mode
33
- * OFF — the host default).
31
+ * The planMode target for one harness + carrying session: `true` iff the
32
+ * SELECTED lifecycle's own compass still steers (compass `status:
33
+ * active|locked`, `iteration_id` matching the snapshot) AND the selected
34
+ * snapshot carries a Prepare window (≥1 plan row `Todo`). Otherwise the
35
+ * target is OFF.
34
36
  * @param harnessDir - the resolved `{HARNESS_DIR}`.
37
+ * @param hint - the root session's carrying hint (lease / cwd / durable pick).
38
+ * @returns `true`/`false` for a resolved selection, or `undefined` when this
39
+ * session has NO selected lifecycle (unbound multi-active / unreadable
40
+ * binding record) — the caller must leave plan mode alone rather than
41
+ * fabricate `false`, which would assert a state for a workflow the session
42
+ * never selected.
35
43
  */
36
- export declare function planModeTarget(harnessDir: string): boolean;
44
+ export declare function planModeTarget(harnessDir: string, hint?: SessionHint): boolean | undefined;
37
45
  /** Inputs of the sync: the structural planMode view and the per-workspace resolver. */
38
46
  export interface PlanModeSyncInput {
39
47
  resolver: HarnessResolver;
@@ -43,11 +51,12 @@ export interface PlanModeSyncInput {
43
51
  /**
44
52
  * Mirror the harness Prepare state into the planMode selection of ONE agent:
45
53
  * root-like agent (`header.parentSession === undefined`) → resolve the
46
- * workspace → compute {@link planModeTarget} → `planMode.set(agent, target)`.
47
- * The service's `'noop'` return makes repeated evaluation at multiple
48
- * decision points churn-free (no new `plan/mode` event when already in
49
- * target). Non-root agent / unresolvable harness / missing planMode service
50
- * → no set.
54
+ * workspace → compute {@link planModeTarget} from THAT root session's selected
55
+ * lifecycle → `planMode.set(agent, target)`. The service's `'noop'` return
56
+ * makes repeated evaluation at multiple decision points churn-free (no new
57
+ * `plan/mode` event when already in target). Non-root agent / unresolvable
58
+ * harness / missing planMode service / an UNBOUND session (no selected
59
+ * lifecycle) → no set.
51
60
  *
52
61
  * @returns `true` when the sync ran for this agent (set called); `false`
53
62
  * when not applicable or a contained failure occurred. Never throws — the
@@ -57,7 +66,7 @@ export declare function syncPlanMode(agent: unknown, input: PlanModeSyncInput):
57
66
  /**
58
67
  * Register the planMode bridge: an `agent/session-start` listener (root
59
68
  * filter inside — root and children alike fire, `runtime-types.ts:217`) plus
60
- * the EXISTING `subagent/start` decision point (the goal-bridge precedent),
69
+ * the EXISTING `subagent/start` decision point,
61
70
  * resolving the delegating ROOT via the shared `parentSession` walk — the
62
71
  * two edges are idempotent (`'noop'` when already in target — no churn). The
63
72
  * planMode service is an OPTIONAL seam (`ctx.get('planMode')` structural
@@ -228,6 +228,22 @@ export declare function setRolePersonaAgentsDir(dir: string | undefined): string
228
228
  * applying fiber — an HMR fiber swap unwinds it (reads return the raw
229
229
  * service again) and a re-apply restores it.
230
230
  *
231
+ * Composition-order independence (`prepend`): the SAME seam is wrapped by a
232
+ * mounted role-identity layer (`dsh-llm-fallbacks` — its dispatch seam resolves
233
+ * the Assignment's declared role and merges that role's declared `persona`),
234
+ * and BOTH wrappers fill the SAME native `persona` slot, each only when it is
235
+ * free — so whichever wrapper runs FIRST owns the slot. A waterfall runs
236
+ * listeners outermost-first, and `prepend` registers this one at the front,
237
+ * so the harness channel is outermost on ANY row order: the default profile
238
+ * mounts the mstar row first, while a re-ordered profile (or a re-applied
239
+ * plugin row) mounts it last. The operator's `rolePersonas` override — the
240
+ * harness's authoritative role identity for a role it resolves — therefore
241
+ * wins the slot over a fallbacks-side persona for the same role instead of
242
+ * losing it to whichever layer happened to apply first. A role the harness
243
+ * resolves NOTHING for still falls through to the fallbacks seam (the
244
+ * outermost wrapper delegates the caller's own request object on every skip
245
+ * path), and reads of every other service are returned untouched.
246
+ *
231
247
  * Never throws: the wrap step is contained — on any internal error the read
232
248
  * returns the UNWRAPPED service value (persona delivery degrades, the
233
249
  * runtime is untouched).
@@ -0,0 +1,41 @@
1
+ /** The `agents` service surface the `subagent/start` root walk reads. */
2
+ interface AgentsView {
3
+ get(id: string): unknown;
4
+ }
5
+ /**
6
+ * Root-agent discriminator (the root filter the planMode bridge decides on):
7
+ * `header.parentSession === undefined` ⇒ root-like. Conversation forks also
8
+ * carry `parentSession` (seed lineage) → conservatively excluded (accepted
9
+ * boundary).
10
+ */
11
+ export declare function isRootLikeAgent(agent: unknown): boolean;
12
+ /**
13
+ * Resolve the ROOT agent of a published child via the `parentSession` walk
14
+ * (upstream `subagent/src/continuation.ts:819-831` precedent): in-process
15
+ * subagent children stamp `header.parentSession` = the parent SESSION id,
16
+ * which IS the parent agent id (a session per agent); the walk stops at the
17
+ * first root-like ancestor. `undefined` when unresolvable (fork lineage,
18
+ * non-in-process provider, registry gap, or a cycle) — the decision point
19
+ * then silently skips. Cycle guard: a `seen` set over visited session ids
20
+ * (the upstream `liveLineage` guard) breaks on ANY revisited id — a 1-hop self-loop, a 2+ hop cycle
21
+ * (A→B→A), or a longer malformed lineage — instead of spinning forever on
22
+ * the synchronous `subagent/start` decision-point listeners (reachable via
23
+ * HMR remounts, resumed/forked sessions with stale headers, or a future
24
+ * host change). The same `subagent/start` decision-point root walk the
25
+ * planMode bridge consumes.
26
+ */
27
+ export declare function rootAgentOf(agent: unknown, agents: AgentsView): unknown | undefined;
28
+ /**
29
+ * Locate the steering iteration compass (mirror of the engine's
30
+ * `resolveCompassEnforcement` scan + the catalog's `steeringCompassPath`):
31
+ * the FIRST `{ITERATION_DIR}/<id>/delivery-compass.md` whose frontmatter
32
+ * `status` is `active` or `locked` — the directory name IS the iteration id
33
+ * (plan-conventions `{ITERATION_DIR}/<id>/`). Completed/status-less/archived
34
+ * compasses do not steer. Silent on any read failure (advisory degrade).
35
+ * The same "is an active iteration steering" read the planMode bridge consumes.
36
+ * @param harnessDir - the resolved `{HARNESS_DIR}`.
37
+ */
38
+ export declare function steeringCompass(harnessDir: string): {
39
+ iterationId: string;
40
+ } | undefined;
41
+ export {};
@@ -4,11 +4,16 @@
4
4
  * into the CALLING PARENT session's log (top-level runs only — nested
5
5
  * transport calls record nothing upstream; Task 1 seam notes §4). The
6
6
  * consumer has THREE halves (the architect-verified seam):
7
- * 1. COLD SCAN at apply — iterate `ctx.get('sessions').list()`, read each
8
- * session's `events` snapshot, and record any `tool-workflow/*` rows
7
+ * 1. COLD SCAN at apply — iterate `ctx.get('sessions').list()`, walk each
8
+ * session's log through the installed `seq` + `eventAt(seq)` surface
9
+ * (never a whole-log copy), and record any `tool-workflow/*` rows
9
10
  * already present. Constructor-seeded events (replay/resume/fork) NEVER
10
11
  * publish on the `session/event` firehose (`firstLiveSeq`), so without
11
- * the cold scan pre-restart runs would be invisible.
12
+ * the cold scan pre-restart runs would be invisible. A forked
13
+ * conversation's INHERITED prefix (`Session.inheritedEventCount`) is
14
+ * never part of that walk: those envelopes are the fork PARENT's rows,
15
+ * so a child records only its own events (never a second copy of the
16
+ * parent's rows, attributed to the child, in the same workflow dir).
12
17
  * 2. LIVE FIREHOSE — `ctx.events.on('session/event', …)`: the post-commit
13
18
  * append feed, delivered to ALL sessions for a root-context listener
14
19
  * (scope-null event, untagged listeners admitted).
@@ -80,6 +85,28 @@
80
85
  * record degrades the observation with one warn — the ledger row is already
81
86
  * appended, the run is never affected.
82
87
  *
88
+ * D4 session binding: every row is attributed to the lifecycle the CARRYING
89
+ * session is bound to — its durable picker record (`session.header.id` +
90
+ * exact cwd) folded into the structural hint the shared write resolver
91
+ * consumes — and the record's durable `excludedBeforeSeq` is an intentional
92
+ * EXCLUSION floor: rows the session observed while unbound (below a pick) are
93
+ * never replayed, including after a restart. Rows observed while unbound are
94
+ * not written anywhere; their next seq is persisted as the floor (with zero
95
+ * workflow-dir writes) so the later pick cannot backfill them. An unreadable
96
+ * binding record pauses attribution for that session (no write, no floor,
97
+ * row re-evaluated next scan) — an unverifiable record is never treated as
98
+ * "no pick".
99
+ *
100
+ * The hint ALSO carries the session's VERIFIED live lease holder, read at
101
+ * this edge from the `agents` service (`ctx.get('agents')` → `get(sessionId)`
102
+ * → the shared {@link verifiedLeaseHolderOf} check). The dispatch gate
103
+ * forwards the same identity, so a session whose lifecycle is only decidable
104
+ * by its lease (two active lifecycles sharing one `control_worktree_path`)
105
+ * records its rows under the lifecycle whose lease authorized the dispatch
106
+ * instead of resolving unbound and excluding them permanently. A cold/resumed
107
+ * session with no live Agent carries no holder — the hint then falls back to
108
+ * the picker identity, exactly as before.
109
+ *
83
110
  * Observe-only (plan Global Constraints: W3 / N5): ZERO gating — every read
84
111
  * and append is try/catch-contained; a throwing session read logs one warn
85
112
  * and the run is unaffected; all appends go through `recordWorkflowEvent`
@@ -184,7 +211,7 @@ export declare function advanceWatermark(workflowDir: string, sid: string, nextS
184
211
  * Register the workflow-ledger consumer: (1) a `session/created` backfill
185
212
  * listener (registered FIRST — — so no apply-time window exists
186
213
  * between the snapshot and the attach); (2) a bounded cold scan over
187
- * `ctx.sessions.list()` reading each session's `events` snapshot for
214
+ * `ctx.sessions.list()` walking each session's `seq` + `eventAt(seq)` log for
188
215
  * `tool-workflow/*` rows (covers pre-restart runs — constructor-seeded
189
216
  * events never hit the firehose, `firstLiveSeq`); (3) a live
190
217
  * `ctx.events.on('session/event', …)` listener filtering the four types.
@@ -1,6 +1,21 @@
1
1
  import type { WorkflowSelectionView } from '../types.ts';
2
- /** The active-set resolver result: the first active lifecycle or a clear error. */
2
+ /** The active-set resolver result: the session's active lifecycle or a clear error. */
3
3
  export type ActiveWorkflowSelection = WorkflowSelectionView;
4
+ /**
5
+ * What the carrying session can tell the resolver (structural — no
6
+ * dsh-session import, so cold/raw Session consumers can build one too).
7
+ * `sessionId` is the durable picker key (`session.header.id`), NEVER a
8
+ * holder fallback; `leaseHolder` is the dispatching dsh `Agent.id` when an
9
+ * Agent exists; `selectedWorkflowId` is the session's durable pick (loaded
10
+ * by the caller from the engine-status store). Every field is optional —
11
+ * an omitted hint just misses the corresponding rung.
12
+ */
13
+ export interface SessionHint {
14
+ cwd?: string;
15
+ sessionId?: string;
16
+ leaseHolder?: string;
17
+ selectedWorkflowId?: string;
18
+ }
4
19
  /**
5
20
  * Test-only observability hook :
6
21
  * whether a snapshot path is still cached. Production code never calls
@@ -9,30 +24,36 @@ export type ActiveWorkflowSelection = WorkflowSelectionView;
9
24
  */
10
25
  export declare function _terminalStatusCacheHas(snapshotPath: string): boolean;
11
26
  /**
12
- * Resolve the ACTIVE lifecycle set (root v2 `status.json` `workflows[]` —
13
- * the list holds non-terminal lifecycles only, removal-at-terminal). This
14
- * is the ONLY resolver the agent-flow writer / ledger may use: no active
15
- * entry → a clear error, never a terminal snapshot and never the root v1
16
- * file.
27
+ * Resolve the ACTIVE lifecycle this session writes under (root v2
28
+ * `status.json` `workflows[]` — the list holds non-terminal lifecycles
29
+ * only, removal-at-terminal). This is the ONLY resolver the agent-flow
30
+ * writer / ledger may use: no bound entry → a clear error, never a terminal
31
+ * snapshot, never the root v1 file, and never the registry's first entry.
17
32
  *
18
- * Active-set definition (explicit decision,
19
- * membership in `workflows[]` — the engine lifecycle enum's
20
- * non-terminal states are `running` AND `paused` (terminal lifecycles are
21
- * removed from the list at terminal). A PAUSED lifecycle therefore stays in
22
- * the active set and the agent-flow writer / ledger append to its workflow
23
- * dir (a paused lifecycle is still the operator's current lifecycle — its
24
- * ledger must keep recording; only a TERMINAL lifecycle is never a write
25
- * target).
33
+ * EVERY registry entry is validated before N or the candidates are exposed:
34
+ * the engine `validateWorkflowEntry` contract (`id`/`type`/`started_at` and a
35
+ * harness-relative `dir` with no absolute path or `..` segment) plus
36
+ * duplicate-id rejection. A bad row is an error, not a smaller active set —
37
+ * and never a path the resolver would read outside the harness. Only a real
38
+ * empty array is `workflow.selection.no-active`. Active-set definition:
39
+ * membership in `workflows[]` — the engine lifecycle enum's non-terminal
40
+ * states are `running` AND `paused`, so a PAUSED lifecycle stays in the
41
+ * active set (it is still the operator's current lifecycle; only a TERMINAL
42
+ * lifecycle is never a write target).
26
43
  * @param harnessDir - the resolved `{HARNESS_DIR}`.
44
+ * @param hint - the carrying session's structural identity, when it has one
45
+ * (omitted ⇒ the automatic rungs and the durable pick miss; unique-active
46
+ * still works).
27
47
  */
28
- export declare function resolveActiveWorkflow(harnessDir: string): ActiveWorkflowSelection;
48
+ export declare function resolveActiveWorkflow(harnessDir: string, hint?: SessionHint): ActiveWorkflowSelection;
29
49
  /**
30
50
  * Resolve the workflow the catalog/panel READ path aggregates (compass
31
- * v3.0.0 § Catalog selection rule): active `workflows[]` first (multiple
32
- * active → first + a structured warning), else the latest terminal
33
- * snapshot by mtime (history view), else a clear error. Never reads the
51
+ * v3.0.0 § Catalog selection rule): the SAME binding order as the write
52
+ * path, else the latest terminal snapshot by mtime (history view — only
53
+ * when the active registry is EMPTY), else a clear error. Never reads the
34
54
  * root v1 `plans[]` / root `agent-flow.jsonl` as primary or as a quiet
35
- * fallback.
55
+ * fallback, and never substitutes history for an unbound active set.
36
56
  * @param harnessDir - the resolved `{HARNESS_DIR}`.
57
+ * @param hint - forwarded verbatim to the active-set resolver.
37
58
  */
38
- export declare function resolveReadWorkflow(harnessDir: string): WorkflowSelectionView;
59
+ export declare function resolveReadWorkflow(harnessDir: string, hint?: SessionHint): WorkflowSelectionView;