@visulima/task-runner 1.0.0-alpha.4 → 1.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/archive.d.ts +38 -0
  3. package/dist/cache.d.ts +39 -4
  4. package/dist/chrome-trace.d.ts +53 -0
  5. package/dist/file-access-tracker.d.ts +7 -1
  6. package/dist/fingerprint.d.ts +9 -0
  7. package/dist/flow-controllers/input-handler.d.ts +1 -1
  8. package/dist/incremental-hasher.d.ts +18 -0
  9. package/dist/index.d.ts +9 -4
  10. package/dist/index.js +23 -20
  11. package/dist/life-cycle.d.ts +2 -0
  12. package/dist/log-reporter.d.ts +34 -0
  13. package/dist/output-resolver.d.ts +20 -0
  14. package/dist/packem_shared/{Cache-C23LywYn.js → Cache-CWaX_c8U.js} +137 -46
  15. package/dist/packem_shared/{CompositeLifeCycle-7AtYw1dv.js → CompositeLifeCycle-CSVbRC_5.js} +10 -0
  16. package/dist/packem_shared/{FileAccessTracker-CrtBAt5D.js → FileAccessTracker-CQ5Ot7Hd.js} +68 -16
  17. package/dist/packem_shared/{FingerprintManager-Cu-ta9ee.js → FingerprintManager-CV7U4f4f.js} +22 -1
  18. package/dist/packem_shared/{IncrementalFileHasher-Cm_kJY5V.js → IncrementalFileHasher-BRS76-mb.js} +26 -0
  19. package/dist/packem_shared/LogReporter-BDt52HLu.js +44 -0
  20. package/dist/packem_shared/{RemoteCache-BFceSe4a.js → RemoteCache-DSU3lc87.js} +77 -37
  21. package/dist/packem_shared/{TaskOrchestrator-lLn-PH1m.js → TaskOrchestrator-rf45vW5c.js} +94 -15
  22. package/dist/packem_shared/{TerminalBuffer-D6zP2zLh.js → TerminalBuffer-qVJvbRQZ.js} +53 -37
  23. package/dist/packem_shared/{TrackedTaskExecutor-BGUKFE-7.js → TrackedTaskExecutor-CFPpQfXF.js} +1 -1
  24. package/dist/packem_shared/archive-UQHAnZUa.js +102 -0
  25. package/dist/packem_shared/{buildForwardDependencyMap-Cu08NWB1.js → buildForwardDependencyMap-DLPgKEto.js} +6 -3
  26. package/dist/packem_shared/{computeTaskHash-B2SVZqgp.js → computeTaskHash-DYqfrDGq.js} +122 -6
  27. package/dist/packem_shared/{createTaskGraph-CcsFaSrz.js → createTaskGraph-B7nH0kY_.js} +2 -2
  28. package/dist/packem_shared/{defaultTaskRunner-X1MIynHu.js → defaultTaskRunner-Cp7jCmIl.js} +28 -6
  29. package/dist/packem_shared/{enforceProjectConstraints-_Ej0zHch.js → enforceProjectConstraints-C5Jp_C3u.js} +23 -2
  30. package/dist/packem_shared/{extractPackageName-CbVNW-dr.js → extractPackageName-BllKetnz.js} +2 -1
  31. package/dist/packem_shared/{generateRunSummary-qn-_jKwt.js → generateRunSummary-BE1jnQ3H.js} +19 -1
  32. package/dist/packem_shared/{parsePartition-C4-P5RjK.js → parsePartition-BfLbHGAx.js} +18 -0
  33. package/dist/packem_shared/{projectGraphToDot-C8uYeaPo.js → projectGraphToDot-DU1lSe-c.js} +1 -1
  34. package/dist/packem_shared/resolveOutputs-n6MCKoTe.js +111 -0
  35. package/dist/packem_shared/{runConcurrentFallback-3q46z4AS.js → runConcurrentFallback-BTmgGV1H.js} +20 -6
  36. package/dist/packem_shared/{runConcurrently-ATDwJNR6.js → runConcurrently-CmfC4r-f.js} +1 -1
  37. package/dist/packem_shared/toChromeTrace-B2tZoJ-7.js +121 -0
  38. package/dist/project-constraints.d.ts +3 -10
  39. package/dist/remote-cache.d.ts +45 -0
  40. package/dist/run-summary.d.ts +26 -4
  41. package/dist/task-hasher.d.ts +37 -0
  42. package/dist/task-orchestrator.d.ts +2 -2
  43. package/dist/types.d.ts +168 -15
  44. package/index.js +723 -553
  45. package/package.json +13 -13
