@danypops/papyrus 0.21.0 → 0.21.2
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/package.json +1 -1
- package/src/adapters/sqlite-gate-runner.ts +2 -2
- package/src/cli.ts +10 -1
- package/src/domain/gate.ts +46 -1
- package/src/modules/tasks.ts +2 -1
- package/src/ops.ts +61 -12
- package/src/ports/gate-runner.ts +1 -1
- package/src/service.ts +1 -0
- package/src/task-service.ts +18 -5
package/package.json
CHANGED
|
@@ -6,8 +6,8 @@ import { runGates, runGatesAsync } from "../ops.ts";
|
|
|
6
6
|
export class SQLiteGateRunner implements GateRunner {
|
|
7
7
|
constructor(private readonly db: Db) {}
|
|
8
8
|
|
|
9
|
-
run(artifactId: string): GateResult[] {
|
|
10
|
-
return runGates(this.db, artifactId);
|
|
9
|
+
run(artifactId: string, options?: GateRunOptions): GateResult[] {
|
|
10
|
+
return runGates(this.db, artifactId, options);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
runAsync(artifactId: string, options?: GateRunOptions): Promise<GateResult[]> {
|
package/src/cli.ts
CHANGED
|
@@ -157,6 +157,7 @@ const USAGE = `Usage:
|
|
|
157
157
|
papyrus tasks show <id> [--json]
|
|
158
158
|
papyrus tasks run-gates <id> [--json]
|
|
159
159
|
papyrus tasks set-checklist <id> --checklist-json <json> [--json]
|
|
160
|
+
papyrus tasks set-gates <id> --gates-json <json> [--json]
|
|
160
161
|
papyrus tasks context [--scope <project|graph|all>] [--root-task-id <id>] [--session-id <id>] [--json]
|
|
161
162
|
|
|
162
163
|
A "--session-id" scopes Task Focus to one agent session; omit it to use the shared "global" Focus (today's behavior).
|
|
@@ -1322,6 +1323,14 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
|
|
|
1322
1323
|
human = `Updated checklist: ${artifactLabel(artifact)}`;
|
|
1323
1324
|
break;
|
|
1324
1325
|
}
|
|
1326
|
+
case "set-gates": {
|
|
1327
|
+
if (!id || dependencyId) throw new Error("tasks set-gates requires exactly one task id");
|
|
1328
|
+
if (!gates) throw new Error("tasks set-gates requires --gates-json");
|
|
1329
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.set_gates", { id, gates });
|
|
1330
|
+
result = artifact;
|
|
1331
|
+
human = `Updated gates: ${artifactLabel(artifact)}`;
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1325
1334
|
case "context": {
|
|
1326
1335
|
if (id) throw new Error("tasks context accepts no positional arguments");
|
|
1327
1336
|
const summary = await client.call<Record<string, unknown>, string | null>("tasks.context", {
|
|
@@ -1478,7 +1487,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
|
|
|
1478
1487
|
break;
|
|
1479
1488
|
}
|
|
1480
1489
|
default:
|
|
1481
|
-
throw new Error("tasks action must be create, list, show, active, focused, focus, pause, unpause, clear-focus, update, graph, plan, context, history, scope, assign-project, complete, start, submit, reject, retry, cancel, depend, undepend, contain, uncontain, run-gates, or set-
|
|
1490
|
+
throw new Error("tasks action must be create, list, show, active, focused, focus, pause, unpause, clear-focus, update, graph, plan, context, history, scope, assign-project, complete, start, submit, reject, retry, cancel, depend, undepend, contain, uncontain, run-gates, set-checklist, or set-gates");
|
|
1482
1491
|
}
|
|
1483
1492
|
return json ? JSON.stringify(result) : human;
|
|
1484
1493
|
}
|
package/src/domain/gate.ts
CHANGED
|
@@ -1,12 +1,57 @@
|
|
|
1
|
+
export const GATE_TYPES = ["file-exists", "command", "contains", "test"] as const;
|
|
2
|
+
export type GateType = typeof GATE_TYPES[number];
|
|
3
|
+
|
|
1
4
|
export interface Gate {
|
|
2
|
-
type:
|
|
5
|
+
type: GateType;
|
|
3
6
|
target: string;
|
|
4
7
|
expect?: string;
|
|
5
8
|
}
|
|
6
9
|
|
|
10
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validates a Gate[] the same way validateChecklist validates a Checklist -- gates previously had
|
|
16
|
+
* no validation at all (create() assigned `input.gates` to extra verbatim), and no way to change
|
|
17
|
+
* them after creation except by re-typing the whole task, silently accepted by "tasks update"
|
|
18
|
+
* (which only ever reads title/body/labels/status) as if it had worked. See Tasks.setGates.
|
|
19
|
+
*/
|
|
20
|
+
export function validateGates(value: unknown): Gate[] {
|
|
21
|
+
if (!Array.isArray(value)) throw new Error("gates must be an array");
|
|
22
|
+
return value.map((entry, index) => {
|
|
23
|
+
if (!isRecord(entry) || !GATE_TYPES.includes(entry["type"] as GateType)) {
|
|
24
|
+
throw new Error(`gate at index ${index} requires a valid type (${GATE_TYPES.join(", ")})`);
|
|
25
|
+
}
|
|
26
|
+
if (typeof entry["target"] !== "string" || entry["target"].trim().length === 0) {
|
|
27
|
+
throw new Error(`gate at index ${index} requires a non-empty target`);
|
|
28
|
+
}
|
|
29
|
+
if (entry["expect"] !== undefined && typeof entry["expect"] !== "string") {
|
|
30
|
+
throw new Error(`gate at index ${index} expect must be a string`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
type: entry["type"] as GateType,
|
|
34
|
+
target: entry["target"],
|
|
35
|
+
...(typeof entry["expect"] === "string" ? { expect: entry["expect"] } : {}),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
7
40
|
export interface GateRunOptions {
|
|
8
41
|
/** Absolute Unix epoch deadline for the full gate sequence. */
|
|
9
42
|
deadlineMs?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Working directory for "command"/"test" gates. Without this, a command gate inherits the
|
|
45
|
+
* Papyrus daemon's own process cwd (its systemd unit's launch directory, e.g. the user's home
|
|
46
|
+
* directory) rather than the task's project -- a real incident: a `bun test` command gate ran
|
|
47
|
+
* against the daemon's home directory instead of the task's project, recursively discovering
|
|
48
|
+
* and attempting to run every test file under every project on the machine, which exhausted
|
|
49
|
+
* memory and crashed the `bun` process outright (SIGILL/SIGABRT), well past the configured
|
|
50
|
+
* gate timeout because the timeout only ever terminated the immediate shell, not the process
|
|
51
|
+
* group it spawned (see executeGateCommand). Task-scoped completion must always pass the
|
|
52
|
+
* task's project_root here.
|
|
53
|
+
*/
|
|
54
|
+
cwd?: string;
|
|
10
55
|
}
|
|
11
56
|
|
|
12
57
|
export interface GateResult {
|
package/src/modules/tasks.ts
CHANGED
|
@@ -89,7 +89,7 @@ export const TASKS_OPERATION_NAMES = [
|
|
|
89
89
|
"tasks.create", "tasks.update", "tasks.list", "tasks.graph", "tasks.plan", "tasks.show", "tasks.history",
|
|
90
90
|
"tasks.scope", "tasks.set_scope", "tasks.assign_project", "tasks.active", "tasks.focused", "tasks.focus",
|
|
91
91
|
"tasks.pause", "tasks.unpause", "tasks.clear_focus", "tasks.start", "tasks.submit", "tasks.complete",
|
|
92
|
-
"tasks.run_gates", "tasks.set_checklist", "tasks.context", "tasks.reject", "tasks.retry", "tasks.cancel",
|
|
92
|
+
"tasks.run_gates", "tasks.set_checklist", "tasks.set_gates", "tasks.context", "tasks.reject", "tasks.retry", "tasks.cancel",
|
|
93
93
|
"tasks.depend", "tasks.undepend", "tasks.contain", "tasks.uncontain", "tasks.reap_stale_focus",
|
|
94
94
|
] as const;
|
|
95
95
|
|
|
@@ -157,6 +157,7 @@ export function tasksOperations(tasks: Tasks, artifacts: ArtifactStore, sessionI
|
|
|
157
157
|
define("tasks.complete", (input: OperationInput) => tasks.completeAsync(string(input, "id"), eventContext(input))),
|
|
158
158
|
define("tasks.run_gates", (input: OperationInput) => tasks.runGates(string(input, "id"), eventContext(input))),
|
|
159
159
|
define("tasks.set_checklist", (input: OperationInput) => tasks.setChecklist(string(input, "id"), input["checklist"] as Checklist)),
|
|
160
|
+
define("tasks.set_gates", (input: OperationInput) => tasks.setGates(string(input, "id"), input["gates"] as Parameters<Tasks["setGates"]>[1])),
|
|
160
161
|
define("tasks.context", (input: OperationInput) => taskContext(
|
|
161
162
|
artifacts,
|
|
162
163
|
tasks.active(taskFilter(input))?.id,
|
package/src/ops.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Enforces the schema protocol (kinds, statuses, relations) via FK + app validation.
|
|
4
4
|
*/
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
-
import { exec } from "node:child_process";
|
|
7
6
|
import type { Db } from "./db.ts";
|
|
8
7
|
import { inTransaction } from "./db.ts";
|
|
9
8
|
import { ARTIFACT_TRASH_RETENTION_MS, DEFAULT_STATUS_BY_KIND } from "./constants.ts";
|
|
@@ -508,10 +507,11 @@ function readBoundedGateFile(path: string): string {
|
|
|
508
507
|
return readFileSync(path, "utf-8") as string;
|
|
509
508
|
}
|
|
510
509
|
|
|
511
|
-
export function runGates(db: Db, artifactId: string): GateResult[] {
|
|
510
|
+
export function runGates(db: Db, artifactId: string, options: GateRunOptions = {}): GateResult[] {
|
|
512
511
|
const art = getArtifact(db, artifactId);
|
|
513
512
|
if (!art) throw new Error("artifact not found");
|
|
514
513
|
const gates = (art.extra["gates"] as Gate[]) ?? [];
|
|
514
|
+
const cwd = options.cwd;
|
|
515
515
|
return gates.map((gate) => {
|
|
516
516
|
switch (gate.type) {
|
|
517
517
|
case "file-exists": {
|
|
@@ -531,7 +531,7 @@ export function runGates(db: Db, artifactId: string): GateResult[] {
|
|
|
531
531
|
case "command": {
|
|
532
532
|
const { execSync } = require_("node:child_process");
|
|
533
533
|
try {
|
|
534
|
-
const output = execSync(gate.target, { encoding: "utf-8", timeout: GATE_COMMAND_TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
534
|
+
const output = execSync(gate.target, { encoding: "utf-8", timeout: GATE_COMMAND_TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"], ...(cwd ? { cwd } : {}) }).trim();
|
|
535
535
|
const passed = gate.expect ? output.includes(gate.expect) : true;
|
|
536
536
|
return { gate, passed, output: output.slice(0, GATE_OUTPUT_LIMIT) };
|
|
537
537
|
} catch (e) {
|
|
@@ -541,7 +541,7 @@ export function runGates(db: Db, artifactId: string): GateResult[] {
|
|
|
541
541
|
case "test": {
|
|
542
542
|
const { execSync } = require_("node:child_process");
|
|
543
543
|
try {
|
|
544
|
-
execSync(`npx vitest run ${gate.target} --reporter=dot`, { encoding: "utf-8", timeout: GATE_TEST_TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"] });
|
|
544
|
+
execSync(`npx vitest run ${gate.target} --reporter=dot`, { encoding: "utf-8", timeout: GATE_TEST_TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"], ...(cwd ? { cwd } : {}) });
|
|
545
545
|
return { gate, passed: true, output: "tests passed" };
|
|
546
546
|
} catch (e) {
|
|
547
547
|
return { gate, passed: false, output: e instanceof Error ? e.message.slice(0, GATE_OUTPUT_LIMIT) : "tests failed" };
|
|
@@ -553,15 +553,64 @@ export function runGates(db: Db, artifactId: string): GateResult[] {
|
|
|
553
553
|
});
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
-
|
|
556
|
+
/**
|
|
557
|
+
* Runs one gate command with two invariants a prior implementation lacked (a real incident; see
|
|
558
|
+
* GateRunOptions.cwd's doc comment):
|
|
559
|
+
* 1. `cwd` is always explicit, never inherited from the daemon's own process cwd.
|
|
560
|
+
* 2. The whole process group is killed on timeout, not just the immediate shell. `exec()`'s own
|
|
561
|
+
* `timeout` option only signals the process it directly spawned (the shell running
|
|
562
|
+
* `command`); a shell's own child (e.g. `bun` under `sh -c "bun test"`) is not in general
|
|
563
|
+
* killed by that signal and can be reparented and keep running -- and consuming memory --
|
|
564
|
+
* indefinitely after Papyrus considers the gate "timed out". Spawning detached (its own
|
|
565
|
+
* process group) and killing the negated pid on our own timer reaches the whole tree.
|
|
566
|
+
*/
|
|
567
|
+
function executeGateCommand(command: string, timeout: number, cwd?: string): Promise<{ passed: boolean; output: string }> {
|
|
568
|
+
// `spawn(..., { shell: true, detached: true })` instead of the `exec()` convenience wrapper:
|
|
569
|
+
// `detached` (needed to make the shell the leader of its own process group, so the negated pid
|
|
570
|
+
// below reaches every descendant, not just the shell) is not part of Node's `exec()`/
|
|
571
|
+
// `ExecOptions` type at all -- `spawn`'s options support it directly and correctly.
|
|
572
|
+
const { spawn } = require_("node:child_process") as typeof import("node:child_process");
|
|
557
573
|
return new Promise((resolve) => {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
574
|
+
let settled = false;
|
|
575
|
+
let buffered = "";
|
|
576
|
+
let truncated = false;
|
|
577
|
+
const child = spawn(command, { shell: true, detached: true, ...(cwd ? { cwd } : {}) });
|
|
578
|
+
|
|
579
|
+
const append = (chunk: Buffer): void => {
|
|
580
|
+
if (truncated) return;
|
|
581
|
+
buffered += chunk.toString("utf8");
|
|
582
|
+
if (buffered.length > GATE_MAX_BUFFER_BYTES) {
|
|
583
|
+
buffered = buffered.slice(0, GATE_MAX_BUFFER_BYTES);
|
|
584
|
+
truncated = true;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
child.stdout?.on("data", append);
|
|
588
|
+
child.stderr?.on("data", append);
|
|
589
|
+
|
|
590
|
+
const finish = (result: { passed: boolean; output: string }): void => {
|
|
591
|
+
if (settled) return;
|
|
592
|
+
settled = true;
|
|
593
|
+
clearTimeout(timer);
|
|
594
|
+
resolve(result);
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
child.on("error", (error) => finish({ passed: false, output: error.message.slice(0, GATE_OUTPUT_LIMIT) }));
|
|
598
|
+
child.on("close", (code) => {
|
|
599
|
+
const output = buffered.trim().slice(0, GATE_OUTPUT_LIMIT);
|
|
600
|
+
finish({ passed: code === 0, output: output || (code === 0 ? "ok" : `command exited with code ${code}`) });
|
|
564
601
|
});
|
|
602
|
+
|
|
603
|
+
const timer = setTimeout(() => {
|
|
604
|
+
if (settled) return;
|
|
605
|
+
if (child.pid !== undefined) {
|
|
606
|
+
try {
|
|
607
|
+
process.kill(-child.pid, "SIGKILL");
|
|
608
|
+
} catch {
|
|
609
|
+
child.kill("SIGKILL");
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
finish({ passed: false, output: `gate command timed out after ${timeout}ms` });
|
|
613
|
+
}, timeout);
|
|
565
614
|
});
|
|
566
615
|
}
|
|
567
616
|
|
|
@@ -599,7 +648,7 @@ export async function runGatesAsync(db: Db, artifactId: string, options: GateRun
|
|
|
599
648
|
const command = gate.type === "test" ? `npx vitest run ${gate.target} --reporter=dot` : gate.target;
|
|
600
649
|
const configuredTimeout = gate.type === "test" ? GATE_TEST_TIMEOUT_MS : GATE_COMMAND_TIMEOUT_MS;
|
|
601
650
|
const timeout = remainingMs === undefined ? configuredTimeout : Math.max(1, Math.min(configuredTimeout, remainingMs));
|
|
602
|
-
const executed = await executeGateCommand(command, timeout);
|
|
651
|
+
const executed = await executeGateCommand(command, timeout, options.cwd);
|
|
603
652
|
results.push({
|
|
604
653
|
gate,
|
|
605
654
|
passed: executed.passed && (gate.expect ? executed.output.includes(gate.expect) : true),
|
package/src/ports/gate-runner.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GateResult, GateRunOptions } from "../domain/gate.ts";
|
|
2
2
|
|
|
3
3
|
export interface GateRunner {
|
|
4
|
-
run(artifactId: string): GateResult[];
|
|
4
|
+
run(artifactId: string, options?: GateRunOptions): GateResult[];
|
|
5
5
|
runAsync(artifactId: string, options?: GateRunOptions): Promise<GateResult[]>;
|
|
6
6
|
}
|
package/src/service.ts
CHANGED
|
@@ -323,6 +323,7 @@ function handlers(
|
|
|
323
323
|
"tasks.complete": forwardToModule("tasks.complete"),
|
|
324
324
|
"tasks.run_gates": forwardToModule("tasks.run_gates"),
|
|
325
325
|
"tasks.set_checklist": forwardToModule("tasks.set_checklist"),
|
|
326
|
+
"tasks.set_gates": forwardToModule("tasks.set_gates"),
|
|
326
327
|
"tasks.context": forwardToModule("tasks.context"),
|
|
327
328
|
"tasks.reject": forwardToModule("tasks.reject"),
|
|
328
329
|
"tasks.retry": forwardToModule("tasks.retry"),
|
package/src/task-service.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import type { Artifact } from "./domain/artifact.ts";
|
|
13
13
|
import { checklistEntries, validateChecklist, type Checklist, type ProofReference } from "./domain/checklist.ts";
|
|
14
14
|
import { isDiscussionArtifact, readDiscussionExtra } from "./domain/discussion.ts";
|
|
15
|
-
import type
|
|
15
|
+
import { validateGates, type Gate, type GateResult } from "./domain/gate.ts";
|
|
16
16
|
import type { AppendTaskEvent, TaskEventContext, TaskHistoryPage, TaskHistoryQuery, TaskLifecycleStatus } from "./domain/task-event.ts";
|
|
17
17
|
import { normalizeProjectRoot, taskScopeLabel, type TaskScopeSource, type TaskViewMode, type TaskViewSelection } from "./domain/task-scope.ts";
|
|
18
18
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
@@ -141,7 +141,7 @@ export class Tasks {
|
|
|
141
141
|
if (input.parentId) this.require(input.parentId);
|
|
142
142
|
for (const dependency of input.dependsOn ?? []) this.require(dependency);
|
|
143
143
|
const extra: Record<string, unknown> = { ...(input.extra ?? {}) };
|
|
144
|
-
if (input.gates !== undefined) extra["gates"] = input.gates;
|
|
144
|
+
if (input.gates !== undefined) extra["gates"] = validateGates(input.gates);
|
|
145
145
|
if (input.checklist !== undefined) extra["checklist"] = validateChecklist(input.checklist);
|
|
146
146
|
const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
|
|
147
147
|
if (input.parentId && this.scopes.get(input.parentId)?.projectRoot !== projectRoot) {
|
|
@@ -424,7 +424,7 @@ export class Tasks {
|
|
|
424
424
|
const attemptId = crypto.randomUUID();
|
|
425
425
|
this.events.atomic(() => this.appendEvent({ taskId: id, type: "completion_attempted", fromStatus: "review", toStatus: "review", attemptId }, context));
|
|
426
426
|
const checklist = this.reviewChecklist(task);
|
|
427
|
-
const results = this.gates.run(id);
|
|
427
|
+
const results = this.gates.run(id, { cwd: this.scopes.get(id)?.projectRoot });
|
|
428
428
|
return this.resolveCompletion(id, attemptId, results, checklist, context, options);
|
|
429
429
|
}
|
|
430
430
|
|
|
@@ -434,14 +434,17 @@ export class Tasks {
|
|
|
434
434
|
const attemptId = crypto.randomUUID();
|
|
435
435
|
this.events.atomic(() => this.appendEvent({ taskId: id, type: "completion_attempted", fromStatus: "review", toStatus: "review", attemptId }, context));
|
|
436
436
|
const checklist = this.reviewChecklist(task);
|
|
437
|
-
|
|
437
|
+
// project_root, never the daemon's own inherited process cwd -- see GateRunOptions.cwd's doc
|
|
438
|
+
// comment for the real incident this fixes (a command gate once tested the daemon's entire
|
|
439
|
+
// home directory instead of the task's project and crashed the bun process outright).
|
|
440
|
+
const results = await this.gates.runAsync(id, { deadlineMs: options.gateDeadlineMs, cwd: this.scopes.get(id)?.projectRoot });
|
|
438
441
|
this.requireReview(id);
|
|
439
442
|
return this.resolveCompletion(id, attemptId, results, checklist, context, options);
|
|
440
443
|
}
|
|
441
444
|
|
|
442
445
|
async runGates(id: string, context: TaskEventContext = {}): Promise<GateResult[]> {
|
|
443
446
|
this.require(id);
|
|
444
|
-
const results = await this.gates.runAsync(id);
|
|
447
|
+
const results = await this.gates.runAsync(id, { cwd: this.scopes.get(id)?.projectRoot });
|
|
445
448
|
this.events.atomic(() => this.appendEvent({ taskId: id, type: "gates_evaluated", evidence: { gates: results, result: results.every((gate) => gate.passed) ? "passed" : "failed" } }, context));
|
|
446
449
|
return results;
|
|
447
450
|
}
|
|
@@ -456,6 +459,16 @@ export class Tasks {
|
|
|
456
459
|
return this.artifacts.setExtra(id, { ...task.extra, checklist: validateChecklist(checklist) })!;
|
|
457
460
|
}
|
|
458
461
|
|
|
462
|
+
/**
|
|
463
|
+
* The only way to change a task's gates after creation. "tasks update" (title/body/labels/
|
|
464
|
+
* status only) silently ignored a `gates` field with no error at all -- a real incident (see
|
|
465
|
+
* GateRunOptions.cwd's doc comment for the crash this masked while debugging).
|
|
466
|
+
*/
|
|
467
|
+
setGates(id: string, gates: Gate[]): Artifact {
|
|
468
|
+
const task = this.require(id);
|
|
469
|
+
return this.artifacts.setExtra(id, { ...task.extra, gates: validateGates(gates) })!;
|
|
470
|
+
}
|
|
471
|
+
|
|
459
472
|
depend(id: string, dependencyId: string, context: TaskEventContext = {}): Artifact {
|
|
460
473
|
return this.events.atomic(() => {
|
|
461
474
|
this.require(id);
|