@nanmicoder/dsh-agent-teams 0.1.22 → 0.1.23

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 (42) hide show
  1. package/README.md +32 -10
  2. package/README_ZH.md +32 -10
  3. package/lib/artifacts.js +342 -0
  4. package/lib/capabilities.js +6 -4
  5. package/lib/client/ActivityPanel.js +5 -2
  6. package/lib/client/agent-teams-card-definition.js +100 -6
  7. package/lib/client.js +90 -7
  8. package/lib/client.js.map +1 -1
  9. package/lib/command.js +3 -2
  10. package/lib/git-artifact-stamp.json +2 -2
  11. package/lib/harness-compat.js +70 -21
  12. package/lib/index.js +26 -3
  13. package/lib/lineage.js +198 -0
  14. package/lib/mailbox.js +59 -34
  15. package/lib/members.js +301 -53
  16. package/lib/profiles.js +3 -2
  17. package/lib/quality-gates.js +850 -65
  18. package/lib/scheduler.js +22 -3
  19. package/lib/snapshot.js +22 -1
  20. package/lib/state.js +279 -13
  21. package/lib/tool-policies.js +187 -0
  22. package/lib/tools.js +370 -89
  23. package/lib/types/artifacts.d.ts +66 -0
  24. package/lib/types/client/activity-monitor.d.ts +6 -0
  25. package/lib/types/client/agent-teams-card-definition.d.ts +56 -0
  26. package/lib/types/harness-compat.d.ts +56 -14
  27. package/lib/types/index.d.ts +26 -1
  28. package/lib/types/lineage.d.ts +92 -0
  29. package/lib/types/members.d.ts +112 -1
  30. package/lib/types/profiles.d.ts +6 -0
  31. package/lib/types/quality-gates.d.ts +216 -10
  32. package/lib/types/scheduler.d.ts +2 -1
  33. package/lib/types/snapshot.d.ts +20 -0
  34. package/lib/types/state.d.ts +22 -5
  35. package/lib/types/tool-policies.d.ts +119 -0
  36. package/lib/types/tools.d.ts +12 -1
  37. package/lib/types/types.d.ts +95 -0
  38. package/lib/types/workspace-audit.d.ts +27 -0
  39. package/lib/workspace-audit.js +52 -0
  40. package/package.json +12 -5
  41. package/release-notes/v0.1.23.md +34 -0
  42. package/scripts/legacy-session-source-repair.mjs +344 -0
@@ -3,7 +3,7 @@
3
3
  * coverage, and resume. Tools and persistence call these; they do not I/O.
4
4
  * @module dsh-agent-teams/quality-gates
5
5
  */
6
- import { type AcceptanceResult, type CommandResult, type FindingSeverity, type ReviewFinding, type ReviewPolicy, type ReviewVerdict, type TaskKind, type TaskEvidence, type TaskRevision, type TaskStatus, type TeamState, type TeamTask } from './types.ts';
6
+ import { type AcceptanceResult, type ArtifactDigest, type ArtifactScopeGap, type ArtifactSnapshot, type CommandResult, type FindingSeverity, type ReviewFinding, type ReviewPolicy, type ReviewVerdict, type TaskKind, type TaskEvidence, type TaskRevision, type TaskStatus, type TeamState, type TeamTask } from './types.ts';
7
7
  declare const QUALITY_KINDS: readonly TaskKind[];
8
8
  declare const WRITE_KINDS: readonly TaskKind[];
9
9
  declare const DEFAULT_REVIEW_POLICY: Required<Pick<ReviewPolicy, 'requirementsMinRounds' | 'requirementsMaxRounds' | 'codeMaxRounds' | 'maxRepairAttempts'>>;
