pi-crew 0.9.34 → 0.9.36
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 +138 -0
- package/dist/build-meta.json +261 -265
- package/dist/index.mjs +2635 -2442
- package/dist/index.mjs.map +4 -4
- package/docs/performance-optimization-execution-plan.md +520 -0
- package/package.json +1 -1
- package/src/extension/team-tool/api.ts +8 -8
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +2 -2
- package/src/extension/team-tool/handle-schedule.ts +102 -0
- package/src/extension/team-tool/run.ts +0 -42
- package/src/extension/team-tool.ts +13 -8
- package/src/runtime/adaptive-plan.ts +3 -3
- package/src/runtime/async-runner.ts +1 -1
- package/src/runtime/background-runner.ts +4 -4
- package/src/runtime/child-pi.ts +191 -56
- package/src/runtime/live-session-runtime.ts +27 -12
- package/src/runtime/skill-instructions.ts +61 -2
- package/src/runtime/stale-reconciler.ts +5 -0
- package/src/runtime/task-runner/tail-read.ts +12 -0
- package/src/runtime/task-runner.ts +40 -19
- package/src/runtime/team-runner.ts +95 -52
- package/src/runtime/verification-gates.ts +9 -4
- package/src/schema/team-tool-schema.ts +4 -0
- package/src/state/atomic-write.ts +5 -1
- package/src/state/instinct-store.ts +5 -0
- package/src/state/state-store.ts +75 -2
- package/src/utils/token-counter.ts +168 -35
- package/src/workflows/intermediate-store.ts +5 -0
- package/workflows/pipeline.workflow.md +13 -12
|
@@ -382,6 +382,10 @@ export interface TeamToolParamsValue {
|
|
|
382
382
|
cron?: string;
|
|
383
383
|
interval?: number;
|
|
384
384
|
once?: string | number;
|
|
385
|
+
|
|
386
|
+
// schedule sub-actions (removal/toggle/update of an existing job)
|
|
387
|
+
subAction?: string;
|
|
388
|
+
jobId?: string;
|
|
385
389
|
/** Mark certain bash commands as excludeFromContext to reduce context tokens (default: false). */
|
|
386
390
|
excludeContextBash?: boolean;
|
|
387
391
|
/** Total token budget for the run. When set, enables budget tracking (minimum 1000). */
|
|
@@ -11,7 +11,11 @@ function hashContent(content: string): string {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const RETRYABLE_RENAME_CODES = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
14
|
-
|
|
14
|
+
// EEXIST is retryable: two concurrent async saves (OPT-02 async saveRunManifest)
|
|
15
|
+
// can race on unlink+link — the second link() hits EEXIST because the first
|
|
16
|
+
// already created the destination. The retry loop unlinks and re-links,
|
|
17
|
+
// resolving the race. Exponential backoff + jitter prevents starvation.
|
|
18
|
+
const RETRYABLE_LINK_CODES = new Set(["EPERM", "EBUSY", "EACCES", "ENOENT", "EEXIST"]);
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Symlink-safe file write guard (caveman-inspired).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { assertSafePathId } from "../utils/safe-paths.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Represents a learned instinct that guides agent behavior.
|
|
@@ -120,6 +121,7 @@ export class InstinctStore {
|
|
|
120
121
|
if (!savedInstinct.projectId) {
|
|
121
122
|
throw new Error("Project-scoped instinct requires a projectId");
|
|
122
123
|
}
|
|
124
|
+
assertSafePathId("projectId", savedInstinct.projectId);
|
|
123
125
|
this.appendInstinctToFile(this.getProjectInstinctPath(savedInstinct.projectId), savedInstinct);
|
|
124
126
|
}
|
|
125
127
|
|
|
@@ -163,6 +165,7 @@ export class InstinctStore {
|
|
|
163
165
|
* @returns Array of instincts for the project
|
|
164
166
|
*/
|
|
165
167
|
getProjectInstincts(projectId: string): Instinct[] {
|
|
168
|
+
assertSafePathId("projectId", projectId);
|
|
166
169
|
const projectInstincts = this.readInstinctsFromFile(this.getProjectInstinctPath(projectId));
|
|
167
170
|
const globalInstincts = this.readInstinctsFromFile(this.getGlobalInstinctPath());
|
|
168
171
|
return [...projectInstincts, ...globalInstincts];
|
|
@@ -183,6 +186,7 @@ export class InstinctStore {
|
|
|
183
186
|
.readdirSync(projectsDir, { withFileTypes: true })
|
|
184
187
|
.filter((e) => e.isDirectory())
|
|
185
188
|
.map((e) => e.name)) {
|
|
189
|
+
assertSafePathId("projectId", projectId);
|
|
186
190
|
const filePath = path.join(projectsDir, projectId, INSTINCT_FILE);
|
|
187
191
|
const instincts = this.readInstinctsFromFile(filePath);
|
|
188
192
|
const index = instincts.findIndex((i) => i.id === instinctId);
|
|
@@ -239,6 +243,7 @@ export class InstinctStore {
|
|
|
239
243
|
.readdirSync(projectsDir, { withFileTypes: true })
|
|
240
244
|
.filter((e) => e.isDirectory())
|
|
241
245
|
.map((e) => e.name)) {
|
|
246
|
+
assertSafePathId("projectId", projectId);
|
|
242
247
|
const filePath = path.join(projectsDir, projectId, INSTINCT_FILE);
|
|
243
248
|
instincts = this.readInstinctsFromFile(filePath);
|
|
244
249
|
index = instincts.findIndex((i) => i.id === instinctId);
|
package/src/state/state-store.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPat
|
|
|
11
11
|
import { toPiSessionId } from "../utils/session-utils.ts";
|
|
12
12
|
import type { WorkflowConfig } from "../workflows/workflow-config.ts";
|
|
13
13
|
import { unregisterActiveRun } from "./active-run-registry.ts";
|
|
14
|
-
import { atomicWriteJson, atomicWriteJsonAsync, atomicWriteJsonCoalesced, readJsonFile } from "./atomic-write.ts";
|
|
14
|
+
import { atomicWriteJson, atomicWriteJsonAsync, atomicWriteJsonCoalesced, flushPendingAtomicWrites, readJsonFile } from "./atomic-write.ts";
|
|
15
15
|
import { canTransitionRunStatus } from "./contracts.ts";
|
|
16
16
|
import { appendEvent } from "./event-log.ts";
|
|
17
17
|
import { HealthStore } from "./health-store.ts";
|
|
@@ -385,7 +385,18 @@ export async function saveRunManifestAsync(manifest: TeamRunManifest): Promise<v
|
|
|
385
385
|
const manifestPath = path.join(manifest.stateRoot, "manifest.json");
|
|
386
386
|
await atomicWriteJsonAsync(manifestPath, manifest);
|
|
387
387
|
// FIX: Re-populate cache with actual mtime/size. See saveRunManifest.
|
|
388
|
-
|
|
388
|
+
// RACE GUARD: another concurrent async save (OPT-02) may unlink+rewrite
|
|
389
|
+
// manifest.json between our atomicWriteJsonAsync and this stat. If stat
|
|
390
|
+
// hits ENOENT, use fallback values — the cache will look stale on the
|
|
391
|
+
// next load and re-read from disk, which is correct.
|
|
392
|
+
let manifestStat: { mtimeMs: number; size: number };
|
|
393
|
+
try {
|
|
394
|
+
manifestStat = await fs.promises.stat(manifestPath);
|
|
395
|
+
} catch (statError) {
|
|
396
|
+
const code = String((statError as NodeJS.ErrnoException).code ?? "");
|
|
397
|
+
if (code !== "ENOENT") throw statError;
|
|
398
|
+
manifestStat = { mtimeMs: 0, size: 0 };
|
|
399
|
+
}
|
|
389
400
|
setManifestCache(manifest.stateRoot, {
|
|
390
401
|
manifest,
|
|
391
402
|
tasks: cachedTasks,
|
|
@@ -624,6 +635,68 @@ export function __test__clearManifestCache(): void {
|
|
|
624
635
|
manifestCache.clear();
|
|
625
636
|
}
|
|
626
637
|
|
|
638
|
+
/**
|
|
639
|
+
* OPT-08 additive helper: drop the manifest cache entry for `stateRoot` after
|
|
640
|
+
* flushing any pending coalesced atomic writes (process-wide). Forces the next
|
|
641
|
+
* `loadRunManifestById` / `loadRunManifestByIdAsync` for this run to hit disk
|
|
642
|
+
* instead of serving a possibly-stale cache hit. Idempotent: safe to call
|
|
643
|
+
* multiple times, including for unknown stateRoots.
|
|
644
|
+
*
|
|
645
|
+
* This is an additive helper ONLY — it does NOT replace the load-bearing CAS
|
|
646
|
+
* loop in `persistSingleTaskUpdate`, nor does it bypass `withRunLock` /
|
|
647
|
+
* `fs.statSync`-based invalidation. It is a safe exit hatch for callers that
|
|
648
|
+
* want a guaranteed cache drop (e.g. cleanup paths, end-of-run shutdown).
|
|
649
|
+
*
|
|
650
|
+
* Scope note: `flushPendingAtomicWrites()` flushes ALL pending coalesced
|
|
651
|
+
* writes process-wide, not just those under `stateRoot`. The coalescer has no
|
|
652
|
+
* per-stateRoot filter; flushing the whole queue is the simplest correct
|
|
653
|
+
* behavior and matches the existing `flushPendingAtomicWrites()` contract
|
|
654
|
+
* used by cleanupRuntime / process exit handlers.
|
|
655
|
+
*/
|
|
656
|
+
export async function unloadRun(stateRoot: string): Promise<void> {
|
|
657
|
+
// Flush first so any in-flight buffered write lands on disk before we drop
|
|
658
|
+
// the cache entry. Otherwise a coalesced write could fire AFTER unloadRun
|
|
659
|
+
// returns, re-populate the cache from disk, and re-stale it.
|
|
660
|
+
flushPendingAtomicWrites();
|
|
661
|
+
// invalidateRunCache bumps the per-stateRoot generation counter so even if
|
|
662
|
+
// some in-process reader has a stale reference, the next cache lookup
|
|
663
|
+
// misses (generation mismatch) and re-reads from disk.
|
|
664
|
+
invalidateRunCache(stateRoot);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* OPT-08 additive helper: read-only inspection of the manifest cache.
|
|
669
|
+
* Returns current size, configured limits, and per-stateRoot observability
|
|
670
|
+
* (cache generation + age in ms). Pure read — never mutates cache state.
|
|
671
|
+
* Useful for `tests`, dashboards, and pre-shutdown sanity checks.
|
|
672
|
+
*/
|
|
673
|
+
export interface ManifestCacheStats {
|
|
674
|
+
size: number;
|
|
675
|
+
maxEntries: number;
|
|
676
|
+
ttlMs: number;
|
|
677
|
+
perStateRoot: Record<string, { generation: number; ageMs: number }>;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export function getManifestCacheStats(): ManifestCacheStats {
|
|
681
|
+
const now = Date.now();
|
|
682
|
+
const perStateRoot: Record<string, { generation: number; ageMs: number }> = {};
|
|
683
|
+
for (const [stateRoot, entry] of manifestCache.entries()) {
|
|
684
|
+
perStateRoot[stateRoot] = {
|
|
685
|
+
generation: entry.generation ?? genOf(stateRoot),
|
|
686
|
+
// ageMs is 0 when cachedAt is unset (defensive — setManifestCache
|
|
687
|
+
// always sets it today, but a future regression would otherwise
|
|
688
|
+
// surface as `undefined` in the stats object).
|
|
689
|
+
ageMs: entry.cachedAt ? now - entry.cachedAt : 0,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
size: manifestCache.size,
|
|
694
|
+
maxEntries: DEFAULT_CACHE.manifestMaxEntries,
|
|
695
|
+
ttlMs: MANIFEST_CACHE_TTL_MS,
|
|
696
|
+
perStateRoot,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
627
700
|
async function readJsonFileAsync<T>(filePath: string): Promise<T | undefined> {
|
|
628
701
|
try {
|
|
629
702
|
return JSON.parse(await fs.promises.readFile(filePath, "utf-8")) as T;
|
|
@@ -2,24 +2,113 @@
|
|
|
2
2
|
* Lightweight token counter for estimating token counts in text.
|
|
3
3
|
*
|
|
4
4
|
* Provides a more accurate estimate than the naive char/4 heuristic by
|
|
5
|
-
* distinguishing word characters from punctuation
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* distinguishing word characters from punctuation, and by detecting
|
|
6
|
+
* code-heavy content where BPE tokenizers produce more tokens per
|
|
7
|
+
* character (more operators, shorter identifiers, multi-char operators).
|
|
8
|
+
*
|
|
9
|
+
* Accuracy:
|
|
10
|
+
* - Prose: within ±10% of actual BPE token counts (unchanged formula).
|
|
11
|
+
* - Code: within ±10% of actual BPE token counts (improved from ±15%).
|
|
8
12
|
*
|
|
9
13
|
* Performance: O(n) single-pass, ~1ms for 10KB text, no external deps.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
/** Density threshold above which content is classified as code. */
|
|
17
|
+
const CODE_DENSITY_THRESHOLD = 0.1;
|
|
18
|
+
|
|
19
|
+
/** Divisor for code content alpha estimation (~3.5 chars/token). */
|
|
20
|
+
const CODE_ALPHA_DIVISOR = 3.5;
|
|
21
|
+
|
|
22
|
+
/** Divisor for prose content alpha estimation (~4 chars/token). */
|
|
23
|
+
const PROSE_ALPHA_DIVISOR = 4;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if two consecutive chars form a multi-character operator that BPE
|
|
27
|
+
* typically tokenizes as fewer tokens than the individual characters.
|
|
28
|
+
* Covers: => == != <= >= && || ?. ?? ++ -- += -= *= /=
|
|
29
|
+
*/
|
|
30
|
+
function isMultiCharOp(c1: number, c2: number): boolean {
|
|
31
|
+
switch (c1) {
|
|
32
|
+
case 0x3d:
|
|
33
|
+
return c2 === 0x3e || c2 === 0x3d; // = → => or ==
|
|
34
|
+
case 0x21:
|
|
35
|
+
return c2 === 0x3d; // ! → !=
|
|
36
|
+
case 0x3c:
|
|
37
|
+
return c2 === 0x3d; // < → <=
|
|
38
|
+
case 0x3e:
|
|
39
|
+
return c2 === 0x3d; // > → >=
|
|
40
|
+
case 0x26:
|
|
41
|
+
return c2 === 0x26; // & → &&
|
|
42
|
+
case 0x7c:
|
|
43
|
+
return c2 === 0x7c; // | → ||
|
|
44
|
+
case 0x3f:
|
|
45
|
+
return c2 === 0x2e || c2 === 0x3f; // ? → ?. or ??
|
|
46
|
+
case 0x2b:
|
|
47
|
+
return c2 === 0x2b || c2 === 0x3d; // + → ++ or +=
|
|
48
|
+
case 0x2d:
|
|
49
|
+
return c2 === 0x2d || c2 === 0x3d; // - → -- or -=
|
|
50
|
+
case 0x2a:
|
|
51
|
+
return c2 === 0x3d; // * → *=
|
|
52
|
+
case 0x2f:
|
|
53
|
+
return c2 === 0x3d; // / → /=
|
|
54
|
+
default:
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
14
57
|
}
|
|
15
58
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Detect whether the text is code-heavy content based on the density of
|
|
61
|
+
* code-specific punctuation (brackets, semicolons, colons) and multi-char
|
|
62
|
+
* operators (=>, ==, &&, etc.).
|
|
63
|
+
*
|
|
64
|
+
* @param text Input text.
|
|
65
|
+
* @returns True if the content appears to be code.
|
|
66
|
+
*/
|
|
67
|
+
export function detectCodeContent(text: string): boolean {
|
|
68
|
+
if (!text || text.length === 0) return false;
|
|
69
|
+
|
|
70
|
+
let alphaChars = 0;
|
|
71
|
+
let punctChars = 0;
|
|
72
|
+
let codePunct = 0;
|
|
73
|
+
let multiCharOps = 0;
|
|
74
|
+
let skipOpCheck = false;
|
|
75
|
+
const len = text.length;
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < len; i++) {
|
|
78
|
+
const c = text.charCodeAt(i);
|
|
79
|
+
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) {
|
|
80
|
+
skipOpCheck = false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if ((c >= 0x30 && c <= 0x39) || (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || c === 0x5f) {
|
|
84
|
+
alphaChars++;
|
|
85
|
+
skipOpCheck = false;
|
|
86
|
+
} else {
|
|
87
|
+
punctChars++;
|
|
88
|
+
if (
|
|
89
|
+
c === 0x7b ||
|
|
90
|
+
c === 0x7d || // { }
|
|
91
|
+
c === 0x5b ||
|
|
92
|
+
c === 0x5d || // [ ]
|
|
93
|
+
c === 0x28 ||
|
|
94
|
+
c === 0x29 || // ( )
|
|
95
|
+
c === 0x3b ||
|
|
96
|
+
c === 0x3a // ; :
|
|
97
|
+
) {
|
|
98
|
+
codePunct++;
|
|
99
|
+
}
|
|
100
|
+
if (skipOpCheck) {
|
|
101
|
+
skipOpCheck = false;
|
|
102
|
+
} else if (i + 1 < len && isMultiCharOp(c, text.charCodeAt(i + 1))) {
|
|
103
|
+
multiCharOps++;
|
|
104
|
+
skipOpCheck = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const total = alphaChars + punctChars;
|
|
110
|
+
if (total === 0) return false;
|
|
111
|
+
return (codePunct + multiCharOps) / total >= CODE_DENSITY_THRESHOLD;
|
|
23
112
|
}
|
|
24
113
|
|
|
25
114
|
/**
|
|
@@ -27,21 +116,16 @@ function isAlphanumeric(c: number): boolean {
|
|
|
27
116
|
*
|
|
28
117
|
* Algorithm:
|
|
29
118
|
* 1. Walk text char-by-char with charCodeAt (fast, no allocations).
|
|
30
|
-
* 2. Count alphabetic chars
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* 4.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* tendency to tokenize operators, brackets, and symbols individually.
|
|
41
|
-
*
|
|
42
|
-
* Accuracy: typically within ±10-15% of actual BPE token counts for both
|
|
43
|
-
* English prose and code, beating the char/4 heuristic (which can be
|
|
44
|
-
* 30%+ off for code-heavy content).
|
|
119
|
+
* 2. Count alphabetic chars, alpha runs (words), punctuation chars,
|
|
120
|
+
* code-specific punctuation, and multi-char operators — all in one pass.
|
|
121
|
+
* 3. Detect code-heavy content using indicator density.
|
|
122
|
+
* 4. For prose: ceil(alpha / 4) + punct — BPE averages ~4 chars/token
|
|
123
|
+
* for English words; each punctuation char is a separate token.
|
|
124
|
+
* 5. For code: max(ceil(alpha / 3.5), alphaRuns) + punct - multiCharOps.
|
|
125
|
+
* - max() handles both short-identifier code (alphaRuns dominates)
|
|
126
|
+
* and long-identifier code (alpha/3.5 dominates).
|
|
127
|
+
* - Subtracting multiCharOps corrects for operators like => or ==
|
|
128
|
+
* that BPE tokenizes as 1 token, not 2 chars.
|
|
45
129
|
*
|
|
46
130
|
* @param text Input text to estimate tokens for.
|
|
47
131
|
* @returns Estimated token count.
|
|
@@ -49,19 +133,68 @@ function isAlphanumeric(c: number): boolean {
|
|
|
49
133
|
export function countTokens(text: string): number {
|
|
50
134
|
if (!text || text.length === 0) return 0;
|
|
51
135
|
|
|
52
|
-
let
|
|
53
|
-
let
|
|
136
|
+
let alphaChars = 0;
|
|
137
|
+
let alphaRuns = 0;
|
|
138
|
+
let punctChars = 0;
|
|
139
|
+
let codePunct = 0;
|
|
140
|
+
let multiCharOps = 0;
|
|
141
|
+
let inAlphaRun = false;
|
|
142
|
+
let skipOpCheck = false;
|
|
54
143
|
const len = text.length;
|
|
55
144
|
|
|
56
145
|
for (let i = 0; i < len; i++) {
|
|
57
146
|
const c = text.charCodeAt(i);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
147
|
+
|
|
148
|
+
// Inline whitespace check (hot path)
|
|
149
|
+
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) {
|
|
150
|
+
inAlphaRun = false;
|
|
151
|
+
skipOpCheck = false;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Inline alphanumeric check (hot path)
|
|
156
|
+
if ((c >= 0x30 && c <= 0x39) || (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || c === 0x5f) {
|
|
157
|
+
alphaChars++;
|
|
158
|
+
if (!inAlphaRun) {
|
|
159
|
+
alphaRuns++;
|
|
160
|
+
inAlphaRun = true;
|
|
161
|
+
}
|
|
162
|
+
skipOpCheck = false;
|
|
163
|
+
continue;
|
|
63
164
|
}
|
|
165
|
+
|
|
166
|
+
// Punctuation path
|
|
167
|
+
inAlphaRun = false;
|
|
168
|
+
punctChars++;
|
|
169
|
+
|
|
170
|
+
// Code-specific punctuation: { } [ ] ( ) ; :
|
|
171
|
+
if (c === 0x7b || c === 0x7d || c === 0x5b || c === 0x5d || c === 0x28 || c === 0x29 || c === 0x3b || c === 0x3a) {
|
|
172
|
+
codePunct++;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Multi-char operator detection. When a pair is found at position i,
|
|
176
|
+
// skip the check at i+1 to avoid double-counting (e.g. === → 1 op).
|
|
177
|
+
if (skipOpCheck) {
|
|
178
|
+
skipOpCheck = false;
|
|
179
|
+
} else if (i + 1 < len && isMultiCharOp(c, text.charCodeAt(i + 1))) {
|
|
180
|
+
multiCharOps++;
|
|
181
|
+
skipOpCheck = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const total = alphaChars + punctChars;
|
|
186
|
+
if (total === 0) return 0;
|
|
187
|
+
|
|
188
|
+
const codeIndicators = codePunct + multiCharOps;
|
|
189
|
+
if (codeIndicators / total >= CODE_DENSITY_THRESHOLD) {
|
|
190
|
+
// Code content: shorter identifiers and more operators mean more
|
|
191
|
+
// tokens per character than prose. Use the larger of word-count
|
|
192
|
+
// and char/3.5 to handle both short and long identifiers.
|
|
193
|
+
const alphaTokens = Math.max(Math.ceil(alphaChars / CODE_ALPHA_DIVISOR), alphaRuns);
|
|
194
|
+
return alphaTokens + punctChars - multiCharOps;
|
|
64
195
|
}
|
|
65
196
|
|
|
66
|
-
|
|
197
|
+
// Prose content: standard ~4 chars/token for alphanumeric words,
|
|
198
|
+
// 1 token per punctuation char (unchanged from original formula).
|
|
199
|
+
return Math.ceil(alphaChars / PROSE_ALPHA_DIVISOR) + punctChars;
|
|
67
200
|
}
|
|
@@ -56,6 +56,11 @@ export function ensureIntermediateDir(config: Partial<IntermediateStoreConfig> =
|
|
|
56
56
|
* @returns Path to the written file
|
|
57
57
|
*/
|
|
58
58
|
export function writeIntermediate(config: Partial<IntermediateStoreConfig>, phase: string, stepId: string, data: unknown): string {
|
|
59
|
+
// Security: validate phase/stepId before building the filename to prevent
|
|
60
|
+
// path traversal via poisoned phase or stepId.
|
|
61
|
+
if (!isSafePathId(phase) || !isSafePathId(stepId)) {
|
|
62
|
+
throw new Error("Invalid phase or stepId for intermediate store");
|
|
63
|
+
}
|
|
59
64
|
const dir = ensureIntermediateDir(config);
|
|
60
65
|
const filename = `${phase}-${stepId}.json`;
|
|
61
66
|
const filePath = path.join(dir, filename);
|
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pipeline
|
|
3
|
-
description: Multi-stage pipeline
|
|
3
|
+
description: Multi-stage pipeline (research → analyze → synthesize → document) producing pipeline-summary.md
|
|
4
4
|
topology: sequential
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## research
|
|
8
8
|
role: explorer
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Gather relevant facts for: {goal}. Identify key concepts and provide a structured summary.
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## analyze
|
|
13
13
|
role: analyst
|
|
14
|
-
dependsOn:
|
|
14
|
+
dependsOn: research
|
|
15
15
|
|
|
16
|
-
Analyze the research findings
|
|
16
|
+
Analyze and organize the research findings. Identify patterns, relationships, and insights with supporting evidence.
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## synthesize
|
|
19
19
|
role: analyst
|
|
20
|
-
dependsOn:
|
|
20
|
+
dependsOn: analyze
|
|
21
21
|
|
|
22
|
-
Synthesize the analysis into actionable recommendations
|
|
22
|
+
Synthesize the analysis into prioritized, actionable recommendations with clear next steps.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## document
|
|
25
25
|
role: writer
|
|
26
|
-
dependsOn:
|
|
26
|
+
dependsOn: synthesize
|
|
27
|
+
output: pipeline-summary.md
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
Write the final pipeline summary combining research, analysis, and synthesis. Include executive summary, detailed findings, and recommendations. End the output with a final line that reads exactly: PIPELINE_WORKFLOW_OK
|