@ianwremmel/dispatch 0.32.1-bootstrap.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/.claude-plugin/plugin.json +59 -0
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/agents/.gitkeep +0 -0
- package/agents/build-graph.md +99 -0
- package/agents/milestone-reviewer.md +50 -0
- package/agents/pr-worker.md +172 -0
- package/agents/ticket-worker.md +97 -0
- package/bin/dispatch +101 -0
- package/bin/dispatch-mcp +19 -0
- package/bin/pr-status +931 -0
- package/commands/.gitkeep +0 -0
- package/commands/orchestrate.md +6 -0
- package/hooks/.gitkeep +0 -0
- package/hooks/claim-guard.mts +98 -0
- package/hooks/hooks.json +15 -0
- package/package.json +46 -0
- package/skills/.gitkeep +0 -0
- package/skills/land/SKILL.md +238 -0
- package/skills/land/credentials-dedicated.md +33 -0
- package/skills/land/credentials-shared.md +76 -0
- package/skills/land/mode-solo.md +76 -0
- package/skills/land/mode-team.md +103 -0
- package/skills/land/reference.md +152 -0
- package/skills/land/ticket.md +94 -0
- package/skills/orchestrate/SKILL.md +87 -0
- package/skills/tracker-adapter-linear/SKILL.md +142 -0
- package/src/commands/CLAUDE.md +12 -0
- package/src/commands/claim/check.mts +88 -0
- package/src/commands/claim/guard.mts +95 -0
- package/src/commands/claim/status.mts +49 -0
- package/src/commands/edge/add.mts +44 -0
- package/src/commands/edge/rm.mts +44 -0
- package/src/commands/edge/set.mts +56 -0
- package/src/commands/greet.mts +34 -0
- package/src/commands/mcp/ack.mts +43 -0
- package/src/commands/mcp/ping.mts +61 -0
- package/src/commands/mcp/status.mts +89 -0
- package/src/commands/mcp.mts +155 -0
- package/src/commands/milestone/rm.mts +35 -0
- package/src/commands/milestone/set.mts +49 -0
- package/src/commands/outcome/rm.mts +36 -0
- package/src/commands/outcome/set.mts +86 -0
- package/src/commands/pr/rm.mts +33 -0
- package/src/commands/pr/set.mts +110 -0
- package/src/commands/pr/yield.mts +114 -0
- package/src/commands/project/rm.mts +33 -0
- package/src/commands/project/set.mts +50 -0
- package/src/commands/queue.mts +41 -0
- package/src/commands/refresh/done.mts +42 -0
- package/src/commands/refresh/status.mts +40 -0
- package/src/commands/refresh.mts +56 -0
- package/src/commands/review/record.mts +46 -0
- package/src/commands/review/release.mts +49 -0
- package/src/commands/status.mts +85 -0
- package/src/commands/ticket/missing.mts +31 -0
- package/src/commands/ticket/rm.mts +33 -0
- package/src/commands/ticket/set.mts +134 -0
- package/src/commands/worker/rm.mts +46 -0
- package/src/commands/worker/set.mts +63 -0
- package/src/lib/cli/CLAUDE.md +13 -0
- package/src/lib/cli/cli.mts +226 -0
- package/src/lib/cli/index.mts +1 -0
- package/src/lib/command/CLAUDE.md +26 -0
- package/src/lib/command/__fixtures__/bad-export/oops.mts +1 -0
- package/src/lib/command/__fixtures__/bad-name/mismatch.mts +19 -0
- package/src/lib/command/__fixtures__/commands/cli-only.mts +20 -0
- package/src/lib/command/__fixtures__/commands/greet.mts +39 -0
- package/src/lib/command/__fixtures__/commands/math/add.mts +32 -0
- package/src/lib/command/__fixtures__/commands/mcp-only.mts +20 -0
- package/src/lib/command/__fixtures__/commands/needs-token.mts +19 -0
- package/src/lib/command/__fixtures__/commands/store/get.mts +26 -0
- package/src/lib/command/__fixtures__/commands/store.mts +26 -0
- package/src/lib/command/abstract-command.mts +104 -0
- package/src/lib/command/discovery.mts +100 -0
- package/src/lib/command/env.mts +19 -0
- package/src/lib/command/index.mts +6 -0
- package/src/lib/command/parse.mts +64 -0
- package/src/lib/command/test-support.mts +81 -0
- package/src/lib/command/transports.mts +17 -0
- package/src/lib/command/types.mts +53 -0
- package/src/lib/db/CLAUDE.md +13 -0
- package/src/lib/db/database.mts +160 -0
- package/src/lib/db/index.mts +4 -0
- package/src/lib/db/schema.mts +195 -0
- package/src/lib/db/time.mts +24 -0
- package/src/lib/db/with-database.mts +56 -0
- package/src/lib/errors/CLAUDE.md +18 -0
- package/src/lib/errors/command-error.mts +13 -0
- package/src/lib/errors/data-error.mts +12 -0
- package/src/lib/errors/definition-error.mts +6 -0
- package/src/lib/errors/dispatch-error.mts +27 -0
- package/src/lib/errors/ensure.mts +22 -0
- package/src/lib/errors/environment-error.mts +7 -0
- package/src/lib/errors/index.mts +8 -0
- package/src/lib/errors/json-rpc-error.mts +18 -0
- package/src/lib/errors/usage-error.mts +7 -0
- package/src/lib/graph/CLAUDE.md +17 -0
- package/src/lib/graph/anomalies.mts +110 -0
- package/src/lib/graph/derive.mts +96 -0
- package/src/lib/graph/index.mts +26 -0
- package/src/lib/graph/pipeline.mts +410 -0
- package/src/lib/graph/queries.mts +207 -0
- package/src/lib/graph/rows.mts +99 -0
- package/src/lib/graph/types.mts +137 -0
- package/src/lib/liveness/CLAUDE.md +14 -0
- package/src/lib/liveness/index.mts +10 -0
- package/src/lib/liveness/liveness.mts +147 -0
- package/src/lib/liveness/retire.mts +63 -0
- package/src/lib/logger/CLAUDE.md +12 -0
- package/src/lib/logger/index.mts +2 -0
- package/src/lib/logger/logger.mts +58 -0
- package/src/lib/logger/stream-sink.mts +23 -0
- package/src/lib/mcp/CLAUDE.md +21 -0
- package/src/lib/mcp/channel.mts +41 -0
- package/src/lib/mcp/dispatch.mts +60 -0
- package/src/lib/mcp/drain.mts +83 -0
- package/src/lib/mcp/index.mts +5 -0
- package/src/lib/mcp/mcp.mts +267 -0
- package/src/lib/mcp/tools.mts +77 -0
- package/src/lib/model/CLAUDE.md +8 -0
- package/src/lib/model/index.mts +3 -0
- package/src/lib/model/repo-caps.mts +95 -0
- package/src/lib/model/status.mts +91 -0
- package/src/lib/model/types.mts +83 -0
- package/src/lib/refresh/index.mts +2 -0
- package/src/lib/refresh/placeholders.mts +43 -0
- package/src/lib/refresh/refresh-service.mts +203 -0
- package/src/lib/schedule/CLAUDE.md +18 -0
- package/src/lib/schedule/caps.mts +113 -0
- package/src/lib/schedule/correlate.mts +69 -0
- package/src/lib/schedule/index.mts +7 -0
- package/src/lib/schedule/scheduler.mts +355 -0
- package/src/lib/schedule/tick.mts +266 -0
- package/src/lib/stores/CLAUDE.md +24 -0
- package/src/lib/stores/coordination.mts +359 -0
- package/src/lib/stores/cursor.mts +41 -0
- package/src/lib/stores/edge.mts +138 -0
- package/src/lib/stores/fetch-request.mts +346 -0
- package/src/lib/stores/index.mts +19 -0
- package/src/lib/stores/materialize.mts +69 -0
- package/src/lib/stores/milestone.mts +74 -0
- package/src/lib/stores/notice.mts +57 -0
- package/src/lib/stores/policy.mts +48 -0
- package/src/lib/stores/pr-event.mts +94 -0
- package/src/lib/stores/pr.mts +167 -0
- package/src/lib/stores/project.mts +79 -0
- package/src/lib/stores/refresh.mts +197 -0
- package/src/lib/stores/review.mts +113 -0
- package/src/lib/stores/session.mts +170 -0
- package/src/lib/stores/ticket.mts +246 -0
- package/src/lib/stores/watch.mts +360 -0
- package/src/lib/stores/worker.mts +121 -0
- package/src/lib/watch/adopt.mts +151 -0
- package/src/lib/watch/arm.mts +48 -0
- package/src/lib/watch/cadence.mts +45 -0
- package/src/lib/watch/diff.mts +274 -0
- package/src/lib/watch/index.mts +11 -0
- package/src/lib/watch/marker.mts +24 -0
- package/src/lib/watch/payload.mts +56 -0
- package/src/lib/watch/poll.mts +87 -0
- package/src/lib/watch/render.mts +61 -0
- package/src/lib/watch/snapshot.mts +312 -0
- package/src/main.mts +18 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {AbstractCommand} from '../../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
|
|
4
|
+
import {RefreshService} from '../../lib/refresh/index.mts';
|
|
5
|
+
import {ProjectStore} from '../../lib/stores/index.mts';
|
|
6
|
+
|
|
7
|
+
const options = {
|
|
8
|
+
id: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'Tracker identifier for the project.',
|
|
11
|
+
positional: false,
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
name: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Human-readable project name.',
|
|
17
|
+
positional: false,
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
tracker: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description:
|
|
23
|
+
'Tracker the project lives on, e.g. linear. Every ticket in it inherits this.',
|
|
24
|
+
positional: false,
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
db: DB_OPTION,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export class Command extends AbstractCommand {
|
|
31
|
+
readonly name = 'set';
|
|
32
|
+
readonly summary = 'Create or update one project.';
|
|
33
|
+
readonly env = [];
|
|
34
|
+
readonly options = options;
|
|
35
|
+
|
|
36
|
+
async run(
|
|
37
|
+
parsed: ParsedOptions<typeof options>,
|
|
38
|
+
ctx: CommandContext
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
41
|
+
await new ProjectStore(db).upsertProject({
|
|
42
|
+
id: parsed.id,
|
|
43
|
+
name: parsed.name,
|
|
44
|
+
source: parsed.tracker,
|
|
45
|
+
});
|
|
46
|
+
await new RefreshService(db).reconcile();
|
|
47
|
+
ctx.io.write(`project ${parsed.id}\n`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {AbstractCommand} from '../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, withDatabase} from '../lib/db/index.mts';
|
|
4
|
+
import {dispatchQueue} from '../lib/graph/index.mts';
|
|
5
|
+
|
|
6
|
+
const options = {
|
|
7
|
+
project: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
description: 'Restrict the queue to one project id.',
|
|
10
|
+
positional: false,
|
|
11
|
+
required: false,
|
|
12
|
+
},
|
|
13
|
+
db: DB_OPTION,
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export class Command extends AbstractCommand {
|
|
17
|
+
readonly name = 'queue';
|
|
18
|
+
readonly summary =
|
|
19
|
+
'Print what the scheduler would hand out next, in dispatch order.';
|
|
20
|
+
readonly env = [];
|
|
21
|
+
readonly options = options;
|
|
22
|
+
|
|
23
|
+
async run(
|
|
24
|
+
parsed: ParsedOptions<typeof options>,
|
|
25
|
+
ctx: CommandContext
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
28
|
+
const entries = await dispatchQueue(db, {project: parsed.project});
|
|
29
|
+
for (const {entry, pass} of entries) {
|
|
30
|
+
ctx.io.write(
|
|
31
|
+
`${pass ?? 'available'} ${entry.item.id} kind=${entry.item.kind}` +
|
|
32
|
+
(entry.item.project === null
|
|
33
|
+
? ''
|
|
34
|
+
: ` project=${entry.item.project}`) +
|
|
35
|
+
`${entry.item.injected ? ' injected' : ''}\n`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (entries.length === 0) ctx.io.write('queue empty\n');
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {AbstractCommand} from '../../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
|
|
4
|
+
import {RefreshService} from '../../lib/refresh/index.mts';
|
|
5
|
+
|
|
6
|
+
const options = {
|
|
7
|
+
tracker: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
description: 'Tracker whose scan is complete.',
|
|
10
|
+
positional: false,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
cursor: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description:
|
|
16
|
+
'Opaque tracker token marking how far this scan read. Recorded only when the refresh closes.',
|
|
17
|
+
positional: false,
|
|
18
|
+
required: false,
|
|
19
|
+
},
|
|
20
|
+
db: DB_OPTION,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export class Command extends AbstractCommand {
|
|
24
|
+
readonly name = 'done';
|
|
25
|
+
readonly summary = 'Report that everything the scan found has been written.';
|
|
26
|
+
readonly env = [];
|
|
27
|
+
readonly options = options;
|
|
28
|
+
|
|
29
|
+
async run(
|
|
30
|
+
parsed: ParsedOptions<typeof options>,
|
|
31
|
+
ctx: CommandContext
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
34
|
+
const {state, pending} = await new RefreshService(db).completeScan({
|
|
35
|
+
source: parsed.tracker,
|
|
36
|
+
cursor: parsed.cursor ?? null,
|
|
37
|
+
});
|
|
38
|
+
ctx.io.write(`refresh ${parsed.tracker} ${state}\n`);
|
|
39
|
+
for (const id of pending) ctx.io.write(`pending ${id}\n`);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {AbstractCommand} from '../../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
|
|
4
|
+
import {RefreshService} from '../../lib/refresh/index.mts';
|
|
5
|
+
|
|
6
|
+
const options = {
|
|
7
|
+
tracker: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
description: 'Tracker to report on.',
|
|
10
|
+
positional: false,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
db: DB_OPTION,
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export class Command extends AbstractCommand {
|
|
17
|
+
readonly name = 'status';
|
|
18
|
+
readonly summary =
|
|
19
|
+
'Print the refresh state and every outstanding instruction.';
|
|
20
|
+
readonly env = [];
|
|
21
|
+
readonly options = options;
|
|
22
|
+
|
|
23
|
+
async run(
|
|
24
|
+
parsed: ParsedOptions<typeof options>,
|
|
25
|
+
ctx: CommandContext
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
28
|
+
const {refresh, requests} = await new RefreshService(db).status(
|
|
29
|
+
parsed.tracker
|
|
30
|
+
);
|
|
31
|
+
ctx.io.write(`refresh ${parsed.tracker} ${refresh?.state ?? 'none'}\n`);
|
|
32
|
+
for (const request of requests) {
|
|
33
|
+
if (request.resolution !== null) continue;
|
|
34
|
+
ctx.io.write(
|
|
35
|
+
`${request.kind} ${JSON.stringify(request.payload)} delivered=${String(request.deliveredAt !== null)}\n`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {AbstractCommand} from '../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, withDatabase} from '../lib/db/index.mts';
|
|
4
|
+
import {RefreshService} from '../lib/refresh/index.mts';
|
|
5
|
+
|
|
6
|
+
const options = {
|
|
7
|
+
tracker: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
description: 'Tracker to refresh, e.g. linear.',
|
|
10
|
+
positional: false,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
project: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Comma-separated project ids to scan.',
|
|
16
|
+
positional: false,
|
|
17
|
+
required: true,
|
|
18
|
+
},
|
|
19
|
+
rebuild: {
|
|
20
|
+
type: 'boolean',
|
|
21
|
+
description: 'Drop the graph and scan from scratch, ignoring the cursor.',
|
|
22
|
+
positional: false,
|
|
23
|
+
required: false,
|
|
24
|
+
},
|
|
25
|
+
db: DB_OPTION,
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export class Command extends AbstractCommand {
|
|
29
|
+
readonly name = 'refresh';
|
|
30
|
+
readonly summary =
|
|
31
|
+
'Start or resume building the project graph for a tracker.';
|
|
32
|
+
readonly env = [];
|
|
33
|
+
readonly options = options;
|
|
34
|
+
|
|
35
|
+
async run(
|
|
36
|
+
parsed: ParsedOptions<typeof options>,
|
|
37
|
+
ctx: CommandContext
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const projects = parsed.project
|
|
40
|
+
.split(',')
|
|
41
|
+
.map((id) => id.trim())
|
|
42
|
+
.filter((id) => id !== '');
|
|
43
|
+
|
|
44
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
45
|
+
const {resumed} = await new RefreshService(db).startScan({
|
|
46
|
+
source: parsed.tracker,
|
|
47
|
+
projects,
|
|
48
|
+
sessionId: ctx.env.CLAUDE_CODE_SESSION_ID ?? null,
|
|
49
|
+
rebuild: parsed.rebuild,
|
|
50
|
+
});
|
|
51
|
+
ctx.io.write(
|
|
52
|
+
`refresh ${parsed.tracker} ${resumed ? 'resumed' : 'opened'}\n`
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {AbstractCommand} from '../../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, nowIso, withDatabase} from '../../lib/db/index.mts';
|
|
4
|
+
import {RefreshService} from '../../lib/refresh/index.mts';
|
|
5
|
+
import {correlateSession} from '../../lib/schedule/index.mts';
|
|
6
|
+
import {ReviewStore} from '../../lib/stores/index.mts';
|
|
7
|
+
|
|
8
|
+
const options = {
|
|
9
|
+
milestone: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'The milestone whose review just ran.',
|
|
12
|
+
positional: false,
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
session: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description:
|
|
18
|
+
'Registry id whose claim this releases; defaults to the session correlated from the environment.',
|
|
19
|
+
positional: false,
|
|
20
|
+
required: false,
|
|
21
|
+
},
|
|
22
|
+
db: DB_OPTION,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export class Command extends AbstractCommand {
|
|
26
|
+
readonly name = 'record';
|
|
27
|
+
readonly summary =
|
|
28
|
+
'Record a milestone review with a member snapshot, opening its gate.';
|
|
29
|
+
readonly env = [];
|
|
30
|
+
readonly options = options;
|
|
31
|
+
|
|
32
|
+
async run(
|
|
33
|
+
parsed: ParsedOptions<typeof options>,
|
|
34
|
+
ctx: CommandContext
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
37
|
+
// No server means no claim of this caller's to release; ambiguous
|
|
38
|
+
// correlation refuses rather than leave a real claim held.
|
|
39
|
+
const session =
|
|
40
|
+
(await correlateSession(db, ctx.env, parsed.session)) ?? '';
|
|
41
|
+
await new ReviewStore(db).record(parsed.milestone, nowIso(), session);
|
|
42
|
+
await new RefreshService(db).reconcile();
|
|
43
|
+
ctx.io.write(`review ${parsed.milestone}\n`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {AbstractCommand} from '../../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
|
|
4
|
+
import {correlateSession} from '../../lib/schedule/index.mts';
|
|
5
|
+
import {ReviewStore} from '../../lib/stores/index.mts';
|
|
6
|
+
|
|
7
|
+
const options = {
|
|
8
|
+
milestone: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'The milestone whose review ended without recording.',
|
|
11
|
+
positional: false,
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
session: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description:
|
|
17
|
+
'Registry id whose claim this releases; defaults to the session correlated from the environment.',
|
|
18
|
+
positional: false,
|
|
19
|
+
required: false,
|
|
20
|
+
},
|
|
21
|
+
db: DB_OPTION,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export class Command extends AbstractCommand {
|
|
25
|
+
readonly name = 'release';
|
|
26
|
+
readonly summary =
|
|
27
|
+
'Release a milestone-review claim without opening the gate.';
|
|
28
|
+
readonly env = [];
|
|
29
|
+
readonly options = options;
|
|
30
|
+
|
|
31
|
+
async run(
|
|
32
|
+
parsed: ParsedOptions<typeof options>,
|
|
33
|
+
ctx: CommandContext
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
36
|
+
// No server means no claim of this caller's to release; ambiguous
|
|
37
|
+
// correlation refuses rather than leave a real claim held.
|
|
38
|
+
const session =
|
|
39
|
+
(await correlateSession(db, ctx.env, parsed.session)) ?? '';
|
|
40
|
+
const released = await new ReviewStore(db).release(
|
|
41
|
+
parsed.milestone,
|
|
42
|
+
session
|
|
43
|
+
);
|
|
44
|
+
ctx.io.write(
|
|
45
|
+
`released review ${parsed.milestone} existed=${String(released)}\n`
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {AbstractCommand} from '../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, withDatabase} from '../lib/db/index.mts';
|
|
4
|
+
import {derive, dispatchQueue, repoPrLoad} from '../lib/graph/index.mts';
|
|
5
|
+
import {RepoAdmission} from '../lib/schedule/index.mts';
|
|
6
|
+
import {PolicyStore} from '../lib/stores/index.mts';
|
|
7
|
+
|
|
8
|
+
const options = {
|
|
9
|
+
project: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description:
|
|
12
|
+
'Restrict the report to one project id. Anomalies are graph-wide and stay unfiltered.',
|
|
13
|
+
positional: false,
|
|
14
|
+
required: false,
|
|
15
|
+
},
|
|
16
|
+
db: DB_OPTION,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export class Command extends AbstractCommand {
|
|
20
|
+
readonly name = 'status';
|
|
21
|
+
readonly summary =
|
|
22
|
+
'Print per-project counts, milestone gates, anomalies, and the terminal verdict.';
|
|
23
|
+
readonly env = [];
|
|
24
|
+
readonly options = options;
|
|
25
|
+
|
|
26
|
+
async run(
|
|
27
|
+
parsed: ParsedOptions<typeof options>,
|
|
28
|
+
ctx: CommandContext
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
31
|
+
const graph = await derive(db, {project: parsed.project});
|
|
32
|
+
|
|
33
|
+
for (const counts of graph.counts) {
|
|
34
|
+
ctx.io.write(
|
|
35
|
+
`project ${counts.project} total=${String(counts.total)} ` +
|
|
36
|
+
`available=${String(counts.available)} blocked=${String(counts.blocked)} ` +
|
|
37
|
+
`human-blocked=${String(counts.humanBlocked)} in-flight=${String(counts.inFlight)} ` +
|
|
38
|
+
`dormant=${String(counts.dormant)} verified=${String(counts.verified)} ` +
|
|
39
|
+
`canceled=${String(counts.canceled)} terminal=${String(counts.terminal)}\n`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
for (const milestone of graph.milestones) {
|
|
43
|
+
ctx.io.write(
|
|
44
|
+
`milestone ${milestone.id} project=${milestone.project} ` +
|
|
45
|
+
`members=${String(milestone.memberCount)} unresolved=${String(milestone.openCount)} ` +
|
|
46
|
+
`ready-for-review=${String(milestone.readyForReview)} ` +
|
|
47
|
+
`review-recorded=${String(milestone.reviewRecorded)} open=${String(milestone.open)}\n`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
for (const entry of graph.prs) {
|
|
51
|
+
ctx.io.write(
|
|
52
|
+
`pr ${entry.item.id}` +
|
|
53
|
+
(entry.item.ticket === null ? '' : ` ticket=${entry.item.ticket}`) +
|
|
54
|
+
` ${entry.classification}\n`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
// Why the queue is idle when it is: a repo at one of its caps holds
|
|
58
|
+
// work back with nothing else to show for it, which otherwise reads as
|
|
59
|
+
// a wedged system. Deliberately no `reserve` here — this reports the
|
|
60
|
+
// caps holding work *now*, from load already on the forge. Simulating
|
|
61
|
+
// the fill pass instead would blame a cap for a queue the compute budget
|
|
62
|
+
// is what is actually holding, and the scheduler's own claims land in
|
|
63
|
+
// this reading within a tick anyway.
|
|
64
|
+
const admission = new RepoAdmission(
|
|
65
|
+
await new PolicyStore(db).getRepoCaps(),
|
|
66
|
+
await repoPrLoad(db)
|
|
67
|
+
);
|
|
68
|
+
for (const {entry} of await dispatchQueue(db, {
|
|
69
|
+
project: parsed.project,
|
|
70
|
+
})) {
|
|
71
|
+
if (entry.item.kind === 'pr') admission.admit(entry.item);
|
|
72
|
+
}
|
|
73
|
+
for (const hold of admission.holds()) {
|
|
74
|
+
ctx.io.write(
|
|
75
|
+
`cap-hold ${hold.repo} ${hold.cap}=${String(hold.observed)}/${String(hold.limit)} ` +
|
|
76
|
+
`waiting=${String(hold.waiting)}\n`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
for (const anomaly of graph.anomalies) {
|
|
80
|
+
ctx.io.write(`anomaly ${anomaly.kind} ${anomaly.detail}\n`);
|
|
81
|
+
}
|
|
82
|
+
ctx.io.write(`terminal=${String(graph.terminal)}\n`);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {AbstractCommand} from '../../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
|
|
4
|
+
import {RefreshService} from '../../lib/refresh/index.mts';
|
|
5
|
+
|
|
6
|
+
const options = {
|
|
7
|
+
id: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
description: 'The ticket id the tracker has no record of.',
|
|
10
|
+
positional: false,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
db: DB_OPTION,
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export class Command extends AbstractCommand {
|
|
17
|
+
readonly name = 'missing';
|
|
18
|
+
readonly summary = 'Report that a requested ticket does not exist.';
|
|
19
|
+
readonly env = [];
|
|
20
|
+
readonly options = options;
|
|
21
|
+
|
|
22
|
+
async run(
|
|
23
|
+
parsed: ParsedOptions<typeof options>,
|
|
24
|
+
ctx: CommandContext
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
27
|
+
await new RefreshService(db).markMissing(parsed.id);
|
|
28
|
+
ctx.io.write(`missing ticket ${parsed.id}\n`);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {AbstractCommand} from '../../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
|
|
4
|
+
import {RefreshService} from '../../lib/refresh/index.mts';
|
|
5
|
+
import {TicketStore} from '../../lib/stores/index.mts';
|
|
6
|
+
|
|
7
|
+
const options = {
|
|
8
|
+
id: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'Tracker identifier for the ticket.',
|
|
11
|
+
positional: false,
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
db: DB_OPTION,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export class Command extends AbstractCommand {
|
|
18
|
+
readonly name = 'rm';
|
|
19
|
+
readonly summary = 'Delete one ticket.';
|
|
20
|
+
readonly env = [];
|
|
21
|
+
readonly options = options;
|
|
22
|
+
|
|
23
|
+
async run(
|
|
24
|
+
parsed: ParsedOptions<typeof options>,
|
|
25
|
+
ctx: CommandContext
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
28
|
+
const existed = await new TicketStore(db).removeTicket(parsed.id);
|
|
29
|
+
await new RefreshService(db).reconcile();
|
|
30
|
+
ctx.io.write(`removed ticket ${parsed.id} existed=${String(existed)}\n`);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {AbstractCommand} from '../../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
|
|
4
|
+
import {STATUSES, TARGET_KINDS} from '../../lib/model/status.mts';
|
|
5
|
+
import {RefreshService} from '../../lib/refresh/index.mts';
|
|
6
|
+
import {TicketStore} from '../../lib/stores/index.mts';
|
|
7
|
+
|
|
8
|
+
const options = {
|
|
9
|
+
id: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'Tracker identifier, e.g. CLC-945.',
|
|
12
|
+
positional: false,
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
project: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Project the ticket belongs to.',
|
|
18
|
+
positional: false,
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
status: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Normalized lifecycle status; map the tracker state yourself.',
|
|
24
|
+
positional: false,
|
|
25
|
+
required: true,
|
|
26
|
+
choices: STATUSES,
|
|
27
|
+
},
|
|
28
|
+
title: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Ticket title.',
|
|
31
|
+
positional: false,
|
|
32
|
+
required: false,
|
|
33
|
+
},
|
|
34
|
+
url: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'Ticket URL.',
|
|
37
|
+
positional: false,
|
|
38
|
+
required: false,
|
|
39
|
+
},
|
|
40
|
+
'target-kind': {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'What finishing this ticket produces.',
|
|
43
|
+
positional: false,
|
|
44
|
+
required: false,
|
|
45
|
+
choices: TARGET_KINDS,
|
|
46
|
+
},
|
|
47
|
+
'requires-human': {
|
|
48
|
+
type: 'boolean',
|
|
49
|
+
description: 'Only a human may work this ticket.',
|
|
50
|
+
positional: false,
|
|
51
|
+
required: false,
|
|
52
|
+
},
|
|
53
|
+
injected: {
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
description: 'Rank this ticket to the top of the frontier.',
|
|
56
|
+
positional: false,
|
|
57
|
+
required: false,
|
|
58
|
+
},
|
|
59
|
+
priority: {
|
|
60
|
+
type: 'number',
|
|
61
|
+
description: 'Lower is more urgent; omit if the tracker has none.',
|
|
62
|
+
positional: false,
|
|
63
|
+
required: false,
|
|
64
|
+
},
|
|
65
|
+
labels: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'Comma-separated tracker labels, passed through as-is.',
|
|
68
|
+
positional: false,
|
|
69
|
+
required: false,
|
|
70
|
+
},
|
|
71
|
+
'branch-hint': {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Branch-name seed the tracker suggests.',
|
|
74
|
+
positional: false,
|
|
75
|
+
required: false,
|
|
76
|
+
},
|
|
77
|
+
'updated-at': {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: 'When the tracker last saw the ticket move (RFC 3339).',
|
|
80
|
+
positional: false,
|
|
81
|
+
required: false,
|
|
82
|
+
},
|
|
83
|
+
db: DB_OPTION,
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
export class Command extends AbstractCommand {
|
|
87
|
+
readonly name = 'set';
|
|
88
|
+
readonly summary = 'Create or update one ticket.';
|
|
89
|
+
readonly env = [];
|
|
90
|
+
readonly options = options;
|
|
91
|
+
|
|
92
|
+
async run(
|
|
93
|
+
parsed: ParsedOptions<typeof options>,
|
|
94
|
+
ctx: CommandContext
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const labels =
|
|
97
|
+
parsed.labels === undefined
|
|
98
|
+
? undefined
|
|
99
|
+
: parsed.labels
|
|
100
|
+
.split(',')
|
|
101
|
+
.map((label) => label.trim())
|
|
102
|
+
.filter((label) => label !== '');
|
|
103
|
+
|
|
104
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
105
|
+
// Only what the caller named. A re-read reports the fields it was asked
|
|
106
|
+
// about and says nothing about the rest, so absent must mean "leave it"
|
|
107
|
+
// — otherwise answering a refresh blanks the ticket's title and labels.
|
|
108
|
+
// `injected` and `requires-human` are carried only when set: the parser
|
|
109
|
+
// cannot tell an absent boolean from a false one.
|
|
110
|
+
await new TicketStore(db).patchTicket({
|
|
111
|
+
id: parsed.id,
|
|
112
|
+
project: parsed.project,
|
|
113
|
+
status: parsed.status,
|
|
114
|
+
...(parsed.url === undefined ? {} : {url: parsed.url}),
|
|
115
|
+
...(parsed.title === undefined ? {} : {title: parsed.title}),
|
|
116
|
+
...(parsed['target-kind'] === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: {targetKind: parsed['target-kind']}),
|
|
119
|
+
...(parsed['requires-human'] ? {requiresHuman: true} : {}),
|
|
120
|
+
...(parsed.injected ? {injected: true} : {}),
|
|
121
|
+
...(parsed.priority === undefined ? {} : {priority: parsed.priority}),
|
|
122
|
+
...(parsed['branch-hint'] === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: {branchHint: parsed['branch-hint']}),
|
|
125
|
+
...(labels === undefined ? {} : {labels}),
|
|
126
|
+
...(parsed['updated-at'] === undefined
|
|
127
|
+
? {}
|
|
128
|
+
: {updatedAt: parsed['updated-at']}),
|
|
129
|
+
});
|
|
130
|
+
await new RefreshService(db).reconcile();
|
|
131
|
+
ctx.io.write(`ticket ${parsed.id}\n`);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {AbstractCommand} from '../../lib/command/index.mts';
|
|
2
|
+
import type {CommandContext, ParsedOptions} from '../../lib/command/index.mts';
|
|
3
|
+
import {DB_OPTION, withDatabase} from '../../lib/db/index.mts';
|
|
4
|
+
import {DataError, ensure} from '../../lib/errors/index.mts';
|
|
5
|
+
import {correlateSession} from '../../lib/schedule/index.mts';
|
|
6
|
+
import {WorkerStore} from '../../lib/stores/index.mts';
|
|
7
|
+
|
|
8
|
+
const options = {
|
|
9
|
+
node: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'The node whose worker address to drop.',
|
|
12
|
+
positional: false,
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
db: DB_OPTION,
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hand a node from warm relay to cold recovery, for a relay that provably
|
|
20
|
+
* went nowhere: drops the caller's own address and releases its claim, so
|
|
21
|
+
* the scheduler re-serves the item as a `resume` pass.
|
|
22
|
+
*/
|
|
23
|
+
export class Command extends AbstractCommand {
|
|
24
|
+
readonly name = 'rm';
|
|
25
|
+
readonly summary = "Drop a node's worker address.";
|
|
26
|
+
readonly env = [];
|
|
27
|
+
readonly options = options;
|
|
28
|
+
|
|
29
|
+
async run(
|
|
30
|
+
parsed: ParsedOptions<typeof options>,
|
|
31
|
+
ctx: CommandContext
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
await withDatabase(parsed.db, ctx.env, async (db) => {
|
|
34
|
+
const session = await correlateSession(db, ctx.env, undefined);
|
|
35
|
+
ensure(
|
|
36
|
+
session !== null,
|
|
37
|
+
() =>
|
|
38
|
+
new DataError('no live server correlates to this session', {
|
|
39
|
+
hint: 'only the session that recorded the address can revoke it.',
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
const removed = await new WorkerStore(db).remove(parsed.node, session);
|
|
43
|
+
ctx.io.write(`worker ${parsed.node} removed=${String(removed)}\n`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|