@mstar-harness/dsh 2.4.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +10 -8
  3. package/README.zh.md +7 -5
  4. package/bundle/README.md +4 -4
  5. package/dist/client/panel/graph/project-graph.d.ts +29 -3
  6. package/dist/client/panel/locale.d.ts +1 -1
  7. package/dist/client/panel/zones/ProjectRollup.d.ts +24 -0
  8. package/dist/client.js +270 -34
  9. package/dist/gates/_shared.d.ts +4 -4
  10. package/dist/gates/adapter.d.ts +38 -12
  11. package/dist/gates/agent-flow.d.ts +117 -12
  12. package/dist/gates/dispatch.d.ts +16 -10
  13. package/dist/gates/goal-bridge.d.ts +1 -1
  14. package/dist/gates/status.d.ts +64 -14
  15. package/dist/gates/system-prompt.d.ts +6 -2
  16. package/dist/gates/workflow-ledger.d.ts +88 -17
  17. package/dist/gates/workflow-policy.d.ts +2 -1
  18. package/dist/gates/workflow-selection.d.ts +31 -0
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1573 -690
  21. package/dist/service.d.ts +14 -9
  22. package/dist/types.d.ts +64 -2
  23. package/harness-agents/code-reviewer.md +0 -8
  24. package/harness-skills/mstar-branch-worktree/SKILL.md +14 -14
  25. package/harness-skills/mstar-branch-worktree/references/parallel-writable-pre-dispatch.md +2 -2
  26. package/harness-skills/mstar-compound/SKILL.md +2 -2
  27. package/harness-skills/mstar-compound/references/compound-workflow.md +3 -3
  28. package/harness-skills/mstar-compound-refresh/SKILL.md +4 -4
  29. package/harness-skills/mstar-compound-refresh/references/project-knowledge-bootstrap.md +1 -1
  30. package/harness-skills/mstar-dispatch-gates/SKILL.md +2 -2
  31. package/harness-skills/mstar-engine-legacy/SKILL.md +53 -0
  32. package/harness-skills/mstar-engine-legacy/references/anti-recursion-checklists.md +58 -0
  33. package/harness-skills/mstar-engine-legacy/references/engine-check-boilerplate.md +26 -0
  34. package/harness-skills/mstar-engine-legacy/references/lease-protocol.md +92 -0
  35. package/harness-skills/mstar-engine-legacy/references/qc-seat-n-restatements.md +57 -0
  36. package/harness-skills/mstar-engine-legacy/references/status-field-history.md +186 -0
  37. package/harness-skills/mstar-harness-core/SKILL.md +6 -3
  38. package/harness-skills/mstar-host/references/_shared/plan-mode-bridge-core.md +8 -8
  39. package/harness-skills/mstar-host/references/codex.md +1 -1
  40. package/harness-skills/mstar-host/references/cursor-plan-mode-bridge.md +19 -8
  41. package/harness-skills/mstar-host/references/cursor.md +5 -5
  42. package/harness-skills/mstar-host/references/dsh.md +10 -7
  43. package/harness-skills/mstar-host/references/kimi-plan-mode-bridge.md +3 -3
  44. package/harness-skills/mstar-host/references/kimi.md +1 -1
  45. package/harness-skills/mstar-host/references/omp-plan-mode-bridge.md +2 -2
  46. package/harness-skills/mstar-host/references/omp.md +2 -2
  47. package/harness-skills/mstar-host/references/zcode-plan-mode-bridge.md +3 -3
  48. package/harness-skills/mstar-host/references/zcode.md +1 -1
  49. package/harness-skills/mstar-iteration/SKILL.md +38 -37
  50. package/harness-skills/mstar-iteration/references/autonomous-direction-lock.md +2 -2
  51. package/harness-skills/mstar-iteration/references/iteration-compass-template.md +3 -3
  52. package/harness-skills/mstar-iteration/references/phase-2-worktree-lease.md +50 -38
  53. package/harness-skills/mstar-iteration/references/phase-3-iteration-close.md +7 -7
  54. package/harness-skills/mstar-iteration/references/phase-4-5-pr-delivery.md +2 -2
  55. package/harness-skills/mstar-phase-gates/SKILL.md +3 -3
  56. package/harness-skills/mstar-plan-artifacts/SKILL.md +21 -23
  57. package/harness-skills/mstar-plan-artifacts/references/knowledge-and-designs.md +16 -6
  58. package/harness-skills/mstar-plan-artifacts/references/plan-files-and-reports.md +10 -10
  59. package/harness-skills/mstar-plan-artifacts/references/status-and-residuals.md +190 -309
  60. package/harness-skills/mstar-plan-artifacts/templates/README.md +1 -3
  61. package/harness-skills/mstar-plan-artifacts/templates/status.empty.json +2 -4
  62. package/harness-skills/mstar-plan-conventions/SKILL.md +53 -13
  63. package/harness-skills/mstar-plan-conventions/references/artifact-storage-paths.md +7 -3
  64. package/harness-skills/mstar-plan-conventions/references/harness-bootstrap-and-agents-layering.md +9 -10
  65. package/harness-skills/mstar-project-governance/SKILL.md +119 -0
  66. package/harness-skills/mstar-review-qc/SKILL.md +3 -3
  67. package/harness-skills/mstar-roles/references/project-manager/dispatch-and-assignment.md +1 -1
  68. package/harness-skills/mstar-roles/references/project-manager/plan-management.md +6 -8
  69. package/harness-skills/mstar-roles/references/project-manager/qc-and-residuals.md +6 -6
  70. package/harness-skills/mstar-roles/references/project-manager.md +7 -7
  71. package/harness-skills/mstar-roles/references/qc-specialist/deep-review-lenses.md +1 -1
  72. package/harness-skills/mstar-roles/references/qc-specialist/report-template.md +1 -1
  73. package/harness-skills/mstar-roles/references/qc-specialist/reviewer-workflow.md +1 -1
  74. package/harness-skills/mstar-roles/references/qc-specialist-shared.md +2 -2
  75. package/harness-skills/mstar-sdd/SKILL.md +2 -2
  76. package/harness-skills/mstar-sdd/references/file-handoffs.md +1 -1
  77. package/package.json +14 -14
  78. package/harness-skills/mstar-plan-artifacts/references/done-compaction.md +0 -88
  79. package/harness-skills/mstar-plan-artifacts/templates/notes.empty.json +0 -5
  80. package/harness-skills/mstar-plan-artifacts/templates/plans-done.empty.json +0 -3
