@godv61/dsh-task-engine 0.23.9 → 0.25.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.
package/lib/engine.d.ts CHANGED
@@ -42,13 +42,107 @@ export interface CommitRule {
42
42
  /** When true (default), a commit may only touch files listed in the task's `files`. */
43
43
  file_scope: boolean;
44
44
  }
45
- /** Skills and rules a stage pins for progressive disclosure when the task enters it. */
45
+ /**
46
+ * A resource reference that names where the resource lives.
47
+ *
48
+ * A bare name is ambiguous once project and user directories can both hold one:
49
+ * `coding-conventions` in a project and `coding-conventions` in the user home are
50
+ * different resources that happen to share a name, and a binding written as a
51
+ * plain string cannot say which was meant. Every stored reference therefore
52
+ * carries its source layer, and a legacy bare name is resolved by the documented
53
+ * precedence and recorded as the layer it resolved to.
54
+ */
55
+ export interface ResourceRef {
56
+ /** Source layer the resource belongs to. */
57
+ source: ResourceSource;
58
+ /** Resource name within that layer. */
59
+ name: string;
60
+ }
61
+ /** Where a skill or rule came from. `bundled` ships with the plugin. */
62
+ export type ResourceSource = 'bundled' | 'project' | 'user';
63
+ /** Render a reference as `source:name`, the form used in config and status output. */
64
+ export declare function formatResourceRef(ref: ResourceRef): string;
65
+ /**
66
+ * Parse a `source:name` reference. Returns undefined for a bare name, which is
67
+ * the legacy shape and must go through precedence resolution instead.
68
+ * @param text - the stored reference text.
69
+ * @returns the parsed reference, or undefined when it carries no source.
70
+ */
71
+ export declare function parseResourceRef(text: string): ResourceRef | undefined;
72
+ /**
73
+ * One skill bound to a stage, with the complete list of rules that apply to it.
74
+ *
75
+ * Rules belong to the SKILL, not to the stage. A skill has exactly one rule list:
76
+ * wherever that skill is bound, the same rules come with it. There is no
77
+ * per-stage rule list, no inheritance from a stage or a preset, and no override
78
+ * layer, because a stage-level list forced every reader to compute a merge before
79
+ * knowing what actually applied, and made "which rules is this skill running
80
+ * under?" unanswerable without inspecting the whole composition.
81
+ *
82
+ * To use one skill under two different rule sets, copy it into a distinct skill
83
+ * and configure that one separately. Explicit duplication is easier to reason
84
+ * about than an implicit inheritance chain, and it keeps the answer to the
85
+ * question above local to the skill.
86
+ */
87
+ export interface SkillBinding {
88
+ /** The skill, named with its source layer. */
89
+ skill: ResourceRef;
90
+ /** Rules this skill follows, each named with its source layer. */
91
+ rules: ResourceRef[];
92
+ /**
93
+ * What kind of evidence proves this skill was actually executed.
94
+ *
95
+ * A shell command is one kind of proof, not the only one: a requirement or design
96
+ * skill produces a document, and demanding a "real validation command" from it
97
+ * forced an irrelevant command in order to satisfy a gate. Absent means the
98
+ * historical default (`command`), so existing configs and frozen snapshots keep
99
+ * behaving exactly as they did.
100
+ */
101
+ evidence?: EvidenceKind;
102
+ }
103
+ /**
104
+ * How a bound skill proves it ran.
105
+ *
106
+ * - `command`: a shell command with a captured receipt (the default).
107
+ * - `artifact`: a recorded artifact body, for skills whose output is a document.
108
+ * - `review`: a recorded review verdict, for skills that judge rather than build.
109
+ * - `manual`: an explicit human statement, for work no machine can verify.
110
+ * - `none`: the skill is advisory and needs no separate proof.
111
+ *
112
+ * Naming them lets a flow say what it actually wants, instead of encoding one kind
113
+ * of evidence as though it were the only kind.
114
+ */
115
+ export type EvidenceKind = 'command' | 'artifact' | 'review' | 'manual' | 'none';
116
+ /**
117
+ * What a stage binds for progressive disclosure when the task enters it.
118
+ *
119
+ * A stage selects skills only. Its rules are whatever those skills carry.
120
+ */
46
121
  export interface StageBinding {
47
- /** Skill names to load for this stage (resolved through the DSH skill registry). */
48
- skills?: string[];
49
- /** Rule names to read for this stage (resolved under the configured rule roots). */
50
- rules?: string[];
122
+ skills?: SkillBinding[];
123
+ /**
124
+ * Stage-level rule names from a pre-migration config, preserved verbatim.
125
+ *
126
+ * Rules used to belong to the stage, so a stage with several skills gave no
127
+ * indication which skill a rule was meant for. Assigning them silently would
128
+ * invent an answer the config never contained, and dropping them would remove a
129
+ * constraint the user configured. They are therefore carried here unchanged:
130
+ * still disclosed, still enforced — so nothing is lost — while the status output
131
+ * names them as unassigned so a human can decide which skill each belongs to, or
132
+ * split the skill if the answer differs per stage.
133
+ */
134
+ legacy_rules?: string[];
51
135
  }