@@ -0,0 +1,121 @@
1
+ import { createRequire as __cjs_createRequire } from "node:module";
2
+
3
+ const __cjs_require = __cjs_createRequire(import.meta.url);
4
+
5
+ const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
6
+
7
+ const __cjs_getBuiltinModule = (module) => {
8
+ // Check if we're in Node.js and version supports getBuiltinModule
9
+ if (typeof __cjs_getProcess !== "undefined" && __cjs_getProcess.versions && __cjs_getProcess.versions.node) {
10
+ const [major, minor] = __cjs_getProcess.versions.node.split(".").map(Number);
11
+ // Node.js 20.16.0+ and 22.3.0+
12
+ if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
13
+ return __cjs_getProcess.getBuiltinModule(module);
14
+ }
15
+ }
16
+ // Fallback to createRequire
17
+ return __cjs_require(module);
18
+ };
19
+
20
+ const {
21
+ writeFile
22
+ } = __cjs_getBuiltinModule("node:fs/promises");
23
+
24
+ const TASK_CATEGORY = "task";
25
+ const FLOW_CATEGORY = "dep";
26
+ const PID = 1;
27
+ const toChromeTrace = (summary) => {
28
+ const events = [];
29
+ const traceStartMs = Date.parse(summary.startTime);
30
+ const tasksByStart = [...summary.tasks].sort((a, b) => {
31
+ const aStart = a.startTime ? Date.parse(a.startTime) : Number.POSITIVE_INFINITY;
32
+ const bStart = b.startTime ? Date.parse(b.startTime) : Number.POSITIVE_INFINITY;
33
+ return aStart - bStart;
34
+ });
35
+ const lanes = [];
36
+ const taskLane = /* @__PURE__ */ new Map();
37
+ for (const task of tasksByStart) {
38
+ if (!task.startTime || !task.endTime) {
39
+ continue;
40
+ }
41
+ const startMs = Date.parse(task.startTime);
42
+ const endMs = Date.parse(task.endTime);
43
+ let lane = lanes.findIndex((laneEnd) => laneEnd <= startMs);
44
+ if (lane === -1) {
45
+ lane = lanes.length;
46
+ lanes.push(endMs);
47
+ } else {
48
+ lanes[lane] = endMs;
49
+ }
50
+ taskLane.set(task.taskId, lane);
51
+ events.push({
52
+ args: {
53
+ cacheStatus: task.cacheStatus,
54
+ exitCode: task.exitCode,
55
+ hash: task.hash,
56
+ project: task.target.project,
57
+ target: task.target.target
58
+ },
59
+ cat: TASK_CATEGORY,
60
+ dur: Math.max(0, (endMs - startMs) * 1e3),
61
+ name: task.taskId,
62
+ ph: "X",
63
+ pid: PID,
64
+ tid: lane,
65
+ ts: (startMs - traceStartMs) * 1e3
66
+ });
67
+ }
68
+ let flowId = 1;
69
+ for (const task of summary.tasks) {
70
+ if (!task.startTime || !task.endTime) {
71
+ continue;
72
+ }
73
+ const dependents = task.dependencies ?? [];
74
+ for (const depId of dependents) {
75
+ const dep = summary.tasks.find((t) => t.taskId === depId);
76
+ if (!dep?.endTime || !task.startTime) {
77
+ continue;
78
+ }
79
+ const depEndUs = (Date.parse(dep.endTime) - traceStartMs) * 1e3;
80
+ const taskStartUs = (Date.parse(task.startTime) - traceStartMs) * 1e3;
81
+ const id = flowId;
82
+ flowId += 1;
83
+ events.push(
84
+ {
85
+ cat: FLOW_CATEGORY,
86
+ id,
87
+ name: `${depId} → ${task.taskId}`,
88
+ ph: "s",
89
+ pid: PID,
90
+ tid: taskLane.get(depId) ?? 0,
91
+ ts: depEndUs
92
+ },
93
+ {
94
+ cat: FLOW_CATEGORY,
95
+ id,
96
+ name: `${depId} → ${task.taskId}`,
97
+ ph: "f",
98
+ pid: PID,
99
+ tid: taskLane.get(task.taskId) ?? 0,
100
+ ts: taskStartUs
101
+ }
102
+ );
103
+ }
104
+ }
105
+ events.unshift({
106
+ args: { name: `vis run (${summary.id})` },
107
+ cat: "__metadata",
108
+ name: "process_name",
109
+ ph: "M",
110
+ pid: PID,
111
+ tid: 0,
112
+ ts: 0
113
+ });
114
+ return events;
115
+ };
116
+ const writeChromeTrace = async (summary, outputPath) => {
117
+ const events = toChromeTrace(summary);
118
+ await writeFile(outputPath, JSON.stringify({ traceEvents: events }));
119
+ };
120
+
121
+ export { toChromeTrace, writeChromeTrace };
@@ -1,16 +1,9 @@
1
1
  import type { ConstraintsConfig, ConstraintViolation, ProjectGraph } from "./types.d.ts";
