agentplane 0.1.7 → 0.1.9
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/AGENTS.md +13 -2
- package/dist/backends/task-backend.d.ts +28 -0
- package/dist/backends/task-backend.d.ts.map +1 -1
- package/dist/backends/task-backend.js +85 -4
- package/dist/backends/task-index.d.ts.map +1 -1
- package/dist/backends/task-index.js +3 -6
- package/dist/cli/command-guide.d.ts.map +1 -1
- package/dist/cli/command-guide.js +10 -11
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +7 -5
- package/dist/cli/run-cli.d.ts.map +1 -1
- package/dist/cli/run-cli.js +75 -74
- package/dist/commands/backend.d.ts.map +1 -1
- package/dist/commands/backend.js +17 -2
- package/dist/commands/branch/index.d.ts.map +1 -1
- package/dist/commands/branch/index.js +3 -1
- package/dist/commands/guard/index.d.ts +24 -3
- package/dist/commands/guard/index.d.ts.map +1 -1
- package/dist/commands/guard/index.js +175 -61
- package/dist/commands/hooks/index.d.ts.map +1 -1
- package/dist/commands/hooks/index.js +39 -29
- package/dist/commands/pr/index.d.ts.map +1 -1
- package/dist/commands/pr/index.js +15 -12
- package/dist/commands/recipes.d.ts +75 -6
- package/dist/commands/recipes.d.ts.map +1 -1
- package/dist/commands/recipes.js +76 -538
- package/dist/commands/scenario.d.ts +7 -0
- package/dist/commands/scenario.d.ts.map +1 -0
- package/dist/commands/scenario.js +501 -0
- package/dist/commands/shared/network-approval.d.ts +8 -0
- package/dist/commands/shared/network-approval.d.ts.map +1 -0
- package/dist/commands/shared/network-approval.js +25 -0
- package/dist/commands/shared/task-backend.d.ts +19 -3
- package/dist/commands/shared/task-backend.d.ts.map +1 -1
- package/dist/commands/shared/task-backend.js +19 -5
- package/dist/commands/task/block.d.ts.map +1 -1
- package/dist/commands/task/block.js +22 -16
- package/dist/commands/task/comment.d.ts.map +1 -1
- package/dist/commands/task/comment.js +9 -2
- package/dist/commands/task/finish.d.ts.map +1 -1
- package/dist/commands/task/finish.js +36 -25
- package/dist/commands/task/index.d.ts +3 -0
- package/dist/commands/task/index.d.ts.map +1 -1
- package/dist/commands/task/index.js +3 -0
- package/dist/commands/task/migrate-doc.d.ts +8 -0
- package/dist/commands/task/migrate-doc.d.ts.map +1 -0
- package/dist/commands/task/migrate-doc.js +147 -0
- package/dist/commands/task/plan.d.ts +14 -0
- package/dist/commands/task/plan.d.ts.map +1 -0
- package/dist/commands/task/plan.js +217 -0
- package/dist/commands/task/scaffold.d.ts.map +1 -1
- package/dist/commands/task/scaffold.js +15 -4
- package/dist/commands/task/set-status.d.ts.map +1 -1
- package/dist/commands/task/set-status.js +18 -4
- package/dist/commands/task/shared.d.ts +5 -2
- package/dist/commands/task/shared.d.ts.map +1 -1
- package/dist/commands/task/shared.js +47 -28
- package/dist/commands/task/start.d.ts.map +1 -1
- package/dist/commands/task/start.js +24 -17
- package/dist/commands/task/verify-record.d.ts +16 -0
- package/dist/commands/task/verify-record.d.ts.map +1 -0
- package/dist/commands/task/verify-record.js +284 -0
- package/dist/commands/task/verify.d.ts +1 -13
- package/dist/commands/task/verify.d.ts.map +1 -1
- package/dist/commands/task/verify.js +1 -362
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +17 -2
- package/dist/commands/workflow.d.ts +1 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +1 -1
- package/dist/shared/git-log.d.ts +5 -0
- package/dist/shared/git-log.d.ts.map +1 -0
- package/dist/shared/git-log.js +14 -0
- package/dist/shared/git-path.d.ts +3 -0
- package/dist/shared/git-path.d.ts.map +1 -0
- package/dist/shared/git-path.js +30 -0
- package/dist/shared/guards.d.ts +2 -0
- package/dist/shared/guards.d.ts.map +1 -0
- package/dist/shared/guards.js +3 -0
- package/dist/shared/protected-paths.d.ts +12 -0
- package/dist/shared/protected-paths.d.ts.map +1 -0
- package/dist/shared/protected-paths.js +51 -0
- package/dist/shared/strings.d.ts +2 -0
- package/dist/shared/strings.d.ts.map +1 -0
- package/dist/shared/strings.js +14 -0
- package/package.json +2 -2
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
import { extractTaskSuffix, getStagedFiles, getUnstagedFiles, loadConfig, resolveProject, validateCommitSubject, } from "@agentplaneorg/core";
|
|
1
|
+
import { extractTaskSuffix, getStagedFiles, getUnstagedFiles, loadConfig, resolveBaseBranch, resolveProject, validateCommitSubject, } from "@agentplaneorg/core";
|
|
2
2
|
import { mapCoreError } from "../../cli/error-map.js";
|
|
3
3
|
import { invalidValueMessage, successMessage } from "../../cli/output.js";
|
|
4
4
|
import { CliError } from "../../shared/errors.js";
|
|
5
|
-
import { formatCommentBodyForCommit } from "../../shared/comment-format.js";
|
|
5
|
+
import { formatCommentBodyForCommit, normalizeCommentBodyForCommit, } from "../../shared/comment-format.js";
|
|
6
|
+
import { gitPathIsUnderPrefix, normalizeGitPathPrefix } from "../../shared/git-path.js";
|
|
6
7
|
import { execFileAsync } from "../shared/git.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return
|
|
8
|
+
import { gitCurrentBranch } from "../shared/git-ops.js";
|
|
9
|
+
import { getProtectedPathOverride, protectedPathKindForFile, } from "../../shared/protected-paths.js";
|
|
10
|
+
import { parseGitLogHashSubject } from "../../shared/git-log.js";
|
|
11
|
+
function parseNullSeparatedPaths(stdout) {
|
|
12
|
+
const text = Buffer.isBuffer(stdout) ? stdout.toString("utf8") : stdout;
|
|
13
|
+
return text
|
|
14
|
+
.split("\0")
|
|
15
|
+
.map((entry) => entry.trim())
|
|
16
|
+
.filter((entry) => entry.length > 0);
|
|
13
17
|
}
|
|
14
|
-
function
|
|
15
|
-
return
|
|
18
|
+
export function buildGitCommitEnv(opts) {
|
|
19
|
+
return {
|
|
20
|
+
...process.env,
|
|
21
|
+
AGENTPLANE_TASK_ID: opts.taskId,
|
|
22
|
+
AGENTPLANE_ALLOW_TASKS: opts.allowTasks ? "1" : "0",
|
|
23
|
+
AGENTPLANE_ALLOW_BASE: opts.allowBase ? "1" : "0",
|
|
24
|
+
AGENTPLANE_ALLOW_POLICY: opts.allowPolicy ? "1" : "0",
|
|
25
|
+
AGENTPLANE_ALLOW_CONFIG: opts.allowConfig ? "1" : "0",
|
|
26
|
+
AGENTPLANE_ALLOW_HOOKS: opts.allowHooks ? "1" : "0",
|
|
27
|
+
AGENTPLANE_ALLOW_CI: opts.allowCI ? "1" : "0",
|
|
28
|
+
};
|
|
16
29
|
}
|
|
17
30
|
export function suggestAllowPrefixes(paths) {
|
|
18
31
|
const out = new Set();
|
|
@@ -27,10 +40,10 @@ export function suggestAllowPrefixes(paths) {
|
|
|
27
40
|
}
|
|
28
41
|
return [...out].toSorted((a, b) => a.localeCompare(b));
|
|
29
42
|
}
|
|
30
|
-
export const GUARD_COMMIT_USAGE = "Usage: agentplane guard commit <task-id> -m <message> --allow <path> [--allow <path>...] [--auto-allow] [--allow-tasks] [--require-clean] [--quiet]";
|
|
31
|
-
export const GUARD_COMMIT_USAGE_EXAMPLE = 'agentplane guard commit 202602030608-F1Q8AB -m "✨ F1Q8AB
|
|
43
|
+
export const GUARD_COMMIT_USAGE = "Usage: agentplane guard commit <task-id> -m <message> --allow <path> [--allow <path>...] [--auto-allow] [--allow-tasks] [--allow-base] [--allow-policy] [--allow-config] [--allow-hooks] [--allow-ci] [--require-clean] [--quiet]";
|
|
44
|
+
export const GUARD_COMMIT_USAGE_EXAMPLE = 'agentplane guard commit 202602030608-F1Q8AB -m "✨ F1Q8AB task: implement allowlist guard" --allow packages/agentplane';
|
|
32
45
|
export const COMMIT_USAGE = "Usage: agentplane commit <task-id> -m <message>";
|
|
33
|
-
export const COMMIT_USAGE_EXAMPLE = 'agentplane commit 202602030608-F1Q8AB -m "✨ F1Q8AB
|
|
46
|
+
export const COMMIT_USAGE_EXAMPLE = 'agentplane commit 202602030608-F1Q8AB -m "✨ F1Q8AB task: implement allowlist guard"';
|
|
34
47
|
async function guardCommitCheck(opts) {
|
|
35
48
|
const resolved = await resolveProject({
|
|
36
49
|
cwd: opts.cwd,
|
|
@@ -60,10 +73,8 @@ async function guardCommitCheck(opts) {
|
|
|
60
73
|
message: "Provide at least one --allow <path> prefix",
|
|
61
74
|
});
|
|
62
75
|
}
|
|
63
|
-
const allow = opts.allow.map((prefix) =>
|
|
64
|
-
const
|
|
65
|
-
if (!opts.allowTasks)
|
|
66
|
-
denied.add(".agentplane/tasks.json");
|
|
76
|
+
const allow = opts.allow.map((prefix) => normalizeGitPathPrefix(prefix));
|
|
77
|
+
const tasksPath = loaded.config.paths.tasks_path;
|
|
67
78
|
if (opts.requireClean) {
|
|
68
79
|
const unstaged = await getUnstagedFiles({
|
|
69
80
|
cwd: opts.cwd,
|
|
@@ -73,15 +84,62 @@ async function guardCommitCheck(opts) {
|
|
|
73
84
|
throw new CliError({ exitCode: 5, code: "E_GIT", message: "Working tree is dirty" });
|
|
74
85
|
}
|
|
75
86
|
}
|
|
87
|
+
if (loaded.config.workflow_mode === "branch_pr") {
|
|
88
|
+
const baseBranch = await resolveBaseBranch({
|
|
89
|
+
cwd: opts.cwd,
|
|
90
|
+
rootOverride: opts.rootOverride ?? null,
|
|
91
|
+
cliBaseOpt: null,
|
|
92
|
+
mode: loaded.config.workflow_mode,
|
|
93
|
+
});
|
|
94
|
+
if (!baseBranch) {
|
|
95
|
+
throw new CliError({
|
|
96
|
+
exitCode: 2,
|
|
97
|
+
code: "E_USAGE",
|
|
98
|
+
message: "Base branch could not be resolved (use `agentplane branch base set`).",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const currentBranch = await gitCurrentBranch(resolved.gitRoot);
|
|
102
|
+
const tasksStaged = staged.includes(tasksPath);
|
|
103
|
+
const nonTasks = staged.filter((entry) => entry !== tasksPath);
|
|
104
|
+
if (tasksStaged && currentBranch !== baseBranch) {
|
|
105
|
+
throw new CliError({
|
|
106
|
+
exitCode: 5,
|
|
107
|
+
code: "E_GIT",
|
|
108
|
+
message: `${tasksPath} commits are allowed only on ${baseBranch} in branch_pr mode`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (nonTasks.length > 0 && currentBranch === baseBranch && !opts.allowBase) {
|
|
112
|
+
throw new CliError({
|
|
113
|
+
exitCode: 5,
|
|
114
|
+
code: "E_GIT",
|
|
115
|
+
message: `Code commits are forbidden on ${baseBranch} in branch_pr mode`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
76
119
|
for (const filePath of staged) {
|
|
77
|
-
|
|
120
|
+
const kind = protectedPathKindForFile({ filePath, tasksPath });
|
|
121
|
+
if (kind === "tasks" && !opts.allowTasks) {
|
|
78
122
|
throw new CliError({
|
|
79
123
|
exitCode: 5,
|
|
80
124
|
code: "E_GIT",
|
|
81
125
|
message: `Staged file is forbidden by default: ${filePath} (use --allow-tasks to override)`,
|
|
82
126
|
});
|
|
83
127
|
}
|
|
84
|
-
if (
|
|
128
|
+
if (kind && kind !== "tasks") {
|
|
129
|
+
const override = getProtectedPathOverride(kind);
|
|
130
|
+
const allowed = (kind === "policy" && opts.allowPolicy) ||
|
|
131
|
+
(kind === "config" && opts.allowConfig) ||
|
|
132
|
+
(kind === "hooks" && opts.allowHooks) ||
|
|
133
|
+
(kind === "ci" && opts.allowCI);
|
|
134
|
+
if (!allowed) {
|
|
135
|
+
throw new CliError({
|
|
136
|
+
exitCode: 5,
|
|
137
|
+
code: "E_GIT",
|
|
138
|
+
message: `Staged file is protected by default: ${filePath} (use ${override.cliFlag} to override)`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!allow.some((prefix) => gitPathIsUnderPrefix(filePath, prefix))) {
|
|
85
143
|
throw new CliError({
|
|
86
144
|
exitCode: 5,
|
|
87
145
|
code: "E_GIT",
|
|
@@ -95,22 +153,22 @@ export async function gitStatusChangedPaths(opts) {
|
|
|
95
153
|
cwd: opts.cwd,
|
|
96
154
|
rootOverride: opts.rootOverride ?? null,
|
|
97
155
|
});
|
|
98
|
-
const
|
|
156
|
+
const optsExec = {
|
|
99
157
|
cwd: resolved.gitRoot,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return files;
|
|
158
|
+
encoding: "buffer",
|
|
159
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
160
|
+
};
|
|
161
|
+
const [unstaged, staged, untracked] = await Promise.all([
|
|
162
|
+
execFileAsync("git", ["diff", "--name-only", "-z"], optsExec),
|
|
163
|
+
execFileAsync("git", ["diff", "--name-only", "--cached", "-z"], optsExec),
|
|
164
|
+
execFileAsync("git", ["ls-files", "--others", "--exclude-standard", "-z"], optsExec),
|
|
165
|
+
]);
|
|
166
|
+
const files = [
|
|
167
|
+
...parseNullSeparatedPaths(unstaged.stdout),
|
|
168
|
+
...parseNullSeparatedPaths(staged.stdout),
|
|
169
|
+
...parseNullSeparatedPaths(untracked.stdout),
|
|
170
|
+
];
|
|
171
|
+
return [...new Set(files)].toSorted((a, b) => a.localeCompare(b));
|
|
114
172
|
}
|
|
115
173
|
export async function ensureGitClean(opts) {
|
|
116
174
|
const staged = await getStagedFiles({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
@@ -142,15 +200,15 @@ async function stageAllowlist(opts) {
|
|
|
142
200
|
message: "No changes to stage (working tree clean)",
|
|
143
201
|
});
|
|
144
202
|
}
|
|
145
|
-
const allow = opts.allow.map((prefix) =>
|
|
203
|
+
const allow = opts.allow.map((prefix) => normalizeGitPathPrefix(prefix));
|
|
146
204
|
const denied = new Set();
|
|
147
205
|
if (!opts.allowTasks)
|
|
148
|
-
denied.add(
|
|
206
|
+
denied.add(opts.tasksPath);
|
|
149
207
|
const staged = [];
|
|
150
208
|
for (const filePath of changed) {
|
|
151
209
|
if (denied.has(filePath))
|
|
152
210
|
continue;
|
|
153
|
-
if (allow.some((prefix) =>
|
|
211
|
+
if (allow.some((prefix) => gitPathIsUnderPrefix(filePath, prefix))) {
|
|
154
212
|
staged.push(filePath);
|
|
155
213
|
}
|
|
156
214
|
}
|
|
@@ -162,13 +220,23 @@ async function stageAllowlist(opts) {
|
|
|
162
220
|
message: "No changes matched allowed prefixes (use --commit-auto-allow or update --commit-allow)",
|
|
163
221
|
});
|
|
164
222
|
}
|
|
165
|
-
|
|
223
|
+
// `git add <pathspec>` is not reliable for staging deletes/renames across versions/configs.
|
|
224
|
+
// `-A -- <pathspec...>` makes the allowlist staging semantics deterministic.
|
|
225
|
+
await execFileAsync("git", ["add", "-A", "--", ...unique], { cwd: resolved.gitRoot });
|
|
166
226
|
return unique;
|
|
167
227
|
}
|
|
168
228
|
function deriveCommitMessageFromComment(opts) {
|
|
169
|
-
const
|
|
229
|
+
const raw = (opts.formattedComment ?? formatCommentBodyForCommit(opts.body, opts.config))
|
|
170
230
|
.trim()
|
|
171
231
|
.replaceAll(/\s+/g, " ");
|
|
232
|
+
if (!raw) {
|
|
233
|
+
throw new CliError({
|
|
234
|
+
exitCode: 2,
|
|
235
|
+
code: "E_USAGE",
|
|
236
|
+
message: "Comment body is required to build a commit message from the task comment",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const summary = raw.replace(/^(start|blocked|verified):\s+/i, "").trim();
|
|
172
240
|
if (!summary) {
|
|
173
241
|
throw new CliError({
|
|
174
242
|
exitCode: 2,
|
|
@@ -192,13 +260,33 @@ function deriveCommitMessageFromComment(opts) {
|
|
|
192
260
|
message: invalidValueMessage("task id", opts.taskId, "valid task id"),
|
|
193
261
|
});
|
|
194
262
|
}
|
|
195
|
-
return `${prefix} ${suffix} ${summary}`;
|
|
263
|
+
return `${prefix} ${suffix} task: ${summary}`;
|
|
264
|
+
}
|
|
265
|
+
function deriveCommitBodyFromComment(opts) {
|
|
266
|
+
const lines = [
|
|
267
|
+
`Task: ${opts.taskId}`,
|
|
268
|
+
...(opts.author ? [`Agent: ${opts.author}`] : []),
|
|
269
|
+
...(opts.statusFrom && opts.statusTo ? [`Status: ${opts.statusFrom} -> ${opts.statusTo}`] : []),
|
|
270
|
+
`Comment: ${normalizeCommentBodyForCommit(opts.formattedComment || opts.commentBody)}`,
|
|
271
|
+
];
|
|
272
|
+
return lines.join("\n").trimEnd();
|
|
196
273
|
}
|
|
197
274
|
export async function commitFromComment(opts) {
|
|
198
275
|
let allowPrefixes = opts.allow.map((prefix) => prefix.trim()).filter(Boolean);
|
|
199
276
|
if (opts.autoAllow && allowPrefixes.length === 0) {
|
|
200
277
|
const changed = await gitStatusChangedPaths({ cwd: opts.cwd, rootOverride: opts.rootOverride });
|
|
201
|
-
|
|
278
|
+
const tasksPath = opts.config.paths.tasks_path;
|
|
279
|
+
// Auto-allow is for ergonomic status commits. It must never silently
|
|
280
|
+
// broaden into policy/config/CI changes (those require explicit intent).
|
|
281
|
+
const eligible = changed.filter((filePath) => {
|
|
282
|
+
const kind = protectedPathKindForFile({ filePath, tasksPath });
|
|
283
|
+
if (!kind)
|
|
284
|
+
return true;
|
|
285
|
+
if (kind === "tasks")
|
|
286
|
+
return opts.allowTasks;
|
|
287
|
+
return false;
|
|
288
|
+
});
|
|
289
|
+
allowPrefixes = suggestAllowPrefixes(eligible);
|
|
202
290
|
}
|
|
203
291
|
if (allowPrefixes.length === 0) {
|
|
204
292
|
throw new CliError({
|
|
@@ -212,6 +300,7 @@ export async function commitFromComment(opts) {
|
|
|
212
300
|
rootOverride: opts.rootOverride,
|
|
213
301
|
allow: allowPrefixes,
|
|
214
302
|
allowTasks: opts.allowTasks,
|
|
303
|
+
tasksPath: opts.config.paths.tasks_path,
|
|
215
304
|
});
|
|
216
305
|
const message = deriveCommitMessageFromComment({
|
|
217
306
|
taskId: opts.taskId,
|
|
@@ -220,13 +309,27 @@ export async function commitFromComment(opts) {
|
|
|
220
309
|
formattedComment: opts.formattedComment,
|
|
221
310
|
config: opts.config,
|
|
222
311
|
});
|
|
312
|
+
const formattedComment = opts.formattedComment ?? formatCommentBodyForCommit(opts.commentBody, opts.config);
|
|
313
|
+
const body = deriveCommitBodyFromComment({
|
|
314
|
+
taskId: opts.taskId,
|
|
315
|
+
author: opts.author,
|
|
316
|
+
statusFrom: opts.statusFrom,
|
|
317
|
+
statusTo: opts.statusTo,
|
|
318
|
+
commentBody: opts.commentBody,
|
|
319
|
+
formattedComment,
|
|
320
|
+
});
|
|
223
321
|
await guardCommitCheck({
|
|
224
322
|
cwd: opts.cwd,
|
|
225
323
|
rootOverride: opts.rootOverride,
|
|
226
324
|
taskId: opts.taskId,
|
|
227
325
|
message,
|
|
228
326
|
allow: allowPrefixes,
|
|
327
|
+
allowBase: false,
|
|
229
328
|
allowTasks: opts.allowTasks,
|
|
329
|
+
allowPolicy: false,
|
|
330
|
+
allowConfig: false,
|
|
331
|
+
allowHooks: false,
|
|
332
|
+
allowCI: false,
|
|
230
333
|
requireClean: opts.requireClean,
|
|
231
334
|
quiet: opts.quiet,
|
|
232
335
|
});
|
|
@@ -234,22 +337,26 @@ export async function commitFromComment(opts) {
|
|
|
234
337
|
cwd: opts.cwd,
|
|
235
338
|
rootOverride: opts.rootOverride ?? null,
|
|
236
339
|
});
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
340
|
+
// Never allow base-branch code commits implicitly from comment-driven commits.
|
|
341
|
+
// Base overrides must be explicit via the `commit` command's --allow-base flag.
|
|
342
|
+
const env = buildGitCommitEnv({
|
|
343
|
+
taskId: opts.taskId,
|
|
344
|
+
allowTasks: opts.allowTasks,
|
|
345
|
+
allowBase: false,
|
|
346
|
+
allowPolicy: false,
|
|
347
|
+
allowConfig: false,
|
|
348
|
+
allowHooks: false,
|
|
349
|
+
allowCI: false,
|
|
350
|
+
});
|
|
351
|
+
await execFileAsync("git", ["commit", "-m", message, "-m", body], { cwd: resolved.gitRoot, env });
|
|
352
|
+
const { stdout } = await execFileAsync("git", ["log", "-1", "--pretty=%H%x00%s"], {
|
|
245
353
|
cwd: resolved.gitRoot,
|
|
246
354
|
});
|
|
247
|
-
const
|
|
248
|
-
const [hash, subject] = trimmed.split(":", 2);
|
|
355
|
+
const { hash, subject } = parseGitLogHashSubject(stdout);
|
|
249
356
|
if (!opts.quiet) {
|
|
250
357
|
process.stdout.write(`${successMessage("committed", `${hash?.slice(0, 12) ?? ""} ${subject ?? ""}`.trim(), `staged=${staged.join(", ")}`)}\n`);
|
|
251
358
|
}
|
|
252
|
-
return { hash
|
|
359
|
+
return { hash, message: subject, staged };
|
|
253
360
|
}
|
|
254
361
|
export async function cmdGuardClean(opts) {
|
|
255
362
|
try {
|
|
@@ -334,7 +441,12 @@ export async function cmdCommit(opts) {
|
|
|
334
441
|
taskId: opts.taskId,
|
|
335
442
|
message: opts.message,
|
|
336
443
|
allow,
|
|
444
|
+
allowBase: opts.allowBase,
|
|
337
445
|
allowTasks: opts.allowTasks,
|
|
446
|
+
allowPolicy: opts.allowPolicy,
|
|
447
|
+
allowConfig: opts.allowConfig,
|
|
448
|
+
allowHooks: opts.allowHooks,
|
|
449
|
+
allowCI: opts.allowCI,
|
|
338
450
|
requireClean: opts.requireClean,
|
|
339
451
|
quiet: opts.quiet,
|
|
340
452
|
});
|
|
@@ -342,19 +454,21 @@ export async function cmdCommit(opts) {
|
|
|
342
454
|
cwd: opts.cwd,
|
|
343
455
|
rootOverride: opts.rootOverride ?? null,
|
|
344
456
|
});
|
|
345
|
-
const env = {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
457
|
+
const env = buildGitCommitEnv({
|
|
458
|
+
taskId: opts.taskId,
|
|
459
|
+
allowTasks: opts.allowTasks,
|
|
460
|
+
allowBase: opts.allowBase,
|
|
461
|
+
allowPolicy: opts.allowPolicy,
|
|
462
|
+
allowConfig: opts.allowConfig,
|
|
463
|
+
allowHooks: opts.allowHooks,
|
|
464
|
+
allowCI: opts.allowCI,
|
|
465
|
+
});
|
|
351
466
|
await execFileAsync("git", ["commit", "-m", opts.message], { cwd: resolved.gitRoot, env });
|
|
352
467
|
if (!opts.quiet) {
|
|
353
|
-
const { stdout } = await execFileAsync("git", ["log", "-1", "--pretty=%H
|
|
468
|
+
const { stdout } = await execFileAsync("git", ["log", "-1", "--pretty=%H%x00%s"], {
|
|
354
469
|
cwd: resolved.gitRoot,
|
|
355
470
|
});
|
|
356
|
-
const
|
|
357
|
-
const [hash, subject] = trimmed.split(":", 2);
|
|
471
|
+
const { hash, subject } = parseGitLogHashSubject(stdout);
|
|
358
472
|
process.stdout.write(`${successMessage("committed", `${hash?.slice(0, 12) ?? ""} ${subject ?? ""}`.trim())}\n`);
|
|
359
473
|
}
|
|
360
474
|
return 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/index.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,UAAU,mDAAoD,CAAC;AAgG5E,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;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2IlB"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { getStagedFiles, loadConfig, resolveBaseBranch, resolveProject } from "@agentplaneorg/core";
|
|
4
|
-
import { loadTaskBackend } from "../../backends/task-backend.js";
|
|
3
|
+
import { getStagedFiles, loadConfig, resolveBaseBranch, resolveProject, validateCommitSubject, } from "@agentplaneorg/core";
|
|
5
4
|
import { mapBackendError, mapCoreError } from "../../cli/error-map.js";
|
|
6
5
|
import { fileExists } from "../../cli/fs-utils.js";
|
|
7
6
|
import { infoMessage, successMessage } from "../../cli/output.js";
|
|
8
7
|
import { CliError } from "../../shared/errors.js";
|
|
8
|
+
import { getProtectedPathOverride, protectedPathKindForFile, } from "../../shared/protected-paths.js";
|
|
9
9
|
import { gitCurrentBranch, gitRevParse } from "../shared/git-ops.js";
|
|
10
10
|
import { isPathWithin } from "../shared/path.js";
|
|
11
11
|
const HOOK_MARKER = "agentplane-hook";
|
|
@@ -96,10 +96,6 @@ function readCommitSubject(message) {
|
|
|
96
96
|
}
|
|
97
97
|
return "";
|
|
98
98
|
}
|
|
99
|
-
function subjectHasSuffix(subject, suffixes) {
|
|
100
|
-
const lowered = subject.toLowerCase();
|
|
101
|
-
return suffixes.some((suffix) => suffix && lowered.includes(suffix.toLowerCase()));
|
|
102
|
-
}
|
|
103
99
|
export async function cmdHooksInstall(opts) {
|
|
104
100
|
try {
|
|
105
101
|
const resolved = await resolveProject({
|
|
@@ -187,39 +183,32 @@ export async function cmdHooksRun(opts) {
|
|
|
187
183
|
message: "Commit message subject is empty",
|
|
188
184
|
});
|
|
189
185
|
}
|
|
186
|
+
const resolved = await resolveProject({
|
|
187
|
+
cwd: opts.cwd,
|
|
188
|
+
rootOverride: opts.rootOverride ?? null,
|
|
189
|
+
});
|
|
190
|
+
const loaded = await loadConfig(resolved.agentplaneDir);
|
|
190
191
|
const taskId = (process.env.AGENTPLANE_TASK_ID ?? "").trim();
|
|
191
192
|
if (taskId) {
|
|
192
|
-
const
|
|
193
|
-
|
|
193
|
+
const policy = validateCommitSubject({
|
|
194
|
+
subject,
|
|
195
|
+
taskId,
|
|
196
|
+
genericTokens: loaded.config.commit.generic_tokens,
|
|
197
|
+
});
|
|
198
|
+
if (!policy.ok) {
|
|
194
199
|
throw new CliError({
|
|
195
200
|
exitCode: 5,
|
|
196
201
|
code: "E_GIT",
|
|
197
|
-
message: "
|
|
202
|
+
message: policy.errors.join("\n"),
|
|
198
203
|
});
|
|
199
204
|
}
|
|
200
205
|
return 0;
|
|
201
206
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
207
|
+
throw new CliError({
|
|
208
|
+
exitCode: 5,
|
|
209
|
+
code: "E_GIT",
|
|
210
|
+
message: "AGENTPLANE_TASK_ID is required (use `agentplane commit ...`)",
|
|
205
211
|
});
|
|
206
|
-
const tasks = await backend.listTasks();
|
|
207
|
-
const suffixes = tasks.map((task) => task.id.split("-").at(-1) ?? "").filter(Boolean);
|
|
208
|
-
if (suffixes.length === 0) {
|
|
209
|
-
throw new CliError({
|
|
210
|
-
exitCode: 5,
|
|
211
|
-
code: "E_GIT",
|
|
212
|
-
message: "No task IDs available to validate commit subject",
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
if (!subjectHasSuffix(subject, suffixes)) {
|
|
216
|
-
throw new CliError({
|
|
217
|
-
exitCode: 5,
|
|
218
|
-
code: "E_GIT",
|
|
219
|
-
message: "Commit subject must mention a task suffix",
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
return 0;
|
|
223
212
|
}
|
|
224
213
|
if (opts.hook === "pre-commit") {
|
|
225
214
|
const staged = await getStagedFiles({
|
|
@@ -230,6 +219,10 @@ export async function cmdHooksRun(opts) {
|
|
|
230
219
|
return 0;
|
|
231
220
|
const allowTasks = (process.env.AGENTPLANE_ALLOW_TASKS ?? "").trim() === "1";
|
|
232
221
|
const allowBase = (process.env.AGENTPLANE_ALLOW_BASE ?? "").trim() === "1";
|
|
222
|
+
const allowPolicy = (process.env.AGENTPLANE_ALLOW_POLICY ?? "").trim() === "1";
|
|
223
|
+
const allowConfig = (process.env.AGENTPLANE_ALLOW_CONFIG ?? "").trim() === "1";
|
|
224
|
+
const allowHooks = (process.env.AGENTPLANE_ALLOW_HOOKS ?? "").trim() === "1";
|
|
225
|
+
const allowCI = (process.env.AGENTPLANE_ALLOW_CI ?? "").trim() === "1";
|
|
233
226
|
const resolved = await resolveProject({
|
|
234
227
|
cwd: opts.cwd,
|
|
235
228
|
rootOverride: opts.rootOverride ?? null,
|
|
@@ -245,6 +238,23 @@ export async function cmdHooksRun(opts) {
|
|
|
245
238
|
message: `${tasksPath} is protected by agentplane hooks (set AGENTPLANE_ALLOW_TASKS=1 to override)`,
|
|
246
239
|
});
|
|
247
240
|
}
|
|
241
|
+
for (const filePath of staged) {
|
|
242
|
+
const kind = protectedPathKindForFile({ filePath, tasksPath });
|
|
243
|
+
if (!kind || kind === "tasks")
|
|
244
|
+
continue;
|
|
245
|
+
const override = getProtectedPathOverride(kind);
|
|
246
|
+
const allowed = (kind === "policy" && allowPolicy) ||
|
|
247
|
+
(kind === "config" && allowConfig) ||
|
|
248
|
+
(kind === "hooks" && allowHooks) ||
|
|
249
|
+
(kind === "ci" && allowCI);
|
|
250
|
+
if (!allowed) {
|
|
251
|
+
throw new CliError({
|
|
252
|
+
exitCode: 5,
|
|
253
|
+
code: "E_GIT",
|
|
254
|
+
message: `${filePath} is protected by agentplane hooks (set ${override.envVar}=1 to override)`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
248
258
|
if (loaded.config.workflow_mode === "branch_pr") {
|
|
249
259
|
const baseBranch = await resolveBaseBranch({
|
|
250
260
|
cwd: opts.cwd,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/pr/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/pr/index.ts"],"names":[],"mappings":"AAwCA,eAAO,MAAM,aAAa,wEAAwE,CAAC;AACnG,eAAO,MAAM,qBAAqB,0DAA0D,CAAC;AAC7F,eAAO,MAAM,eAAe,0CAA0C,CAAC;AACvE,eAAO,MAAM,uBAAuB,6CAA6C,CAAC;AAClF,eAAO,MAAM,cAAc,yCAAyC,CAAC;AACrE,eAAO,MAAM,sBAAsB,4CAA4C,CAAC;AAChF,eAAO,MAAM,aAAa,oEAAoE,CAAC;AAC/F,eAAO,MAAM,qBAAqB,4EACuC,CAAC;AAC1E,eAAO,MAAM,eAAe,wJAC2H,CAAC;AACxJ,eAAO,MAAM,uBAAuB,0DAA0D,CAAC;AA2H/F,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,MAAM,CAAC,CAoElB;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyFlB;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgElB;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,MAAM,CAAC,CAwClB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC7C,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAogBlB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { mkdir, readFile
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { loadConfig, resolveBaseBranch, resolveProject, } from "@agentplaneorg/core";
|
|
3
|
+
import { atomicWriteFile, loadConfig, resolveBaseBranch, resolveProject, } from "@agentplaneorg/core";
|
|
4
4
|
import { mapBackendError, mapCoreError } from "../../cli/error-map.js";
|
|
5
5
|
import { fileExists } from "../../cli/fs-utils.js";
|
|
6
6
|
import { successMessage, unknownEntityMessage, usageMessage, workflowModeMessage, } from "../../cli/output.js";
|
|
@@ -14,6 +14,7 @@ import { appendVerifyLog, extractLastVerifiedSha, parsePrMeta, runShellCommand,
|
|
|
14
14
|
import { isPathWithin } from "../shared/path.js";
|
|
15
15
|
import { loadBackendTask } from "../shared/task-backend.js";
|
|
16
16
|
import { cmdFinish } from "../task/index.js";
|
|
17
|
+
import { ensurePlanApprovedIfRequired, ensureVerificationSatisfiedIfRequired, } from "../task/shared.js";
|
|
17
18
|
export const PR_OPEN_USAGE = "Usage: agentplane pr open <task-id> --author <id> [--branch <name>]";
|
|
18
19
|
export const PR_OPEN_USAGE_EXAMPLE = "agentplane pr open 202602030608-F1Q8AB --author CODER";
|
|
19
20
|
export const PR_UPDATE_USAGE = "Usage: agentplane pr update <task-id>";
|
|
@@ -167,14 +168,14 @@ export async function cmdPrOpen(opts) {
|
|
|
167
168
|
last_verified_at: meta?.last_verified_at ?? null,
|
|
168
169
|
verify: meta?.verify ?? { status: "skipped" },
|
|
169
170
|
};
|
|
170
|
-
await
|
|
171
|
+
await atomicWriteFile(metaPath, `${JSON.stringify(nextMeta, null, 2)}\n`, "utf8");
|
|
171
172
|
if (!(await fileExists(diffstatPath)))
|
|
172
|
-
await
|
|
173
|
+
await atomicWriteFile(diffstatPath, "", "utf8");
|
|
173
174
|
if (!(await fileExists(verifyLogPath)))
|
|
174
|
-
await
|
|
175
|
+
await atomicWriteFile(verifyLogPath, "", "utf8");
|
|
175
176
|
if (!(await fileExists(reviewPath))) {
|
|
176
177
|
const review = renderPrReviewTemplate({ author, createdAt, branch });
|
|
177
|
-
await
|
|
178
|
+
await atomicWriteFile(reviewPath, review, "utf8");
|
|
178
179
|
}
|
|
179
180
|
process.stdout.write(`${successMessage("pr open", path.relative(resolved.gitRoot, prDir))}\n`);
|
|
180
181
|
return 0;
|
|
@@ -228,7 +229,7 @@ export async function cmdPrUpdate(opts) {
|
|
|
228
229
|
const branch = await gitCurrentBranch(resolved.gitRoot);
|
|
229
230
|
const { stdout: diffStatOut } = await execFileAsync("git", ["diff", "--stat", `${baseBranch}...HEAD`], { cwd: resolved.gitRoot, env: gitEnv() });
|
|
230
231
|
const diffstat = diffStatOut.trimEnd();
|
|
231
|
-
await
|
|
232
|
+
await atomicWriteFile(diffstatPath, diffstat ? `${diffstat}\n` : "", "utf8");
|
|
232
233
|
const { stdout: headOut } = await execFileAsync("git", ["rev-parse", "HEAD"], {
|
|
233
234
|
cwd: resolved.gitRoot,
|
|
234
235
|
env: gitEnv(),
|
|
@@ -245,7 +246,7 @@ export async function cmdPrUpdate(opts) {
|
|
|
245
246
|
];
|
|
246
247
|
const reviewText = await readFile(reviewPath, "utf8");
|
|
247
248
|
const nextReview = updateAutoSummaryBlock(reviewText, summaryLines.join("\n"));
|
|
248
|
-
await
|
|
249
|
+
await atomicWriteFile(reviewPath, nextReview, "utf8");
|
|
249
250
|
const rawMeta = await readFile(metaPath, "utf8");
|
|
250
251
|
const meta = parsePrMeta(rawMeta, opts.taskId);
|
|
251
252
|
const nextMeta = {
|
|
@@ -255,7 +256,7 @@ export async function cmdPrUpdate(opts) {
|
|
|
255
256
|
last_verified_sha: meta.last_verified_sha ?? null,
|
|
256
257
|
last_verified_at: meta.last_verified_at ?? null,
|
|
257
258
|
};
|
|
258
|
-
await
|
|
259
|
+
await atomicWriteFile(metaPath, `${JSON.stringify(nextMeta, null, 2)}\n`, "utf8");
|
|
259
260
|
process.stdout.write(`${successMessage("pr update", path.relative(resolved.gitRoot, prDir))}\n`);
|
|
260
261
|
return 0;
|
|
261
262
|
}
|
|
@@ -359,7 +360,7 @@ export async function cmdPrNote(opts) {
|
|
|
359
360
|
}
|
|
360
361
|
const review = await readFile(reviewPath, "utf8");
|
|
361
362
|
const updated = appendHandoffNote(review, `${author}: ${body}`);
|
|
362
|
-
await
|
|
363
|
+
await atomicWriteFile(reviewPath, updated, "utf8");
|
|
363
364
|
process.stdout.write(`${successMessage("pr note", opts.taskId)}\n`);
|
|
364
365
|
return 0;
|
|
365
366
|
}
|
|
@@ -390,6 +391,8 @@ export async function cmdIntegrate(opts) {
|
|
|
390
391
|
message: workflowModeMessage(loaded.config.workflow_mode, "branch_pr"),
|
|
391
392
|
});
|
|
392
393
|
}
|
|
394
|
+
ensurePlanApprovedIfRequired(task, loaded.config);
|
|
395
|
+
ensureVerificationSatisfiedIfRequired(task, loaded.config);
|
|
393
396
|
await ensureGitClean({ cwd: opts.cwd, rootOverride: opts.rootOverride });
|
|
394
397
|
if (opts.base?.trim().length === 0) {
|
|
395
398
|
throw new CliError({
|
|
@@ -793,9 +796,9 @@ export async function cmdIntegrate(opts) {
|
|
|
793
796
|
? { ...mergedMeta.verify, status: "pass" }
|
|
794
797
|
: { status: "pass", command: verifyCommands.join(" && ") };
|
|
795
798
|
}
|
|
796
|
-
await
|
|
799
|
+
await atomicWriteFile(metaPath, `${JSON.stringify(nextMeta, null, 2)}\n`, "utf8");
|
|
797
800
|
const diffstat = await gitDiffStat(resolved.gitRoot, baseShaBeforeMerge, branch);
|
|
798
|
-
await
|
|
801
|
+
await atomicWriteFile(diffstatPath, diffstat ? `${diffstat}\n` : "", "utf8");
|
|
799
802
|
const verifyDesc = verifyCommands.length === 0
|
|
800
803
|
? "skipped(no commands)"
|
|
801
804
|
: shouldRunVerify
|