136
+ /**
137
+ * How thoroughly a flow audits each implementation item.
138
+ *
139
+ * `two-stage` requires both a specification-conformance and a quality verdict per
140
+ * item, which suits a full-development flow. `single` requires one combined
141
+ * verdict, which is what a fast-change flow needs: the work still has to be
142
+ * checked before it counts as done, but the same change is not re-reviewed twice
143
+ * under two headings that a short change rarely distinguishes.
144
+ */
145
+ export type ReviewDepth = 'two-stage' | 'single';
52
146
  export interface WorkflowConfig {
53
147
  stages: string[];
54
148
  start_stage: string;
@@ -60,6 +154,55 @@ export interface WorkflowConfig {
60
154
  high_risk_requires_verification: boolean;
61
155
  /** Per-stage skill/rule bindings disclosed when the task enters the stage. */
62
156
  stage_bindings?: Record<string, StageBinding>;
157
+ /**
158
+ * How much per-item review this flow demands before `todos_done` passes.
159
+ *
160
+ * The three presets differ in task complexity, not in how seriously they take
161
+ * safety, so a lighter flow must be able to ask for a lighter audit rather than
162
+ * inheriting the heaviest one. Omitting the field keeps the historical
163
+ * behaviour (`two-stage`), so configs and frozen snapshots written before this
164
+ * field existed read exactly as they did.
165
+ */
166
+ review_depth?: ReviewDepth;
167
+ /**
168
+ * Whether finishing the task must include a recorded Git commit.
169
+ *
170
+ * A Git commit is one delivery mode, not a universal completion condition: a
171
+ * non-code task or a project outside version control has nothing to commit.
172
+ * Omitting the field keeps the historical behaviour (`true`), so existing
173
+ * configs and frozen snapshots are unaffected.
174
+ */
175
+ commit_required?: boolean;
176
+ /**
177
+ * Guards a flow requires to hold at COMPLETION, for stages that have no way out.
178
+ *
179
+ * A guard normally rides an edge: `代码审核 → 完成` requiring `review_passed` is how
180
+ * the standard flow states that the review must pass before the end. But a flow
181
+ * whose final stage is where the work happens has no such edge — the agile flow's
182
+ * `审查` IS the review, and it is terminal. Hanging a review guard on `交付 → 审查`
183
+ * would be circular (demanding the verdict before the stage that produces it) and
184
+ * would also block the commit at `交付`, because a commit requires its stage's
185
+ * outgoing guards.
186
+ *
187
+ * Declaring the requirement here lets completion check it directly and keeps the
188
+ * statement next to the stage it describes, rather than hidden in edge order.
189
+ */
190
+ completion_guards?: GuardName[];
191
+ }
192
+ /**
193
+ * A frozen copy of one resource's body, taken when the task was created.
194
+ *
195
+ * Names alone cannot keep an in-flight task stable: editing a skill or rule that
196
+ * a running task uses would silently change what that task is doing. The body is
197
+ * copied, and identical bodies are stored once and shared by content hash, so two
198
+ * tasks using the same rule do not duplicate several kilobytes each.
199
+ */
200
+ export interface FrozenResource {
201
+ ref: ResourceRef;
202
+ /** SHA-256 of the body, which is also the key under which it is stored. */
203
+ hash: string;
204
+ /** The body itself, so a deleted or edited source cannot change a running task. */
205
+ content: string;
63
206
  }
64
207
  /**
65
208
  * A task's frozen workflow: the preset id, its version, and the complete
@@ -74,6 +217,12 @@ export interface FlowSnapshot {
74
217
  config: WorkflowConfig;
75
218
  /** SHA-256 of the canonical JSON of `config`; a mismatch proves the snapshot was edited after creation. Absent on pre-0.22 records. */
76
219
  hash?: string;
220
+ /**
221
+ * The skill and rule bodies this task actually resolved, captured at creation.
222
+ * Absent on records written before resource freezing; those fall back to live
223
+ * resolution.
224
+ */
225
+ resources?: FrozenResource[];
77
226
  }