2
2
  /**
3
3
  * Enforces project dependency constraints on a project graph.
4
- * Returns an array of violations found. Does not throw — the caller
5
- * decides how to handle violations (fatal error, warning, etc.).
6
- *
7
- * Three constraint mechanisms:
8
- * 1. **Tag relationships**: If a project has a tag listed in `tagRelationships`,
9
- * its dependencies must have at least one of the required tags.
10
- * 2. **Type boundaries**: Controls which project types can depend on which.
11
- * By default, no project may depend on an "application" type project.
12
- * 3. **Dependency kind rules**: Controls rules based on whether the dependency
13
- * is a production dependency, devDependency, or peerDependency.
4
+ * @param projectGraph The workspace project graph to validate.
5
+ * @param constraints The constraint rules to enforce.
6
+ * @returns Array of violations found. Empty means all constraints pass.
14
7
  */
15
8
  declare const enforceProjectConstraints: (projectGraph: ProjectGraph, constraints: ConstraintsConfig) => ConstraintViolation[];
16
9
  export { enforceProjectConstraints };
@@ -1,7 +1,45 @@
1
+ /**
2
+ * Compression algorithm used for artifact tarballs on the wire.
3
+ * - `"gzip"` (default): tar+gzip, matches Turborepo's protocol format
4
+ * and stays interop-safe with existing remote cache servers.
5
+ * - `"brotli"`: tar + Node brotli (BROTLI_MODE_TEXT, quality 4) — a
6
+ * solid ratio/speed trade-off for source-tree tarballs. Both upload
7
+ * and download sides must agree; switching invalidates existing
8
+ * remote entries (they will simply re-populate on next run).
9
+ */
10
+ export type RemoteCacheCompression = "brotli" | "gzip";
11
+ /**
12
+ * HMAC signing configuration for cache integrity.
13
+ *
14
+ * When set, every upload carries an `X-Artifact-Signature` header
15
+ * containing the HMAC-SHA256 digest of `hash | body`. On download,
16
+ * the client recomputes the HMAC and rejects any artifact whose
17
+ * signature doesn't match using a constant-time comparison.
18
+ *
19
+ * Prevents cache poisoning in shared team environments where a
20
+ * compromised cache server (or a MITM) could inject malicious
21
+ * artifacts. Unsigned entries (written before signing was enabled)
22
+ * are accepted only when `verifyOnDownload === false` — the default.
23
+ */
24
+ export interface RemoteCacheSigning {
25
+ /** Shared secret. Must be at least 16 characters. */
26
+ secret: string;
27
+ /**
28
+ * Reject downloads whose signature doesn't match or is missing.
29
+ * Set to `true` once every upload on your server is signed.
30
+ * @default false
31
+ */
32
+ verifyOnDownload?: boolean;
33
+ }
1
34
  /**
2
35
  * Options for the remote cache.
3
36
  */
