@piagent/platform 1.2.9 → 1.2.11
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 +179 -0
- package/README.md +29 -6
- package/adapters/backend-api/profile.json +1 -1
- package/adapters/be-readonly-fe/profile.json +2 -2
- package/adapters/data/profile.json +1 -1
- package/adapters/devops/profile.json +1 -3
- package/adapters/docs/profile.json +1 -1
- package/adapters/fullstack/profile.json +2 -4
- package/adapters/generic/profile.json +1 -1
- package/adapters/mobile/profile.json +1 -2
- package/adapters/node-typescript/profile.json +1 -3
- package/benchmarks/core-v1/suite.json +51 -0
- package/benchmarks/core-v1/tasks/discount-order/grade.mjs +17 -0
- package/benchmarks/core-v1/tasks/discount-order/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/discount-order/project/src/discount.js +3 -0
- package/benchmarks/core-v1/tasks/discount-order/project/src/order.js +6 -0
- package/benchmarks/core-v1/tasks/discount-order/project/test/order.test.js +7 -0
- package/benchmarks/core-v1/tasks/discount-order/prompt.md +4 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/grade.mjs +17 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/src/invoice.js +3 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/test/invoice.test.js +7 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/prompt.md +3 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/grade.mjs +11 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/README.md +4 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/test/smoke.test.js +4 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/prompt.md +4 -0
- package/benchmarks/core-v1/tasks/targeted-session/grade.mjs +14 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/auth/session.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/billing/currency.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/catalog/search.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/notifications/channel.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/search/tokenize.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/test/session.test.js +7 -0
- package/benchmarks/core-v1/tasks/targeted-session/prompt.md +4 -0
- package/catalog/capabilities.json +12 -12
- package/docs/command-reference-vietnamese.md +15 -8
- package/docs/context-engine.md +56 -15
- package/docs/distribution-standard.md +11 -8
- package/docs/mcp-and-tools.md +20 -7
- package/docs/model-options.md +7 -10
- package/docs/operator-manual-vietnamese.md +102 -37
- package/docs/package-architecture-notes.md +3 -1
- package/docs/publishing-for-teams.md +5 -0
- package/docs/quality-benchmark.md +225 -58
- package/docs/quickstart-vietnamese.md +5 -0
- package/docs/release-install-policy.md +41 -7
- package/docs/runtime-harness-standard.md +47 -29
- package/docs/runtime-policy-design.md +23 -12
- package/docs/runtime-quality-baseline.md +31 -17
- package/docs/security-threat-model.md +7 -2
- package/docs/subagents-and-multiagent.md +1 -1
- package/docs/task-implementation-contract.md +113 -177
- package/docs/task-lifecycle-vietnamese.md +129 -125
- package/docs/team-onboarding.md +12 -4
- package/docs/usage-observability.md +15 -8
- package/docs-site/architecture.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/context-engine.html +8 -1
- package/docs-site/content/runtime.html +10 -0
- package/docs-site/content/security.html +1 -0
- package/docs-site/context-engine.html +9 -2
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +11 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +2 -1
- package/docs-site/team.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/task-contract.invalid.json +15 -1
- package/evals/fixtures/task-contract.valid.json +18 -2
- package/package.json +3 -2
- package/packages/piagent-core/README.md +43 -4
- package/packages/piagent-core/benchmark/benchmark-core.js +691 -0
- package/packages/piagent-core/extensions/context-engine.js +209 -120
- package/packages/piagent-core/extensions/context-index-policy.js +1 -1
- package/packages/piagent-core/extensions/guard-types.ts +35 -3
- package/packages/piagent-core/extensions/local-state-path.js +59 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +2073 -222
- package/packages/piagent-core/extensions/policy-core.js +11 -1
- package/packages/piagent-core/extensions/runtime-evidence.js +39 -19
- package/packages/piagent-core/extensions/state-retention.js +218 -0
- package/packages/piagent-core/extensions/task-lifecycle.js +138 -0
- package/packages/piagent-core/extensions/task-state.js +630 -0
- package/packages/piagent-core/mcp/mcp-config-layers.js +52 -6
- package/packages/piagent-core/mcp/mcp-tool-naming.js +12 -1
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +1 -0
- package/packages/piagent-core/prompts/be-to-fe.md +14 -22
- package/packages/piagent-core/prompts/platform-improve.md +12 -17
- package/packages/piagent-core/prompts/review.md +7 -11
- package/packages/piagent-core/prompts/scout.md +6 -10
- package/packages/piagent-core/prompts/task.md +11 -17
- package/packages/piagent-core/skills/piagent-ops/SKILL.md +12 -15
- package/packages/piagent-core/subagents/piagent-reviewer.md +3 -3
- package/schemas/task-contract.schema.json +267 -104
- package/scripts/benchmark-runner.mjs +1172 -0
- package/scripts/install-global.sh +1 -1
- package/scripts/migrate-project-state.mjs +94 -3
- package/scripts/piagent-cli.mjs +1 -1
- package/scripts/update-global.mjs +36 -3
- package/scripts/verify-local.sh +15 -0
- package/templates/project/.pi/task-contract.template.json +19 -2
- package/templates/project/AGENTS.md +10 -20
|
@@ -1987,6 +1987,7 @@ function extractAttachedRedirectionPaths(segment) {
|
|
|
1987
1987
|
index += 1;
|
|
1988
1988
|
}
|
|
1989
1989
|
|
|
1990
|
+
const writesFile = char === ">" || (char === "<" && next === ">");
|
|
1990
1991
|
if ((char === ">" && (next === ">" || next === "|")) || (char === "<" && next === ">")) index += 1;
|
|
1991
1992
|
let cursor = index + 1;
|
|
1992
1993
|
while (/\s/.test(segment[cursor] ?? "")) cursor += 1;
|
|
@@ -2054,13 +2055,22 @@ function extractAttachedRedirectionPaths(segment) {
|
|
|
2054
2055
|
target += targetChar;
|
|
2055
2056
|
targetMask += (targetQuote || insideSubstitution) ? "1" : "0";
|
|
2056
2057
|
}
|
|
2057
|
-
if (target) paths.push({ value: target, literalMask: targetMask });
|
|
2058
|
+
if (target) paths.push({ value: target, literalMask: targetMask, writesFile });
|
|
2058
2059
|
index = Math.max(index, cursor - 1);
|
|
2059
2060
|
}
|
|
2060
2061
|
|
|
2061
2062
|
return paths;
|
|
2062
2063
|
}
|
|
2063
2064
|
|
|
2065
|
+
export function shellHasFileWriteRedirection(command) {
|
|
2066
|
+
return splitShellSegments(String(command ?? "")).some((segment) => (
|
|
2067
|
+
extractAttachedRedirectionPaths(segment)
|
|
2068
|
+
.filter((redirect) => redirect.writesFile)
|
|
2069
|
+
.flatMap(redirectionTargetWords)
|
|
2070
|
+
.length > 0
|
|
2071
|
+
));
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2064
2074
|
export function extractShellPathCandidates(command) {
|
|
2065
2075
|
const candidates = [...xargsPipelineInputCandidates(command)];
|
|
2066
2076
|
const assignments = new Map();
|
|
@@ -2,6 +2,10 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { redactSensitiveText } from "../security/sensitive-data.js";
|
|
5
|
+
import { appendJsonlBounded, readJsonlTail } from "./state-retention.js";
|
|
6
|
+
import { resolveLocalStatePath } from "./local-state-path.js";
|
|
7
|
+
|
|
8
|
+
const MAX_OBSERVED_BASH_BYTES = 8 * 1024 * 1024;
|
|
5
9
|
|
|
6
10
|
export function normalizeEvidenceCommand(command) {
|
|
7
11
|
return String(command ?? "")
|
|
@@ -180,40 +184,56 @@ function persistedObservedEntry(entry) {
|
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
export function readObservedBashResults(filePath, options = {}) {
|
|
183
|
-
if (!filePath
|
|
187
|
+
if (!filePath) return [];
|
|
184
188
|
const maxEntries = Number.isInteger(options.maxEntries) ? options.maxEntries : 5000;
|
|
185
|
-
const text = fs.readFileSync(filePath, "utf8");
|
|
186
189
|
const entries = [];
|
|
187
|
-
for (const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// Ignore a partially written/corrupt JSONL line. The ledger is advisory evidence,
|
|
196
|
-
// and the final gate remains fail-closed when no valid observation is available.
|
|
197
|
-
}
|
|
190
|
+
for (const parsed of readJsonlTail(filePath, {
|
|
191
|
+
maxEntries,
|
|
192
|
+
limit: maxEntries,
|
|
193
|
+
maxBytes: MAX_OBSERVED_BASH_BYTES,
|
|
194
|
+
projectRoot: options.projectRoot
|
|
195
|
+
})) {
|
|
196
|
+
const entry = canonicalObservedEntry(parsed);
|
|
197
|
+
if (entry) entries.push(entry);
|
|
198
198
|
}
|
|
199
199
|
return entries.slice(-maxEntries);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
function pruneObservedBashFile(filePath, maxPersistedEntries) {
|
|
202
|
+
function pruneObservedBashFile(filePath, maxPersistedEntries, projectRoot) {
|
|
203
203
|
if (!Number.isInteger(maxPersistedEntries) || maxPersistedEntries <= 0) return;
|
|
204
|
-
const entries = readObservedBashResults(filePath, { maxEntries: maxPersistedEntries });
|
|
204
|
+
const entries = readObservedBashResults(filePath, { maxEntries: maxPersistedEntries, projectRoot });
|
|
205
205
|
const lines = entries.map((entry) => JSON.stringify(persistedObservedEntry(entry))).filter(Boolean);
|
|
206
|
-
|
|
206
|
+
const safePath = projectRoot
|
|
207
|
+
? resolveLocalStatePath(projectRoot, filePath, { label: "Observed bash ledger", kind: "file" })
|
|
208
|
+
: filePath;
|
|
209
|
+
const descriptor = fs.openSync(
|
|
210
|
+
safePath,
|
|
211
|
+
fs.constants.O_WRONLY | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW ?? 0),
|
|
212
|
+
0o600
|
|
213
|
+
);
|
|
214
|
+
try {
|
|
215
|
+
fs.writeSync(descriptor, `${lines.join("\n")}${lines.length ? "\n" : ""}`);
|
|
216
|
+
try {
|
|
217
|
+
fs.fchmodSync(descriptor, 0o600);
|
|
218
|
+
} catch {
|
|
219
|
+
// Best effort on filesystems that do not expose POSIX modes.
|
|
220
|
+
}
|
|
221
|
+
} finally {
|
|
222
|
+
fs.closeSync(descriptor);
|
|
223
|
+
}
|
|
207
224
|
}
|
|
208
225
|
|
|
209
226
|
export function appendObservedBashResult(filePath, entry, options = {}) {
|
|
210
227
|
if (!filePath) return undefined;
|
|
211
228
|
const persisted = persistedObservedEntry(entry);
|
|
212
229
|
if (!persisted) return undefined;
|
|
213
|
-
|
|
214
|
-
|
|
230
|
+
appendJsonlBounded(filePath, persisted, {
|
|
231
|
+
maxBytes: MAX_OBSERVED_BASH_BYTES,
|
|
232
|
+
mode: 0o600,
|
|
233
|
+
projectRoot: options.projectRoot
|
|
234
|
+
});
|
|
215
235
|
if (Number.isInteger(options.maxPersistedEntries)) {
|
|
216
|
-
pruneObservedBashFile(filePath, options.maxPersistedEntries);
|
|
236
|
+
pruneObservedBashFile(filePath, options.maxPersistedEntries, options.projectRoot);
|
|
217
237
|
}
|
|
218
238
|
return persisted;
|
|
219
239
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
|
|
4
|
+
|
|
5
|
+
const LOCK_WAIT_MS = 500;
|
|
6
|
+
const LOCK_STALE_MS = 5_000;
|
|
7
|
+
const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
8
|
+
|
|
9
|
+
function ensurePrivateParent(filePath, projectRoot) {
|
|
10
|
+
const safeFilePath = projectRoot
|
|
11
|
+
? resolveLocalStatePath(projectRoot, filePath, { label: "Local JSONL state" })
|
|
12
|
+
: filePath;
|
|
13
|
+
if (projectRoot) ensurePrivateStateDirectory(projectRoot, path.dirname(filePath), "Local JSONL state directory");
|
|
14
|
+
else fs.mkdirSync(path.dirname(safeFilePath), { recursive: true, mode: 0o700 });
|
|
15
|
+
try {
|
|
16
|
+
fs.chmodSync(path.dirname(safeFilePath), 0o700);
|
|
17
|
+
} catch {
|
|
18
|
+
// Best effort on filesystems that do not expose POSIX modes.
|
|
19
|
+
}
|
|
20
|
+
return safeFilePath;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function withFileLock(filePath, action) {
|
|
24
|
+
const lockPath = `${filePath}.lock`;
|
|
25
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
26
|
+
let descriptor;
|
|
27
|
+
while (descriptor === undefined) {
|
|
28
|
+
try {
|
|
29
|
+
descriptor = fs.openSync(lockPath, "wx", 0o600);
|
|
30
|
+
fs.writeFileSync(descriptor, `${process.pid}\n`);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error?.code !== "EEXIST") throw error;
|
|
33
|
+
try {
|
|
34
|
+
if (Date.now() - fs.lstatSync(lockPath).mtimeMs > LOCK_STALE_MS) {
|
|
35
|
+
fs.rmSync(lockPath, { force: true });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
} catch (statError) {
|
|
39
|
+
if (statError?.code === "ENOENT") continue;
|
|
40
|
+
}
|
|
41
|
+
if (Date.now() >= deadline) {
|
|
42
|
+
throw new Error(`Timed out waiting for state lock: ${path.basename(filePath)}`);
|
|
43
|
+
}
|
|
44
|
+
Atomics.wait(sleepBuffer, 0, 0, 10);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return action();
|
|
49
|
+
} finally {
|
|
50
|
+
try {
|
|
51
|
+
fs.closeSync(descriptor);
|
|
52
|
+
} finally {
|
|
53
|
+
fs.rmSync(lockPath, { force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function rotateJsonl(filePath, maxBytes, incomingBytes = 0) {
|
|
59
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0) return false;
|
|
60
|
+
let size;
|
|
61
|
+
try {
|
|
62
|
+
size = fs.statSync(filePath).size;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error?.code === "ENOENT") return false;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
if (size === 0 || size + incomingBytes <= maxBytes) return false;
|
|
68
|
+
const previous = `${filePath}.1`;
|
|
69
|
+
try {
|
|
70
|
+
fs.rmSync(previous, { force: true });
|
|
71
|
+
fs.renameSync(filePath, previous);
|
|
72
|
+
fs.chmodSync(previous, 0o600);
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function boundedJsonLine(record, maxBytes) {
|
|
80
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
81
|
+
const originalBytes = Buffer.byteLength(line);
|
|
82
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0 || originalBytes <= maxBytes) return { line, record };
|
|
83
|
+
const summary = {
|
|
84
|
+
schemaVersion: Number.isInteger(record?.schemaVersion) ? record.schemaVersion : 1,
|
|
85
|
+
recordedAt: typeof record?.recordedAt === "string" ? record.recordedAt : new Date().toISOString(),
|
|
86
|
+
event: typeof record?.event === "string" ? record.event : "oversized_record",
|
|
87
|
+
truncated: true,
|
|
88
|
+
originalBytes
|
|
89
|
+
};
|
|
90
|
+
const summaryLine = `${JSON.stringify(summary)}\n`;
|
|
91
|
+
if (Buffer.byteLength(summaryLine) > maxBytes) {
|
|
92
|
+
throw new RangeError(`maxBytes ${maxBytes} is too small for a bounded JSONL record`);
|
|
93
|
+
}
|
|
94
|
+
return { line: summaryLine, record: summary };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function appendJsonlBounded(filePath, record, options = {}) {
|
|
98
|
+
const safeFilePath = ensurePrivateParent(filePath, options.projectRoot);
|
|
99
|
+
const bounded = boundedJsonLine(record, options.maxBytes);
|
|
100
|
+
return withFileLock(safeFilePath, () => {
|
|
101
|
+
rotateJsonl(safeFilePath, options.maxBytes, Buffer.byteLength(bounded.line));
|
|
102
|
+
const descriptor = fs.openSync(
|
|
103
|
+
safeFilePath,
|
|
104
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND | (fs.constants.O_NOFOLLOW ?? 0),
|
|
105
|
+
options.mode ?? 0o600
|
|
106
|
+
);
|
|
107
|
+
try {
|
|
108
|
+
fs.writeSync(descriptor, bounded.line);
|
|
109
|
+
try {
|
|
110
|
+
fs.fchmodSync(descriptor, options.mode ?? 0o600);
|
|
111
|
+
} catch {
|
|
112
|
+
// Best effort on filesystems that do not expose POSIX modes.
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
fs.closeSync(descriptor);
|
|
116
|
+
}
|
|
117
|
+
return bounded.record;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readTailText(filePath, maxBytes) {
|
|
122
|
+
try {
|
|
123
|
+
const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
124
|
+
try {
|
|
125
|
+
const stat = fs.fstatSync(descriptor);
|
|
126
|
+
const start = Math.max(0, stat.size - maxBytes);
|
|
127
|
+
const length = stat.size - start;
|
|
128
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
129
|
+
fs.readSync(descriptor, buffer, 0, length, start);
|
|
130
|
+
let text = buffer.toString("utf8");
|
|
131
|
+
if (start > 0) text = text.slice(Math.max(0, text.indexOf("\n") + 1));
|
|
132
|
+
return text;
|
|
133
|
+
} finally {
|
|
134
|
+
fs.closeSync(descriptor);
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error?.code === "ENOENT") return "";
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function readJsonlTail(filePath, options = {}) {
|
|
143
|
+
const limit = Math.max(1, Number.isInteger(options.limit) ? options.limit : 100);
|
|
144
|
+
const maxBytes = Math.max(64 * 1024, Number.isInteger(options.maxBytes) ? options.maxBytes : 8 * 1024 * 1024);
|
|
145
|
+
const safeFilePath = options.projectRoot
|
|
146
|
+
? resolveLocalStatePath(options.projectRoot, filePath, { label: "Local JSONL state" })
|
|
147
|
+
: filePath;
|
|
148
|
+
const sources = options.projectRoot
|
|
149
|
+
? [
|
|
150
|
+
resolveLocalStatePath(options.projectRoot, `${filePath}.1`, { label: "Rotated local JSONL state" }),
|
|
151
|
+
safeFilePath
|
|
152
|
+
]
|
|
153
|
+
: [`${safeFilePath}.1`, safeFilePath];
|
|
154
|
+
const lines = sources.flatMap((source) => readTailText(source, maxBytes).split(/\r?\n/).filter(Boolean));
|
|
155
|
+
const records = [];
|
|
156
|
+
for (const line of lines.slice(-limit)) {
|
|
157
|
+
try {
|
|
158
|
+
records.push(JSON.parse(line));
|
|
159
|
+
} catch {
|
|
160
|
+
// Ignore a partial record left by interruption or concurrent rotation.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return records;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function pruneCaptureFiles(root, options = {}) {
|
|
167
|
+
const safeRoot = options.projectRoot
|
|
168
|
+
? resolveLocalStatePath(options.projectRoot, root, { label: "Tool-result capture root" })
|
|
169
|
+
: root;
|
|
170
|
+
if (!fs.existsSync(safeRoot)) return { removed: 0, kept: 0, bytes: 0 };
|
|
171
|
+
if (options.projectRoot) resolveLocalStatePath(options.projectRoot, root, { label: "Tool-result capture root", kind: "directory" });
|
|
172
|
+
const maxFiles = Math.max(1, Number.isInteger(options.maxFiles) ? options.maxFiles : 500);
|
|
173
|
+
const maxBytes = Math.max(1024 * 1024, Number.isInteger(options.maxBytes) ? options.maxBytes : 128 * 1024 * 1024);
|
|
174
|
+
const maxAgeMs = Math.max(60_000, Number.isFinite(options.maxAgeMs) ? options.maxAgeMs : 30 * 24 * 60 * 60 * 1000);
|
|
175
|
+
const now = Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
|
|
176
|
+
const files = [];
|
|
177
|
+
const walk = (directory) => {
|
|
178
|
+
let entries;
|
|
179
|
+
try {
|
|
180
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error?.code === "ENOENT") return;
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
const absolute = path.join(directory, entry.name);
|
|
187
|
+
if (entry.isDirectory()) walk(absolute);
|
|
188
|
+
else if (entry.isFile() && entry.name.endsWith(".log")) {
|
|
189
|
+
try {
|
|
190
|
+
const stat = fs.statSync(absolute);
|
|
191
|
+
files.push({ absolute, mtimeMs: stat.mtimeMs, size: stat.size });
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error?.code !== "ENOENT") throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
walk(safeRoot);
|
|
199
|
+
files.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
200
|
+
let kept = 0;
|
|
201
|
+
let bytes = 0;
|
|
202
|
+
let removed = 0;
|
|
203
|
+
for (const file of files) {
|
|
204
|
+
const keep = kept < maxFiles && bytes + file.size <= maxBytes && now - file.mtimeMs <= maxAgeMs;
|
|
205
|
+
if (keep) {
|
|
206
|
+
kept += 1;
|
|
207
|
+
bytes += file.size;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
fs.rmSync(file.absolute);
|
|
212
|
+
removed += 1;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error?.code !== "ENOENT") throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { removed, kept, bytes };
|
|
218
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
function sameValues(left, right) {
|
|
4
|
+
return JSON.stringify(left ?? []) === JSON.stringify(right ?? []);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function matchesStep(step, expected) {
|
|
8
|
+
return step?.id === expected.id
|
|
9
|
+
&& step?.role === expected.role
|
|
10
|
+
&& step?.mode === expected.mode
|
|
11
|
+
&& sameValues(step?.dependsOn, expected.dependsOn);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const AUTOMATIC_TINY_PLAN = [
|
|
15
|
+
{ id: "implement", role: "parent", mode: "single-writer", dependsOn: undefined },
|
|
16
|
+
{ id: "verify", role: "parent", mode: "review", dependsOn: ["implement"] }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const ASSISTED_NORMAL_PLAN = [
|
|
20
|
+
{ id: "plan", role: "parent", mode: "read-only", dependsOn: undefined },
|
|
21
|
+
{ id: "implement", role: "parent", mode: "single-writer", dependsOn: ["plan"] },
|
|
22
|
+
{ id: "review", role: "piagent-reviewer", mode: "review", dependsOn: ["implement"] }
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const AUTOMATIC_READ_ONLY_PLAN = [
|
|
26
|
+
{ id: "scout", role: "parent", mode: "read-only", dependsOn: undefined }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const ASSISTED_READ_ONLY_PLAN = [
|
|
30
|
+
{ id: "scout", role: "parent", mode: "read-only", dependsOn: undefined },
|
|
31
|
+
{ id: "review", role: "piagent-reviewer", mode: "review", dependsOn: ["scout"] }
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export function runtimeLifecycleMode(task) {
|
|
35
|
+
if (!Array.isArray(task?.workPlan)) return "manual";
|
|
36
|
+
if (
|
|
37
|
+
task.changeMode === "read-only"
|
|
38
|
+
&& task.riskLane === "tiny"
|
|
39
|
+
&& task.workPlan.length === AUTOMATIC_READ_ONLY_PLAN.length
|
|
40
|
+
&& task.workPlan.every((step, index) => matchesStep(step, AUTOMATIC_READ_ONLY_PLAN[index]))
|
|
41
|
+
) return "automatic-readonly";
|
|
42
|
+
if (
|
|
43
|
+
task.changeMode === "read-only"
|
|
44
|
+
&& task.riskLane === "normal"
|
|
45
|
+
&& task.workPlan.length === ASSISTED_READ_ONLY_PLAN.length
|
|
46
|
+
&& task.workPlan.every((step, index) => matchesStep(step, ASSISTED_READ_ONLY_PLAN[index]))
|
|
47
|
+
) return "assisted-readonly";
|
|
48
|
+
if (task.changeMode !== "source-change") return "manual";
|
|
49
|
+
if (
|
|
50
|
+
task.riskLane === "tiny"
|
|
51
|
+
&& task.workPlan.length === AUTOMATIC_TINY_PLAN.length
|
|
52
|
+
&& task.workPlan.every((step, index) => matchesStep(step, AUTOMATIC_TINY_PLAN[index]))
|
|
53
|
+
) return "automatic";
|
|
54
|
+
if (
|
|
55
|
+
task.riskLane === "normal"
|
|
56
|
+
&& task.workPlan.length === ASSISTED_NORMAL_PLAN.length
|
|
57
|
+
&& task.workPlan.every((step, index) => matchesStep(step, ASSISTED_NORMAL_PLAN[index]))
|
|
58
|
+
) return "assisted";
|
|
59
|
+
return "manual";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function workingTreeEvidenceDigest(snapshot) {
|
|
63
|
+
const entries = Object.entries(snapshot ?? {})
|
|
64
|
+
.filter(([file, digest]) => typeof file === "string" && typeof digest === "string")
|
|
65
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
66
|
+
return crypto.createHash("sha256").update(JSON.stringify(entries)).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function setStep(step, status, note, recordedAt) {
|
|
70
|
+
if (!step || (step.status === status && step.note === note)) return false;
|
|
71
|
+
step.status = status;
|
|
72
|
+
step.note = note;
|
|
73
|
+
step.updatedAt = recordedAt;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function applyRuntimeLifecycleObservation(task, observation, recordedAt = new Date().toISOString()) {
|
|
78
|
+
const mode = runtimeLifecycleMode(task);
|
|
79
|
+
if (mode === "manual") return { changed: false, mode };
|
|
80
|
+
|
|
81
|
+
const byId = new Map(task.workPlan.map((step) => [step.id, step]));
|
|
82
|
+
let changed = false;
|
|
83
|
+
if (observation === "context-complete") {
|
|
84
|
+
if (mode === "automatic-readonly") {
|
|
85
|
+
changed = setStep(byId.get("scout"), "done", "Runtime observed bounded read-only evidence for this scout.", recordedAt) || changed;
|
|
86
|
+
} else if (mode === "assisted-readonly") {
|
|
87
|
+
changed = setStep(byId.get("scout"), "done", "Runtime observed bounded read-only evidence for this scout.", recordedAt) || changed;
|
|
88
|
+
const review = byId.get("review");
|
|
89
|
+
if (review?.status === "pending") {
|
|
90
|
+
changed = setStep(review, "in-progress", "Review the evidence and stated unknowns before handoff.", recordedAt) || changed;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { changed, mode };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (mode === "automatic-readonly" || mode === "assisted-readonly") {
|
|
97
|
+
return { changed: false, mode };
|
|
98
|
+
}
|
|
99
|
+
if (observation === "mutation") {
|
|
100
|
+
if (mode === "assisted") {
|
|
101
|
+
changed = setStep(byId.get("plan"), "done", "Runtime observed bounded context followed by a project mutation.", recordedAt) || changed;
|
|
102
|
+
}
|
|
103
|
+
changed = setStep(byId.get("implement"), "in-progress", "Runtime observed a project mutation; current verification is required.", recordedAt) || changed;
|
|
104
|
+
const terminalStep = byId.get(mode === "automatic" ? "verify" : "review");
|
|
105
|
+
if (terminalStep && terminalStep.status !== "pending") {
|
|
106
|
+
changed = setStep(terminalStep, "pending", "A later mutation invalidated the previous terminal evidence.", recordedAt) || changed;
|
|
107
|
+
}
|
|
108
|
+
return { changed, mode };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (observation === "verification-pending") {
|
|
112
|
+
if (mode === "assisted") {
|
|
113
|
+
changed = setStep(byId.get("plan"), "done", "Runtime observed bounded context followed by implementation.", recordedAt) || changed;
|
|
114
|
+
}
|
|
115
|
+
changed = setStep(byId.get("implement"), "done", "Runtime observed project changes and an exact configured verifier.", recordedAt) || changed;
|
|
116
|
+
const verifyStep = byId.get(mode === "automatic" ? "verify" : "review");
|
|
117
|
+
if (verifyStep?.status === "pending") {
|
|
118
|
+
changed = setStep(verifyStep, "in-progress", "Waiting for current passing verification or explicit review evidence.", recordedAt) || changed;
|
|
119
|
+
}
|
|
120
|
+
return { changed, mode };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (observation === "verification-complete") {
|
|
124
|
+
if (mode === "assisted") {
|
|
125
|
+
changed = setStep(byId.get("plan"), "done", "Runtime observed bounded context followed by implementation.", recordedAt) || changed;
|
|
126
|
+
}
|
|
127
|
+
changed = setStep(byId.get("implement"), "done", "All configured verifiers passed against the current working tree.", recordedAt) || changed;
|
|
128
|
+
if (mode === "automatic") {
|
|
129
|
+
changed = setStep(byId.get("verify"), "done", "Runtime observed every configured verifier passing against the current working tree.", recordedAt) || changed;
|
|
130
|
+
} else {
|
|
131
|
+
const review = byId.get("review");
|
|
132
|
+
if (review?.status === "pending") {
|
|
133
|
+
changed = setStep(review, "in-progress", "Verification passed; complete the explicit review lens before handoff.", recordedAt) || changed;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { changed, mode };
|
|
138
|
+
}
|