agentplane 0.3.21 → 0.3.22
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/assets/policy/incidents.md +3 -0
- package/dist/.build-manifest.json +33 -28
- package/dist/cli/bootstrap-guide.d.ts +1 -0
- package/dist/cli/bootstrap-guide.d.ts.map +1 -1
- package/dist/cli/bootstrap-guide.js +3 -2
- package/dist/cli/command-guide.d.ts.map +1 -1
- package/dist/cli/command-guide.js +5 -4
- package/dist/cli.js +351 -336
- package/dist/commands/branch/work-start.hook-shim.d.ts.map +1 -1
- package/dist/commands/branch/work-start.hook-shim.js +16 -4
- package/dist/commands/doctor/hook-readiness.d.ts +2 -0
- package/dist/commands/doctor/hook-readiness.d.ts.map +1 -0
- package/dist/commands/doctor/hook-readiness.js +171 -0
- package/dist/commands/doctor/workspace.d.ts.map +1 -1
- package/dist/commands/doctor/workspace.js +2 -1
- package/dist/commands/guard/impl/commit.d.ts.map +1 -1
- package/dist/commands/guard/impl/commit.js +26 -0
- package/dist/commands/hooks/install.d.ts.map +1 -1
- package/dist/commands/hooks/install.js +16 -4
- package/dist/commands/hooks/run.pre-push.d.ts.map +1 -1
- package/dist/commands/hooks/run.pre-push.js +234 -13
- package/dist/commands/task/derive.command.js +1 -1
- package/dist/commands/task/finish-execute.d.ts.map +1 -1
- package/dist/commands/task/finish-execute.js +8 -1
- package/dist/commands/task/shared/transitions.d.ts.map +1 -1
- package/dist/commands/task/shared/transitions.js +11 -2
- package/dist/commands/task/verify-command-shared.d.ts.map +1 -1
- package/dist/commands/task/verify-command-shared.js +8 -2
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work-start.hook-shim.d.ts","sourceRoot":"","sources":["../../../src/commands/branch/work-start.hook-shim.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"work-start.hook-shim.d.ts","sourceRoot":"","sources":["../../../src/commands/branch/work-start.hook-shim.ts"],"names":[],"mappings":"AAgDA,wBAAsB,8BAA8B,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOxF"}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileExists } from "../../cli/fs-utils.js";
|
|
4
|
+
import { resolveAgentplaneBinPath } from "../../shared/package-paths.js";
|
|
4
5
|
const HOOK_SHIM_MARKER = "agentplane-hook-shim";
|
|
5
|
-
function
|
|
6
|
+
function shellSingleQuote(value) {
|
|
7
|
+
return `'${value.replaceAll("'", String.raw `'\''`)}'`;
|
|
8
|
+
}
|
|
9
|
+
function resolveInstalledHookRunnerPath() {
|
|
10
|
+
const activeBin = String(process.env.AGENTPLANE_RUNTIME_ACTIVE_BIN ?? "").trim();
|
|
11
|
+
return activeBin || resolveAgentplaneBinPath();
|
|
12
|
+
}
|
|
13
|
+
function repoLocalHookShimText(installedRunnerPath) {
|
|
6
14
|
return [
|
|
7
15
|
"#!/usr/bin/env sh",
|
|
8
16
|
`# ${HOOK_SHIM_MARKER} (do not edit)`,
|
|
@@ -13,6 +21,10 @@ function repoLocalHookShimText() {
|
|
|
13
21
|
'if command -v node >/dev/null 2>&1 && [ -f "$LOCAL_BIN" ]; then',
|
|
14
22
|
' exec node "$LOCAL_BIN" "$@"',
|
|
15
23
|
"fi",
|
|
24
|
+
`INSTALL_BIN=${shellSingleQuote(installedRunnerPath)}`,
|
|
25
|
+
'if command -v node >/dev/null 2>&1 && [ -f "$INSTALL_BIN" ]; then',
|
|
26
|
+
' exec node "$INSTALL_BIN" "$@"',
|
|
27
|
+
"fi",
|
|
16
28
|
'ENV_BIN="${AGENTPLANE_HOOK_RUNNER:-}"',
|
|
17
29
|
'if [ -n "$ENV_BIN" ] && command -v node >/dev/null 2>&1 && [ -f "$ENV_BIN" ]; then',
|
|
18
30
|
' exec node "$ENV_BIN" "$@"',
|
|
@@ -20,10 +32,10 @@ function repoLocalHookShimText() {
|
|
|
20
32
|
"if command -v agentplane >/dev/null 2>&1; then",
|
|
21
33
|
' exec agentplane "$@"',
|
|
22
34
|
"fi",
|
|
23
|
-
|
|
35
|
+
'if [ "${AGENTPLANE_HOOK_ALLOW_NPX:-}" = "1" ] && command -v npx >/dev/null 2>&1; then',
|
|
24
36
|
' exec npx --yes agentplane "$@"',
|
|
25
37
|
"fi",
|
|
26
|
-
'echo "agentplane shim: runner not found (need env runner, repo-local source, agentplane in PATH, or
|
|
38
|
+
'echo "agentplane shim: runner not found (need installed runner, env runner, repo-local source, agentplane in PATH, or AGENTPLANE_HOOK_ALLOW_NPX=1)." >&2',
|
|
27
39
|
" exit 127",
|
|
28
40
|
"",
|
|
29
41
|
].join("\n");
|
|
@@ -33,6 +45,6 @@ export async function materializeHookShimForWorktree(worktreePath) {
|
|
|
33
45
|
if (await fileExists(shimPath))
|
|
34
46
|
return;
|
|
35
47
|
await mkdir(path.dirname(shimPath), { recursive: true });
|
|
36
|
-
await writeFile(shimPath, repoLocalHookShimText(), "utf8");
|
|
48
|
+
await writeFile(shimPath, repoLocalHookShimText(resolveInstalledHookRunnerPath()), "utf8");
|
|
37
49
|
await chmod(shimPath, 0o755);
|
|
38
50
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-readiness.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/hook-readiness.ts"],"names":[],"mappings":"AA4EA,wBAAsB,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAyIvF"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { renderDiagnosticFinding } from "../shared/diagnostics.js";
|
|
4
|
+
import { fileIsManaged, HOOK_MARKER, HOOK_NAMES, resolveGitHooksDir, SHIM_MARKER, } from "../hooks/shared.js";
|
|
5
|
+
async function pathExists(absPath) {
|
|
6
|
+
try {
|
|
7
|
+
await fs.access(absPath);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function readTextIfExists(absPath) {
|
|
15
|
+
try {
|
|
16
|
+
return await fs.readFile(absPath, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function unquoteShellSingleQuoted(value) {
|
|
23
|
+
return value.replaceAll(String.raw `'\''`, "'");
|
|
24
|
+
}
|
|
25
|
+
function extractInstallBinFromShim(shimText) {
|
|
26
|
+
const match = /\nINSTALL_BIN='((?:'\\''|[^'])*)'/.exec(shimText);
|
|
27
|
+
if (!match)
|
|
28
|
+
return null;
|
|
29
|
+
return unquoteShellSingleQuoted(match[1] ?? "").trim() || null;
|
|
30
|
+
}
|
|
31
|
+
async function resolveManagedHookNames(repoRoot) {
|
|
32
|
+
let hooksDir;
|
|
33
|
+
try {
|
|
34
|
+
hooksDir = await resolveGitHooksDir(repoRoot);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { hooksDir: null, managedHooks: [], unmanagedHooks: [] };
|
|
38
|
+
}
|
|
39
|
+
const managedHooks = [];
|
|
40
|
+
const unmanagedHooks = [];
|
|
41
|
+
for (const hook of HOOK_NAMES) {
|
|
42
|
+
const hookPath = path.join(hooksDir, hook);
|
|
43
|
+
if (!(await pathExists(hookPath)))
|
|
44
|
+
continue;
|
|
45
|
+
if (await fileIsManaged(hookPath, HOOK_MARKER))
|
|
46
|
+
managedHooks.push(hook);
|
|
47
|
+
else
|
|
48
|
+
unmanagedHooks.push(hook);
|
|
49
|
+
}
|
|
50
|
+
return { hooksDir, managedHooks, unmanagedHooks };
|
|
51
|
+
}
|
|
52
|
+
function renderUnmanagedHooksFinding(unmanagedHooks) {
|
|
53
|
+
return renderDiagnosticFinding({
|
|
54
|
+
severity: "INFO",
|
|
55
|
+
state: "unmanaged git hooks are present and will not be overwritten by AgentPlane",
|
|
56
|
+
likelyCause: "the repository has custom hook files outside the AgentPlane managed hook set",
|
|
57
|
+
nextAction: {
|
|
58
|
+
command: "agentplane hooks install",
|
|
59
|
+
reason: "refresh managed hooks only after manually reconciling custom hook files that should remain custom",
|
|
60
|
+
},
|
|
61
|
+
details: [`Unmanaged hooks: ${unmanagedHooks.join(", ")}`],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export async function checkManagedHookShimReadiness(repoRoot) {
|
|
65
|
+
const findings = [];
|
|
66
|
+
const { managedHooks, unmanagedHooks } = await resolveManagedHookNames(repoRoot);
|
|
67
|
+
const shimRelPath = ".agentplane/bin/agentplane";
|
|
68
|
+
const shimPath = path.join(repoRoot, shimRelPath);
|
|
69
|
+
const shimText = await readTextIfExists(shimPath);
|
|
70
|
+
const managedShim = shimText?.includes(SHIM_MARKER) === true;
|
|
71
|
+
if (managedHooks.length === 0 && unmanagedHooks.length === 0 && !shimText)
|
|
72
|
+
return findings;
|
|
73
|
+
if (managedHooks.length > 0 && !shimText) {
|
|
74
|
+
findings.push(renderDiagnosticFinding({
|
|
75
|
+
severity: "WARN",
|
|
76
|
+
state: "managed git hooks are installed but the AgentPlane hook shim is missing",
|
|
77
|
+
likelyCause: "the repository hook files still call .agentplane/bin/agentplane, but that shim was deleted or never committed",
|
|
78
|
+
nextAction: {
|
|
79
|
+
command: "agentplane hooks install",
|
|
80
|
+
reason: "recreate the managed hook shim and refresh hook files",
|
|
81
|
+
},
|
|
82
|
+
details: [`Managed hooks: ${managedHooks.join(", ")}`],
|
|
83
|
+
}));
|
|
84
|
+
return findings;
|
|
85
|
+
}
|
|
86
|
+
if (shimText && !managedShim) {
|
|
87
|
+
findings.push(renderDiagnosticFinding({
|
|
88
|
+
severity: "WARN",
|
|
89
|
+
state: "AgentPlane hook shim path exists but is not managed by AgentPlane",
|
|
90
|
+
likelyCause: "a custom .agentplane/bin/agentplane file is shadowing the managed hook runner shim",
|
|
91
|
+
nextAction: {
|
|
92
|
+
command: "inspect .agentplane/bin/agentplane, remove the custom file if unintended, then run agentplane hooks install",
|
|
93
|
+
reason: "restore the managed shim without overwriting custom executable content silently",
|
|
94
|
+
},
|
|
95
|
+
details: [`Shim path: ${shimRelPath}`],
|
|
96
|
+
}));
|
|
97
|
+
return findings;
|
|
98
|
+
}
|
|
99
|
+
if (!shimText) {
|
|
100
|
+
if (unmanagedHooks.length > 0) {
|
|
101
|
+
findings.push(renderUnmanagedHooksFinding(unmanagedHooks));
|
|
102
|
+
}
|
|
103
|
+
return findings;
|
|
104
|
+
}
|
|
105
|
+
const installedBin = extractInstallBinFromShim(shimText);
|
|
106
|
+
if (!installedBin) {
|
|
107
|
+
findings.push(renderDiagnosticFinding({
|
|
108
|
+
severity: "WARN",
|
|
109
|
+
state: "managed AgentPlane hook shim uses a stale format without an installed runner fallback",
|
|
110
|
+
likelyCause: "the shim was generated by an older AgentPlane release before installed-binary fallback was embedded",
|
|
111
|
+
nextAction: {
|
|
112
|
+
command: "agentplane hooks install",
|
|
113
|
+
reason: "rewrite the managed shim with the current installed-runtime fallback contract",
|
|
114
|
+
},
|
|
115
|
+
details: [`Shim path: ${shimRelPath}`],
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
else if (!(await pathExists(installedBin))) {
|
|
119
|
+
findings.push(renderDiagnosticFinding({
|
|
120
|
+
severity: "WARN",
|
|
121
|
+
state: "managed AgentPlane hook shim points to a missing installed runner",
|
|
122
|
+
likelyCause: "AgentPlane was reinstalled, removed, or moved after hooks were installed in this repository",
|
|
123
|
+
nextAction: {
|
|
124
|
+
command: "agentplane hooks install",
|
|
125
|
+
reason: "refresh the embedded installed runner path after reinstalling or updating AgentPlane",
|
|
126
|
+
},
|
|
127
|
+
details: [`Embedded runner: ${installedBin}`, `Shim path: ${shimRelPath}`],
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
const hasEnvFallback = shimText.includes("AGENTPLANE_HOOK_RUNNER");
|
|
131
|
+
const hasNpxFallback = shimText.includes("AGENTPLANE_HOOK_ALLOW_NPX");
|
|
132
|
+
if (!hasEnvFallback || !hasNpxFallback) {
|
|
133
|
+
findings.push(renderDiagnosticFinding({
|
|
134
|
+
severity: "WARN",
|
|
135
|
+
state: "managed AgentPlane hook shim is missing current fallback branches",
|
|
136
|
+
likelyCause: "the repository still has an older managed shim that cannot use env-runner or explicit npx fallback recovery",
|
|
137
|
+
nextAction: {
|
|
138
|
+
command: "agentplane hooks install",
|
|
139
|
+
reason: "rewrite the shim with the current fallback chain",
|
|
140
|
+
},
|
|
141
|
+
details: [
|
|
142
|
+
`Shim path: ${shimRelPath}`,
|
|
143
|
+
`AGENTPLANE_HOOK_RUNNER fallback: ${hasEnvFallback ? "present" : "missing"}`,
|
|
144
|
+
`AGENTPLANE_HOOK_ALLOW_NPX fallback: ${hasNpxFallback ? "present" : "missing"}`,
|
|
145
|
+
],
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
if (managedHooks.includes("pre-push")) {
|
|
149
|
+
const repoPrePushScript = path.join(repoRoot, "scripts", "run-pre-push-hook.mjs");
|
|
150
|
+
const packageJsonText = await readTextIfExists(path.join(repoRoot, "package.json"));
|
|
151
|
+
if (!(await pathExists(repoPrePushScript)) && packageJsonText) {
|
|
152
|
+
findings.push(renderDiagnosticFinding({
|
|
153
|
+
severity: "INFO",
|
|
154
|
+
state: "managed pre-push hook will use installed clean-project fallback checks",
|
|
155
|
+
likelyCause: "this project does not ship a repository-local scripts/run-pre-push-hook.mjs",
|
|
156
|
+
nextAction: {
|
|
157
|
+
command: "add project-local package scripts such as format:check or test, or keep the installed fallback",
|
|
158
|
+
reason: "make pre-push behavior explicit when the project needs stricter repository-specific checks",
|
|
159
|
+
},
|
|
160
|
+
details: [
|
|
161
|
+
"Missing repository script: scripts/run-pre-push-hook.mjs",
|
|
162
|
+
"Installed fallback skips package scripts that are not declared by the clean project.",
|
|
163
|
+
],
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (unmanagedHooks.length > 0) {
|
|
168
|
+
findings.push(renderUnmanagedHooksFinding(unmanagedHooks));
|
|
169
|
+
}
|
|
170
|
+
return findings;
|
|
171
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/workspace.ts"],"names":[],"mappings":"AAYA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/workspace.ts"],"names":[],"mappings":"AAYA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAIpF,KAAK,eAAe,GAAG;IACrB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAqFF,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,MAAM,EAAE,CAyDnF;AAED,wBAAsB,6BAA6B,CACjD,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,cAAc,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAOnB;AAoMD,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,cAAc,CAAA;CAAE,GAC9B,OAAO,CAAC,MAAM,EAAE,CAAC,CA4DnB"}
|
|
@@ -6,6 +6,7 @@ import { resolvePolicyGatewayForRepo } from "../../shared/policy-gateway.js";
|
|
|
6
6
|
import { GitContext } from "@agentplaneorg/core/git";
|
|
7
7
|
import { listTaskProjection } from "../shared/task-backend.js";
|
|
8
8
|
import { resolveAgentplaneAssetPath } from "../../shared/package-paths.js";
|
|
9
|
+
import { checkManagedHookShimReadiness } from "./hook-readiness.js";
|
|
9
10
|
async function pathExists(absPath) {
|
|
10
11
|
try {
|
|
11
12
|
await fs.access(absPath);
|
|
@@ -365,6 +366,6 @@ export async function checkWorkspace(repoRoot, opts) {
|
|
|
365
366
|
if (!hasJson) {
|
|
366
367
|
problems.push("No agent profiles found in .agentplane/agents (*.json expected).");
|
|
367
368
|
}
|
|
368
|
-
problems.push(...checkBackendReadiness(opts?.ctx), ...(await checkTaskReadmeMigrationState(repoRoot, opts?.ctx)), ...(await checkDoneTaskReadmeArchiveDrift(repoRoot, opts?.ctx)), ...(await checkTaskProjectionDrift(repoRoot, opts?.ctx)));
|
|
369
|
+
problems.push(...checkBackendReadiness(opts?.ctx), ...(await checkManagedHookShimReadiness(repoRoot)), ...(await checkTaskReadmeMigrationState(repoRoot, opts?.ctx)), ...(await checkDoneTaskReadmeArchiveDrift(repoRoot, opts?.ctx)), ...(await checkTaskProjectionDrift(repoRoot, opts?.ctx)));
|
|
369
370
|
return problems;
|
|
370
371
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commit.d.ts","sourceRoot":"","sources":["../../../../src/commands/guard/impl/commit.ts"],"names":[],"mappings":"AAaA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"commit.d.ts","sourceRoot":"","sources":["../../../../src/commands/guard/impl/commit.ts"],"names":[],"mappings":"AAaA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,8BAA8B,CAAC;AAwLtC,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB,EAAE,OAAO,CAAC;IAC5B,cAAc,EAAE,OAAO,CAAC;IACxB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC,GAAG,OAAO,CAAC,MAAM,CAAC,CAwIlB"}
|
|
@@ -137,6 +137,22 @@ function formatCommitRef(commit) {
|
|
|
137
137
|
return "";
|
|
138
138
|
return `${commit.hash?.slice(0, 12) ?? ""} ${commit.subject ?? ""}`.trim();
|
|
139
139
|
}
|
|
140
|
+
async function stageActiveTaskArtifactsFromAllowTasks(opts) {
|
|
141
|
+
if (!opts.allowTasks)
|
|
142
|
+
return [];
|
|
143
|
+
const changedPaths = await opts.ctx.git.statusChangedPaths();
|
|
144
|
+
const taskArtifactPaths = changedPaths.filter((relPath) => isTaskLocalAdvancePath({
|
|
145
|
+
workflowDir: opts.ctx.config.paths.workflow_dir,
|
|
146
|
+
taskId: opts.taskId,
|
|
147
|
+
tasksPath: opts.ctx.config.paths.tasks_path,
|
|
148
|
+
relPath,
|
|
149
|
+
}));
|
|
150
|
+
if (taskArtifactPaths.length === 0)
|
|
151
|
+
return [];
|
|
152
|
+
const unique = [...new Set(taskArtifactPaths)].toSorted((a, b) => a.localeCompare(b));
|
|
153
|
+
await opts.ctx.git.stage(unique);
|
|
154
|
+
return unique;
|
|
155
|
+
}
|
|
140
156
|
export async function cmdCommit(opts) {
|
|
141
157
|
try {
|
|
142
158
|
const ctx = opts.ctx ??
|
|
@@ -181,6 +197,16 @@ export async function cmdCommit(opts) {
|
|
|
181
197
|
process.stdout.write(`${infoMessage(`commit auto-staged ${autoStaged.length} path(s) from allowlist`)}\n`);
|
|
182
198
|
}
|
|
183
199
|
}
|
|
200
|
+
else {
|
|
201
|
+
autoStaged = await stageActiveTaskArtifactsFromAllowTasks({
|
|
202
|
+
ctx,
|
|
203
|
+
taskId: opts.taskId,
|
|
204
|
+
allowTasks: opts.allowTasks,
|
|
205
|
+
});
|
|
206
|
+
if (autoStaged.length > 0 && !opts.quiet) {
|
|
207
|
+
process.stdout.write(`${infoMessage(`commit auto-staged ${autoStaged.length} active task artifact path(s) from --allow-tasks`)}\n`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
184
210
|
await guardCommitCheck({
|
|
185
211
|
ctx,
|
|
186
212
|
cwd: opts.cwd,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/install.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/install.ts"],"names":[],"mappings":"AAgGA,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmClB;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BlB"}
|
|
@@ -5,6 +5,7 @@ import { mapCoreError } from "../../cli/error-map.js";
|
|
|
5
5
|
import { fileExists } from "../../cli/fs-utils.js";
|
|
6
6
|
import { infoMessage, successMessage } from "../../cli/output.js";
|
|
7
7
|
import { CliError } from "../../shared/errors.js";
|
|
8
|
+
import { resolveAgentplaneBinPath } from "../../shared/package-paths.js";
|
|
8
9
|
import { fileIsManaged, HOOK_MARKER, HOOK_NAMES, resolveGitHooksDir, SHIM_MARKER, } from "./shared.js";
|
|
9
10
|
function hookScriptText(hook) {
|
|
10
11
|
return [
|
|
@@ -24,7 +25,14 @@ function hookScriptText(hook) {
|
|
|
24
25
|
"",
|
|
25
26
|
].join("\n");
|
|
26
27
|
}
|
|
27
|
-
function
|
|
28
|
+
function shellSingleQuote(value) {
|
|
29
|
+
return `'${value.replaceAll("'", String.raw `'\''`)}'`;
|
|
30
|
+
}
|
|
31
|
+
function resolveInstalledHookRunnerPath() {
|
|
32
|
+
const activeBin = String(process.env.AGENTPLANE_RUNTIME_ACTIVE_BIN ?? "").trim();
|
|
33
|
+
return activeBin || resolveAgentplaneBinPath();
|
|
34
|
+
}
|
|
35
|
+
function shimScriptText(installedRunnerPath) {
|
|
28
36
|
return [
|
|
29
37
|
"#!/usr/bin/env sh",
|
|
30
38
|
`# ${SHIM_MARKER} (do not edit)`,
|
|
@@ -35,6 +43,10 @@ function shimScriptText() {
|
|
|
35
43
|
'if command -v node >/dev/null 2>&1 && [ -f "$LOCAL_BIN" ]; then',
|
|
36
44
|
' exec node "$LOCAL_BIN" "$@"',
|
|
37
45
|
"fi",
|
|
46
|
+
`INSTALL_BIN=${shellSingleQuote(installedRunnerPath)}`,
|
|
47
|
+
'if command -v node >/dev/null 2>&1 && [ -f "$INSTALL_BIN" ]; then',
|
|
48
|
+
' exec node "$INSTALL_BIN" "$@"',
|
|
49
|
+
"fi",
|
|
38
50
|
'ENV_BIN="${AGENTPLANE_HOOK_RUNNER:-}"',
|
|
39
51
|
'if [ -n "$ENV_BIN" ] && command -v node >/dev/null 2>&1 && [ -f "$ENV_BIN" ]; then',
|
|
40
52
|
' exec node "$ENV_BIN" "$@"',
|
|
@@ -42,10 +54,10 @@ function shimScriptText() {
|
|
|
42
54
|
"if command -v agentplane >/dev/null 2>&1; then",
|
|
43
55
|
' exec agentplane "$@"',
|
|
44
56
|
"fi",
|
|
45
|
-
|
|
57
|
+
'if [ "${AGENTPLANE_HOOK_ALLOW_NPX:-}" = "1" ] && command -v npx >/dev/null 2>&1; then',
|
|
46
58
|
' exec npx --yes agentplane "$@"',
|
|
47
59
|
"fi",
|
|
48
|
-
'echo "agentplane shim: runner not found (need env runner, repo-local source, agentplane in PATH, or
|
|
60
|
+
'echo "agentplane shim: runner not found (need installed runner, env runner, repo-local source, agentplane in PATH, or AGENTPLANE_HOOK_ALLOW_NPX=1)." >&2',
|
|
49
61
|
" exit 127",
|
|
50
62
|
"",
|
|
51
63
|
].join("\n");
|
|
@@ -64,7 +76,7 @@ async function ensureShim(agentplaneDir, gitRoot) {
|
|
|
64
76
|
});
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
|
-
await writeFile(shimPath, shimScriptText(), "utf8");
|
|
79
|
+
await writeFile(shimPath, shimScriptText(resolveInstalledHookRunnerPath()), "utf8");
|
|
68
80
|
await chmod(shimPath, 0o755);
|
|
69
81
|
}
|
|
70
82
|
export async function cmdHooksInstall(opts) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.pre-push.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/run.pre-push.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"run.pre-push.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/run.pre-push.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAyBhD,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GACxC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMxB;AA+TD,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAuB3E"}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { resolveProject } from "@agentplaneorg/core/project";
|
|
2
2
|
import { runProcessSync } from "@agentplaneorg/core/process";
|
|
3
|
+
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileExists } from "../../cli/fs-utils.js";
|
|
5
|
-
import { CliError } from "../../shared/errors.js";
|
|
6
6
|
import { resolveAgentplaneRepoScriptPath } from "../../shared/package-paths.js";
|
|
7
7
|
function resolveBundledPrePushHookScriptPath() {
|
|
8
8
|
return resolveAgentplaneRepoScriptPath("run-pre-push-hook.mjs");
|
|
9
9
|
}
|
|
10
|
+
class HookFailure extends Error {
|
|
11
|
+
details;
|
|
12
|
+
constructor(message, details = []) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.details = details;
|
|
15
|
+
this.name = "HookFailure";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
10
18
|
export async function resolvePrePushHookScriptPath(gitRoot, opts = {}) {
|
|
11
19
|
const repoScriptPath = path.join(gitRoot, "scripts", "run-pre-push-hook.mjs");
|
|
12
20
|
if (await fileExists(repoScriptPath))
|
|
@@ -16,6 +24,228 @@ export async function resolvePrePushHookScriptPath(gitRoot, opts = {}) {
|
|
|
16
24
|
return bundledScriptPath;
|
|
17
25
|
return null;
|
|
18
26
|
}
|
|
27
|
+
function parsePrePushStdin(rawStdin) {
|
|
28
|
+
return rawStdin
|
|
29
|
+
.split("\n")
|
|
30
|
+
.map((line) => line.trim())
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.map((line) => {
|
|
33
|
+
const [localRef = "", localSha = "", remoteRef = "", remoteSha = ""] = line.split(/\s+/);
|
|
34
|
+
return { localRef, localSha, remoteRef, remoteSha };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function isAllZeroSha(value) {
|
|
38
|
+
return /^[0]+$/.test(value);
|
|
39
|
+
}
|
|
40
|
+
function isBranchRef(ref) {
|
|
41
|
+
return ref.startsWith("refs/heads/");
|
|
42
|
+
}
|
|
43
|
+
function runHookCommand(gitRoot, command, args) {
|
|
44
|
+
const result = runProcessSync({
|
|
45
|
+
command,
|
|
46
|
+
args,
|
|
47
|
+
cwd: gitRoot,
|
|
48
|
+
env: process.env,
|
|
49
|
+
stdout: "inherit",
|
|
50
|
+
stderr: "inherit",
|
|
51
|
+
reject: false,
|
|
52
|
+
});
|
|
53
|
+
return result.exitCode ?? (result.signal ? 1 : 0);
|
|
54
|
+
}
|
|
55
|
+
function runHookCommandWithEnv(gitRoot, command, args, env) {
|
|
56
|
+
const result = runProcessSync({
|
|
57
|
+
command,
|
|
58
|
+
args,
|
|
59
|
+
cwd: gitRoot,
|
|
60
|
+
env,
|
|
61
|
+
stdout: "inherit",
|
|
62
|
+
stderr: "inherit",
|
|
63
|
+
reject: false,
|
|
64
|
+
});
|
|
65
|
+
return result.exitCode ?? (result.signal ? 1 : 0);
|
|
66
|
+
}
|
|
67
|
+
function readGitText(gitRoot, args) {
|
|
68
|
+
const result = runProcessSync({
|
|
69
|
+
command: "git",
|
|
70
|
+
args,
|
|
71
|
+
cwd: gitRoot,
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
reject: false,
|
|
74
|
+
});
|
|
75
|
+
if (result.exitCode !== 0)
|
|
76
|
+
return "";
|
|
77
|
+
return String(result.stdout ?? "").trim();
|
|
78
|
+
}
|
|
79
|
+
function readPackageScripts(gitRoot) {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(gitRoot, "package.json"), "utf8"));
|
|
82
|
+
if (!parsed.scripts || typeof parsed.scripts !== "object" || Array.isArray(parsed.scripts)) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
const scripts = {};
|
|
86
|
+
for (const [name, value] of Object.entries(parsed.scripts)) {
|
|
87
|
+
if (typeof value === "string")
|
|
88
|
+
scripts[name] = value;
|
|
89
|
+
}
|
|
90
|
+
return scripts;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function hasProjectScript(scripts, name) {
|
|
97
|
+
return Object.hasOwn(scripts, name);
|
|
98
|
+
}
|
|
99
|
+
function runOptionalProjectScript(gitRoot, scripts, name, opts = {}) {
|
|
100
|
+
if (!hasProjectScript(scripts, name)) {
|
|
101
|
+
process.stdout.write(`Skipping ${name}: package.json script is not defined.\n`);
|
|
102
|
+
return { exitCode: 0, skipped: true };
|
|
103
|
+
}
|
|
104
|
+
if (opts.heading)
|
|
105
|
+
process.stdout.write(opts.heading);
|
|
106
|
+
const exitCode = opts.env
|
|
107
|
+
? runHookCommandWithEnv(gitRoot, "bun", ["run", name], opts.env)
|
|
108
|
+
: runHookCommand(gitRoot, "bun", ["run", name]);
|
|
109
|
+
return { exitCode, skipped: false };
|
|
110
|
+
}
|
|
111
|
+
function trackedChangesShort(gitRoot) {
|
|
112
|
+
return readGitText(gitRoot, ["status", "--short", "--untracked-files=no"]);
|
|
113
|
+
}
|
|
114
|
+
function fail(message, details = []) {
|
|
115
|
+
throw new HookFailure(message, details);
|
|
116
|
+
}
|
|
117
|
+
function failIfTrackedChanges(gitRoot, message) {
|
|
118
|
+
const changes = trackedChangesShort(gitRoot);
|
|
119
|
+
if (!changes)
|
|
120
|
+
return;
|
|
121
|
+
fail(message, [changes]);
|
|
122
|
+
}
|
|
123
|
+
function gitRefExists(gitRoot, ref) {
|
|
124
|
+
return readGitText(gitRoot, ["rev-parse", "--verify", "--quiet", ref]).length > 0;
|
|
125
|
+
}
|
|
126
|
+
function hasReleaseTagPush(updates) {
|
|
127
|
+
return updates.some((update) => update.remoteRef.startsWith("refs/tags/"));
|
|
128
|
+
}
|
|
129
|
+
function isDeleteOnlyPush(updates) {
|
|
130
|
+
return (updates.length > 0 &&
|
|
131
|
+
updates.every((update) => isBranchRef(update.remoteRef) && isAllZeroSha(update.localSha) && Boolean(update.remoteSha)));
|
|
132
|
+
}
|
|
133
|
+
function resolveDefaultBaseRef(gitRoot) {
|
|
134
|
+
const remoteHead = readGitText(gitRoot, [
|
|
135
|
+
"symbolic-ref",
|
|
136
|
+
"--quiet",
|
|
137
|
+
"--short",
|
|
138
|
+
"refs/remotes/origin/HEAD",
|
|
139
|
+
]);
|
|
140
|
+
if (remoteHead)
|
|
141
|
+
return remoteHead;
|
|
142
|
+
if (gitRefExists(gitRoot, "origin/main"))
|
|
143
|
+
return "origin/main";
|
|
144
|
+
if (gitRefExists(gitRoot, "main"))
|
|
145
|
+
return "main";
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function selectBranchDiffRange(updates, opts = {}) {
|
|
149
|
+
const branchUpdates = updates.filter((update) => isBranchRef(update.localRef) && isBranchRef(update.remoteRef));
|
|
150
|
+
if (branchUpdates.length !== 1)
|
|
151
|
+
return null;
|
|
152
|
+
const [update] = branchUpdates;
|
|
153
|
+
if (!update?.localSha || !update.remoteSha)
|
|
154
|
+
return null;
|
|
155
|
+
if (isAllZeroSha(update.localSha))
|
|
156
|
+
return null;
|
|
157
|
+
if (isAllZeroSha(update.remoteSha)) {
|
|
158
|
+
const fallbackRef = typeof opts.newBranchFallbackRef === "string" ? opts.newBranchFallbackRef.trim() : "";
|
|
159
|
+
return fallbackRef ? { from: fallbackRef, to: update.localSha } : null;
|
|
160
|
+
}
|
|
161
|
+
return { from: update.remoteSha, to: update.localSha };
|
|
162
|
+
}
|
|
163
|
+
function readChangedFilesForRange(gitRoot, range) {
|
|
164
|
+
if (!range)
|
|
165
|
+
return [];
|
|
166
|
+
const output = readGitText(gitRoot, ["diff", "--name-only", `${range.from}..${range.to}`]);
|
|
167
|
+
return output
|
|
168
|
+
.split("\n")
|
|
169
|
+
.map((line) => line.trim())
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
}
|
|
172
|
+
function fileExistsSync(filePath) {
|
|
173
|
+
try {
|
|
174
|
+
return fs.statSync(filePath).isFile();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function isTruthyHookEnv(name) {
|
|
181
|
+
return (String(process.env[name] ?? "")
|
|
182
|
+
.trim()
|
|
183
|
+
.toLowerCase() === "1");
|
|
184
|
+
}
|
|
185
|
+
function runInternalPrePushHook(gitRoot, stdin) {
|
|
186
|
+
try {
|
|
187
|
+
const updates = parsePrePushStdin(stdin);
|
|
188
|
+
const envRelease = isTruthyHookEnv("AGENTPLANE_HOOKS_RELEASE");
|
|
189
|
+
const envFull = isTruthyHookEnv("AGENTPLANE_HOOKS_FULL");
|
|
190
|
+
const isReleasePush = envRelease || envFull || hasReleaseTagPush(updates);
|
|
191
|
+
if (!isReleasePush && isDeleteOnlyPush(updates)) {
|
|
192
|
+
process.stdout.write("Skipping pre-push checks for delete-only remote branch cleanup.\n");
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
const mode = isReleasePush ? "release" : "standard";
|
|
196
|
+
process.stdout.write(`Running pre-push checks in ${mode} mode.\n`);
|
|
197
|
+
const ciScript = envFull ? "ci:local:full" : "ci:local:fast";
|
|
198
|
+
const scripts = readPackageScripts(gitRoot);
|
|
199
|
+
const changedFiles = readChangedFilesForRange(gitRoot, selectBranchDiffRange(updates, {
|
|
200
|
+
newBranchFallbackRef: resolveDefaultBaseRef(gitRoot),
|
|
201
|
+
}));
|
|
202
|
+
const ciEnv = changedFiles.length > 0
|
|
203
|
+
? { ...process.env, AGENTPLANE_FAST_CHANGED_FILES: changedFiles.join("\n") }
|
|
204
|
+
: process.env;
|
|
205
|
+
const formatResult = runOptionalProjectScript(gitRoot, scripts, "format:check", {
|
|
206
|
+
heading: "\n== Format (check) ==\n",
|
|
207
|
+
});
|
|
208
|
+
if (formatResult.exitCode !== 0) {
|
|
209
|
+
failIfTrackedChanges(gitRoot, "pre-push blocked: format:check changed tracked files unexpectedly. Revert or commit those changes and push again.");
|
|
210
|
+
fail("pre-push blocked: formatting check failed. Run `bun run format`, review the diff, commit it, and push again.");
|
|
211
|
+
}
|
|
212
|
+
if (!formatResult.skipped) {
|
|
213
|
+
failIfTrackedChanges(gitRoot, "pre-push blocked: format:check changed tracked files unexpectedly. Revert or commit those changes and push again.");
|
|
214
|
+
}
|
|
215
|
+
const ciResult = runOptionalProjectScript(gitRoot, scripts, ciScript, { env: ciEnv });
|
|
216
|
+
if (!ciResult.skipped) {
|
|
217
|
+
failIfTrackedChanges(gitRoot, `pre-push blocked: ${ciScript} changed tracked files. Commit or revert those changes and push again.`);
|
|
218
|
+
}
|
|
219
|
+
if (ciResult.exitCode !== 0) {
|
|
220
|
+
fail(`pre-push blocked: ${ciScript} failed. Fix the reported checks and push again.`);
|
|
221
|
+
}
|
|
222
|
+
if (isReleasePush) {
|
|
223
|
+
const releaseNotesScript = path.join(gitRoot, "scripts", "check-release-notes.mjs");
|
|
224
|
+
if (fileExistsSync(releaseNotesScript)) {
|
|
225
|
+
const notesExitCode = runHookCommand(gitRoot, "node", ["scripts/check-release-notes.mjs"]);
|
|
226
|
+
if (notesExitCode !== 0)
|
|
227
|
+
return notesExitCode;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
process.stdout.write("Skipping release notes check: scripts/check-release-notes.mjs is not defined.\n");
|
|
231
|
+
}
|
|
232
|
+
return runOptionalProjectScript(gitRoot, scripts, "release:prepublish").exitCode;
|
|
233
|
+
}
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (error instanceof HookFailure) {
|
|
238
|
+
process.stderr.write(`\n${error.message}\n`);
|
|
239
|
+
for (const detail of error.details) {
|
|
240
|
+
if (!detail)
|
|
241
|
+
continue;
|
|
242
|
+
process.stderr.write(`${detail}\n`);
|
|
243
|
+
}
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
19
249
|
async function readHookStdinUtf8(timeoutMs = 25) {
|
|
20
250
|
if (process.stdin.isTTY)
|
|
21
251
|
return "";
|
|
@@ -59,19 +289,10 @@ export async function runPrePushHook(opts) {
|
|
|
59
289
|
cwd: opts.cwd,
|
|
60
290
|
rootOverride: opts.rootOverride ?? null,
|
|
61
291
|
});
|
|
292
|
+
const stdin = await readHookStdinUtf8();
|
|
62
293
|
const scriptPath = await resolvePrePushHookScriptPath(resolved.gitRoot);
|
|
63
294
|
if (!scriptPath) {
|
|
64
|
-
|
|
65
|
-
exitCode: 2,
|
|
66
|
-
code: "E_USAGE",
|
|
67
|
-
message: [
|
|
68
|
-
"Missing pre-push hook script: scripts/run-pre-push-hook.mjs",
|
|
69
|
-
"The pre-push hook needs a repository-local script or an installed CLI bundle that ships the fallback.",
|
|
70
|
-
"Fix:",
|
|
71
|
-
" 1) Restore scripts/run-pre-push-hook.mjs in this repository, or",
|
|
72
|
-
" 2) Run `agentplane hooks uninstall` if this repository should not use the agentplane pre-push gate.",
|
|
73
|
-
].join("\n"),
|
|
74
|
-
});
|
|
295
|
+
return runInternalPrePushHook(resolved.gitRoot, stdin);
|
|
75
296
|
}
|
|
76
297
|
const result = runProcessSync({
|
|
77
298
|
command: "node",
|
|
@@ -79,7 +300,7 @@ export async function runPrePushHook(opts) {
|
|
|
79
300
|
cwd: resolved.gitRoot,
|
|
80
301
|
env: process.env,
|
|
81
302
|
encoding: "utf8",
|
|
82
|
-
input:
|
|
303
|
+
input: stdin,
|
|
83
304
|
stdin: "pipe",
|
|
84
305
|
stdout: "inherit",
|
|
85
306
|
stderr: "inherit",
|
|
@@ -55,7 +55,7 @@ export const taskDeriveSpec = {
|
|
|
55
55
|
],
|
|
56
56
|
examples: [
|
|
57
57
|
{
|
|
58
|
-
cmd: 'agentplane task derive 202602070101-ABCD --title "Implement X" --description "Do the thing" --owner CODER --tag code --verify "bun test"',
|
|
58
|
+
cmd: 'agentplane task derive 202602070101-ABCD --title "Implement X" --description "Do the thing" --owner CODER --tag code --verify "bun run test:project -- cli-core"',
|
|
59
59
|
why: "Create an implementation task derived from a spike with seeded verify steps.",
|
|
60
60
|
},
|
|
61
61
|
],
|