wood-fired-tasks 2.1.0 → 2.2.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/CHANGELOG.md +75 -4
- package/README.md +3 -2
- package/SECURITY.md +2 -2
- package/dist/api/api-response.d.ts +4 -0
- package/dist/api/routes/models/index.d.ts +16 -0
- package/dist/api/routes/models/index.js +1 -7
- package/dist/api/routes/projects/resolve-model.js +17 -8
- package/dist/api/routes/projects/wsjf.js +4 -0
- package/dist/api/routes/settings/model-policy.js +8 -4
- package/dist/api/routes/tasks/schemas.d.ts +8 -0
- package/dist/api/routes/tasks/schemas.js +10 -0
- package/dist/api/server.d.ts +1 -1
- package/dist/api/server.js +6 -19
- package/dist/cli/api/client.d.ts +7 -7
- package/dist/cli/api/types.d.ts +6 -0
- package/dist/cli/auth/browser-open.js +0 -11
- package/dist/cli/auth/credentials.d.ts +19 -0
- package/dist/cli/auth/credentials.js +30 -0
- package/dist/cli/commands/comment-add.js +8 -1
- package/dist/cli/commands/create.js +9 -1
- package/dist/cli/commands/list.js +30 -7
- package/dist/cli/commands/login.d.ts +10 -0
- package/dist/cli/commands/login.js +2 -1
- package/dist/cli/commands/models.d.ts +38 -3
- package/dist/cli/commands/models.js +48 -4
- package/dist/cli/commands/project-set-models.js +6 -19
- package/dist/cli/commands/self-update.d.ts +16 -3
- package/dist/cli/commands/self-update.js +21 -0
- package/dist/cli/commands/settings-set-models.js +6 -19
- package/dist/cli/commands/setup.d.ts +8 -0
- package/dist/cli/commands/setup.js +1 -0
- package/dist/cli/commands/update.js +20 -0
- package/dist/events/event-bus.d.ts +2 -1
- package/dist/events/event-bus.js +1 -0
- package/dist/events/sse-manager.d.ts +30 -1
- package/dist/events/sse-manager.js +100 -18
- package/dist/events/types.d.ts +19 -2
- package/dist/events/types.js +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +36 -1
- package/dist/mcp/index.js +5 -24
- package/dist/mcp/lib/model-tool-definitions.d.ts +122 -0
- package/dist/mcp/lib/model-tool-definitions.js +112 -0
- package/dist/mcp/remote/register-tools.js +14 -73
- package/dist/mcp/remote/rest-client.d.ts +4 -10
- package/dist/mcp/resources/events.js +2 -1
- package/dist/mcp/tools/health-tools.d.ts +14 -0
- package/dist/mcp/tools/health-tools.js +52 -1
- package/dist/mcp/tools/model-tools.d.ts +4 -3
- package/dist/mcp/tools/model-tools.js +13 -82
- package/dist/mcp/tools/task-tools.js +5 -1
- package/dist/repositories/interfaces.d.ts +39 -0
- package/dist/repositories/project-charter-history.repository.js +2 -12
- package/dist/repositories/project.repository.js +16 -48
- package/dist/repositories/task.repository.d.ts +48 -0
- package/dist/repositories/task.repository.js +90 -18
- package/dist/repositories/wsjf-history.repository.js +6 -16
- package/dist/schemas/model-catalog.schema.d.ts +22 -0
- package/dist/schemas/model-catalog.schema.js +21 -0
- package/dist/schemas/model-policy.schema.d.ts +14 -0
- package/dist/schemas/model-policy.schema.js +11 -0
- package/dist/schemas/task.schema.d.ts +13 -1
- package/dist/schemas/task.schema.js +11 -0
- package/dist/services/claim-release.service.d.ts +11 -0
- package/dist/services/claim-release.service.js +31 -2
- package/dist/services/job-size-backfill.d.ts +43 -0
- package/dist/services/job-size-backfill.js +81 -0
- package/dist/services/model-catalog.service.d.ts +28 -8
- package/dist/services/model-catalog.service.js +16 -4
- package/dist/services/model-policy.service.d.ts +87 -9
- package/dist/services/model-policy.service.js +96 -4
- package/dist/services/settings.service.d.ts +3 -2
- package/dist/services/settings.service.js +30 -10
- package/dist/services/task.service.d.ts +87 -3
- package/dist/services/task.service.js +351 -16
- package/dist/services/wsjf-health.service.d.ts +7 -1
- package/dist/services/wsjf-health.service.js +27 -0
- package/dist/services/wsjf.service.d.ts +19 -0
- package/dist/services/wsjf.service.js +33 -0
- package/dist/skills/tasks/blocked.md +2 -1
- package/dist/skills/tasks/decompose.md +30 -4
- package/dist/skills/tasks/loop-dag.md +3 -1
- package/dist/skills/tasks/loop.md +1 -1
- package/dist/slack/notifier.js +1 -0
- package/dist/slack/task-formatter.js +1 -0
- package/dist/types/task.d.ts +12 -17
- package/dist/types/wsjf.d.ts +1 -1
- package/dist/types/wsjf.js +2 -0
- package/dist/utils/parse-json-column.d.ts +25 -0
- package/dist/utils/parse-json-column.js +39 -0
- package/docs/API.md +23 -10
- package/docs/ARCHITECTURE.md +16 -14
- package/docs/CLI.md +11 -12
- package/docs/INTERFACES.md +1 -1
- package/docs/MCP.md +105 -4
- package/docs/SETUP.md +7 -2
- package/docs/SLACK.md +1 -0
- package/package.json +1 -1
- package/packages/wft-router/README.md +12 -0
- package/packages/wft-router/dist/bin/wft-router.d.ts +10 -0
- package/packages/wft-router/dist/bin/wft-router.js +18 -0
- package/packages/wft-router/dist/config/event-types.d.ts +1 -1
- package/packages/wft-router/dist/config/event-types.js +1 -0
- package/packages/wft-router/dist/config/triggers-schema.d.ts +9 -0
- package/packages/wft-router/dist/config/triggers-schema.js +10 -0
- package/packages/wft-router/dist/daemon.d.ts +36 -0
- package/packages/wft-router/dist/daemon.js +101 -2
- package/packages/wft-router/dist/dispatch/startup-sweep.d.ts +96 -0
- package/packages/wft-router/dist/dispatch/startup-sweep.js +163 -0
- package/packages/wft-router/dist/index.d.ts +2 -2
- package/packages/wft-router/dist/index.js +1 -1
- package/packages/wft-router/dist/metrics.d.ts +64 -4
- package/packages/wft-router/dist/metrics.js +0 -0
- package/packages/wft-router/dist/sse/client.d.ts +19 -0
- package/packages/wft-router/dist/sse/client.js +57 -3
- package/packages/wft-router/dist/sse/index.d.ts +1 -1
- package/packages/wft-router/dist/sse/index.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,7 +11,75 @@ vulnerabilities, supply-chain pinning) are always called out under `Security`.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## [v2.2.0] - 2026-06-11
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Guaranteed task sizing.** Every task now carries a server-derived job size
|
|
18
|
+
so the Configurable-Task-Models `resolve_model` size routing (shipped in
|
|
19
|
+
v2.1.0) engages on the live backlog, while each task stays honestly
|
|
20
|
+
*unscored* for WSJF ranking: a deterministic `minutesToTier` mapping,
|
|
21
|
+
`auto_size` / `boot_sweep` history triggers, a server-internal size-only
|
|
22
|
+
write path, a `decomp-*` submission-contract gate, auto-sizing of WSJF-less
|
|
23
|
+
creates, a minutes-vs-jobSize conflict gate, recompute on `update_task` when
|
|
24
|
+
`estimated_minutes` changes (manual sizes are never clobbered), an idempotent
|
|
25
|
+
**boot sweep** that backfills NULL job sizes on every server start, and a
|
|
26
|
+
`wsjf_health` auto-sized-pending finding.
|
|
27
|
+
- **`wft list --status all`** explicit sentinel and a `statusFilter` echo in
|
|
28
|
+
`--json` output (task #1006). The default `wft list` view already returns
|
|
29
|
+
every status — open, in_progress, blocked, done, and closed — and the
|
|
30
|
+
`--help` text now states this plainly so machine consumers don't read a
|
|
31
|
+
status transition (e.g. open → blocked) as a deleted task. `--status all`
|
|
32
|
+
is accepted as a self-documenting way to ask for "every status", and the
|
|
33
|
+
JSON envelope's `metadata.statusFilter` names the effective filter
|
|
34
|
+
(`all` or the requested status). The default view is unchanged.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- **Configurable Task Models hardening** (PR-#55 review follow-ups, #928–#933):
|
|
38
|
+
`resolve_model` parity hardening for missing-project and foreign/nonexistent
|
|
39
|
+
`task_id`; single-sourced `PIPELINE_ROLES` / `FAMILY_LADDER` /
|
|
40
|
+
`DEFAULT_MODEL_MAP` with a code-level `resolveAuto`; the `modelPolicyService`
|
|
41
|
+
is now wired once in `createApp` with a memoized global policy and a prepared
|
|
42
|
+
resolver lookup; the model-catalog wire shape, model-tool definitions, and
|
|
43
|
+
JSON-column parser are single-sourced.
|
|
44
|
+
- **wft-router**: real-event age gauge, start time, by-kind split, debug
|
|
45
|
+
pings, and stale-subscription resubscribe; plus an opt-in cold-start sweep
|
|
46
|
+
that dispatches pre-existing open backlogs so a freshly started router does
|
|
47
|
+
not miss work created while it was down.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
- **SSE**: close the stream on every server-initiated eviction and align the
|
|
51
|
+
buffer TTL with the connection age window, so long-lived clients no longer
|
|
52
|
+
silently go deaf at the eviction boundary (#1001).
|
|
53
|
+
- **Claims**: same-assignee renewal, a `task.claim_released` event, and TTL
|
|
54
|
+
visibility — long-running workers no longer lose their claim without a signal
|
|
55
|
+
(#1003).
|
|
56
|
+
- **Atomic block-with-dependency** via `update_task blocked_by`: a `blocked`
|
|
57
|
+
status without a blocking edge is rejected rather than becoming a dead end
|
|
58
|
+
(#1004).
|
|
59
|
+
- **Non-interactive `tasks create` / `tasks comment-add` default attribution to
|
|
60
|
+
the logged-in identity** (task #1007). Scripted runs no longer fail with
|
|
61
|
+
"Missing required field: created-by" (or `author`) when `--created-by` /
|
|
62
|
+
`--author` is omitted but credentials are present — the value defaults to the
|
|
63
|
+
credentials display name (email fallback), the same identity `tasks whoami`
|
|
64
|
+
reports. The original error is preserved only when no identity can be resolved
|
|
65
|
+
(no credentials file).
|
|
66
|
+
- **wft-router**: spell the NUL label-separator as a `\u0000` escape rather than
|
|
67
|
+
a raw byte.
|
|
68
|
+
|
|
69
|
+
## [v2.1.1] - 2026-06-09
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
- **`tasks self-update` now re-syncs bundled skills/agents into `~/.claude`**
|
|
73
|
+
(PR #57, task #934). The command previously only ran
|
|
74
|
+
`npm i -g wood-fired-tasks@latest`, so a release that added or changed a
|
|
75
|
+
skill (first hit: v2.1.0's `/tasks:set-models`) left self-updaters with
|
|
76
|
+
stale `~/.claude/commands/tasks/` while reporting success — violating the
|
|
77
|
+
README's "keep it up to date" contract. After a clean install, self-update
|
|
78
|
+
now runs the same idempotent `copySkills()`/`copyAgents()` sync `tasks
|
|
79
|
+
setup` uses, reports what it refreshed, and exits non-zero with a
|
|
80
|
+
`tasks setup` remediation hint if the sync itself fails. A contract test
|
|
81
|
+
pins the default sync to setup's own implementation so the update and
|
|
82
|
+
onboarding paths cannot drift.
|
|
15
83
|
|
|
16
84
|
## [v2.1.0] - 2026-06-09
|
|
17
85
|
|
|
@@ -50,8 +118,9 @@ _No changes yet._
|
|
|
50
118
|
- **Test suite no longer launches the developer's real browser.**
|
|
51
119
|
`setup.remote.test.ts` drove the device flow with `openBrowser: true`, so
|
|
52
120
|
every `npm test` on a DISPLAY-set desktop spawned `xdg-open` at a mock
|
|
53
|
-
server. `
|
|
54
|
-
`
|
|
121
|
+
server. `runDeviceLogin` now takes an injectable `opener` seam (defaulting to
|
|
122
|
+
the real `openBrowser`); tests inject a stub instead of relying on a global
|
|
123
|
+
env flag that, if leaked, would silently disable browser login for real users.
|
|
55
124
|
- **Documentation counts drift.** README / `docs/INTERFACES.md` / `docs/MCP.md`
|
|
56
125
|
had stale tool (27 vs 31), route (52/45 vs 59/52), and CLI command (42 vs 45)
|
|
57
126
|
counts, plus a stale "model tools are stdio-only" claim from before remote
|
|
@@ -814,7 +883,9 @@ and the task/project/dependency/comment/subtask domain model.
|
|
|
814
883
|
- Task hierarchy (subtasks), dependency service, comments, time estimates
|
|
815
884
|
(phase 06).
|
|
816
885
|
|
|
817
|
-
[Unreleased]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v2.
|
|
886
|
+
[Unreleased]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v2.1.1...HEAD
|
|
887
|
+
[v2.1.1]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v2.1.0...v2.1.1
|
|
888
|
+
[v2.1.0]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v2.0.6...v2.1.0
|
|
818
889
|
[v2.0.0]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v1.18.2...v2.0.0
|
|
819
890
|
[v1.15]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v1.14...v1.15
|
|
820
891
|
[v1.14]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v1.13...v1.14
|
package/README.md
CHANGED
|
@@ -143,7 +143,7 @@ user scope.
|
|
|
143
143
|
|
|
144
144
|
```bash
|
|
145
145
|
wood-fired-tasks service install # Linux: user-scoped systemd unit (admin-free)
|
|
146
|
-
wood-fired-tasks self-update # npm i -g
|
|
146
|
+
wood-fired-tasks self-update # npm i -g @latest + re-sync skills (no sudo)
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
**Point at a shared remote server** (the *Remote client* mode above) with
|
|
@@ -658,7 +658,8 @@ Wood Fired Tasks streams real-time task and project change notifications via Ser
|
|
|
658
658
|
| task.updated | Task fields modified |
|
|
659
659
|
| task.deleted | Task deleted |
|
|
660
660
|
| task.status_changed | Task status transition |
|
|
661
|
-
| task.claimed | Task claimed by agent |
|
|
661
|
+
| task.claimed | Task claimed by agent (also emitted on a same-assignee claim renewal) |
|
|
662
|
+
| task.claim_released | Stale claim auto-released by the TTL sweep (carries `previous_assignee`, `expired_claimed_at`, `released_at`) |
|
|
662
663
|
| project.created | New project created |
|
|
663
664
|
| project.updated | Project modified |
|
|
664
665
|
| project.deleted | Project deleted |
|
package/SECURITY.md
CHANGED
|
@@ -14,11 +14,11 @@ security updates. Older tags are provided as-is.
|
|
|
14
14
|
| Version | Supported |
|
|
15
15
|
| ----------------- | ------------------ |
|
|
16
16
|
| `main` (HEAD) | :white_check_mark: |
|
|
17
|
-
| `v2.
|
|
17
|
+
| `v2.2.0` (latest) | :white_check_mark: |
|
|
18
18
|
| `v1.0` – `v2.0.6` | :x: |
|
|
19
19
|
|
|
20
20
|
"Latest" tracks whichever tag is most recent on GitHub; at the time of
|
|
21
|
-
writing that is `v2.
|
|
21
|
+
writing that is `v2.2.0`. If you are reading this on an older checkout,
|
|
22
22
|
verify the current latest release via
|
|
23
23
|
`git tag --sort=-creatordate | head -1` or the GitHub Releases page.
|
|
24
24
|
|
|
@@ -94,6 +94,8 @@ export declare function parseTaskResponse(payload: unknown, endpoint: string): {
|
|
|
94
94
|
verifier_request_id?: string | undefined;
|
|
95
95
|
verified_at?: string | undefined;
|
|
96
96
|
} | null;
|
|
97
|
+
claim_ttl_minutes?: number | undefined;
|
|
98
|
+
claim_remaining_seconds?: number | undefined;
|
|
97
99
|
};
|
|
98
100
|
/** Convenience: validate a single project response body. */
|
|
99
101
|
export declare function parseProjectResponse(payload: unknown, endpoint: string): {
|
|
@@ -186,6 +188,8 @@ export declare function parseTaskListResponse(payload: unknown, endpoint: string
|
|
|
186
188
|
verifier_request_id?: string | undefined;
|
|
187
189
|
verified_at?: string | undefined;
|
|
188
190
|
} | null;
|
|
191
|
+
claim_ttl_minutes?: number | undefined;
|
|
192
|
+
claim_remaining_seconds?: number | undefined;
|
|
189
193
|
}[];
|
|
190
194
|
total: number;
|
|
191
195
|
limit: number;
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* Configurable Task Models (Task 13) — GET /api/v1/models
|
|
5
|
+
*
|
|
6
|
+
* Exposes the runtime-discovered Claude model catalog (task #917's
|
|
7
|
+
* `model-catalog.service`) over REST so the remote MCP proxy / dashboard /
|
|
8
|
+
* `set-models` interview can enumerate the available models. The 200 body is
|
|
9
|
+
* `{ models, stale }` — identical to the service's `ModelCatalog` and the
|
|
10
|
+
* stdio MCP `list_models` tool's structured output.
|
|
11
|
+
*
|
|
12
|
+
* `stale: true` signals the catalog was served from the static fallback (no
|
|
13
|
+
* ANTHROPIC_API_KEY, non-OK Models API response, or a network error). The
|
|
14
|
+
* service NEVER throws from `list()`, so this route always returns 200.
|
|
15
|
+
*
|
|
16
|
+
* Auth: inherits the standard `/api/v1` auth chain (the parent plugin is
|
|
17
|
+
* mounted inside the `/api/v1` scope that wires `authPlugin`). No custom guard.
|
|
18
|
+
*/
|
|
3
19
|
/** The `{ models, stale }` envelope returned by GET /models. */
|
|
4
20
|
export declare const ModelCatalogResponseSchema: z.ZodObject<{
|
|
5
21
|
models: z.ZodArray<z.ZodObject<{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { ModelCatalogEntrySchema } from '../../../schemas/model-catalog.schema.js';
|
|
2
3
|
/**
|
|
3
4
|
* Configurable Task Models (Task 13) — GET /api/v1/models
|
|
4
5
|
*
|
|
@@ -15,13 +16,6 @@ import { z } from 'zod';
|
|
|
15
16
|
* Auth: inherits the standard `/api/v1` auth chain (the parent plugin is
|
|
16
17
|
* mounted inside the `/api/v1` scope that wires `authPlugin`). No custom guard.
|
|
17
18
|
*/
|
|
18
|
-
/** One discovered model, mirroring `ModelCatalogEntry` from the catalog service. */
|
|
19
|
-
const ModelCatalogEntrySchema = z.object({
|
|
20
|
-
id: z.string(),
|
|
21
|
-
display_name: z.string(),
|
|
22
|
-
family: z.string(),
|
|
23
|
-
created_at: z.string(),
|
|
24
|
-
});
|
|
25
19
|
/** The `{ models, stale }` envelope returned by GET /models. */
|
|
26
20
|
export const ModelCatalogResponseSchema = z.object({
|
|
27
21
|
models: z.array(ModelCatalogEntrySchema),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ErrorResponseSchema } from '../tasks/schemas.js';
|
|
3
|
+
import { PipelineRoleSchema } from '../../../schemas/model-policy.schema.js';
|
|
3
4
|
/**
|
|
4
5
|
* Configurable Task Models (Task #926) — GET /api/v1/projects/:id/resolve-model
|
|
5
6
|
*
|
|
@@ -21,10 +22,15 @@ import { ErrorResponseSchema } from '../tasks/schemas.js';
|
|
|
21
22
|
* wires in-process (project policy ?? global default, per-slot merge, jobSize→
|
|
22
23
|
* category routing when `task_id` is supplied).
|
|
23
24
|
*
|
|
24
|
-
* Project-existence is enforced
|
|
25
|
-
* ProblemDetails (mirrors the sibling
|
|
26
|
-
* routes)
|
|
27
|
-
* default (or null) silently
|
|
25
|
+
* Project-existence is enforced by the resolver itself (task #928): a missing
|
|
26
|
+
* project throws NotFoundError → 404 ProblemDetails (mirrors the sibling
|
|
27
|
+
* `/:id/topology` and `/:id/dependency-graph` routes), so an unknown project
|
|
28
|
+
* never resolves to the global default (or null) silently. The route carries
|
|
29
|
+
* no separate pre-fetch guard (task #931): the resolver's single project
|
|
30
|
+
* fetch doubles as the 404 check. The resolver also validates `task_id`: a
|
|
31
|
+
* nonexistent one throws NotFoundError (→ 404) and one belonging to a
|
|
32
|
+
* different project throws ValidationError (→ 400), so size-routing can never
|
|
33
|
+
* silently use a foreign task's jobSize.
|
|
28
34
|
*
|
|
29
35
|
* Auth: inherits the standard projects-route auth chain (the parent
|
|
30
36
|
* `projectRoutes` plugin is mounted inside the `/api/v1` scope that wires
|
|
@@ -43,18 +49,21 @@ const resolveModelRoutes = async (fastify) => {
|
|
|
43
49
|
'MCP tool output. Read-only.',
|
|
44
50
|
params: z.object({ id: z.coerce.number().int().positive() }),
|
|
45
51
|
querystring: z.object({
|
|
46
|
-
role:
|
|
52
|
+
role: PipelineRoleSchema,
|
|
47
53
|
task_id: z.coerce.number().int().positive().optional(),
|
|
48
54
|
}),
|
|
49
55
|
response: {
|
|
50
56
|
200: ResolveModelResponseSchema,
|
|
57
|
+
400: ErrorResponseSchema,
|
|
51
58
|
404: ErrorResponseSchema,
|
|
52
59
|
},
|
|
53
60
|
},
|
|
54
61
|
}, async (request, reply) => {
|
|
55
|
-
// Existence guard → 404 on a missing project
|
|
56
|
-
// NotFoundError
|
|
57
|
-
|
|
62
|
+
// Existence guard → 404 on a missing project: since task #928 the
|
|
63
|
+
// resolver itself throws NotFoundError (mapped to the 404
|
|
64
|
+
// ProblemDetails by the error handler), so the former
|
|
65
|
+
// `projectService.getProject` pre-fetch here was a redundant second
|
|
66
|
+
// fetch + full inflation of the same row (task #931 — one shared fetch).
|
|
58
67
|
const resolved = fastify.modelPolicyService.resolveModel(request.params.id, request.query.role, request.query.task_id);
|
|
59
68
|
// `resolved` is `{ model } | { model: 'auto' } | null` — sent verbatim so
|
|
60
69
|
// the remote MCP tool is transport-indistinguishable from the stdio one.
|
|
@@ -104,6 +104,10 @@ const HealthFindingSchema = z.object({
|
|
|
104
104
|
'stale-time-criticality',
|
|
105
105
|
'high-fallback-ratio',
|
|
106
106
|
'score-churn',
|
|
107
|
+
'auto-sized-pending',
|
|
108
|
+
// Task #1004: emitted by check_health (edge-less blocked lint); part of
|
|
109
|
+
// the shared HealthCheckId union, never produced by the WSJF linter.
|
|
110
|
+
'blocked-without-edge',
|
|
107
111
|
]),
|
|
108
112
|
severity: z.enum(['info', 'warning', 'critical']),
|
|
109
113
|
message: z.string(),
|
|
@@ -48,10 +48,14 @@ const modelPolicyRoutes = async (fastify) => {
|
|
|
48
48
|
// the clear path persists SQL NULL rather than tripping the service's
|
|
49
49
|
// non-null Zod parse. A non-null body has already passed the
|
|
50
50
|
// `ModelPolicyNullableSchema` body validator (invalid → 400 upstream).
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
51
|
+
const policy = request.body ?? null;
|
|
52
|
+
fastify.settingsService.setModelPolicyDefault(policy);
|
|
53
|
+
// Echo the validated body back. `request.body` has already been parsed
|
|
54
|
+
// through `ModelPolicyNullableSchema` by the route's body validator —
|
|
55
|
+
// the SAME canonical shape the service just persisted — so re-reading +
|
|
56
|
+
// re-validating the singleton row here was a redundant round-trip
|
|
57
|
+
// (task #931). A write failure throws before this line.
|
|
58
|
+
return reply.send(policy);
|
|
55
59
|
});
|
|
56
60
|
};
|
|
57
61
|
export default modelPolicyRoutes;
|
|
@@ -31,6 +31,8 @@ export declare const TaskResponseSchema: z.ZodObject<{
|
|
|
31
31
|
updated_at: z.ZodString;
|
|
32
32
|
version: z.ZodNumber;
|
|
33
33
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
34
|
+
claim_ttl_minutes: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
claim_remaining_seconds: z.ZodOptional<z.ZodNumber>;
|
|
34
36
|
tags: z.ZodArray<z.ZodString>;
|
|
35
37
|
acceptance_criteria: z.ZodNullable<z.ZodString>;
|
|
36
38
|
verification_evidence: z.ZodNullable<z.ZodObject<{
|
|
@@ -88,6 +90,8 @@ export declare const TaskListResponseSchema: z.ZodArray<z.ZodObject<{
|
|
|
88
90
|
updated_at: z.ZodString;
|
|
89
91
|
version: z.ZodNumber;
|
|
90
92
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
93
|
+
claim_ttl_minutes: z.ZodOptional<z.ZodNumber>;
|
|
94
|
+
claim_remaining_seconds: z.ZodOptional<z.ZodNumber>;
|
|
91
95
|
tags: z.ZodArray<z.ZodString>;
|
|
92
96
|
acceptance_criteria: z.ZodNullable<z.ZodString>;
|
|
93
97
|
verification_evidence: z.ZodNullable<z.ZodObject<{
|
|
@@ -145,6 +149,8 @@ export declare const TaskListPaginatedResponseSchema: z.ZodObject<{
|
|
|
145
149
|
updated_at: z.ZodString;
|
|
146
150
|
version: z.ZodNumber;
|
|
147
151
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
152
|
+
claim_ttl_minutes: z.ZodOptional<z.ZodNumber>;
|
|
153
|
+
claim_remaining_seconds: z.ZodOptional<z.ZodNumber>;
|
|
148
154
|
tags: z.ZodArray<z.ZodString>;
|
|
149
155
|
acceptance_criteria: z.ZodNullable<z.ZodString>;
|
|
150
156
|
verification_evidence: z.ZodNullable<z.ZodObject<{
|
|
@@ -210,6 +216,8 @@ export declare const ClaimResponseSchema: z.ZodObject<{
|
|
|
210
216
|
updated_at: z.ZodString;
|
|
211
217
|
version: z.ZodNumber;
|
|
212
218
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
219
|
+
claim_ttl_minutes: z.ZodOptional<z.ZodNumber>;
|
|
220
|
+
claim_remaining_seconds: z.ZodOptional<z.ZodNumber>;
|
|
213
221
|
tags: z.ZodArray<z.ZodString>;
|
|
214
222
|
acceptance_criteria: z.ZodNullable<z.ZodString>;
|
|
215
223
|
verification_evidence: z.ZodNullable<z.ZodObject<{
|
|
@@ -21,6 +21,16 @@ export const TaskResponseSchema = z.object({
|
|
|
21
21
|
updated_at: z.string(),
|
|
22
22
|
version: z.number(),
|
|
23
23
|
claimed_at: z.string().nullable(),
|
|
24
|
+
/**
|
|
25
|
+
* Task #1003: claim-TTL visibility. Computed at read time by
|
|
26
|
+
* `TaskService.getTask` for tasks holding an active claim (in_progress +
|
|
27
|
+
* assignee + claimed_at); absent otherwise. `claim_remaining_seconds` is
|
|
28
|
+
* the floor-clamped time until the ClaimReleaseService sweep may
|
|
29
|
+
* auto-release the claim — renew with a same-assignee `claim` call
|
|
30
|
+
* before it reaches 0.
|
|
31
|
+
*/
|
|
32
|
+
claim_ttl_minutes: z.number().int().positive().optional(),
|
|
33
|
+
claim_remaining_seconds: z.number().int().nonnegative().optional(),
|
|
24
34
|
tags: z.array(z.string()),
|
|
25
35
|
/**
|
|
26
36
|
* Wave 1.3 (task #311): optional free-form acceptance criteria (markdown).
|
package/dist/api/server.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { TopologyService } from '../services/topology.service.js';
|
|
|
9
9
|
import { CommentService } from '../services/comment.service.js';
|
|
10
10
|
import { SettingsService } from '../services/settings.service.js';
|
|
11
11
|
import { ModelCatalogService } from '../services/model-catalog.service.js';
|
|
12
|
-
import {
|
|
12
|
+
import type { ModelPolicyService } from '../services/model-policy.service.js';
|
|
13
13
|
import { SSEManager } from '../events/sse-manager.js';
|
|
14
14
|
import { IdempotencyService } from '../services/idempotency.service.js';
|
|
15
15
|
/**
|
package/dist/api/server.js
CHANGED
|
@@ -10,9 +10,6 @@ import fastifyFormbody from '@fastify/formbody';
|
|
|
10
10
|
import { createApp } from '../index.js';
|
|
11
11
|
import { config } from '../config/env.js';
|
|
12
12
|
import { SESSION_LIFETIME_SECONDS } from '../web/session-constants.js';
|
|
13
|
-
import { createModelPolicyService, } from '../services/model-policy.service.js';
|
|
14
|
-
import { ProjectRepository } from '../repositories/project.repository.js';
|
|
15
|
-
import { TaskRepository } from '../repositories/task.repository.js';
|
|
16
13
|
import { SSEManager } from '../events/sse-manager.js';
|
|
17
14
|
import { IdempotencyService } from '../services/idempotency.service.js';
|
|
18
15
|
import { ClaimReleaseService } from '../services/claim-release.service.js';
|
|
@@ -109,22 +106,11 @@ export async function createServer(options) {
|
|
|
109
106
|
// back the /settings/model-policy and /models routes respectively.
|
|
110
107
|
server.decorate('settingsService', app.settingsService);
|
|
111
108
|
server.decorate('modelCatalogService', app.modelCatalogService);
|
|
112
|
-
// Configurable Task Models (Task #926):
|
|
113
|
-
// behind GET /projects/:id/resolve-model.
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
// - getProjectPolicy ← a project's parsed `model_policy` column.
|
|
118
|
-
// - getGlobalPolicy ← the SAME settings service backing get/set defaults.
|
|
119
|
-
// - getJobSize ← a task's WSJF jobSize Fibonacci tier.
|
|
120
|
-
const modelPolicyProjectRepo = new ProjectRepository(app.db);
|
|
121
|
-
const modelPolicyTaskRepo = new TaskRepository(app.db);
|
|
122
|
-
const modelPolicyService = createModelPolicyService({
|
|
123
|
-
getProjectPolicy: (projectId) => modelPolicyProjectRepo.findById(projectId)?.model_policy ?? null,
|
|
124
|
-
getGlobalPolicy: () => app.settingsService.getModelPolicyDefault(),
|
|
125
|
-
getJobSize: (taskId) => modelPolicyTaskRepo.findById(taskId)?.wsjf_job_size ?? null,
|
|
126
|
-
});
|
|
127
|
-
server.decorate('modelPolicyService', modelPolicyService);
|
|
109
|
+
// Configurable Task Models (Task #926 / #931): the model-policy resolver
|
|
110
|
+
// behind GET /projects/:id/resolve-model. Constructed ONCE in `createApp`
|
|
111
|
+
// (same instance the stdio MCP server wires behind its `resolve_model`
|
|
112
|
+
// tool) — see src/index.ts for the dep wiring.
|
|
113
|
+
server.decorate('modelPolicyService', app.modelPolicyService);
|
|
128
114
|
server.decorate('db', app.db);
|
|
129
115
|
// Task #357: expose OIDC boot state to /health/detailed so a degraded
|
|
130
116
|
// discovery is a queryable signal, not just a one-time boot log line.
|
|
@@ -170,6 +156,7 @@ export async function createServer(options) {
|
|
|
170
156
|
eventBus.subscribe('task.deleted', (event) => sseManager.broadcast(event)),
|
|
171
157
|
eventBus.subscribe('task.status_changed', (event) => sseManager.broadcast(event)),
|
|
172
158
|
eventBus.subscribe('task.claimed', (event) => sseManager.broadcast(event)),
|
|
159
|
+
eventBus.subscribe('task.claim_released', (event) => sseManager.broadcast(event)),
|
|
173
160
|
eventBus.subscribe('project.created', (event) => sseManager.broadcast(event)),
|
|
174
161
|
eventBus.subscribe('project.updated', (event) => sseManager.broadcast(event)),
|
|
175
162
|
eventBus.subscribe('project.deleted', (event) => sseManager.broadcast(event)),
|
package/dist/cli/api/client.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TaskResponse, CreateTaskInput, UpdateTaskInput, TaskFilters, ApiErrorResponse, ProjectResponse, CreateProjectInput, UpdateProjectInput, DependencyResponse, DependencyListResponse, CreateDependencyInput, CommentResponse, CreateCommentInput, HealthResponse, PaginatedResponse, PaginationParams } from './types.js';
|
|
2
2
|
import type { ModelPolicyNullable } from '../../schemas/model-policy.schema.js';
|
|
3
|
+
import type { ModelCatalogEntry } from '../../schemas/model-catalog.schema.js';
|
|
3
4
|
/**
|
|
4
5
|
* Custom error class for API client errors.
|
|
5
6
|
* Includes HTTP status code, parsed API error response, and optional request ID for tracing.
|
|
@@ -115,13 +116,12 @@ export declare function getSubtasksPaginated(parentTaskId: number, pagination?:
|
|
|
115
116
|
* Sets assignee and transitions status to in_progress in a single operation.
|
|
116
117
|
*/
|
|
117
118
|
export declare function claimTask(taskId: number, assignee: string, idempotencyKey?: string): Promise<TaskResponse>;
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
119
|
+
/**
|
|
120
|
+
* One discovered catalog model — the server's `GET /models` row (task #930:
|
|
121
|
+
* the shape is declared once in `src/schemas/model-catalog.schema.ts` and
|
|
122
|
+
* re-exported here for CLI consumers).
|
|
123
|
+
*/
|
|
124
|
+
export type { ModelCatalogEntry };
|
|
125
125
|
/** The `{ models, stale }` envelope returned by `GET /models`. */
|
|
126
126
|
export interface ModelCatalogResponse {
|
|
127
127
|
models: ModelCatalogEntry[];
|
package/dist/cli/api/types.d.ts
CHANGED
|
@@ -53,6 +53,12 @@ export interface UpdateTaskInput {
|
|
|
53
53
|
tags?: string[];
|
|
54
54
|
/** Wave 1.3 (#311): patch acceptance criteria; null clears, string sets. */
|
|
55
55
|
acceptance_criteria?: string | null;
|
|
56
|
+
/**
|
|
57
|
+
* Task #1004: atomic block-with-dependency. Only valid alongside
|
|
58
|
+
* `status: 'blocked'` — the server adds the blocking edge(s) and sets the
|
|
59
|
+
* status in one transaction (an invalid edge rolls back the whole call).
|
|
60
|
+
*/
|
|
61
|
+
blocked_by?: number[];
|
|
56
62
|
}
|
|
57
63
|
export interface TaskFilters {
|
|
58
64
|
project_id?: number;
|
|
@@ -69,17 +69,6 @@ function isSafeBrowserUrl(url) {
|
|
|
69
69
|
return true;
|
|
70
70
|
}
|
|
71
71
|
export function openBrowser(url) {
|
|
72
|
-
// Test/automation guard. When WFT_NO_BROWSER is set we never spawn a real
|
|
73
|
-
// browser — the caller still prints the URL as its fallback. The linux
|
|
74
|
-
// `!DISPLAY` branch below already covers headless CI, but it does NOT cover a
|
|
75
|
-
// developer DESKTOP running the suite (DISPLAY is set there): an in-process
|
|
76
|
-
// device-flow/login test (e.g. setup.remote.test.ts's "real device flow"
|
|
77
|
-
// case) would otherwise spawn the developer's actual browser at the mock
|
|
78
|
-
// server's verification_uri and 404. vitest.setup.ts sets this for every test
|
|
79
|
-
// file; browser-open.test.ts opts out locally to exercise the real spawn path.
|
|
80
|
-
if (process.env['WFT_NO_BROWSER']) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
72
|
// WR-03 (Phase 30 review) — validate the URL BEFORE selecting the
|
|
84
73
|
// platform spawn args. A malformed/suspicious URL → false so the caller
|
|
85
74
|
// falls back to printing the URL for the user to paste manually.
|
|
@@ -21,4 +21,23 @@ export declare function getCredentialsPath(): string;
|
|
|
21
21
|
export declare function readCredentials(filePath?: string): Credentials | null;
|
|
22
22
|
export declare function writeCredentials(creds: Credentials, filePath?: string): void;
|
|
23
23
|
export declare function deleteCredentials(filePath?: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the logged-in identity's display name for attribution defaults
|
|
26
|
+
* (e.g. `tasks create --created-by`, `tasks comment-add --author`).
|
|
27
|
+
*
|
|
28
|
+
* Source of truth is the on-disk credentials file (`readCredentials`) — the
|
|
29
|
+
* SAME file `tasks whoami` reads. We prefer `display_name`; if it is empty
|
|
30
|
+
* (service accounts may have a blank display name) we fall back to `email`.
|
|
31
|
+
*
|
|
32
|
+
* Returns `null` when no identity can be resolved (no credentials file, or
|
|
33
|
+
* a credentials file with neither a display name nor an email). Callers then
|
|
34
|
+
* fall back to their existing "missing required field" behaviour so scripted
|
|
35
|
+
* runs without any identity still surface an actionable error.
|
|
36
|
+
*
|
|
37
|
+
* NOTE: this is intentionally a thin, synchronous read of the local file —
|
|
38
|
+
* it does NOT hit the network. The credentials file is written by
|
|
39
|
+
* `tasks login` from the server's identity envelope, so the cached
|
|
40
|
+
* display_name/email already reflect the authenticated user.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveIdentityName(filePath?: string): string | null;
|
|
24
43
|
export declare function resolveAuth(): Promise<AuthSource>;
|
|
@@ -154,6 +154,36 @@ export function deleteCredentials(filePath = getCredentialsPath()) {
|
|
|
154
154
|
unlinkSync(filePath);
|
|
155
155
|
return true;
|
|
156
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Resolve the logged-in identity's display name for attribution defaults
|
|
159
|
+
* (e.g. `tasks create --created-by`, `tasks comment-add --author`).
|
|
160
|
+
*
|
|
161
|
+
* Source of truth is the on-disk credentials file (`readCredentials`) — the
|
|
162
|
+
* SAME file `tasks whoami` reads. We prefer `display_name`; if it is empty
|
|
163
|
+
* (service accounts may have a blank display name) we fall back to `email`.
|
|
164
|
+
*
|
|
165
|
+
* Returns `null` when no identity can be resolved (no credentials file, or
|
|
166
|
+
* a credentials file with neither a display name nor an email). Callers then
|
|
167
|
+
* fall back to their existing "missing required field" behaviour so scripted
|
|
168
|
+
* runs without any identity still surface an actionable error.
|
|
169
|
+
*
|
|
170
|
+
* NOTE: this is intentionally a thin, synchronous read of the local file —
|
|
171
|
+
* it does NOT hit the network. The credentials file is written by
|
|
172
|
+
* `tasks login` from the server's identity envelope, so the cached
|
|
173
|
+
* display_name/email already reflect the authenticated user.
|
|
174
|
+
*/
|
|
175
|
+
export function resolveIdentityName(filePath = getCredentialsPath()) {
|
|
176
|
+
const creds = readCredentials(filePath);
|
|
177
|
+
if (creds === null)
|
|
178
|
+
return null;
|
|
179
|
+
const { display_name, email } = creds.active;
|
|
180
|
+
const name = display_name.trim();
|
|
181
|
+
if (name.length > 0)
|
|
182
|
+
return name;
|
|
183
|
+
if (email && email.trim().length > 0)
|
|
184
|
+
return email.trim();
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
157
187
|
export async function resolveAuth() {
|
|
158
188
|
// 1. --token flag wins unconditionally.
|
|
159
189
|
if (currentTokenOverride !== null) {
|
|
@@ -4,6 +4,7 @@ import { colorError, colorSuccess } from '../output/formatters.js';
|
|
|
4
4
|
import { handleError } from '../output/error-handler.js';
|
|
5
5
|
import { jsonOutput } from '../output/json-output.js';
|
|
6
6
|
import { promptForMissing } from '../prompts/interactive.js';
|
|
7
|
+
import { resolveIdentityName } from '../auth/credentials.js';
|
|
7
8
|
export const commentAddCommand = new Command('comment-add')
|
|
8
9
|
.description('Add a comment to a task')
|
|
9
10
|
.argument('<id>', 'Task ID')
|
|
@@ -22,8 +23,14 @@ export const commentAddCommand = new Command('comment-add')
|
|
|
22
23
|
const program = commentAddCommand.parent;
|
|
23
24
|
const globalOpts = program?.optsWithGlobals() || {};
|
|
24
25
|
const isJsonMode = globalOpts['json'] || false;
|
|
26
|
+
// When --author is omitted, default to the logged-in identity
|
|
27
|
+
// (credentials PAT user) so scripted, non-interactive runs no longer
|
|
28
|
+
// have to hardcode an author (papercut #1007). promptForMissing then
|
|
29
|
+
// short-circuits — interactive prompting / the "Missing required field:
|
|
30
|
+
// author" error remain ONLY when no identity can be resolved.
|
|
31
|
+
const authorOverride = options.author ?? resolveIdentityName() ?? undefined;
|
|
25
32
|
// Prompt for missing required fields
|
|
26
|
-
const author = await promptForMissing('author',
|
|
33
|
+
const author = await promptForMissing('author', authorOverride);
|
|
27
34
|
const content = await promptForMissing('content', options.content);
|
|
28
35
|
// Add comment via API
|
|
29
36
|
const comment = await addComment(id, {
|
|
@@ -4,6 +4,7 @@ import { formatTaskDetail, colorSuccess, colorError } from '../output/formatters
|
|
|
4
4
|
import { handleError } from '../output/error-handler.js';
|
|
5
5
|
import { jsonOutput } from '../output/json-output.js';
|
|
6
6
|
import { promptForMissing } from '../prompts/interactive.js';
|
|
7
|
+
import { resolveIdentityName } from '../auth/credentials.js';
|
|
7
8
|
const VALID_PRIORITIES = ['low', 'medium', 'high', 'urgent'];
|
|
8
9
|
export const createCommand = new Command('create')
|
|
9
10
|
.description('Create a new task')
|
|
@@ -27,10 +28,17 @@ export const createCommand = new Command('create')
|
|
|
27
28
|
const program = createCommand.parent;
|
|
28
29
|
const globalOpts = program?.optsWithGlobals() || {};
|
|
29
30
|
const isJsonMode = globalOpts['json'] || false;
|
|
31
|
+
// When --created-by is omitted, default to the logged-in identity
|
|
32
|
+
// (credentials PAT user) so scripted, non-interactive runs no longer
|
|
33
|
+
// have to hardcode an email (papercut #1007). promptForMissing then
|
|
34
|
+
// short-circuits (value is defined) — interactive prompting and the
|
|
35
|
+
// "Missing required field: created-by" error remain ONLY when no
|
|
36
|
+
// identity can be resolved (no credentials).
|
|
37
|
+
const createdByOverride = options.createdBy ?? resolveIdentityName() ?? undefined;
|
|
30
38
|
// Prompt for missing required fields (interactive mode only)
|
|
31
39
|
const title = await promptForMissing('title', options.title);
|
|
32
40
|
const projectStr = await promptForMissing('project', options.project);
|
|
33
|
-
const createdBy = await promptForMissing('created-by',
|
|
41
|
+
const createdBy = await promptForMissing('created-by', createdByOverride);
|
|
34
42
|
// Parse and validate project ID
|
|
35
43
|
const project = typeof projectStr === 'number' ? projectStr : parseInt(projectStr, 10);
|
|
36
44
|
if (isNaN(project)) {
|