@tea-agent/loop-agent 0.39.0-next.2 → 0.39.0-next.3
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/dist/build-stamp.json +2 -2
- package/dist/commands/init-upgrade.js +97 -19
- package/package.json +1 -1
package/dist/build-stamp.json
CHANGED
|
@@ -938,6 +938,80 @@ function changedWorkspacePaths(before, after) {
|
|
|
938
938
|
.filter((relativePath) => before[relativePath] !== after[relativePath])
|
|
939
939
|
.sort();
|
|
940
940
|
}
|
|
941
|
+
/**
|
|
942
|
+
* Generated-output prefixes treated as verification noise when git ignore
|
|
943
|
+
* rules are unavailable (non-git target). In a git repo the authoritative
|
|
944
|
+
* filter is `.gitignore` via `git check-ignore`; this list only covers the
|
|
945
|
+
* common case where a verification command such as `npm run build` writes
|
|
946
|
+
* untracked artifacts (build/dist/coverage/...) into the workspace.
|
|
947
|
+
*/
|
|
948
|
+
const WORKSPACE_NOISE_PREFIXES = [
|
|
949
|
+
"node_modules/",
|
|
950
|
+
"build/",
|
|
951
|
+
"dist/",
|
|
952
|
+
"coverage/",
|
|
953
|
+
".turbo/",
|
|
954
|
+
".next/",
|
|
955
|
+
".nuxt/",
|
|
956
|
+
".cache/",
|
|
957
|
+
".parcel-cache/",
|
|
958
|
+
".vite/",
|
|
959
|
+
"out/",
|
|
960
|
+
"target/",
|
|
961
|
+
"__pycache__/",
|
|
962
|
+
];
|
|
963
|
+
function isWorkspaceNoisePath(relative) {
|
|
964
|
+
if (relative === ".git" || relative.startsWith(".git/"))
|
|
965
|
+
return true;
|
|
966
|
+
if (relative === RUN_ROOT || relative.startsWith(`${RUN_ROOT}/`))
|
|
967
|
+
return true;
|
|
968
|
+
return WORKSPACE_NOISE_PREFIXES.some((prefix) => relative === prefix.slice(0, -1) || relative.startsWith(prefix));
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Filters changed workspace paths down to meaningful (non-generated) changes
|
|
972
|
+
* so a verification command that legitimately writes git-ignored artifacts is
|
|
973
|
+
* not misjudged as an unauthorized workspace mutation. Git ignore rules are
|
|
974
|
+
* authoritative when the target is a git repo; otherwise the built-in
|
|
975
|
+
* generated-output prefixes apply.
|
|
976
|
+
*/
|
|
977
|
+
async function filterWorkspaceNoise(repoRoot, relativePaths) {
|
|
978
|
+
const builtinFiltered = relativePaths.filter((relative) => !isWorkspaceNoisePath(relative));
|
|
979
|
+
if (builtinFiltered.length === 0)
|
|
980
|
+
return builtinFiltered;
|
|
981
|
+
const ignored = await gitIgnoredPaths(repoRoot, builtinFiltered);
|
|
982
|
+
if (ignored === null)
|
|
983
|
+
return builtinFiltered;
|
|
984
|
+
return builtinFiltered.filter((relative) => !ignored.has(relative));
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Returns the subset of paths matched by git ignore rules, or null when the
|
|
988
|
+
* target is not a git repository (or git is unavailable), in which case the
|
|
989
|
+
* caller falls back to the built-in noise prefixes.
|
|
990
|
+
*/
|
|
991
|
+
async function gitIgnoredPaths(repoRoot, relativePaths) {
|
|
992
|
+
return await new Promise((resolve) => {
|
|
993
|
+
execFile("git", ["-C", repoRoot, "check-ignore", ...relativePaths], {
|
|
994
|
+
timeout: 15_000,
|
|
995
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
996
|
+
windowsHide: true,
|
|
997
|
+
}, (error, stdout) => {
|
|
998
|
+
if (error) {
|
|
999
|
+
// `git check-ignore` exits 1 when no path is ignored: that is a
|
|
1000
|
+
// successful lookup with an empty result set. Any other failure
|
|
1001
|
+
// (e.g. 128 outside a repo) means the git filter is unavailable.
|
|
1002
|
+
if (error.code === 1 && stdout.length === 0)
|
|
1003
|
+
resolve(new Set());
|
|
1004
|
+
else
|
|
1005
|
+
resolve(null);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
resolve(new Set(stdout
|
|
1009
|
+
.split("\n")
|
|
1010
|
+
.filter(Boolean)
|
|
1011
|
+
.map((entry) => entry.split(path.sep).join("/"))));
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
941
1015
|
function isFrozenVerificationSurface(value) {
|
|
942
1016
|
return (isRecord(value) &&
|
|
943
1017
|
value.schemaVersion === 1 &&
|
|
@@ -1587,7 +1661,7 @@ async function validateContinueMerge(directory, state, anchor) {
|
|
|
1587
1661
|
await assertSafeMergePath(state.repoRoot, allowedPath);
|
|
1588
1662
|
}
|
|
1589
1663
|
const current = await snapshotWorkspace(state.repoRoot);
|
|
1590
|
-
const
|
|
1664
|
+
const rawChanged = changedWorkspacePaths(authoritativeGuard.baselineWorkspace, current);
|
|
1591
1665
|
const pending = await readPendingAcceptance({
|
|
1592
1666
|
directory,
|
|
1593
1667
|
state,
|
|
@@ -1602,6 +1676,10 @@ async function validateContinueMerge(directory, state, anchor) {
|
|
|
1602
1676
|
pending,
|
|
1603
1677
|
})
|
|
1604
1678
|
: undefined;
|
|
1679
|
+
// Only the out-of-bound drift check treats git-ignored/generated-output
|
|
1680
|
+
// changes as noise; allowed-path changes are merge content and must stay
|
|
1681
|
+
// intact even when the target is git-ignored (e.g. `.agents/skills/*`).
|
|
1682
|
+
const changed = await filterWorkspaceNoise(state.repoRoot, rawChanged);
|
|
1605
1683
|
const unexpected = changed.filter((relativePath) => {
|
|
1606
1684
|
if (allowed.has(relativePath))
|
|
1607
1685
|
return false;
|
|
@@ -1611,7 +1689,7 @@ async function validateContinueMerge(directory, state, anchor) {
|
|
|
1611
1689
|
if (unexpected.length > 0) {
|
|
1612
1690
|
throw new Error(`init upgrade merge write-guard blocked out-of-bound workspace changes: ${unexpected.join(", ")}`);
|
|
1613
1691
|
}
|
|
1614
|
-
const changedAllowed =
|
|
1692
|
+
const changedAllowed = rawChanged.filter((relativePath) => allowed.has(relativePath));
|
|
1615
1693
|
if (changedAllowed.length === 0) {
|
|
1616
1694
|
if (pending) {
|
|
1617
1695
|
throw new Error("pending merge acceptance cannot authorize a merge without an allowed workspace change");
|
|
@@ -1834,16 +1912,8 @@ async function createVerificationWriteAudit(input) {
|
|
|
1834
1912
|
const note = (scope, file) => {
|
|
1835
1913
|
events.push(`${scope}=${file.split(path.sep).join("/")}`);
|
|
1836
1914
|
};
|
|
1837
|
-
const ignoredWorkspacePath = (candidate) =>
|
|
1838
|
-
|
|
1839
|
-
return (relative === ".git" ||
|
|
1840
|
-
relative.startsWith(".git/") ||
|
|
1841
|
-
relative === "node_modules" ||
|
|
1842
|
-
relative.startsWith("node_modules/") ||
|
|
1843
|
-
relative === RUN_ROOT ||
|
|
1844
|
-
relative.startsWith(`${RUN_ROOT}/`));
|
|
1845
|
-
};
|
|
1846
|
-
const install = async (scope, directory, ignore) => {
|
|
1915
|
+
const ignoredWorkspacePath = (candidate) => isWorkspaceNoisePath(toRepoPath(canonicalRepoRoot, candidate));
|
|
1916
|
+
const install = async (scope, directory, ignore, scopeRoot) => {
|
|
1847
1917
|
const canonical = await realpath(directory);
|
|
1848
1918
|
if (watched.has(canonical))
|
|
1849
1919
|
return;
|
|
@@ -1867,11 +1937,11 @@ async function createVerificationWriteAudit(input) {
|
|
|
1867
1937
|
// traversal opens an existing child directory. Nested watchers own real
|
|
1868
1938
|
// mutations below that child; persistent changes are also snapshot-checked.
|
|
1869
1939
|
if (!existingDirectory) {
|
|
1870
|
-
note(scope, path.relative(directory, candidate) || ".");
|
|
1940
|
+
note(scope, path.relative(scopeRoot ?? directory, candidate) || ".");
|
|
1871
1941
|
}
|
|
1872
1942
|
// A parent rename can introduce a new directory. Installing its watcher
|
|
1873
1943
|
// closes the delayed-descendant gap even when the container event is noise.
|
|
1874
|
-
void install(scope, candidate, ignore).catch((error) => {
|
|
1944
|
+
void install(scope, candidate, ignore, scopeRoot).catch((error) => {
|
|
1875
1945
|
const code = error.code;
|
|
1876
1946
|
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
1877
1947
|
failures.push(`watcher refresh failed for ${scope}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1891,10 +1961,10 @@ async function createVerificationWriteAudit(input) {
|
|
|
1891
1961
|
const child = path.join(canonical, entry.name);
|
|
1892
1962
|
if (ignore?.(child) || !entry.isDirectory() || entry.isSymbolicLink())
|
|
1893
1963
|
continue;
|
|
1894
|
-
await install(scope, child, ignore);
|
|
1964
|
+
await install(scope, child, ignore, scopeRoot);
|
|
1895
1965
|
}
|
|
1896
1966
|
};
|
|
1897
|
-
await install("workspace", canonicalRepoRoot, ignoredWorkspacePath);
|
|
1967
|
+
await install("workspace", canonicalRepoRoot, ignoredWorkspacePath, canonicalRepoRoot);
|
|
1898
1968
|
await install("controller", runRoot(input.repoRoot));
|
|
1899
1969
|
await install("controller-anchor", anchor.anchorRoot);
|
|
1900
1970
|
const piHomeEntry = await lstat(piHome).catch((error) => {
|
|
@@ -2194,17 +2264,25 @@ async function runVerificationCommand(input) {
|
|
|
2194
2264
|
if (audit.failures.length)
|
|
2195
2265
|
throw new Error(audit.failures.join("; "));
|
|
2196
2266
|
await assertFrozenVerificationSurface(repoRoot, surface);
|
|
2197
|
-
const changedWorkspace = changedWorkspacePaths(workspaceBefore, await snapshotWorkspace(repoRoot));
|
|
2267
|
+
const changedWorkspace = await filterWorkspaceNoise(repoRoot, changedWorkspacePaths(workspaceBefore, await snapshotWorkspace(repoRoot)));
|
|
2198
2268
|
const changedController = changedWorkspacePaths(controllerBefore, await snapshotDirectory(runRoot(repoRoot)));
|
|
2199
2269
|
const changedAnchors = changedWorkspacePaths(anchorBefore, await controllerAnchorSnapshot(repoRoot, state));
|
|
2200
2270
|
const changedPiHome = changedWorkspacePaths(piHomeBefore, await snapshotOptionalDirectory(path.join(os.homedir(), ".pi")));
|
|
2201
|
-
|
|
2271
|
+
// Workspace watcher events are repo-root relative (see scopeRoot in
|
|
2272
|
+
// createVerificationWriteAudit); drop git-ignored/generated-output noise
|
|
2273
|
+
// before deciding whether the subprocess wrote the workspace.
|
|
2274
|
+
const workspaceEvents = audit.events.filter((event) => event.startsWith("workspace="));
|
|
2275
|
+
const remainingEvents = [
|
|
2276
|
+
...audit.events.filter((event) => !event.startsWith("workspace=")),
|
|
2277
|
+
...(await filterWorkspaceNoise(repoRoot, workspaceEvents.map((event) => event.slice("workspace=".length)))).map((relative) => `workspace=${relative}`),
|
|
2278
|
+
];
|
|
2279
|
+
if (descendantsObserved || remainingEvents.length || changedWorkspace.length || changedController.length || changedAnchors.length || changedPiHome.length) {
|
|
2202
2280
|
return {
|
|
2203
2281
|
...result,
|
|
2204
2282
|
ok: false,
|
|
2205
2283
|
stderr: boundedOutput(`${result.stderr}\nverification write guard blocked changes: ${[
|
|
2206
2284
|
...(descendantsObserved ? ["process-tree=descendant"] : []),
|
|
2207
|
-
...(
|
|
2285
|
+
...(remainingEvents.length ? [`events=${remainingEvents.join(",")}`] : []),
|
|
2208
2286
|
...(changedWorkspace.length ? [`workspace=${changedWorkspace.join(",")}`] : []),
|
|
2209
2287
|
...(changedController.length ? [`controller=${changedController.join(",")}`] : []),
|
|
2210
2288
|
...(changedAnchors.length ? [`controller-anchor=${changedAnchors.join(",")}`] : []),
|