4
37
  interface RemoteCacheOptions {
38
+ /**
39
+ * Compression format for artifact tarballs. Defaults to `"gzip"`
40
+ * for Turborepo protocol compatibility.
41
+ */
42
+ compression?: RemoteCacheCompression;
5
43
  /**
6
44
  * Called when a fire-and-forget upload fails.
7
45
  * Since uploads are non-blocking, errors are silently swallowed by default.
@@ -10,6 +48,13 @@ interface RemoteCacheOptions {
10
48
  onUploadError?: (hash: string, error: unknown) => void;
11
49
  /** Whether to enable remote cache reads */
12
50
  read?: boolean;
51
+ /**
52
+ * HMAC-SHA256 signing for upload integrity. When set, every
53
+ * uploaded artifact carries an `X-Artifact-Signature` header;
54
+ * downloads with `verifyOnDownload: true` reject unsigned or
55
+ * tampered payloads.
56
+ */
57
+ signing?: RemoteCacheSigning;
13
58
  /** Team ID or namespace for cache isolation */
14
59
  teamId?: string;
15
60
  /** Request timeout in milliseconds (default: 30000) */
@@ -1,4 +1,4 @@
1
- import type { TaskGraph, TaskHashDetails, TaskResults } from "./types.d.ts";
1
+ import type { OutputSpec, TaskGraph, TaskHashDetails, TaskResults } from "./types.d.ts";
2
2
  /**
3
3
  * Summary of a single task execution.
4
4
  */
@@ -19,8 +19,8 @@ interface TaskSummary {
19
19
  hash: string | undefined;
20
20
  /** Detailed hash information */
21
21
  hashDetails: TaskHashDetails | undefined;
22
- /** The task's declared outputs */
23
- outputs: string[];
22
+ /** The task's declared outputs (glob patterns, literals, or `{ auto: true }`). */
23
+ outputs: OutputSpec[];
24
24
  /** Start time (ISO 8601) */
25
25
  startTime: string | undefined;
26
26
  /** The task target */
@@ -85,5 +85,27 @@ declare const generateRunSummary: (results: TaskResults, taskGraph: TaskGraph, s
85
85
  * @returns The path to the written summary file
86
86
  */
87
87
  declare const writeRunSummary: (summary: RunSummary, workspaceRoot: string) => Promise<string>;
88
+ /**
89
+ * Path where the most-recent run summary is persisted.
90
+ * Consumers (e.g. CLIs exposing `--last-details`) read this file
91
+ * to replay or render the previous run without re-executing.
92
+ */
93
+ declare const getLastRunSummaryPath: (workspaceRoot: string) => string;
94
+ /**
95
+ * Persists `summary` as the most-recent run summary at
96
+ * `.task-runner/last-summary.json`, overwriting any previous entry.
97
+ *
98
+ * This is the companion to {@link readLastRunSummary} and powers
99
+ * CLI surfaces that display "last run" details without re-running tasks.
100
+ * @returns The path to the written summary file
101
+ */
102
+ declare const writeLastRunSummary: (summary: RunSummary, workspaceRoot: string) => Promise<string>;
103
+ /**
104
+ * Reads the most-recent run summary written by {@link writeLastRunSummary}.
105
+ * Returns `undefined` when no previous run has been recorded or the file
106
+ * cannot be parsed — callers should render an informational message
107
+ * instead of treating this as an error.
108
+ */
109
+ declare const readLastRunSummary: (workspaceRoot: string) => Promise<RunSummary | undefined>;
88
110
  export type { RunSummary, TaskSummary };
89
- export { generateRunSummary, writeRunSummary };
111
+ export { generateRunSummary, getLastRunSummaryPath, readLastRunSummary, writeLastRunSummary, writeRunSummary };
@@ -1,14 +1,29 @@
1
+ import type { IncrementalFileHasher } from "./incremental-hasher.d.ts";
1
2
  import type { NamedInputs, ProjectConfiguration, TargetConfiguration, Task, TaskHashDetails } from "./types.d.ts";
2
3
  /**
3
4
  * Interface for task hashers.
4
5
  */
5
6
  interface TaskHasher {
6
7
  hashTask: (task: Task) => Promise<TaskHashDetails>;
8
+ /**
9
+ * Rehashes a single file bypassing any in-memory cache. Optional to keep
10
+ * external/custom hashers backward compatible; the orchestrator skips
11
+ * self-modifying-task detection when the implementation is absent.
12
+ */
13
+ rehashFile?: (filePath: string) => Promise<string | undefined>;
7
14
  }
8
15
  /**
9
16
  * Options for creating an InProcessTaskHasher.
10
17
  */
11
18
  interface TaskHasherOptions {
19
+ /**
20
+ * When true, scan each task's resolved command for `$VAR`/`${VAR}`
21
+ * references and auto-fingerprint them. Catches the common case of
22
+ * a script reading `$VERCEL_URL` or `${NEXT_PUBLIC_API}` without
23
+ * the user remembering to declare it in `envVars`/`globalEnv`.
24
+ * @default false
25
+ */
26
+ autoEnvVars?: boolean;
12
27
  /** Additional environment variables to include in hash */
13
28
  envVars?: string[];
14
29
  /**
@@ -27,6 +42,17 @@ interface TaskHasherOptions {
27
42
  * These are workspace-root-relative paths (e.g., "pnpm-lock.yaml").
28
43
  */
29
44
  globalInputs?: string[];
45
+ /**
46
+ * Optional persistent mtime/size-indexed file snapshot. When set,
47
+ * `#hashFile` consults the snapshot first and only re-reads file
48
+ * contents when the file's mtime or size has changed since the
49
+ * previous run. Cuts cold-cache fingerprint time dramatically on
50
+ * large workspaces where most source files don't change run-to-run.
51
+ *
52
+ * The caller is responsible for `load()`ing the snapshot before
53
+ * using the hasher and `save()`ing it after the run completes.
54
+ */
55
+ incrementalHasher?: IncrementalFileHasher;
30
56
  /** Named input definitions */
31
57
  namedInputs?: NamedInputs;
32
58
  /** Project configurations keyed by project name */
@@ -55,6 +81,17 @@ declare class InProcessTaskHasher implements TaskHasher {
55
81
  constructor(options: TaskHasherOptions);
56
82
  hashTask(task: Task): Promise<TaskHashDetails>;
57
83
  clearCache(): void;
84
+ /**
85
+ * Reads `filePath` fresh and returns its content hash, bypassing the
86
+ * in-memory cache used during the initial `hashTask` pass.
87
+ *
88
+ * Used to detect tasks that modify their own tracked inputs: compare
89
+ * a pre-execution hash (from `task.hashDetails.nodes`) against the
90
+ * post-execution result of this method.
91
+ * @param filePath Absolute path to the file.
92
+ * @returns The fresh xxh3 hash, or `undefined` if the file cannot be read.
93
+ */
94
+ rehashFile(filePath: string): Promise<string | undefined>;
58
95
  }
59
96
  /**
60
97
  * Computes the final hash for a task from its hash details.
@@ -2,7 +2,7 @@ import type { Cache } from "./cache.d.ts";
2
2
  import type { RemoteCache } from "./remote-cache.d.ts";
3
3
  import type { TaskHasher } from "./task-hasher.d.ts";
4
4
  import type { TaskScheduler } from "./task-scheduler.d.ts";
5
- import type { LifeCycleInterface, Task, TaskExecutor, TaskResults } from "./types.d.ts";
5
+ import type { LifeCycleInterface, Task, TaskExecutor, TaskGraph, TaskResults } from "./types.d.ts";
6
6
  /**
7
7
  * Options for the TaskOrchestrator.
8
8
  */
@@ -20,7 +20,7 @@ interface TaskOrchestratorOptions {
20
20
  skipCache?: boolean;
21
21
  summarize?: boolean;
22
22
  taskExecutor: TaskExecutor;
23
- taskGraph?: import("./types").TaskGraph;
23
+ taskGraph?: TaskGraph;
24
24
  taskHasher: TaskHasher;
25
25
  untrackedEnvVars?: string[];
26
26
  workspaceRoot: string;
package/dist/types.d.ts CHANGED
@@ -12,6 +12,32 @@ export interface TaskTarget {
12
12
  /**
13
13
  * Represents a single task to be executed.
14
14
  */
15
+ /**
16
+ * Scheduling priority hint. The scheduler picks higher-priority tasks
17
+ * out of the ready-queue first; ties fall through to the graph-derived
18
+ * signals (dependent count, project depth).
19
+ *
20
+ * Use `"high"` for long-running leaves you want to kick off early
21
+ * (integration tests, e2e suites) so their wall-clock overlaps with
22
+ * the main build phase. Use `"low"` for work you don't mind deferring
23
+ * (linters, coverage uploads) when faster tasks contend for slots.
24
+ */
25
+ export type TaskPriority = "high" | "low" | "normal";
26
+ /**
27
+ * A single entry in a task's `outputs` list.
28
+ *
29
+ * - `string` — a literal path *or* a glob pattern relative to the
30
+ * workspace root. Prefix with `!` to exclude files the positive
31
+ * patterns would otherwise include (e.g. `"!dist/cache/**"`).
32
+ * - `{ auto: true }` — use whatever files the task actually wrote
33
+ * during execution (resolved from the file-access tracker). Lets
34
+ * authors who don't know their exact output layout cache results
35
+ * without listing every path. Silently behaves as "no outputs" when
36
+ * tracking isn't active for this task.
37
+ */
38
+ export type OutputSpec = string | {
39
+ auto: true;
40
+ };
15
41
  export interface Task {
16
42
  /** Whether this task is eligible for caching */
17
43
  cache?: boolean;
@@ -21,12 +47,22 @@ export interface Task {
21
47
  hashDetails?: TaskHashDetails;
22
48
  /** Unique identifier for the task, typically "project:target:configuration" */
23
49
  id: string;
24
- /** Output file paths produced by this task */
25
- outputs: string[];
50
+ /**
51
+ * Output patterns this task produces. Each entry is either a
52
+ * literal path, a glob (`"dist/**"`), a negative glob
53
+ * (`"!dist/cache/**"`), or `{ auto: true }` to pick up whatever
54
+ * files the task wrote during execution.
55
+ */
56
+ outputs: OutputSpec[];
26
57
  /** Overrides/extra options passed to the task */
27
58
  overrides: Record<string, unknown>;
28
59
  /** Whether this task supports parallel execution */
29
60
  parallelism?: boolean;
61
+ /**
62
+ * Explicit scheduling priority. Outranks the scheduler's
63
+ * graph-derived heuristics. Defaults to `"normal"` when absent.
64
+ */
65
+ priority?: TaskPriority;
30
66
  /** The project root directory */
31
67
  projectRoot?: string;
32
68
  /** The target this task executes */
@@ -66,8 +102,23 @@ export type TaskStatus = "success" | "failure" | "skipped" | "local-cache" | "lo
66
102
  export interface TaskResult {
67
103
  /** The exit code, if applicable */
68
104
  code?: number;
105
+ /**
106
+ * Set when auto-fingerprint tracking was attempted for this task but
107
+ * returned zero workspace accesses — usually a static binary on
108
+ * macOS/Windows, where the Node preload can't attach. Caching is
109
+ * skipped to avoid persisting an empty fingerprint that would
110
+ * produce false cache hits on every subsequent run.
111
+ */
112
+ emptyFingerprint?: boolean;
69
113
  /** The end time of the task */
70
114
  endTime?: number;
115
+ /**
116
+ * Set when the task modified one or more of its own tracked input
117
+ * files during execution. Caching is skipped in this case — the
118
+ * fingerprint captured before the run would mismatch the post-run
119
+ * contents and produce false cache hits on subsequent runs.
120
+ */
121
+ selfModified?: boolean;
71
122
  /** The start time of the task */
72
123
  startTime?: number;
73
124
  status: TaskStatus;
@@ -85,6 +136,15 @@ export type TaskResults = Map<string, TaskResult>;
85
136
  export interface ProjectConfiguration {
86
137
  /** Implicit dependencies on other projects */
87
138
  implicitDependencies?: string[];
139
+ /**
140
+ * Project layer in the dependency hierarchy. Used by
141
+ * `enforceLayerRelationships` to ensure projects only depend on
142
+ * projects at the same or lower layer.
143
+ *
144
+ * Hierarchy (lowest → highest):
145
+ * `configuration < library < scaffolding < tool < automation < application`
146
+ */
147
+ layer?: "application" | "automation" | "configuration" | "library" | "scaffolding" | "tool";
88
148
  /** The project type */
89
149
  projectType?: "library" | "application";
90
150
  /** The root directory of the project, relative to workspace root */
@@ -136,11 +196,30 @@ export interface TargetDependencyConfig {
136
196
  * Defines an input for cache invalidation.
137
197
  */
138
198
  export type InputDefinition = FileSetInput | RuntimeInput | EnvironmentInput | ExternalDependencyInput;
199
+ /**
200
+ * Controls how a glob pattern is anchored.
201
+ * - "package" (default): pattern is resolved relative to the project root
202
+ * - "workspace": pattern is resolved relative to the workspace root
203
+ */
204
+ export type FileSetBase = "package" | "workspace";
139
205
  /**
140
206
  * An input based on file patterns.
207
+ *
208
+ * `fileset` may be a bare glob string (package-root relative) or an object
209
+ * form `{ pattern, base }` to anchor explicitly to the workspace root.
210
+ * Negation (`!` prefix) works in both forms.
141
211
  */
142
212
  export interface FileSetInput {
143
- fileset: string;
213
+ fileset: FileSetPattern | string;
214
+ }
215
+ /**
216
+ * Object form of a fileset pattern, for anchoring to the workspace root.
217
+ */
218
+ export interface FileSetPattern {
219
+ /** Anchor for the pattern. */
220
+ base: FileSetBase;
221
+ /** Glob pattern (may start with `!` for negation). */
222
+ pattern: string;
144
223
  }
145
224
  /**
146
225
  * An input based on a runtime command.
@@ -210,6 +289,16 @@ export interface DependencyKindRules {
210
289
  export interface ConstraintsConfig {
211
290
  /** Rules based on the dependency kind (static vs devDependency vs peerDependency) */
212
291
  dependencyKindRules?: DependencyKindRules;
292
+ /**
293
+ * When true, projects can only depend on projects at the same or
294
+ * lower layer in the hierarchy:
295
+ *
296
+ * configuration < library < scaffolding < tool < automation < application
297
+ *
298
+ * Projects without an explicit `layer` are unconstrained.
299
+ * @default false
300
+ */
301
+ enforceLayerRelationships?: boolean;
213
302
  /** Tag-based dependency rules */
214
303
  tagRelationships?: TagRelationships;
215
304
  /** Project-type-based dependency rules */
@@ -224,7 +313,7 @@ export interface ConstraintViolation {
224
313
  /** Human-readable description of the violation */
225
314
  message: string;
226
315
  /** The type of rule that was violated */
227
- rule: "dependency-kind" | "tag-relationship" | "type-boundary";
316
+ rule: "dependency-kind" | "layer-relationship" | "tag-relationship" | "type-boundary";
228
317
  /** The project that has the invalid dependency */
229
318
  sourceProject: string;
230
319
  }
@@ -292,6 +381,14 @@ export interface NamedInputs {
292
381
  * Configuration for the task runner.
293
382
  */
294
383
  export interface TaskRunnerOptions {
384
+ /**
385
+ * Scan each task's resolved command text for `$VAR`/`${VAR}`
386
+ * references and automatically fingerprint those env vars. Catches
387
+ * tasks like `curl ${VERCEL_URL}/api` where the user forgot to
388
+ * declare the reference in `envVars`/`globalEnv`.
389
+ * @default false
390
+ */
391
+ autoEnvVars?: boolean;
295
392
  /**
296
393
  * Enable auto-fingerprinting mode (Vite Task-style).
297
394
  * When enabled, the task runner automatically tracks which files
@@ -355,12 +452,37 @@ export interface TaskRunnerOptions {
355
452
  * forcing a full rebuild. This matches Turborepo's `globalDependencies`.
356
453
  */
357
454
  globalInputs?: string[];
455
+ /**
456
+ * When `true`, file hashes are cached in a persistent mtime+size
457
+ * indexed snapshot under `node_modules/.cache/task-runner/`. On
458
+ * subsequent runs, unchanged files skip content re-reads — cuts
459
+ * cold-cache fingerprint time dramatically on large workspaces.
460
+ *
461
+ * Changed or new files still get full content hashing. Safe to
462
+ * leave on by default; overhead when nothing matches is a single
463
+ * `stat` call per file.
464
+ * @default false
465
+ */
466
+ incrementalFileHashing?: boolean;
358
467
  /** Maximum age of cache entries in milliseconds */
359
468
  maxCacheAge?: number;
360
469
  /** Maximum cache size (e.g., "1GB") */
361
470
  maxCacheSize?: string;
362
471
  /** Named inputs for cache invalidation */
363
472
  namedInputs?: NamedInputs;
473
+ /**
474
+ * When `true`, the cache directory is partitioned by a hash of
475
+ * the resolved `globalEnv` values. Changing a globalEnv var moves
476
+ * cache writes into a new namespace; rolling it back reuses the
477
+ * old namespace and its hits. Without this option, any globalEnv
478
+ * change silently busts every cached entry.
479
+ *
480
+ * Keep disabled when globalEnv is stable across runs — the extra
481
+ * path depth offers no value, and misconfigured namespaces can
482
+ * hide stale hits.
483
+ * @default false
484
+ */
485
+ namespaceByGlobalEnv?: boolean;
364
486
  /** Maximum number of parallel tasks */
365
487
  parallel?: number | boolean;
366
488
  /**
@@ -462,12 +584,12 @@ export type ConcurrentCommandInput = string | {
462
584
  cwd?: string;
463
585
  env?: Record<string, string>;
464
586
  name?: string;
465
- stdin?: "inherit" | "null" | "pipe" | "pty";
466
587
  /** Initial PTY dimensions (only used when stdin is "pty"). */
467
588
  ptySize?: {
468
589
  cols: number;
469
590
  rows: number;
470
591
  };
592
+ stdin?: "inherit" | "null" | "pipe" | "pty";
471
593
  };
472
594
  /**
473
595
  * Configuration for a single command to run concurrently.
@@ -481,6 +603,14 @@ export interface ConcurrentCommandConfig {
481
603
  env?: Record<string, string>;
482
604
  /** Human-readable name for this command (used in prefixes/logs). */
483
605
  name?: string;
606
+ /**
607
+ * Initial PTY dimensions. Only used when stdin is "pty".
608
+ * Defaults to 80x24 if not specified.
609
+ */
610
+ ptySize?: {
611
+ cols: number;
612
+ rows: number;
613
+ };
484
614
  /** Whether to use shell execution (default: true). */
485
615
  shell?: boolean;
486
616
  /**
@@ -491,14 +621,6 @@ export interface ConcurrentCommandConfig {
491
621
  * - "pty": child runs inside a pseudo-terminal (isatty() returns true, enables interactive prompts)
492
622
  */
493
623
  stdin?: "inherit" | "null" | "pipe" | "pty";
494
- /**
495
- * Initial PTY dimensions. Only used when stdin is "pty".
496
- * Defaults to 80x24 if not specified.
497
- */
498
- ptySize?: {
499
- cols: number;
500
- rows: number;
501
- };
502
624
  }
503
625
  /**
504
626
  * Options controlling the concurrent runner behavior.
@@ -548,10 +670,10 @@ export interface ProcessEvent {
548
670
  exitCode?: number;
549
671
  /** Index of the command that produced this event. */
550
672
  index: number;
551
- /** Whether the process was killed (for close events). */
552
- killed?: boolean;
553
673
  /** Kill the child process/PTY. Only present on "started" events. */
554
674
  kill?: (signal?: string) => void;
675
+ /** Whether the process was killed (for close events). */
676
+ killed?: boolean;
555
677
  /** Event type: "stdout", "stderr", "close", "error", "started". */
556
678
  kind: "close" | "error" | "started" | "stderr" | "stdout";
557
679
  /** Error message (for error events). */
@@ -595,8 +717,39 @@ export interface ConcurrentRunResult {
595
717
  export interface LifeCycleInterface {
596
718
  endCommand?: () => void;
597
719
  endTasks?: (taskResults: TaskResult[]) => void;
720
+ /**
721
+ * Called when a running task emits data on stderr, in the order the
722
+ * data arrives. Executors that support streaming output (`vis run`'s
723
+ * concurrent executor) forward chunks here verbatim. Executors that
724
+ * only buffer full output (e.g. the simple test executor) skip this
725
+ * hook — rely on `printTaskTerminalOutput` for the final dump.
726
+ *
727
+ * Handlers should be non-blocking; the orchestrator doesn't await.
728
+ */
729
+ onTaskStderr?: (task: Task, chunk: string) => void;
730
+ /**
731
+ * Called when a running task emits data on stdout, in the order the
732
+ * data arrives. See {@link LifeCycleInterface.onTaskStderr} for
733
+ * semantics — same contract, different stream.
734
+ */
735
+ onTaskStdout?: (task: Task, chunk: string) => void;
598
736
  /** Called when a cache miss occurs with diagnostic information */
599
737
  printCacheMiss?: (task: Task, reasons: string) => void;
738
+ /**
739
+ * Called when caching was skipped because auto-fingerprint tracking
740
+ * came back empty — a signal that the tracker (strace or Node
741
+ * preload) couldn't observe the task's file access, typically
742
+ * because it's a static binary on a platform without strace.
743
+ * `reason` is a short human-readable diagnostic.
744
+ */
745
+ printEmptyFingerprintWarning?: (task: Task, reason: string) => void;
746
+ /**
747
+ * Called when caching is skipped because the task modified one or
748
+ * more of its own tracked inputs. `modifiedFiles` lists the
749
+ * workspace-relative paths that changed between pre- and
750
+ * post-execution hashes.
751
+ */
752
+ printSelfModifyingSkip?: (task: Task, modifiedFiles: string[]) => void;
600
753
  printTaskTerminalOutput?: (task: Task, status: TaskStatus, terminalOutput: string) => void;
601
754
  scheduleTask?: (task: Task) => void;
602
755
  startCommand?: () => void;