78
227
  /** One delegated implementation unit: a subagent fetch plus its two-stage review. */
79
228
  export interface ItemDispatch {
@@ -82,6 +231,55 @@ export interface ItemDispatch {
82
231
  /** ISO-8601 timestamp when the subagent was dispatched. */
83
232
  at: string;
84
233
  }
234
+ /**
235
+ * Explicit task completion, recorded once every configured condition holds.
236
+ *
237
+ * Reaching the last configured stage is not the same fact as finishing the work,
238
+ * and conflating them let a flow whose final stage is also its commit checkpoint
239
+ * arrive at that stage with no delivery record at all: the "commit before leaving
240
+ * a checkpoint" rule fires on the way OUT of a stage, and a terminal stage has no
241
+ * way out. Completion is therefore its own recorded event, checked by the engine,
242
+ * so every preset ends through the same lifecycle regardless of how few working
243
+ * stages it has.
244
+ */
245
+ export interface TaskCompletion {
246
+ /** ISO-8601 timestamp of the completion call. */
247
+ at: string;
248
+ /** The commit recorded at completion, when the flow's delivery mode requires one. */
249
+ commit_hash?: string;
250
+ }
251
+ /**
252
+ * A recorded rework: why the task went back, and exactly what that invalidated.
253
+ *
254
+ * The graph only has forward edges, but the work does not: a requirement can
255
+ * change after implementation started, and a defect can be found during review.
256
+ * Before this existed, the only way back was a hand-edited record, which left
257
+ * every downstream conclusion nominally intact — a confirmation, verification
258
+ * receipt or review verdict that described a superseded tree would still read as
259
+ * passing.
260
+ *
261
+ * The invalidated set is computed from the DECLARED kind rather than from a
262
+ * blanket rule, because the two cases differ: a changed requirement invalidates
263
+ * the requirement confirmation and everything downstream of it, while fixing a
264
+ * defect found in review invalidates the verification and review of that work but
265
+ * leaves the agreed requirement standing. Invalidating everything on every
266
+ * rework would be simpler and would also throw away conclusions that are still
267
+ * true, forcing work to be redone for no reason.
268
+ */
269
+ export interface TaskRevision {
270
+ /** What kind of change this was; decides the invalidation scope. */
271
+ kind: 'requirement' | 'solution' | 'defect';
272
+ /** Why the task went back, in the author's words. */
273
+ reason: string;
274
+ /** The stage the task returned to. */
275
+ to: string;
276
+ /** The stage it was at when the rework was recorded. */
277
+ from: string;
278
+ /** ISO-8601 timestamp. */
279
+ at: string;
280
+ /** Conclusion names this rework invalidated, for audit. */
281
+ invalidated: string[];
282
+ }
85
283
  /** Verdict of one review stage over a completed item. */
86
284
  export interface ItemReviewStage {
87
285
  outcome: 'pass' | 'fail';
@@ -186,6 +384,10 @@ export interface TaskState {
186
384
  evidence: string[];
187
385
  receipt?: VerificationReceipt;
188
386
  }>>;
387
+ /** Set by the `complete` operation once every configured condition holds. Absent while the task is still open. */
388
+ completed?: TaskCompletion;
389
+ /** Rework history, newest last. Absent when the task never went back. */
390
+ revisions?: TaskRevision[];
189
391
  }
190
392
  export interface Result {
191
393
  ok: boolean;
@@ -216,7 +418,7 @@ export declare function findTransition(from: string, to: string, config: Workflo
216
418
  * two-stage review whose both stages passed, so the implementation and its
217
419
  * audit trail are completed together.
218
420
  */
219
- export declare function todosBlockers(state: TaskState): string[];
421
+ export declare function todosBlockers(state: TaskState, config?: WorkflowConfig): string[];
220
422
  /**
221
423
  * Validate an advance from `state.stage` to `targetStage`.
222
424
  *
@@ -248,6 +450,44 @@ export interface CommitCheckpoint {
248
450
  label?: string;
249
451
  reason?: string;
250
452
  }
453
+ /**
454
+ * Whether this task's flow ever demands a passing verification.
455
+ *
456
+ * A preset that never guards an edge on `verified` is telling the engine that
457
+ * verification is not one of its completion conditions, so a downstream stage
458
+ * must not invent the requirement.
459
+ * @param config - the frozen workflow config.
460
+ * @returns true when any configured transition requires `verified`.
461
+ */
462
+ export declare function flowRequiresVerification(config: WorkflowConfig): boolean;
463
+ /**
464
+ * Stages where a passing verification must still HOLD, derived from the graph.
465
+ *
466
+ * `verified` guards the edge INTO a stage, so the requirement follows the task
467
+ * past that edge rather than applying from the start: a task sitting at 需求评审
468
+ * has produced nothing to verify. Once a task has crossed a `verified` edge,
469
+ * though, the requirement stays with it — that is the case the old code missed,
470
+ * where re-verifying and failing let the task continue and commit.
471
+ *
472
+ * Computed by walking forward from every `verified` edge's target, so it is a
473
+ * property of the configured graph rather than a hardcoded stage name.
474
+ * @param config - the frozen workflow config.
475
+ * @returns the set of stages at or after a verification gate.
476
+ */
477
+ export declare function verificationHeldStages(config: WorkflowConfig): Set<string>;
478
+ /**
479
+ * Why the current state fails the flow's verification requirement, if it does.
480
+ *
481
+ * Applies only once the task has reached a stage that sits at or after a
482
+ * verification gate, so it never fires on a task that has not yet had anything
483
+ * to verify. Completion and commit share this rule, which is what stops a failed
484
+ * re-verification from being outrun by advancing to a stage whose own outgoing
485
+ * edge happens not to repeat the guard.
486
+ * @param state - the task state.
487
+ * @param config - the frozen workflow config.
488
+ * @returns human-readable blockers; empty when the requirement does not apply or holds.
489
+ */
490
+ export declare function verificationBlockers(state: TaskState, config: WorkflowConfig): string[];
251
491
  /**
252
492
  * Decide whether the current task state permits a commit and with what label.
253
493
  *
@@ -257,6 +497,128 @@ export interface CommitCheckpoint {
257
497
  * hold, so the engine cannot be asked to commit before its own state is valid.
258
498
  */
259
499
  export declare function commitCheckpoint(state: TaskState, config: WorkflowConfig): CommitCheckpoint;
500
+ /**
501
+ * Whether a stage is terminal in the configured graph (no outgoing transition).
502
+ * @param stage - the stage to test.
503
+ * @param config - the frozen workflow config.
504
+ * @returns true when nothing follows this stage.
505
+ */
506
+ export declare function isTerminalStage(stage: string, config: WorkflowConfig): boolean;
507
+ /**
508
+ * Whether the delivery mode requires a recorded commit before completion.
509
+ * @param config - the frozen workflow config.
510
+ * @returns true when a commit must be recorded (the historical default).
511
+ */
512
+ export declare function commitRequired(config: WorkflowConfig): boolean;
513
+ /**
514
+ * Every reason the task may not be marked complete.
515
+ *
516
+ * Completion is its own fact, checked here rather than inferred from standing on
517
+ * the last stage. Three things can hold it back: the task has not reached a
518
+ * terminal stage, the flow's own `todos_done` conditions are unmet, or the
519
+ * delivery mode wants a commit that is not recorded. Deliberately NOT included:
520
+ * a blanket demand for a commit on every flow, because a commit is one delivery
521
+ * mode rather than a universal finish line.
522
+ * @param state - the task state.
523
+ * @param config - the frozen workflow config.
524
+ * @returns human-readable blockers; empty when the task may complete.
525
+ */
526
+ /**
527
+ * Configurations a stage needs but that cannot be resolved.
528
+ *
529
+ * Disclosure is not enforcement. `missing_rules` was reported in status and never
530
+ * blocked anything, so deleting a rule a stage was bound to still let the task
531
+ * advance — leaving a stage running without the constraints it was configured
532
+ * with, while the record looked fine.
533
+ *
534
+ * This is a pure decision over names, so the tool and the hook reach the same
535
+ * verdict from the same inputs. It takes the unresolved names rather than reading
536
+ * anything, because resolution needs a filesystem and this must stay callable from
537
+ * both planes.
538
+ * @param unresolved - references that resolved nowhere, as `source:name` or a bare legacy name.
539
+ * @param stage - the stage whose configuration is being checked.
540
+ * @returns one blocker per unresolved resource, empty when the stage is complete.
541
+ */
542
+ export declare function resourceBlockers(unresolved: readonly string[], stage: string): string[];
543
+ /**
544
+ * The outcomes a flow's FINAL stage is required to have, derived from its guards.
545
+ *
546
+ * Completion used to check only standing-on-a-final-stage, unfinished items and
547
+ * whether a commit existed. A flow that declares `review_passed` or `verified` on
548
+ * its way into the terminal stage was therefore satisfiable by ARRIVING there: the
549
+ * agile flow's review verdict and the standard flow's verification could both be
550
+ * failing while `complete` succeeded.
551
+ *
552
+ * Derived rather than hardcoded, because a flow that declares no verification gate
553
+ * must not be made to demand one — the requirement is whatever the flow said.
554
+ * @param config - the effective workflow.
555
+ * @returns the guard names the final stage's incoming edges require.
556
+ */
557
+ export declare function terminalRequirements(config: WorkflowConfig): GuardName[];
558
+ export declare function completionBlockers(state: TaskState, config: WorkflowConfig): string[];
559
+ /**
560
+ * Which recorded conclusions a rework of the given kind invalidates.
561
+ *
562
+ * Derived from what each kind of change actually supersedes:
563
+ *
564
+ * - `requirement`: the agreed requirement changed, so its confirmation no longer
565
+ * speaks for the work, and neither does the solution built on it. Verification,
566
+ * review and item audits all judged a tree derived from the old requirement.
567
+ * - `solution`: the requirement stands, but the approach changed. Its confirmation
568
+ * falls, along with everything that judged the implementation of the old one.
569
+ * - `defect`: a defect in the work as specified. The requirement and solution stay
570
+ * agreed; what falls is the evidence that the OLD implementation was correct —
571
+ * verification, review, and the per-item audits.
572
+ *
573
+ * A confirmation is always re-askable rather than silently assumed, because the
574
+ * human who approved it approved a different thing.
575
+ * @param kind - the kind of change being recorded.
576
+ * @returns the conclusion names to clear.
577
+ */
578
+ export declare function invalidatedBy(kind: TaskRevision['kind']): string[];
579
+ /**
580
+ * The result of applying one rework to a task record.
581
+ *
582
+ * Kept separate from the mutation so the caller can report what changed, and so
583
+ * the clearing rules above are asserted directly in tests.
584
+ */
585
+ export interface RevisionOutcome {
586
+ /** The stage the task now sits at. */
587
+ stage: string;
588
+ /** The conclusions that were cleared. */
589
+ invalidated: string[];
590
+ }
591
+ /**
592
+ * Apply a rework to a task record, clearing exactly the superseded conclusions.
593
+ *
594
+ * Confirmation flags are reset rather than deleted because the engine reads them
595
+ * as booleans; the other conclusions are cleared by removing the record that
596
+ * carried them, so `status` reports them as absent instead of stale.
597
+ * @param state - the task state, mutated in place.
598
+ * @param revision - the rework to apply.
599
+ * @returns the stage reached and the conclusions cleared.
600
+ */
601
+ export declare function applyRevision(state: TaskState, revision: TaskRevision): RevisionOutcome;
602
+ /**
603
+ * A project's `.dsh/eng.json` as read off disk.
604
+ *
605
+ * This describes what a file MAY contain so the resolver can narrow it; declaring
606
+ * only the fields a valid file is allowed would make an invalid one look valid to
607
+ * the parser and hide the error until much later.
608
+ */
609
+ export interface ParsedProjectConfig {
610
+ flow?: string;
611
+ /** The project's own stage bindings, taken verbatim. */
612
+ stage_bindings?: Record<string, StageBinding>;
613
+ /** The project's own commit policy, replacing any preset default. */
614
+ commit?: CommitRule;
615
+ /** The project's own artifact declarations. */
616
+ artifacts?: ArtifactDef[];
617
+ /** The project's own per-item review depth. */
618
+ review_depth?: ReviewDepth;
619
+ /** Whether a recorded commit is required before completion. */
620
+ commit_required?: boolean;
621
+ }
260
622
  export interface FileScopeResult {
261
623
  ok: boolean;
262
624
  /** Paths outside the declared scope (present only when the scope is set but violated). */