@@ -214,8 +214,15 @@ export type AgentFlowEvent = {
214
214
  * settle carrying the SAME identity fields as its dispatch event.
215
215
  */
216
216
  export interface AgentFlowDispatchRef {
217
- /** The resolved `{HARNESS_DIR}` the dispatch recorded into (settles record into the same ledger). */
217
+ /** The resolved `{HARNESS_DIR}` the dispatch was attributed to. */
218
218
  harnessDir: string;
219
+ /**
220
+ * The ACTIVE workflow dir the dispatch event was appended to
221
+ * (`<harnessDir>/workflows/<id>` — v3 layout). The settle records into
222
+ * the SAME file (settle pairing reads/writes the same ledger), even if
223
+ * the active set changed between dispatch and settle.
224
+ */
225
+ workflowDir: string;
219
226
  /** The dispatching session's stable id ('' when the exec carried none). */
220
227
  agent?: string;
221
228
  /** Assignment `Execute as` ('' when missing — the dispatch event's grammar). */
@@ -293,6 +300,74 @@ export declare function setAgentFlowLogger(sink: AgentFlowLogSink | undefined):
293
300
  * @param prompt - the full Assignment text.
294
301
  */
295
302
  export declare function taskIdOf(prompt: string): string | undefined;
303
+ /**
304
+ * Resolve the agent-flow WRITE target dir for one harness dir: the ACTIVE
305
+ * workflow's dir (root v2 `workflows[]` first entry — the shared active-set
306
+ * resolver; the terminal-mtime fallback is catalog-read-only and MUST NOT
307
+ * enter the writer's module graph). The active set is defined by root
308
+ * `workflows[]` MEMBERSHIP — non-terminal lifecycles (`running` AND
309
+ * `paused`; terminal lifecycles are removed at terminal) — so a paused
310
+ * lifecycle stays a valid append target (explicit decision, plan
311
+ * `20260819-workflow-dsh-viz` Task 2).
312
+ *
313
+ * No active entry → `null`: the record is SKIPPED with a one-time warn
314
+ * (per sink binding) — never a silent write into the root v1 file, never a
315
+ * write into a terminal snapshot dir (compass v3.0.0 § Catalog selection
316
+ * rule — the writer appends only to an active lifecycle).
317
+ * @param harnessDir - the resolved `{HARNESS_DIR}`.
318
+ * @returns the absolute workflow dir (`<harnessDir>/workflows/<id>`), or
319
+ * `null` when no active lifecycle resolves.
320
+ */
321
+ export declare function resolveAgentFlowWriteDir(harnessDir: string): string | null;
322
+ /**
323
+ * Lock-directory name for the per-workflow write lock (qc3 W-1 fix-wave):
324
+ * guards the ledger append + size-gated truncating read-modify-write AND
325
+ * the workflow-ledger cursor sidecar read-modify-write against concurrent
326
+ * dsh processes sharing one active lifecycle (compass ruling 3). The
327
+ * lockdir lands INSIDE the workflow dir — dirname of the ledger — so it
328
+ * never pollutes the harness root (same placement rule as the engine's
329
+ * `withStatusWriteLock` lockdir).
330
+ */
331
+ export declare const WORKFLOW_LEDGER_LOCKDIR = ".ledger-write.lockdir";
332
+ /**
333
+ * Run `fn` under the per-workflow inter-process write lock (qc3 W-1
334
+ * fix-wave): atomic `mkdir` on `<workflowDir>/.ledger-write.lockdir/`
335
+ * acquires (success acquires; existing dir → another writer holds the
336
+ * lock); while another writer holds it, wait up to
337
+ * {@link WORKFLOW_LOCKER_TIMEOUT_MS} and then THROW (Blocked — the
338
+ * lockdir is never removed for another holder). Ownership guard
339
+ * (double-unlock safety): the lockdir's `(dev, ino)` is captured at
340
+ * acquisition; `finally` re-stats the path and removes the directory ONLY
341
+ * when the identity is unchanged. Sync reentrancy detection: a nested
342
+ * acquisition on the same lockdir within this process throws immediately
343
+ * (sync code cannot interleave — a held dir is a bug, not contention).
344
+ * Crash diagnosis: a `holder.pid` file (acquiring process id) is written
345
+ * inside the lockdir on acquisition and removed on release; a hard crash
346
+ * leaks the lockdir and the timeout error names the recovery step.
347
+ *
348
+ * SAME PATTERN as the engine's `withStatusWriteLock` (packages/engine/src/
349
+ * lease.ts) — atomic-mkdir lockdir, the SSOT-documented alternative to
350
+ * `flock` on Bun (no `node:fs` flock); unlike flock, a hard crash leaks
351
+ * the lockdir (documented residual, recovery = remove when no writer is
352
+ * alive).
353
+ *
354
+ * Steady state: ONE writer per workflow dir (one dsh process per
355
+ * lifecycle). The lock serializes the multi-session shared-lifecycle case
356
+ * (compass ruling 3) and is held ONLY around the bounded critical section
357
+ * (append+truncate, or the cursor read-modify-write) — never across scans.
358
+ * @param workflowDir - the workflow dir whose ledger/cursor files the
359
+ * critical section touches.
360
+ * @param fn - the critical section (sync; the lock is sync by design — the
361
+ * ledger record paths are called from sync gate hooks).
362
+ * @param opts - `timeoutMs` / `pollMs` overrides (defaults 30 s / 10 ms) —
363
+ * the same testable options as the engine's `withStatusWriteLock`
364
+ * (`lease.ts`); production callers never set them.
365
+ * @returns the critical section's return value.
366
+ */
367
+ export declare function withWorkflowDirLock<T>(workflowDir: string, fn: () => T, opts?: {
368
+ timeoutMs?: number;
369
+ pollMs?: number;
370
+ }): T;
296
371
  /**
297
372
  * Record one dispatch event (spec §2.1.3). Fully try/catch-contained — NEVER
298
373
  * throws into the gate; a failing record logs only (`mstar/agent-flow`).
@@ -327,14 +402,21 @@ export declare function recordDispatch(input: {
327
402
  /**
328
403
  * Record one settle event (spec §2.1.3). Fully try/catch-contained; a failing
329
404
  * record logs only. Callers resolve the harness dir from the PAIRED dispatch
330
- * (the pairing store's dispatchRef — never a payload probe).
331
- * @param input - harness dir + agent id + outcome + optional duration + the
332
- * PAIRED dispatch's identity (`role`/`planId`/`taskId` same field names +
333
- * semantics as the dispatch event; written for every paired settle, so the
334
- * client can exactly pair the settle back to its dispatch).
405
+ * (the pairing store's dispatchRef — never a payload probe). The settle
406
+ * appends to the ACTIVE workflow dir (v3 layout); a PAIRED settle carries
407
+ * the dispatchRef's `workflowDir` so it lands in the SAME file as its
408
+ * dispatch even if the active set changed in between. An unpaired direct
409
+ * call resolves the active workflow from `harnessDir`; no active lifecycle
410
+ * → skipped with a one-time warn (never a root v1 write).
411
+ * @param input - harness dir + optional paired workflow dir + agent id +
412
+ * outcome + optional duration + the PAIRED dispatch's identity
413
+ * (`role`/`planId`/`taskId` — same field names + semantics as the dispatch
414
+ * event; written for every paired settle, so the client can exactly pair
415
+ * the settle back to its dispatch).
335
416
  */
336
417
  export declare function recordSettle(input: {
337
418
  harnessDir: string;
419
+ workflowDir?: string;
338
420
  agent?: string;
339
421
  outcome: SettleOutcome;
340
422
  durationMs?: number;
@@ -353,14 +435,24 @@ export declare function recordSettle(input: {
353
435
  * from the durable `tool-workflow/*` session event (`ts` takes the envelope's
354
436
  * `time`). Malformed input is a caller bug — the strict narrowing applies on
355
437
  * READ (`eventFromUnknown`), never here.
356
- * @param input - harness dir + the fully-shaped v1 workflow event.
438
+ *
439
+ * v3 write path: the event appends to the ACTIVE workflow dir (root v2
440
+ * `workflows[]` first entry). The consumer passes the already-resolved
441
+ * `workflowDir` (it needs it for the durable watermark anyway — one
442
+ * resolution per row); a direct caller without it resolves from
443
+ * `harnessDir`. No active lifecycle → `false` (skipped with a one-time warn
444
+ * — never a root v1 write, never a terminal snapshot write).
445
+ * @param input - harness dir + optional pre-resolved active workflow dir +
446
+ * the fully-shaped v1 workflow event.
357
447
  * @returns `true` when the row was appended (the caller may durably advance
358
- * its watermark); `false` on a contained append failure the caller must
359
- * leave the cursor behind so the row is re-attempted at the next scan
360
- * (qc3 R-401: advance-then-record made a failed append permanent loss).
448
+ * its watermark); `false` on a contained append failure OR when no active
449
+ * lifecycle resolves — the caller must leave the cursor behind so the row
450
+ * is re-attempted at the next scan (qc3 R-401: advance-then-record made a
451
+ * failed append permanent loss).
361
452
  */
362
453
  export declare function recordWorkflowEvent(input: {
363
454
  harnessDir: string;
455
+ workflowDir?: string;
364
456
  event: AgentFlowWorkflowEvent;
365
457
  }): boolean;
366
458
  /**
@@ -417,11 +509,24 @@ export declare function recordWorkflowVerdict(input: WorkflowVerdictInput): void
417
509
  * state instead of an evidence-missing degrade; only an UNREADABLE file
418
510
  * returns null (advisory degrade — the catalog renders no agent-flow line).
419
511
  * Malformed lines are skipped, never fatal.
420
- * @param harnessDir - the resolved `{HARNESS_DIR}`.
512
+ *
513
+ * v3 layout: the ledger lives in the WORKFLOW dir (`<harnessDir>/workflows/
514
+ * <id>`), never the harness root — the dir parameter IS the workflow dir
515
+ * (the catalog passes `join(harnessDir, selection.dir)`; the writer appends
516
+ * there via `resolveAgentFlowWriteDir`).
517
+ *
518
+ * Large-file reads (plan `20260820-dsh-ledger-tail-read`): ledgers above
519
+ * `AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES` are read as a bounded latest-first
520
+ * tail — seek from EOF, aligned to a line boundary, doubling backward until
521
+ * the window holds `limit` complete lines — so the catalog pays O(window) at
522
+ * the byte layer instead of parsing every historical line. Both paths feed
523
+ * the SAME parse funnel below (`ledgerContent` yields a `content` string),
524
+ * so tail/full parity is structural.
525
+ * @param workflowDir - the workflow dir whose `agent-flow.jsonl` is read.
421
526
  * @param limit - explicit window bound: `undefined` → `AGENT_FLOW_DEFAULT_LIMIT`;
422
527
  * otherwise `Math.max(0, Math.floor(limit))` — `0` requests the EMPTY window.
423
528
  */
424
- export declare function readAgentFlow(harnessDir: string, limit?: number): AgentFlowView | null;
529
+ export declare function readAgentFlow(workflowDir: string, limit?: number): AgentFlowView | null;
425
530
  /**
426
531
  * The `tools/post-execute` settle pairing (plan `20260811-panel-f4-timeliness`
427
532
  * Task 1 — replaces the old defensive payload probing): the VERIFIED
@@ -61,7 +61,8 @@ export declare function isAssignmentShaped(assignmentText: string): boolean;
61
61
  * Resolve the hard-enforcement flag for one dispatch: explicit Config override
62
62
  * wins, else the Assignment's OWN `Enforcement: hard` header flag (opencode
63
63
  * parity — header region only, a body-quoted example never hardens), else the
64
- * iteration compass frontmatter, else warn-only.
64
+ * repo `.mstarc` `[config] enforcement`, else the iteration compass
65
+ * frontmatter, else warn-only.
65
66
  */
66
67
  export declare function resolveDispatchHard(harnessDir: string | null, config: Config, assignmentText: string): boolean;
67
68
  /** A header value that means "no value" (placeholder conventions). Type guard so callers narrow to `string`. Shared with the agent-flow ledger (qc1 F-003 — one grammar, no copy-paste drift). */
@@ -93,8 +94,11 @@ export declare function sessionIdOf(exec: ToolExecution): string | undefined;
93
94
  * `dispatch.execution-mode.*` codes stay out of the parity field set) OR
94
95
  * whose plan row is `InProgress`.
95
96
  *
96
- * Contract (status-and-residuals.md § Pre-dispatch re-verify): before any
97
- * writable implement dispatch, reread `{HARNESS_DIR}/status.json` and confirm
97
+ * Contract (status-and-residuals.md § Pre-dispatch re-verify; v3
98
+ * relocation plan `20260819-workflow-dsh-viz` Task 3): before any
99
+ * writable implement dispatch, reread the ACTIVE workflow snapshot
100
+ * (`workflows/<id>/snapshot.json` — the v1 root `plans[]` home is gone; the
101
+ * root v2 `status.json` supplies the active `workflows[]`) and confirm
98
102
  * the session still passes verify-held-lease — `holder`, `worktree_path` and
99
103
  * `working_branch` must match the Assignment; mismatch or absent lease →
100
104
  * STOP. Engine `verifyPlanExecutionLease` + `validateExecutionLease` carry
@@ -104,13 +108,15 @@ export declare function sessionIdOf(exec: ToolExecution): string | undefined;
104
108
  *
105
109
  * Degrade-allow cases (no false positives): no harness dir, unresolvable plan
106
110
  * id, and non-SDD assignments whose plan row is absent or not InProgress.
107
- * Unverifiable lease states (malformed status.json, MISSING status.json, plan
108
- * row not registered) are violations ONLY for sdd dispatches (the lease state
109
- * cannot be confirmed the status gate already guards the next write);
110
- * unreadable docs never harden a soft workflow. Missing status.json is NOT a
111
- * silent fail-open for sdd: the claim-before-InProgress red line
112
- * needs the plan's execution_lease, and a missing status file cannot confirm
113
- * it `lease.dispatch.unverifiable` fires (advisory in warn, deny under hard).
111
+ * Unverifiable lease states (unreadable/missing root status.json, a
112
+ * selection failure v1/migration-required root, no active workflow an
113
+ * unreadable/missing snapshot, plan row not registered) are violations ONLY
114
+ * for sdd dispatches (the lease state cannot be confirmed the status gate
115
+ * already guards the next write); unreadable docs never harden a soft
116
+ * workflow. A missing status.json is NOT a silent fail-open for sdd: the
117
+ * claim-before-InProgress red line needs the plan's execution_lease, and a
118
+ * missing root/snapshot cannot confirm it — `lease.dispatch.unverifiable`
119
+ * fires (advisory in warn, deny under hard).
114
120
  */
115
121
  export declare function leaseGateViolations(harnessDir: string | null, exec: ToolExecution, writable: boolean | undefined, prompt: string): ValidationResult[];
116
122
  /**
@@ -133,7 +133,7 @@ export declare function rootAgentOf(agent: unknown, agents: AgentsView): unknown
133
133
  * compare when the mirror is in place — no churn) — plus a THIRD, advisory
134
134
  * listener on the `session/event` firehose (Task 3): a `goal/change`
135
135
  * envelope whose goal is blocked logs ONE warn (code + objective summary +
136
- * `{HARNESS_DIR}/status.json` residual pointer) with ZERO harness writes
136
+ * project-register residual pointer) with ZERO harness writes
137
137
  * (the one-way mirror; see {@link warnBlockedGoal}). The goals service is an
138
138
  * OPTIONAL seam (`ctx.get('goals')` structural read): absent → ONE debug log
139
139
  * + the mirror stays inert, never a boot failure — the blocked advisory is
@@ -33,21 +33,66 @@ export interface StatusGateAdvisory {
33
33
  degraded?: boolean;
34
34
  }
35
35
  /**
36
- * Validate a PARSED status document through the status-gate pipeline
37
- * (engine `validateStatus` + `findingsCleanupGate` per plan row that
38
- * CONFIGURES a mode). Shared by {@link validateStatusDoc} (the on-disk
39
- * single-read path) and the host adapter's `beforeStatusWrite` (the
40
- * incoming document) the fs-intent gate, the adapter hook and the repair
41
- * escape all surface the SAME violation codes.
36
+ * Canonical `{HARNESS_DIR}` coordination-document kinds the status gate
37
+ * covers (opencode-parity vocabulary the SAME three kinds the P2-fixed
38
+ * opencode `harnessDocKindOfTarget` classifies): the v2 root `status.json`,
39
+ * `workflows/<id>/snapshot.json`, and `projects/<id>/residuals.json`. Each
40
+ * kind maps to its matching engine validator in
41
+ * {@link validateStatusValue}.
42
42
  */
43
- export declare function validateStatusValue(doc: unknown): GateResult;
43
+ export type HarnessDocKind = 'status' | 'snapshot' | 'register';
44
44
  /**
45
- * Run the status gate over the CURRENT on-disk document. The fs intent
46
- * waterfall carries only `(target, actor)` never the incoming content — so
47
- * the vetoable check is the pre-write state (the opencode hook's fallback for
48
- * the same reason). `findingsCleanupGate` runs per plan row that CONFIGURES a
49
- * mode (`plans[].metadata.findings_cleanup`); schema violations short-circuit
50
- * it (the doc must parse for the cleanup gate to be meaningful).
45
+ * Classify one fs target as a canonical `{HARNESS_DIR}` coordination
46
+ * document: basename {status.json, snapshot.json, residuals.json} AND the
47
+ * harness-relative path matches the canonical home (root `status.json`,
48
+ * `{WORKFLOW_DIR}/<id>/snapshot.json`, `{PROJECT_DIR}/<id>/residuals.json`
49
+ * one path component each, mirroring the P2-fixed opencode
50
+ * `harnessDocKindOfTarget`).
51
+ *
52
+ * Phase-5 F1: the snapshot/register rel is computed against the RESOLVED
53
+ * layout dirs (`resolveWorkflowDir` / `resolveProjectDir` — a `.mstarc`
54
+ * `[config] workflow_dir` / `project_dir` declaration wins, defaults
55
+ * `workflows` / `projects` under the harness dir), so a custom layout
56
+ * classifies at the same location the runtime writes; resolver failure
57
+ * (never expected with the explicit harness dir) falls back to the default
58
+ * names.
59
+ *
60
+ * dsh divergence from the opencode probe (documented): opencode resolves
61
+ * the harness root by marker probe (W-REV-1) with a declared-root fallback;
62
+ * the dsh gates ALREADY resolve `{HARNESS_DIR}` per calling session
63
+ * workspace through the shared `HarnessResolver` BEFORE the intent slot
64
+ * runs, so classification here is against that resolved root — no second
65
+ * probe, no W-REV-3 rebuild. Returns the kind when gated, `null` otherwise
66
+ * (a non-coordination target, or a path outside the resolved harness dir —
67
+ * the gate is inert for them).
68
+ */
69
+ export declare function harnessDocKindOfTarget(harnessDir: string, targetPath: string): HarnessDocKind | null;
70
+ /**
71
+ * Validate a PARSED harness coordination document through the
72
+ * kind-matched engine validator (v2 root / workflow snapshot / project
73
+ * register — the P2-fixed "one validator per kind" shape) plus the
74
+ * snapshot-only `findingsCleanupGate` extension per plan row that
75
+ * CONFIGURES a mode (the v1 per-plan-row cleanup gate relocated: plan rows
76
+ * live on the snapshot, residuals on the project registers). Shared by
77
+ * {@link validateStatusDoc} (the on-disk single-read path) and the host
78
+ * adapter's `beforeStatusWrite` (the incoming document) — the fs-intent
79
+ * gate, the adapter hook and the repair escape all surface the SAME
80
+ * violation codes.
81
+ * @param kind - the target's {@link HarnessDocKind} (matching engine validator).
82
+ * @param harnessDir - the resolved `{HARNESS_DIR}`; required for the
83
+ * snapshot kind's cleanup extension (the registers it reads live under it),
84
+ * otherwise unused.
85
+ */
86
+ export declare function validateStatusValue(doc: unknown, kind: HarnessDocKind, harnessDir?: string | null): GateResult;
87
+ /**
88
+ * Run the status gate over the CURRENT on-disk document at a canonical
89
+ * harness coordination target. The fs intent waterfall carries only
90
+ * `(target, actor)` — never the incoming content — so the vetoable check
91
+ * is the pre-write state (the opencode hook's fallback for the same
92
+ * reason). The kind-matched validator runs via {@link validateStatusValue}
93
+ * (the snapshot kind's `findingsCleanupGate` extension included) — schema
94
+ * violations short-circuit it (the doc must parse for the cleanup gate to
95
+ * be meaningful).
51
96
  *
52
97
  * Single-read contract: the file is parsed exactly once and the
53
98
  * parsed doc is passed to {@link validateStatusValue} — the previous
@@ -57,8 +102,13 @@ export declare function validateStatusValue(doc: unknown): GateResult;
57
102
  * shape; never throws. Missing files are guarded by the callers
58
103
  * (`gateStatusIntent`, {@link DshHostAdapter.statusGate}) — first create has
59
104
  * no document to validate and passes before this function runs.
105
+ * @param path - the canonical coordination-document path (root status.json
106
+ * / workflow snapshot / project register — the caller classifies it).
107
+ * @param kind - the target's {@link HarnessDocKind}.
108
+ * @param harnessDir - the resolved `{HARNESS_DIR}` (the snapshot kind's
109
+ * cleanup extension needs it to locate the project registers).
60
110
  */
61
- export declare function validateStatusDoc(statusPath: string): GateResult;
111
+ export declare function validateStatusDoc(path: string, kind: HarnessDocKind, harnessDir?: string | null): GateResult;
62
112
  /**
63
113
  * `fs/write-intent` listener. Registered with `prepend` so this decider runs
64
114
  * BEFORE dsh-fs-policy regardless of mount order: the slot is first-wins by
@@ -38,9 +38,13 @@
38
38
  * - The context provider reuses the catalog's unified machine-summary
39
39
  * source (`buildCatalogSources` — the SAME builder the engine-status
40
40
  * pre-step catalog row uses) and projects a BOUNDED subset: watermark +
41
- * iteration gate + compact state line. Full status.json content
41
+ * iteration gate + compact state line. Full catalog content
42
42
  * (residual detail, agent-flow events, knowledge digest, branch/policy
43
- * anchors) stays out. The build is TTL-memoized PER RESOLVED HARNESS DIR
43
+ * anchors) stays out. v3 (plan `20260819-workflow-dsh-viz` Task 3): the
44
+ * digest reads ONLY the catalog row (`state` — itself aggregated from the
45
+ * SELECTED workflow snapshot + project registers) — no direct
46
+ * status.json / snapshot file reads to change. The build is TTL-memoized
47
+ * PER RESOLVED HARNESS DIR
44
48
  * (`DEFAULT_CATALOG_TTL_MS`) so the per-assembly hot path does not
45
49
  * re-read status.json / the compass / the ledger on every prompt
46
50
  * assembly (the catalog's documented staleness tradeoff).
@@ -23,19 +23,34 @@
23
23
  *
24
24
  * DEDUPE (qc2 W-1 / qc3 F-301 fix-wave): ONE DURABLE per-session watermark —
25
25
  * the next expected envelope `seq` (session-log position) — persisted to
26
- * `{HARNESS_DIR}/workflow-ledger-cursors.json` (a small bounded sidecar next
27
- * to `agent-flow.jsonl`, written atomically temp-file + rename through the
28
- * same containment discipline). The watermark is consulted AND advanced by
29
- * every scan (cold / created-backfill / live): envelopes with `seq` below it
30
- * were already recorded across cold+live overlap AND across plugin
31
- * re-applies (a re-registration no longer re-records the same live
32
- * sessions). The watermark advances only AFTER the ledger row appended
33
- * successfully (qc3 R-401 a failing append leaves the cursor behind, so
34
- * the row is re-attempted at the next scan, never permanently lost). The
35
- * in-memory Map is the durable file's mirror (module-level
36
- * cache, bounded by the session cap); any watermark read/write failure
37
- * degrades to in-memory-only with one warn a ledger row is never lost and
38
- * the workflow run is never affected.
26
+ * `{HARNESS_DIR}/workflows/<id>/workflow-ledger-cursors.json` (a small
27
+ * bounded sidecar next to the workflow-dir `agent-flow.jsonl`, written
28
+ * atomically temp-file + rename through the same containment discipline; the
29
+ * root cursor file is NOT read after migration no read fallback). The
30
+ * watermark is consulted AND advanced by every scan (cold /
31
+ * created-backfill / live): envelopes with `seq` below it were already
32
+ * recorded across cold+live overlap AND across plugin re-applies (a
33
+ * re-registration no longer re-records the same live sessions). The
34
+ * watermark advances only AFTER the ledger row appended successfully (qc3
35
+ * R-401 a failing append leaves the cursor behind, so the row is
36
+ * re-attempted at the next scan, never permanently lost). The in-memory
37
+ * Map is the durable file's mirror (module-level cache keyed by WORKFLOW
38
+ * DIR, bounded by the session cap AND by a workflow-dir count cap qc3
39
+ * S-4 — so a long-lived process across many workflow ids never grows the
40
+ * cache unbounded); a failed durable write keeps the in-memory mirror
41
+ * advanced (in-process dedupe — qc3 S-6) with one warn — a ledger row is
42
+ * never lost and the workflow run is never affected.
43
+ *
44
+ * INTER-PROCESS SERIALIZATION (qc3 W-1 fix-wave): the watermark
45
+ * read-modify-write (load fresh → mutate → whole-map save) runs inside the
46
+ * per-workflow write lock shared with the ledger append
47
+ * (`withWorkflowDirLock` from agent-flow.ts — the same lockdir pattern as
48
+ * the engine's `withStatusWriteLock`). Steady state is ONE writer per
49
+ * workflow dir; under multi-session shared lifecycle (compass ruling 3)
50
+ * the lock makes the cursor save read-modify-write atomic across
51
+ * processes — a whole-map save can no longer silently clobber another
52
+ * process's just-advanced cursor (the duplicate-row regression mode). The
53
+ * lock is held only around the bounded cursor update, never across scans.
39
54
  *
40
55
  * Mapping (Task 2 schema): `tool-workflow/run-start` → `workflow-run`
41
56
  * (`agent` = the carrying parent session id), `tool-workflow/agent-start` →
@@ -92,12 +107,15 @@ import type { WorkflowAskCache } from './workflow-policy.ts';
92
107
  /** Logger label for the workflow-ledger consumer (dsh logger naming: `<scope>/<subject>`). */
93
108
  export declare const WORKFLOW_LEDGER_LOGGER = "mstar/workflow-ledger";
94
109
  /**
95
- * The durable watermark file name under `{HARNESS_DIR}` (qc2 W-1 / qc3
110
+ * The durable watermark file name under the WORKFLOW dir (qc2 W-1 / qc3
96
111
  * F-301 fix-wave): `{ "v": 1, "cursors": { "<sessionId>": <nextSeq> } }` —
97
112
  * the next expected envelope seq per session id. Written atomically
98
113
  * (temp-file + rename) after every recorded workflow row; read lazily per
99
- * harness dir (module-level cache). Absent on first run (silent); a
100
- * present-but-corrupt file degrades to in-memory-only with one warn.
114
+ * workflow dir (module-level cache). v3 layout: the sidecar lives in the
115
+ * ACTIVE workflow dir (`workflows/<id>/workflow-ledger-cursors.json`) next
116
+ * to the ledger — the root cursor file is NOT read after migration (no read
117
+ * fallback). Absent on first run (silent); a present-but-corrupt file
118
+ * degrades to in-memory-only with one warn.
101
119
  */
102
120
  export declare const WORKFLOW_LEDGER_WATERMARK_FILE = "workflow-ledger-cursors.json";
103
121
  /**
@@ -107,6 +125,15 @@ export declare const WORKFLOW_LEDGER_WATERMARK_FILE = "workflow-ledger-cursors.j
107
125
  * evicted session re-records its rows; bounded by the cap).
108
126
  */
109
127
  export declare const WORKFLOW_LEDGER_WATERMARK_MAX_SESSIONS = 256;
128
+ /**
129
+ * Workflow-dir count cap for the module-level watermark cache (qc3 S-4
130
+ * fix-wave): a long-lived process can touch many workflow ids over its
131
+ * lifetime (each iteration lifecycle creates a new workflow dir) — the
132
+ * cache evicts the OLDEST cached dir when it exceeds this cap. The file is
133
+ * the durable store; the cache is only a mirror, so an evicted dir is
134
+ * re-read (and re-cached) on its next visit — no correctness impact.
135
+ */
136
+ export declare const WORKFLOW_LEDGER_WATERMARK_MAX_DIRS = 64;
110
137
  /** Consumer log levels the module sink understands. */
111
138
  export type WorkflowLedgerLogLevel = 'debug' | 'warn';
112
139
  /** Module-level consumer log sink — bound by `apply` to `ctx.logger(WORKFLOW_LEDGER_LOGGER)` (agent-flow ledger precedent). */
@@ -117,6 +144,49 @@ export type WorkflowLedgerLogSink = (level: WorkflowLedgerLogLevel, message: str
117
144
  * can restore it (test pattern: agent-flow `setAgentFlowLogger`).
118
145
  */
119
146
  export declare function setWorkflowLedgerLogger(sink: WorkflowLedgerLogSink): WorkflowLedgerLogSink;
147
+ /** The number of workflow dirs currently mirrored in the module-level watermark cache (qc3 S-4 observability). */
148
+ export declare function workflowLedgerWatermarkCacheSize(): number;
149
+ /**
150
+ * Advance one session's watermark entry to `nextSeq` and persist. Runs the
151
+ * whole read-modify-write under the per-workflow inter-process lock
152
+ * (qc3 W-1 fix-wave — same lock as the ledger append): the fresh load
153
+ * starts from the latest durable cursors, so two processes sharing one
154
+ * lifecycle never clobber each other's advances; the session entry is
155
+ * monotonic (`Math.max` — a stale concurrent advance never regresses an
156
+ * already-higher cursor). When a NEW session would push the map past the
157
+ * cap, evict first: prefer an entry whose session is no longer live
158
+ * (`isEvictable` — a live session's cursor must never be dropped, or the
159
+ * next re-apply would re-record its rows); fall back to the oldest entry
160
+ * when every entry is live. The residual of eviction (a later restore of an
161
+ * evicted session re-records) is bounded by the cap and documented in the
162
+ * README.
163
+ *
164
+ * DEGRADED PATH (qc3 S-6 fix-wave): a lock timeout (another writer stuck
165
+ * for the full timeout — a crashed/stuck peer), a reentrancy error, or a
166
+ * throwing critical section leaves the durable file untouched, but the
167
+ * in-memory mirror is STILL advanced (the same monotonic `Math.max`;
168
+ * eviction skipped — the cap bounds the durable sidecar only), so
169
+ * re-applies in THIS process stay deduped. The durable file lags: the next
170
+ * successful locked advance re-reads it fresh and catches up; a restart
171
+ * reloads the stale file and re-records the outage rows ONCE (the accepted
172
+ * bounded-duplicate mode — the ledger row above was already appended). One
173
+ * warn names the actual state (durable cursor NOT advanced).
174
+ *
175
+ * Exported as the degraded-path test seam: {@link withWorkflowDirLock}'s
176
+ * `lockOpts` (`timeoutMs`) lets a test shorten the otherwise-30 s wait —
177
+ * the consumer itself cannot reach the catch (the ledger append takes the
178
+ * SAME lock and would fail first), so the post-append window is driven
179
+ * directly.
180
+ *
181
+ * @param isEvictable - `true` for a candidate session that is safe to evict
182
+ * (not live in the sessions store).
183
+ * @param lockOpts - pass-through to {@link withWorkflowDirLock}
184
+ * (`timeoutMs` / `pollMs` overrides; production callers never set them).
185
+ */
186
+ export declare function advanceWatermark(workflowDir: string, sid: string, nextSeq: number, isEvictable: (candidate: string) => boolean, lockOpts?: {
187
+ timeoutMs?: number;
188
+ pollMs?: number;
189
+ }): void;
120
190
  /**
121
191
  * Register the workflow-ledger consumer: (1) a `session/created` backfill
122
192
  * listener (registered FIRST — qc3 S-305 — so no apply-time window exists
@@ -126,7 +196,8 @@ export declare function setWorkflowLedgerLogger(sink: WorkflowLedgerLogSink): Wo
126
196
  * events never hit the firehose, `firstLiveSeq`); (3) a live
127
197
  * `ctx.events.on('session/event', …)` listener filtering the four types.
128
198
  * One DURABLE watermark per session id (session-log `seq` position,
129
- * persisted to `{HARNESS_DIR}/workflow-ledger-cursors.json`) re-applies
199
+ * persisted to `{HARNESS_DIR}/workflows/<id>/workflow-ledger-cursors.json` —
200
+ * the ACTIVE workflow dir, never the root) — re-applies
130
201
  * never duplicate; no other cache. The watermark advances only AFTER a
131
202
  * successful ledger append (qc3 R-401 — a failing append leaves the cursor
132
203
  * behind so the row is re-attempted at the next scan, never lost). Every
@@ -189,7 +189,8 @@ export type WorkflowPolicyVerdict = {
189
189
  * before returning; `markAsked` never throws. NEVER throws.
190
190
  *
191
191
  * P-b: `input.uncoveredPlanId` (the dispatch gate computes it from the
192
- * calling workspace's status.jsonsee dispatch.ts `writableFanOutUncovered`)
192
+ * ACTIVE workflow snapshot`workflows/<id>/snapshot.json`, the v3
193
+ * status.json consumer; see dispatch.ts `writableFanOutUncovered`)
193
194
  * → hard denies (reason cites the plan id), warn/ask emit the advisory
194
195
  * verdict; `off` always allows. Preempts P-a/P-c: an orphan InProgress plan
195
196
  * is a workspace red line independent of the workflow name.
@@ -0,0 +1,31 @@
1
+ import type { WorkflowSelectionView } from '../types.ts';
2
+ /** The active-set resolver result: the first active lifecycle or a clear error. */
3
+ export type ActiveWorkflowSelection = WorkflowSelectionView;
4
+ /**
5
+ * Resolve the ACTIVE lifecycle set (root v2 `status.json` `workflows[]` —
6
+ * the list holds non-terminal lifecycles only, removal-at-terminal). This
7
+ * is the ONLY resolver the agent-flow writer / ledger may use: no active
8
+ * entry → a clear error, never a terminal snapshot and never the root v1
9
+ * file.
10
+ *
11
+ * Active-set definition (explicit decision, plan `20260819-workflow-dsh-viz`
12
+ * Task 2): membership in `workflows[]` — the engine lifecycle enum's
13
+ * non-terminal states are `running` AND `paused` (terminal lifecycles are
14
+ * removed from the list at terminal). A PAUSED lifecycle therefore stays in
15
+ * the active set and the agent-flow writer / ledger append to its workflow
16
+ * dir (a paused lifecycle is still the operator's current lifecycle — its
17
+ * ledger must keep recording; only a TERMINAL lifecycle is never a write
18
+ * target).
19
+ * @param harnessDir - the resolved `{HARNESS_DIR}`.
20
+ */
21
+ export declare function resolveActiveWorkflow(harnessDir: string): ActiveWorkflowSelection;
22
+ /**
23
+ * Resolve the workflow the catalog/panel READ path aggregates (compass
24
+ * v3.0.0 § Catalog selection rule): active `workflows[]` first (multiple
25
+ * active → first + a structured warning), else the latest terminal
26
+ * snapshot by mtime (history view), else a clear error. Never reads the
27
+ * root v1 `plans[]` / root `agent-flow.jsonl` as primary or as a quiet
28
+ * fallback.
29
+ * @param harnessDir - the resolved `{HARNESS_DIR}`.
30
+ */
31
+ export declare function resolveReadWorkflow(harnessDir: string): WorkflowSelectionView;
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ import { DshHostAdapter } from './gates/adapter.ts';
16
16
  import type { DispatchGateAdvisory } from './gates/dispatch.ts';
17
17
  export { DshMstar } from './service.ts';
18
18
  export type { DshMstarOptions } from './service.ts';
19
- export type { MstarEngineStatusSource, MstarHarnessState, MstarIterationGateView, AgentFlowEventView, AgentFlowSummaryRow, AgentFlowView, } from './types.ts';
19
+ export type { MstarEngineStatusSource, MstarHarnessState, MstarIterationGateView, AgentFlowEventView, AgentFlowSummaryRow, AgentFlowView, WorkflowSelectionView, } from './types.ts';
20
20
  export { AGENT_FLOW_FILE, AGENT_FLOW_MAX_EVENTS, SETTLE_SEAM, readAgentFlow, recordDispatch, recordSettle, recordWorkflowVerdict, } from './gates/agent-flow.ts';
21
21
  export type { AgentFlowEvent, DispatchVerdict, SettleOutcome, WorkflowGateMode, WorkflowVerdict, WorkflowVerdictInput, } from './gates/agent-flow.ts';
22
22
  export { Config, HarnessResolver, skillLocalConfig } from './gates/_shared.ts';