@@ -54,6 +54,7 @@ export interface PlannedFollowUpTask {
54
54
  id?: string;
55
55
  kind: TaskKind;
56
56
  subject?: string;
57
+ description?: string;
57
58
  assignee?: string;
58
59
  dependencies?: string[];
59
60
  round?: number;
@@ -64,13 +65,23 @@ export interface PlannedFollowUpTask {
64
65
  verify?: string[];
65
66
  sourceTaskId?: string;
66
67
  sourceFindingIds?: string[];
68
+ findingSourceTaskIds?: string[];
67
69
  reviewedTaskId?: string;
70
+ /**
71
+ * Paths whose current bytes the generated contract was derived from. The
72
+ * caller snapshots them so the work order can later be marked stale.
73
+ */
74
+ contractPaths?: string[];
68
75
  }
69
76
  export interface PlanQualityFollowUpResult {
70
77
  created: PlannedFollowUpTask[];
71
78
  tasks: PlannedFollowUpTask[];
72
79
  escalated?: boolean;
73
80
  status?: 'escalated';
81
+ /** Why the automatic loop stopped; surfaced to the captain. */
82
+ reason?: string;
83
+ /** Findings that were refused as contract material (default-excluded paths). */
84
+ excludedFindings?: string[];
74
85
  }
75
86
  export interface CoverageRow {
76
87
  goal_item: string;
@@ -81,6 +92,38 @@ export interface CoverageRow {
81
92
  export interface DeliveryResult {
82
93
  ok: boolean;
83
94
  blockers: string[];
95
+ /**
96
+ * Non-blocking observations: gates that legitimately drifted because a later
97
+ * revision superseded them, and generated work orders whose cited files
98
+ * moved on. Delivery is still allowed, but never silently.
99
+ */
100
+ warnings: string[];
101
+ }
102
+ /**
103
+ * Current workspace bytes handed to the delivery check. The rules stay pure:
104
+ * the caller (which owns the workspace path) reads the digests and passes them
105
+ * in. Omit it when no workspace is available — the attempt-level staleness
106
+ * checks still run, and content checks simply cannot be made.
107
+ */
108
+ export interface ScopeEntry {
109
+ path: string;
110
+ present: boolean;
111
+ }
112
+ export interface DeliveryAudit {
113
+ /** Current bytes of the paths recorded by verdicts and work orders. */
114
+ artifacts?: readonly ArtifactDigest[];
115
+ /**
116
+ * Current contents of the scope patterns recorded verdicts bound. A path that
117
+ * appears here but not in the verdict's recorded artifacts was never
118
+ * reviewed, so a broad scope cannot silently absorb it.
119
+ */
120
+ expandedScope?: readonly ScopeEntry[];
121
+ /**
122
+ * Parts of the *current* read that could not be completed (expansion cap,
123
+ * unreadable directory or file, illegal pattern). A non-empty list means the
124
+ * audit itself is incomplete, so no verdict may be confirmed from it.
125
+ */
126
+ gaps?: readonly ArtifactScopeGap[];
84
127
  }
85
128
  export interface ResumeTeamResult {
86
129
  ok?: boolean;
@@ -95,6 +138,8 @@ export interface QualityLoopSnapshot {
95
138
  escalated: boolean;
96
139
  deliverable: boolean;
97
140
  summary: string;
141
+ /** Non-blocking gate observations (stale verdicts and work orders). */
142
+ warnings: string[];
98
143
  }
99
144
  export interface QualityGraphDraft {
100
145
  subject: string;
@@ -116,6 +161,13 @@ export declare function isReviewPolicy(value: unknown): value is ReviewPolicy;
116
161
  /** Normalize a workspace-relative POSIX path. `undefined` means illegal. */
117
162
  export declare function normalizeWorkspacePath(path: string): string | undefined;
118
163
  export declare function pathMatchesScope(path: string, pattern: string): boolean;
164
+ /**
165
+ * Paths no quality contract may claim: version control, harness/plugin state,
166
+ * secrets and private keys. Exported because the completion gate and contract
167
+ * generation (including the artifact reader that must not walk into
168
+ * `.agent-teams/`, `secrets/` or `.env*`) have to share exactly one predicate.
169
+ */
170
+ export declare function isDefaultExcludedPath(path: string): boolean;
119
171
  export declare function classifyChangedPath(path: string, inScope?: readonly string[], outOfScope?: readonly string[]): PathClassification;
120
172
  export declare function collectChangedPaths(gitStatusText: string): string[];
121
173
  export declare function inScopeOverlap(left: readonly string[] | undefined, right: readonly string[] | undefined): string[];
@@ -132,15 +184,36 @@ export declare function evaluateQualityCompletion(task: TeamTask, update: Qualit
132
184
  * forbids, so no honest completion exists and the repair dead-locks.
133
185
  *
134
186
  * Absolute and otherwise illegal paths are dropped (they can never match
135
- * workspace-relative scope patterns anyway); when nothing legal remains,
136
- * the source task's own inScope is kept as the fallback. Over-inclusion is
137
- * accepted: inScope is an audit upper bound, and the requiredFix text still
138
- * tells the implementer what to touch.
187
+ * workspace-relative scope patterns anyway), and so are paths the completion
188
+ * gate refuses by default (`secrets/` segments, `.env*`, `.git`, `.dsh`,
189
+ * `id_rsa*` — issue #183). `classifyChangedPath` checks that default-exclusion
190
+ * set BEFORE inScope, so collecting such a path here would generate a contract
191
+ * that demands an edit the gate calls `out_of_scope`: no honest completion
192
+ * exists and the repair burns its rounds. Contract generation and the
193
+ * completion gate must agree; the exemption channel stays the user-configured
194
+ * outOfScope list, never a reviewer's finding.
139
195
  *
140
- * Keep all derived paths until the generator resolves inherited exclusions;
141
- * filtering first would silently discard a required fix target.
196
+ * When nothing legal remains, the source task's own inScope is kept as the
197
+ * fallback. Over-inclusion is accepted otherwise: inScope is an audit upper
198
+ * bound, and the requiredFix text still tells the implementer what to touch.
199
+ * Keep every remaining derived path until the generator resolves inherited
200
+ * exclusions; filtering those first would silently discard a required fix
201
+ * target.
142
202
  */
143
203
  export declare function repairScopeFromFindings(findings: readonly ReviewFinding[], fallback: string[] | undefined): string[] | undefined;
204
+ /**
205
+ * Every workspace-relative path one finding names, before any exclusion rule:
206
+ * the observed file plus the fix targets referenced by `requiredFix`.
207
+ */
208
+ export declare function findingDerivedPaths(finding: ReviewFinding): string[];
209
+ /**
210
+ * Findings whose every cited path is default-excluded. They cannot become an
211
+ * honest repair contract (see {@link repairScopeFromFindings}), so they are
212
+ * surfaced to the captain instead of silently disappearing or dead-locking a
213
+ * member. A finding that names no path at all is NOT excluded: it simply falls
214
+ * back to the judged task's own scope, exactly as before.
215
+ */
216
+ export declare function excludedFindingsOf(findings: readonly ReviewFinding[], pathExists?: (path: string) => boolean): ReviewFinding[];
144
217
  /** Captain-only amendment payload: replacement values for contract fields. */
145
218
  export interface ContractAmendmentInput {
146
219
  objective?: string;
@@ -170,14 +243,136 @@ export interface AmendTaskContractResult {
170
243
  * amended contract.
171
244
  */
172
245
  export declare function amendTaskContract(team: TeamState, task: TeamTask, input: ContractAmendmentInput, by: string, reason: string): AmendTaskContractResult;
173
- export declare function planQualityFollowUp(team: TeamState, closed: TeamTask): PlanQualityFollowUpResult;
246
+ /** Remove drifting line anchors from generated contract text. */
247
+ export declare function stripLineAnchors(text: string): string;
248
+ export interface PlanQualityFollowUpOptions {
249
+ /**
250
+ * Whether a derived path exists in the workspace. Used only to classify a
251
+ * finding as excluded-only; without it every derived path counts.
252
+ */
253
+ pathExists?: (path: string) => boolean;
254
+ }
255
+ export declare function planQualityFollowUp(team: TeamState, closed: TeamTask, options?: PlanQualityFollowUpOptions): PlanQualityFollowUpResult;
174
256
  export declare function buildCoverageMatrix(goalItems: readonly string[], tasks: readonly TeamTask[]): CoverageRow[];
175
- export declare function canDeclareDelivery(team: TeamState): DeliveryResult;
257
+ /** Kinds whose completion is a verdict about a revision. */
258
+ export declare function isVerdictKind(kind: TaskKind | undefined): boolean;
259
+ /**
260
+ * Compare recorded bytes against the bytes on disk now. Every returned entry
261
+ * is a reason a verdict or a work order no longer describes the workspace.
262
+ * "Cannot be read" is reported as its own reason: a permission error is not a
263
+ * match, and a partial read is not proof of an unchanged file.
264
+ */
265
+ /** One reason an artifact set no longer describes the workspace. */
266
+ export interface ArtifactDriftDetail {
267
+ /** The implicated path, when the reason is about one concrete file. */
268
+ path?: string;
269
+ message: string;
270
+ }
271
+ /**
272
+ * Compare recorded bytes against the bytes on disk now, keeping the path each
273
+ * reason belongs to. "Cannot be read" is reported as its own reason: a
274
+ * permission error is not a match, and a partial read is not proof of an
275
+ * unchanged file.
276
+ */
277
+ export declare function artifactDriftDetails(recorded: readonly ArtifactDigest[] | undefined, current: readonly ArtifactDigest[] | undefined): ArtifactDriftDetail[];
278
+ /** Message-only view of {@link artifactDriftDetails}. */
279
+ export declare function artifactDrift(recorded: readonly ArtifactDigest[] | undefined, current: readonly ArtifactDigest[] | undefined): string[];
280
+ /** A partially enumerated scope is incomplete evidence, never a silent pass. */
281
+ export declare function gapDrift(gaps: readonly ArtifactScopeGap[] | undefined): string[];
282
+ /**
283
+ * The task a verdict is about: an explicit `reviewedTaskId`/`sourceTaskId`
284
+ * first, otherwise the implementation/repair this gate depends on.
285
+ */
286
+ export declare function reviewedTargetOf(team: TeamState, task: TeamTask): TeamTask | undefined;
287
+ /**
288
+ * Bind a closing verdict to the revision it judged: the target's execution
289
+ * generation plus the bytes of the paths it changed. Without this a `pass` is
290
+ * only a task record, and delivery cannot tell that the reviewed bytes were
291
+ * edited or deleted afterwards. `gaps` carries the parts of the reviewed scope
292
+ * that could not be enumerated, `scope` the patterns it bound, and
293
+ * `duringDrift` any change observed between claim and close — partial or
294
+ * moving coverage is never read as complete.
295
+ */
296
+ export interface ReviewBinding {
297
+ artifacts: readonly ArtifactDigest[];
298
+ gaps?: readonly ArtifactScopeGap[];
299
+ scope?: readonly string[];
300
+ duringDrift?: readonly string[];
301
+ }
302
+ export declare function bindReviewToRevision(team: TeamState, task: TeamTask, binding: ReviewBinding): TeamTask;
303
+ /**
304
+ * Scope a verdict must bind: the paths the judged task changed, otherwise the
305
+ * scope patterns of its contract (expanded by the caller).
306
+ */
307
+ export declare function verdictScopePatterns(team: TeamState, task: TeamTask): string[];
308
+ /** Scope patterns recorded verdicts bound, for new-file detection. */
309
+ export declare function deliveryScopePatterns(team: TeamState): string[];
310
+ /** Paths whose current bytes decide whether recorded verdicts and orders hold. */
311
+ export declare function deliveryArtifactPaths(team: TeamState): string[];
312
+ /**
313
+ * Every completed verdict that claims to cover the current revision but no
314
+ * longer does.
315
+ *
316
+ * A drifted review/requirements pass is the delivery evidence itself, so it
317
+ * blocks — unless a later sound verdict covers the same lineage, which is the
318
+ * normal shape of `pass → repair → new pass` and must converge instead of
319
+ * blocking forever behind an old snapshot. A drifted `verification` is only a
320
+ * warning: a later repair legitimately supersedes it, but never silently.
321
+ * A verdict that predates content binding is reported as an unauditable gap
322
+ * rather than being read as a pass over the current bytes.
323
+ */
324
+ /** One audited verdict observation, attributed to the task that carries it. */
325
+ export interface VerdictAuditNote {
326
+ taskId: string;
327
+ /**
328
+ * `blocker` when this pass is the delivery evidence itself, `warning` when a
329
+ * later sound verdict supersedes it or when it is an intermediate gate.
330
+ */
331
+ level: 'blocker' | 'warning';
332
+ message: string;
333
+ }
334
+ /**
335
+ * Audit every completed verdict against the current revision (issue #161).
336
+ *
337
+ * A drifted review/requirements pass is the delivery evidence itself, so it
338
+ * blocks — unless a later sound verdict covers the same lineage, which is the
339
+ * normal shape of `pass → repair → new pass` and must converge instead of
340
+ * blocking forever behind an old snapshot. A drifted `verification` or a
341
+ * superseded pass is reported as a warning: a later repair legitimately
342
+ * supersedes it, but never silently. A verdict that predates content binding
343
+ * is reported as an unauditable gap rather than being read as a pass over the
344
+ * current bytes.
345
+ */
346
+ export declare function verdictAuditNotes(team: TeamState, audit?: DeliveryAudit): VerdictAuditNote[];
347
+ /** Staleness of one auto-generated work order against the current workspace. */
348
+ export declare function contractStaleness(task: TeamTask, current: readonly ArtifactDigest[] | undefined): string[];
349
+ export interface WorkOrderWarning {
350
+ taskId: string;
351
+ message: string;
352
+ }
353
+ /**
354
+ * Open auto-generated work orders whose cited bytes moved on (issue #174).
355
+ * A repair round's acceptance/inScope is a snapshot of the workspace at
356
+ * finding-generation time; every later round invalidates it, and nothing used
357
+ * to say so — a member following it literally edits the wrong line or redoes
358
+ * work that is already done.
359
+ */
360
+ export declare function workOrderWarnings(team: TeamState, current: readonly ArtifactDigest[] | undefined): WorkOrderWarning[];
361
+ export declare function canDeclareDelivery(team: TeamState, audit?: DeliveryAudit): DeliveryResult;
176
362
  export declare function resumeTeamState(team: TeamState, reason: string): ResumeTeamResult;
363
+ /**
364
+ * The durable `findings[].line` contract, shared by the write-side parser
365
+ * (`parseFindings`) and the durable validator (`isReviewFinding`) so the two
366
+ * cannot drift apart again (issue #188, #105 class).
367
+ */
368
+ export declare function isValidFindingLine(value: unknown): value is number;
177
369
  export declare function isReviewFinding(value: unknown): value is ReviewFinding;
178
370
  export declare function isAcceptanceResult(value: unknown): value is AcceptanceResult;
179
371
  export declare function isCommandResult(value: unknown): value is CommandResult;
180
372
  export declare function isTaskRevision(value: unknown): value is TaskRevision;
373
+ export declare function isArtifactDigest(value: unknown): value is ArtifactDigest;
374
+ export declare function isArtifactScopeGap(value: unknown): value is ArtifactScopeGap;
375
+ export declare function isArtifactSnapshot(value: unknown): value is ArtifactSnapshot;
181
376
  /**
182
377
  * Normalize blank optional task fields to omitted ("blank means absent").
183
378
  * Blank string scalars are deleted; string lists have blank entries filtered
@@ -186,6 +381,17 @@ export declare function isTaskRevision(value: unknown): value is TaskRevision;
186
381
  * validation stays strict.
187
382
  */
188
383
  export declare function normalizeBlankOptionalTaskFields<T extends object>(task: T): T;
384
+ /**
385
+ * Treat an out-of-contract **numeric** sentinel as absent, including
386
+ * `findings[].line` (contract: `isValidFindingLine`).
387
+ *
388
+ * Only numbers are healed. A value of any other type (`round: "oops"`,
389
+ * `attempt: {}`, `line: "12"`) is left exactly as written and stays a hard,
390
+ * named validation failure, so genuine corruption is never silently swallowed
391
+ * while `-1`-style sentinels — the #188 brick — self-heal. Field order, unknown
392
+ * fields and every in-range value are preserved.
393
+ */
394
+ export declare function normalizeSentinelNumericTaskFields<T extends object>(task: T): T;
189
395
  export declare function hasValidQualityTaskFields(value: Record<string, unknown>): boolean;
190
396
  export declare function isTaskKind(value: unknown): value is TaskKind;
191
397
  export declare function isReviewVerdict(value: unknown): value is ReviewVerdict;
@@ -202,7 +408,7 @@ export declare function defaultQualityDeliveryGraph(input: {
202
408
  integrator?: string;
203
409
  }): QualityGraphDraft[];
204
410
  export declare function qualityPlanningPrompt(): string;
205
- export declare function describeQualityLoop(team: TeamState): QualityLoopSnapshot;
411
+ export declare function describeQualityLoop(team: TeamState, audit?: DeliveryAudit): QualityLoopSnapshot;
206
412
  export { QUALITY_KINDS, WRITE_KINDS };
207
413
  /** Persisted evidence must remain loadable after process restart. */
208
414
  export declare function isTaskEvidence(value: unknown): value is TaskEvidence;
@@ -65,6 +65,7 @@ export interface DispatchTicket {
65
65
  readonly acceptance?: readonly string[];
66
66
  readonly verify?: readonly string[];
67
67
  readonly reviewedTaskId?: string;
68
+ readonly findingSourceTaskIds?: readonly string[];
68
69
  }
69
70
  /**
70
71
  * Recursively collect `status=completed` ancestors of `taskId` in topological
@@ -73,6 +74,6 @@ export interface DispatchTicket {
73
74
  export declare function collectCompletedDependencyOutputs(tasks: readonly TeamTask[], taskId: string, warn?: (message: string) => void): DependencyOutput[];
74
75
  /** Format completed-dependency outputs with per-item and total truncation. */
75
76
  export declare function formatDependencyOutputs(items: readonly DependencyOutput[]): string;
76
- export declare function assignmentPrompt(ticket: DispatchTicket, stateDir: string, teamId: string): string;
77
+ export declare function assignmentPrompt(ticket: DispatchTicket, stateDir: string, teamId: string, workOrderWarnings?: readonly string[]): string;
77
78
  /** Install one scheduler and its member activity observer. */
78
79
  export declare function installTeamScheduler(ctx: Context, config: SchedulerConfig): TeamScheduler;
@@ -42,6 +42,11 @@ export interface TeamActivityTask {
42
42
  readonly kind?: string;
43
43
  readonly round?: number;
44
44
  readonly verdict?: string;
45
+ /**
46
+ * Set when this task's verdict or auto-generated work order no longer
47
+ * describes the current bytes; the panel must not render a bare pass badge.
48
+ */
49
+ readonly verdictStale?: string;
45
50
  /** Durable last-write stamp; drives the finished-member ordering (issue #192). */
46
51
  readonly updatedAt: number;
47
52
  }
@@ -64,6 +69,14 @@ export interface TeamActivitySnapshot {
64
69
  readonly tasks: readonly TeamActivityTask[];
65
70
  readonly messageCount: number;
66
71
  readonly captainInbox: readonly TeamActivityMessage[];
72
+ /** Present when the snapshot could audit the workspace bytes. */
73
+ readonly loopState?: string;
74
+ readonly deliverable?: boolean;
75
+ readonly delivery?: {
76
+ readonly ok: boolean;
77
+ readonly blockers: readonly string[];
78
+ readonly warnings: readonly string[];
79
+ };
67
80
  }
68
81
  /** Snapshot projection switches for live and archived teams. */
69
82
  export interface TeamSnapshotOptions {
@@ -71,6 +84,12 @@ export interface TeamSnapshotOptions {
71
84
  readonly includeRemoved?: boolean;
72
85
  /** Archived teams have no meaningful live activity after their sessions stop. */
73
86
  readonly historic?: boolean;
87
+ /**
88
+ * Absolute workspace path of the owning workspace. When present, the snapshot
89
+ * audits the current bytes behind every recorded verdict, so the panel and
90
+ * the model-facing status tool cannot report different delivery verdicts.
91
+ */
92
+ readonly workspacePath?: string;
74
93
  }
75
94
  /** Compact `provider/model` route for the activity panel, or just the model. */
76
95
  export declare function memberModelRoute(member: {
@@ -95,6 +114,7 @@ export declare function assembleTeamSnapshot(ctx: Context, stateRoot: string, wo
95
114
  export declare function collectTeamsActivity(ctx: Context, roots: readonly {
96
115
  workspace: string;
97
116
  stateRoot: string;
117
+ workspacePath?: string;
98
118
  }[]): Promise<TeamActivitySnapshot[]>;
99
119
  /**
100
120
  * Collect every archived team under the given workspace state roots (the
@@ -13,7 +13,8 @@
13
13
  * @module dsh-agent-teams/state
14
14
  */
15
15
  import { type TaskStatus, type TeamMessage, type TeamState, type TeamTask } from './types.ts';
16
- export { amendTaskContract, buildCoverageMatrix, canDeclareDelivery, classifyChangedPath, collectChangedPaths, defaultQualityDeliveryGraph, describeQualityLoop, evaluateQualityCompletion, hasValidQualityTaskFields, isQualityKind, isTaskRevision, normalizeBlankOptionalTaskFields, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from './quality-gates.ts';
16
+ import { type LeadIdentity } from './lineage.ts';
17
+ export { amendTaskContract, buildCoverageMatrix, canDeclareDelivery, classifyChangedPath, collectChangedPaths, defaultQualityDeliveryGraph, describeQualityLoop, evaluateQualityCompletion, hasValidQualityTaskFields, isQualityKind, isTaskRevision, isValidFindingLine, normalizeBlankOptionalTaskFields, normalizeSentinelNumericTaskFields, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from './quality-gates.ts';
17
18
  export type { ContractAmendmentInput } from './quality-gates.ts';
18
19
  /** Mailbox key of the captain. */
19
20
  export declare const CAPTAIN_KEY = "captain";
@@ -116,18 +117,22 @@ export declare function recordRetiredMemberIds(stateRoot: string, memberIds: rea
116
117
  * Find the team owned by one captain session (at most one per captain).
117
118
  * @param stateRoot - resolved absolute state root directory.
118
119
  * @param captainSessionId - the owning session id.
120
+ * @param lead - durable lead identity of the calling session, when it may lead
121
+ * through a fork chain; omitted keeps the exact-id contract.
119
122
  * @returns the team record, or undefined when the captain leads no team.
120
123
  */
121
- export declare function findTeamByCaptain(stateRoot: string, captainSessionId: string): Promise<TeamState | undefined>;
124
+ export declare function findTeamByCaptain(stateRoot: string, captainSessionId: string, lead?: LeadIdentity): Promise<TeamState | undefined>;
122
125
  /**
123
126
  * Find the team in which one session is an active participant.
124
- * Captains match `captainSessionId`; members match their durable child session
125
- * id. Removed members no longer have access to team-scoped tools.
127
+ * Captains match `captainSessionId` — including a lead session that inherited
128
+ * it through a fork — and members match their durable child session id.
129
+ * Removed members no longer have access to team-scoped tools.
126
130
  * @param stateRoot - resolved absolute state root directory.
127
131
  * @param agentSessionId - calling captain/member session id.
132
+ * @param lead - durable lead identity of the calling session, when available.
128
133
  * @returns the team record, or undefined when the caller belongs to no team.
129
134
  */
130
- export declare function findTeamByParticipant(stateRoot: string, agentSessionId: string): Promise<TeamState | undefined>;
135
+ export declare function findTeamByParticipant(stateRoot: string, agentSessionId: string, lead?: LeadIdentity): Promise<TeamState | undefined>;
131
136
  /** Build a fresh message record. */
132
137
  export declare function createMessage(from: string, to: string, content: string): TeamMessage;
133
138
  /**
@@ -193,6 +198,18 @@ export interface AtomicReplaceOptions {
193
198
  * @returns nothing once the file has been replaced by one of the two paths.
194
199
  */
195
200
  export declare function replaceFileAtomicOrDirect(temporary: string, file: string, content: string, primitives: AtomicReplacePrimitives, options?: AtomicReplaceOptions): Promise<void>;
201
+ /**
202
+ * Name the first durable defect in a record `coerceTeamState` rejected. Before
203
+ * this, a rejected `team.json` surfaced only as
204
+ * `invalid AgentTeams state in team "x"` — no task, no field, no value — while
205
+ * every AgentTeams tool (read *and* write) failed at once, leaving an operator
206
+ * to bisect the file by hand (issue #188). The detail is computed only on the
207
+ * failure path, so the happy path keeps its existing cost.
208
+ * @param raw - the parsed record as read from disk, before coercion.
209
+ * @param expectedId - the team id the record was read for.
210
+ * @returns a `task "t2": findings[0].line -1 must be ...` style detail.
211
+ */
212
+ export declare function describeTeamStateDefect(raw: unknown, expectedId: string): string;
196
213
  export declare function isTeamTask(value: unknown): value is TeamTask;
197
214
  /**
198
215
  * Remove a team's whole directory (members should be interrupted first).
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Owner-declared, reusable per-member tool policies (#59).
3
+ *
4
+ * A deployment owner declares reusable policies under `toolPolicies`; a member
5
+ * *selects* one by id (`tool_policy` on `agent_teams_add_member`, an inline
6
+ * plan member, or a profile member row). The Captain model can therefore only
7
+ * choose an existing policy — it can never submit raw allow/deny lists or
8
+ * invent one at runtime.
9
+ *
10
+ * One pure resolver turns the selection into the immutable effective filter
11
+ * that is persisted on the member (`toolPolicy` + `toolFilter`) and handed to
12
+ * the existing `startContinuable({ toolFilter })` seam, which hides the named
13
+ * tools from the child's schemas and rejects their execution. This is tool
14
+ * visibility / invocation policy for global tools, not an OS sandbox.
15
+ *
16
+ * Invariants (issue #59):
17
+ * 1. the captain-only deny list always applies and cannot be undone by a
18
+ * policy — the host intersects `allow` and `deny`, so a denied name never
19
+ * becomes visible again;
20
+ * 2. the four member coordination tools stay available, and a policy may not
21
+ * deny them;
22
+ * 3. an allow-list policy explicitly includes those coordination tools;
23
+ * 4. the effective deny list is the union of the policy deny list and the
24
+ * captain-only deny list.
25
+ * @module dsh-agent-teams/tool-policies
26
+ */
27
+ /**
28
+ * Deny names the lenient spawn retry may drop when the host reports them
29
+ * unknown: exactly the built-in captain-only and depth entries (#163/#164).
30
+ * Names an owner policy declares are never relaxed — a policy that the host
31
+ * cannot apply must fail loud instead of silently applying a different one.
32
+ */
33
+ export declare const BUILTIN_DENY_TOOLS: readonly string[];
34
+ /** One owner-declared reusable member tool policy. */
35
+ export interface MemberToolPolicy {
36
+ /** Global tool names that stay visible; coordination tools are added automatically. */
37
+ allow?: string[];
38
+ /** Global tool names removed from visibility; captain-only tools are added automatically. */
39
+ deny?: string[];
40
+ }
41
+ /** The immutable effective filter of one member. */
42
+ export interface MemberToolFilter {
43
+ /** Visible global tools; absent means "no allow-list" (everything not denied stays). */
44
+ allow?: string[];
45
+ /** Removed global tools; never empty (the captain-only deny list is always included). */
46
+ deny: string[];
47
+ }
48
+ /** How the running host resolves the depth entries; both fields are optional. */
49
+ export interface ToolPolicyContext {
50
+ /** Member delegation depth cap; `0` denies the host delegation tools when they exist. */
51
+ maxDepth?: number;
52
+ /** Names the host's `tools.restrict()` would admit, or undefined without a registry view. */
53
+ knownTools?: ReadonlySet<string>;
54
+ }
55
+ /** Trim, drop blanks, and de-duplicate names, preserving first-seen order. */
56
+ export declare function dedupeToolNames(names: readonly string[]): string[];
57
+ /**
58
+ * Validate and normalize one declared policy. Empty policies and empty lists
59
+ * are configuration mistakes: an `allow: []` policy would hide even the
60
+ * coordination tools, and a policy with neither list restricts nothing.
61
+ * Denying a required coordination tool is rejected for the same reason.
62
+ * @param id - the policy id, used in error messages.
63
+ * @param policy - the declared policy.
64
+ * @returns the normalized policy (trimmed, de-duplicated, blanks dropped).
65
+ */
66
+ export declare function normalizeToolPolicy(id: string, policy: MemberToolPolicy): MemberToolPolicy;
67
+ /**
68
+ * Validate every configured policy once, at plugin mount, so a malformed
69
+ * deployment config fails loud instead of at the first member creation.
70
+ * @param raw - the configured `toolPolicies` map.
71
+ * @returns normalized policies keyed by trimmed id.
72
+ */
73
+ export declare function parseToolPolicies(raw: Record<string, MemberToolPolicy> | undefined | null): Record<string, MemberToolPolicy>;
74
+ /**
75
+ * The policy behind one selected id, or `undefined` when this deployment no
76
+ * longer declares it (a config change after the member was created). Used on
77
+ * paths that must not fail a durable record merely because the owner edited
78
+ * configuration; selection itself uses {@link requireToolPolicy}.
79
+ * @param policies - normalized configured policies.
80
+ * @param id - the selected policy id.
81
+ * @returns the declared policy, or undefined.
82
+ */
83
+ export declare function optionalToolPolicy(policies: Record<string, MemberToolPolicy>, id: string): MemberToolPolicy | undefined;
84
+ /**
85
+ * The deny names one spawn may relax for a member that selected a policy.
86
+ *
87
+ * The built-in captain/depth entries keep the #163/#164 recovery path, but a
88
+ * name the owner explicitly declared in the policy's own `deny` list is never
89
+ * relaxable — even when it is also a built-in entry (`send_message` and
90
+ * `subagent` are depth entries, and every captain-only name is a built-in
91
+ * deny). Dropping such a name would spawn the member under a different policy
92
+ * than the owner declared, which is contract drift rather than recovery.
93
+ * @param policy - the member's selected policy, or undefined when the member
94
+ * selected none (or the id is no longer configured).
95
+ * @param policySelected - whether the member selected a policy at all. A
96
+ * selected id whose policy is no longer configured relaxes nothing, so the
97
+ * member fails loud instead of spawning under an unknown filter.
98
+ * @returns the deny names the lenient retry may drop.
99
+ */
100
+ export declare function memberSpawnRelaxableDeny(policy: MemberToolPolicy | undefined, policySelected?: boolean): readonly string[];
101
+ /**
102
+ * The policy behind one selected id. A model-facing selection may only name a
103
+ * policy the deployment owner declared, so an unknown id is an error raised
104
+ * before any member is persisted.
105
+ * @param policies - normalized configured policies.
106
+ * @param id - the selected policy id.
107
+ * @returns the declared policy.
108
+ */
109
+ export declare function requireToolPolicy(policies: Record<string, MemberToolPolicy>, id: string): MemberToolPolicy;
110
+ /**
111
+ * Turn a selected policy (or no policy) into the immutable effective filter.
112
+ *
113
+ * Without `knownTools` the host's delegation names are forwarded verbatim, as
114
+ * before #163: the spawn retry drops whatever the host reports unknown.
115
+ * @param policy - the normalized policy, or undefined for the legacy behavior.
116
+ * @param context - depth cap and the host's restrictable-name view.
117
+ * @returns the effective filter handed to `startContinuable`.
118
+ */
119
+ export declare function resolveMemberToolFilter(policy: MemberToolPolicy | undefined, context?: ToolPolicyContext): MemberToolFilter;
@@ -10,6 +10,8 @@
10
10
  */
11
11
  import type { Context } from '@deepseek-ai/cordis';
12
12
  import type { Agent } from '@deepseek-ai/dsh-agent';
13
+ import { type MemberToolPolicy } from './tool-policies.ts';
14
+ import type { ArtifactSnapshot } from './types.ts';
13
15
  import { type TeamState, type TeamTask } from './types.ts';
14
16
  export { steerCaptainReport } from './members.ts';
15
17
  /** Resolved plugin config consumed by the tools. */
@@ -20,6 +22,8 @@ export interface ToolsConfig {
20
22
  memberProvider: string;
21
23
  /** Optional member model override. */
22
24
  memberModel?: string;
25
+ /** Optional member LLM provider, paired with `memberModel` (issue #189). */
26
+ memberLlmProvider?: string;
23
27
  /** Prompt injected into member personas and assignments. */
24
28
  executionPrompt?: string;
25
29
  /** Plugin fallback route. */
@@ -30,6 +34,8 @@ export interface ToolsConfig {
30
34
  maxMembers: number;
31
35
  /** Named team profiles from the active DSH profile. */
32
36
  profiles: Record<string, import('./profiles.ts').TeamProfileConfig>;
37
+ /** Owner-declared reusable member tool policies, keyed by policy id (#59). */
38
+ toolPolicies: Record<string, MemberToolPolicy>;
33
39
  }
34
40
  /** Browser/UI mutations allowed while a plan is waiting for approval. */
35
41
  export type StagedPlanMutation = {
@@ -40,6 +46,8 @@ export type StagedPlanMutation = {
40
46
  model: string;
41
47
  reasoningEffort?: string | null;
42
48
  executionPrompt?: string | null;
49
+ /** Selected tool-policy id; an empty string clears it back to no policy. */
50
+ toolPolicy?: string;
43
51
  } | {
44
52
  action: 'update_task';
45
53
  taskId: string;
@@ -86,6 +94,7 @@ export declare function haltTeamWork(input: {
86
94
  signal?: AbortSignal;
87
95
  }): Promise<{
88
96
  teamName: string;
97
+ captainSessionId: string;
89
98
  cancelledTasks: number;
90
99
  alreadyHalted: boolean;
91
100
  }>;
@@ -101,7 +110,9 @@ export declare function stagedPlanFeedbackContext(teamName: string): string;
101
110
  * @param config - resolved tool config.
102
111
  */
103
112
  export declare function registerAgentTeamsTools(ctx: Context, config: ToolsConfig): AgentTeamsRuntime;
104
- export declare function applyQualityFollowUp(team: TeamState, closed: TeamTask): {
113
+ export declare function applyQualityFollowUp(team: TeamState, closed: TeamTask, snapshotPaths?: (paths: readonly string[]) => ArtifactSnapshot, pathExists?: (path: string) => boolean): {
105
114
  created: TeamTask[];
106
115
  escalated: boolean;
116
+ reason?: string;
117
+ excludedFindings?: string[];
107
118
  };