@fitlab-ai/agent-infra 0.9.8 → 0.9.10
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +11 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +25 -11
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +127 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +63 -92
- package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
- package/dist/lib/agent-clients/adapters/index.js +3 -1
- package/dist/lib/agent-clients/adapters/traecli.js +66 -0
- package/dist/lib/agent-clients/tokens.js +2 -2
- package/dist/lib/agent-clients/types.js +2 -1
- package/dist/lib/defaults.json +5 -0
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/internal/codex-lifecycle.js +20 -6
- package/dist/lib/internal/codex-sandbox-controller.js +9 -15
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +2 -1
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-orchestration.js +156 -291
- package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +75 -42
- package/dist/lib/platform/pr-summary.js +48 -10
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/run/tui.js +5 -2
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/controller-registration.js +275 -0
- package/dist/lib/sandbox/control/executor.js +140 -5
- package/dist/lib/sandbox/control/lifecycle.js +3 -1
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/task/codex-orchestration.js +161 -39
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/delegation-receipts.js +248 -6
- package/dist/lib/task/finalization.js +593 -0
- package/dist/lib/task/index.js +0 -9
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +187 -761
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +110 -60
- package/dist/lib/task/verification.js +2 -1
- package/dist/lib/task/workflow-warning-intents.js +50 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +37 -10
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +163 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +91 -118
- package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
- package/lib/agent-clients/adapters/index.ts +3 -1
- package/lib/agent-clients/adapters/traecli.ts +67 -0
- package/lib/agent-clients/tokens.ts +2 -2
- package/lib/agent-clients/types.ts +2 -1
- package/lib/defaults.json +5 -0
- package/lib/git/workflow.ts +11 -2
- package/lib/internal/codex-lifecycle.ts +19 -6
- package/lib/internal/codex-sandbox-controller.ts +9 -14
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +2 -1
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-orchestration.ts +20 -103
- package/lib/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
- package/lib/platform/pr-summary.ts +55 -11
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/run/tui.ts +5 -3
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/controller-registration.ts +352 -0
- package/lib/sandbox/control/executor.ts +169 -5
- package/lib/sandbox/control/lifecycle.ts +2 -1
- package/lib/sandbox/control/protocol.ts +80 -7
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +3 -3
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/task/codex-orchestration.ts +168 -40
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/delegation-receipts.ts +263 -4
- package/lib/task/finalization.ts +761 -0
- package/lib/task/index.ts +0 -8
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +218 -982
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +115 -63
- package/lib/task/verification.ts +3 -2
- package/lib/task/workflow-warning-intents.ts +57 -2
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +3 -4
- package/templates/.agents/QUICKSTART.zh-CN.md +3 -4
- package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/compatibility-policy.en.md +39 -0
- package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
- package/templates/.agents/rules/pr-sync.en.md +1 -1
- package/templates/.agents/rules/pr-sync.github.en.md +2 -1
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
- package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
- package/templates/.agents/rules/review-method.en.md +1 -0
- package/templates/.agents/rules/review-method.zh-CN.md +1 -0
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +17 -9
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -9
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +2 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
- package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
- package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.codex/hooks.json +4 -4
- package/templates/.trae/skills/update-agent-infra.en.md +8 -0
- package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- package/lib/task/commit-intent.ts +0 -230
|
@@ -9,6 +9,8 @@ import { parseTaskFrontmatter, extractTitle, type Frontmatter } from '../frontma
|
|
|
9
9
|
import { loadShortIdByTaskId } from '../short-id.ts';
|
|
10
10
|
import { getOpenWorkflowWarnings, formatWorkflowWarningSummary, type WorkflowWarning } from '../workflow-warnings.ts';
|
|
11
11
|
import { parseActivityLog, pairEntries } from '../activity-log.ts';
|
|
12
|
+
import { readRun, type OrchestrationRun } from '../orchestration.ts';
|
|
13
|
+
import type { DelegationReceipt } from '../delegation-receipts.ts';
|
|
12
14
|
import { statusCard, type DisplayMessage } from '../../server/display.ts';
|
|
13
15
|
|
|
14
16
|
const USAGE = `Usage: ai task status [<N | TASK-id> | --task <ref> | -t <ref>]
|
|
@@ -219,6 +221,19 @@ type RuntimeInfo = {
|
|
|
219
221
|
log: string;
|
|
220
222
|
};
|
|
221
223
|
|
|
224
|
+
type PendingDelegationInfo = Readonly<Pick<
|
|
225
|
+
DelegationReceipt,
|
|
226
|
+
'id' | 'role' | 'stage' | 'round' | 'artifact' | 'client' | 'status'
|
|
227
|
+
>>;
|
|
228
|
+
|
|
229
|
+
type OrchestrationInfo = Readonly<{
|
|
230
|
+
status: 'absent' | OrchestrationRun['status'];
|
|
231
|
+
runId: string;
|
|
232
|
+
nextStage: string;
|
|
233
|
+
pause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
|
|
234
|
+
pendingDelegation: PendingDelegationInfo | null;
|
|
235
|
+
}>;
|
|
236
|
+
|
|
222
237
|
type ManagedRunRecord = {
|
|
223
238
|
run_id: string;
|
|
224
239
|
engine: string;
|
|
@@ -323,6 +338,39 @@ function collectRuntime(taskDir: string, workflow: WorkflowInfo, run: Runner): R
|
|
|
323
338
|
};
|
|
324
339
|
}
|
|
325
340
|
|
|
341
|
+
function pendingDelegationInfo(receipt: DelegationReceipt | null): PendingDelegationInfo | null {
|
|
342
|
+
if (!receipt) return null;
|
|
343
|
+
return {
|
|
344
|
+
id: receipt.id,
|
|
345
|
+
role: receipt.role,
|
|
346
|
+
stage: receipt.stage,
|
|
347
|
+
round: receipt.round,
|
|
348
|
+
artifact: receipt.artifact,
|
|
349
|
+
client: receipt.client,
|
|
350
|
+
status: receipt.status
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function collectOrchestration(taskDir: string): OrchestrationInfo {
|
|
355
|
+
const run = readRun(taskDir);
|
|
356
|
+
if (!run) {
|
|
357
|
+
return {
|
|
358
|
+
status: 'absent',
|
|
359
|
+
runId: DASH,
|
|
360
|
+
nextStage: DASH,
|
|
361
|
+
pause: null,
|
|
362
|
+
pendingDelegation: null
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
status: run.status,
|
|
367
|
+
runId: run.runId,
|
|
368
|
+
nextStage: run.nextStage ?? DASH,
|
|
369
|
+
pause: run.pause,
|
|
370
|
+
pendingDelegation: pendingDelegationInfo(run.pendingDelegation)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
326
374
|
type StatusModel = {
|
|
327
375
|
taskId: string;
|
|
328
376
|
shortId: string;
|
|
@@ -332,6 +380,7 @@ type StatusModel = {
|
|
|
332
380
|
artifacts: { count: number; groups: { stage: string; files: string[] }[] };
|
|
333
381
|
workflow: WorkflowInfo;
|
|
334
382
|
runtime: RuntimeInfo;
|
|
383
|
+
orchestration: OrchestrationInfo;
|
|
335
384
|
git: GitInfo;
|
|
336
385
|
};
|
|
337
386
|
|
|
@@ -380,6 +429,26 @@ function renderStatus(model: StatusModel): string[] {
|
|
|
380
429
|
lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
|
|
381
430
|
}
|
|
382
431
|
|
|
432
|
+
lines.push(
|
|
433
|
+
'',
|
|
434
|
+
'Orchestration',
|
|
435
|
+
...renderPairs([
|
|
436
|
+
['status', model.orchestration.status],
|
|
437
|
+
['run_id', model.orchestration.runId],
|
|
438
|
+
['next_stage', model.orchestration.nextStage],
|
|
439
|
+
['pause_code', model.orchestration.pause?.code ?? DASH],
|
|
440
|
+
['pause_message', model.orchestration.pause?.message ?? DASH],
|
|
441
|
+
['recoverable', model.orchestration.pause ? String(model.orchestration.pause.recoverable) : DASH],
|
|
442
|
+
['pending_id', model.orchestration.pendingDelegation?.id ?? DASH],
|
|
443
|
+
['pending_role', model.orchestration.pendingDelegation?.role ?? DASH],
|
|
444
|
+
['pending_stage', model.orchestration.pendingDelegation?.stage ?? DASH],
|
|
445
|
+
['pending_round', model.orchestration.pendingDelegation ? String(model.orchestration.pendingDelegation.round) : DASH],
|
|
446
|
+
['pending_artifact', model.orchestration.pendingDelegation?.artifact ?? DASH],
|
|
447
|
+
['pending_client', model.orchestration.pendingDelegation?.client ?? DASH],
|
|
448
|
+
['pending_status', model.orchestration.pendingDelegation?.status ?? DASH]
|
|
449
|
+
])
|
|
450
|
+
);
|
|
451
|
+
|
|
383
452
|
lines.push(
|
|
384
453
|
'',
|
|
385
454
|
'Workflow',
|
|
@@ -425,6 +494,9 @@ function renderStatus(model: StatusModel): string[] {
|
|
|
425
494
|
}
|
|
426
495
|
|
|
427
496
|
function modelTone(model: StatusModel): 'info' | 'success' | 'warning' | 'danger' | 'running' {
|
|
497
|
+
if (model.orchestration.status === 'paused') return 'warning';
|
|
498
|
+
if (model.orchestration.status === 'running') return 'running';
|
|
499
|
+
if (model.orchestration.status === 'completed') return 'success';
|
|
428
500
|
if (model.workflow.state === 'in-progress') return model.workflow.stale === 'yes' ? 'warning' : 'running';
|
|
429
501
|
if (model.metadata.some(([key, value]) => key === 'status' && value === 'completed')) return 'success';
|
|
430
502
|
if (model.metadata.some(([key, value]) => key === 'status' && (value === 'blocked' || value === 'cancelled'))) {
|
|
@@ -449,6 +521,7 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
|
|
|
449
521
|
artifacts: { count: artifacts.length, groups: groupArtifacts(artifacts) },
|
|
450
522
|
workflow,
|
|
451
523
|
runtime: collectRuntime(input.taskDir, workflow, run),
|
|
524
|
+
orchestration: collectOrchestration(input.taskDir),
|
|
452
525
|
git: collectGit(fm.branch ?? '', run)
|
|
453
526
|
};
|
|
454
527
|
}
|
|
@@ -483,6 +556,9 @@ function statusModelToDisplay(model: StatusModel): DisplayMessage {
|
|
|
483
556
|
`Task ${model.taskId} (${model.shortId})`,
|
|
484
557
|
modelTone(model),
|
|
485
558
|
[
|
|
559
|
+
['orchestration', model.orchestration.status],
|
|
560
|
+
['run', model.orchestration.runId],
|
|
561
|
+
['next', model.orchestration.nextStage],
|
|
486
562
|
['workflow', model.workflow.state],
|
|
487
563
|
['step', model.workflow.step],
|
|
488
564
|
['runtime', model.runtime.status],
|
|
@@ -530,8 +606,9 @@ export {
|
|
|
530
606
|
collectGit,
|
|
531
607
|
collectWorkflow,
|
|
532
608
|
collectRuntime,
|
|
609
|
+
collectOrchestration,
|
|
533
610
|
renderStatus,
|
|
534
611
|
statusModelToDisplay,
|
|
535
612
|
METADATA_KEYS
|
|
536
613
|
};
|
|
537
|
-
export type { Runner, GitInfo, WorkflowInfo, RuntimeInfo, StatusModel, BuildStatusModelInput };
|
|
614
|
+
export type { Runner, GitInfo, WorkflowInfo, RuntimeInfo, OrchestrationInfo, PendingDelegationInfo, StatusModel, BuildStatusModelInput };
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
import { commitExplicitPaths, inspectGitWorkflow, pushGitRefs } from '../git/workflow.ts';
|
|
5
|
+
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
6
|
+
import { resolveTaskContext, resolveTaskRef } from './resolve-ref.ts';
|
|
7
|
+
import { commitPushDecision } from './commit-policy.ts';
|
|
8
|
+
import { mergeOperationWarnings, type OperationWarning } from './operation-outcome.ts';
|
|
9
|
+
import { TaskExecutionLockError, withRepositoryMutationLock, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
10
|
+
import { appendActivityEntry, locateActivityLog } from './activity-log.ts';
|
|
11
|
+
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
12
|
+
import type { TaskMutation } from './write.ts';
|
|
13
|
+
import {
|
|
14
|
+
commitOrchestrationStageCompletion,
|
|
15
|
+
planOrchestrationStageCompletion
|
|
16
|
+
} from './orchestration.ts';
|
|
17
|
+
import type { OrchestrationStageCompletion } from './orchestration.ts';
|
|
18
|
+
|
|
19
|
+
type CommitExecutionMode = 'direct' | 'orchestrated';
|
|
20
|
+
|
|
21
|
+
type CommitOperationInput = Readonly<{
|
|
22
|
+
cwd: string;
|
|
23
|
+
paths: readonly string[];
|
|
24
|
+
message: string;
|
|
25
|
+
expectedHead: string;
|
|
26
|
+
expectedTree: string;
|
|
27
|
+
taskRef?: string;
|
|
28
|
+
agent?: string;
|
|
29
|
+
mode?: CommitExecutionMode;
|
|
30
|
+
push?: Readonly<{
|
|
31
|
+
remote: string;
|
|
32
|
+
refs: readonly string[];
|
|
33
|
+
automatic: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
type CommitOperationResult = Readonly<{
|
|
38
|
+
status: 'applied' | 'no-op' | 'failed' | 'blocked';
|
|
39
|
+
changed: boolean;
|
|
40
|
+
result: 'committed' | 'no_op' | 'committed_with_warnings' | 'failed' | 'blocked';
|
|
41
|
+
taskId: string | null;
|
|
42
|
+
mode: CommitExecutionMode;
|
|
43
|
+
warnings: readonly OperationWarning[];
|
|
44
|
+
snapshot: ReturnType<typeof inspectGitWorkflow>['snapshot'];
|
|
45
|
+
operations: readonly unknown[];
|
|
46
|
+
error: Readonly<{ code: string; message: string }> | null;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
type BoundTask = Readonly<{
|
|
50
|
+
repoRoot: string;
|
|
51
|
+
taskId: string;
|
|
52
|
+
taskDir: string;
|
|
53
|
+
taskMdPath: string;
|
|
54
|
+
state: 'active' | 'blocked' | 'completed' | 'archive';
|
|
55
|
+
branch: string;
|
|
56
|
+
}>;
|
|
57
|
+
|
|
58
|
+
const HEAD_REF = /^refs\/heads\/(.+)$/;
|
|
59
|
+
|
|
60
|
+
function isCommitExecutionMode(value: unknown): value is CommitExecutionMode {
|
|
61
|
+
return value === 'direct' || value === 'orchestrated';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function gitText(repoRoot: string, args: readonly string[]): string {
|
|
65
|
+
return execFileSync('git', [...args], {
|
|
66
|
+
cwd: repoRoot,
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
69
|
+
}).trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function canonicalRepositoryRoot(cwd: string): string {
|
|
73
|
+
return gitText(cwd, ['rev-parse', '--show-toplevel']);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function failure(
|
|
77
|
+
input: CommitOperationInput,
|
|
78
|
+
mode: CommitExecutionMode,
|
|
79
|
+
taskId: string | null,
|
|
80
|
+
code: string,
|
|
81
|
+
message: string,
|
|
82
|
+
status: 'failed' | 'blocked' = 'failed',
|
|
83
|
+
snapshot: ReturnType<typeof inspectGitWorkflow>['snapshot'] = null
|
|
84
|
+
): CommitOperationResult {
|
|
85
|
+
return {
|
|
86
|
+
status,
|
|
87
|
+
changed: false,
|
|
88
|
+
result: status,
|
|
89
|
+
taskId,
|
|
90
|
+
mode,
|
|
91
|
+
warnings: [],
|
|
92
|
+
snapshot: snapshot ?? inspectGitWorkflow(input.cwd).snapshot,
|
|
93
|
+
operations: [],
|
|
94
|
+
error: { code, message }
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function warning(
|
|
99
|
+
code: string,
|
|
100
|
+
message: string,
|
|
101
|
+
retryable: boolean,
|
|
102
|
+
step: string,
|
|
103
|
+
target: string,
|
|
104
|
+
severity: OperationWarning['severity']
|
|
105
|
+
): OperationWarning {
|
|
106
|
+
return { code, message, retryable, step, target, severity };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveBoundTask(input: CommitOperationInput):
|
|
110
|
+
| { kind: 'none' }
|
|
111
|
+
| { kind: 'bound'; task: BoundTask }
|
|
112
|
+
| { kind: 'error'; result: CommitOperationResult } {
|
|
113
|
+
const mode = input.mode ?? 'direct';
|
|
114
|
+
if (input.taskRef !== undefined) {
|
|
115
|
+
const resolved = resolveTaskRef(input.taskRef, { repoRoot: input.cwd });
|
|
116
|
+
if (!resolved.ok) return {
|
|
117
|
+
kind: 'error',
|
|
118
|
+
result: failure(input, mode, resolved.taskId, resolved.code, resolved.message)
|
|
119
|
+
};
|
|
120
|
+
return boundTask(input, resolved.taskId, resolved.taskDir, resolved.taskMdPath, resolved.repoRoot, resolved.state);
|
|
121
|
+
}
|
|
122
|
+
const context = resolveTaskContext(undefined, { repoRoot: input.cwd });
|
|
123
|
+
if (context.ok) return boundTask(input, context.taskId, context.taskDir, context.taskMdPath, context.repoRoot, context.state);
|
|
124
|
+
if (context.code === 'TASK_CONTEXT_NOT_FOUND') return { kind: 'none' };
|
|
125
|
+
return {
|
|
126
|
+
kind: 'error',
|
|
127
|
+
result: failure(input, mode, context.taskId, context.code, context.message)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function boundTask(
|
|
132
|
+
input: CommitOperationInput,
|
|
133
|
+
taskId: string,
|
|
134
|
+
taskDir: string,
|
|
135
|
+
taskMdPath: string,
|
|
136
|
+
repoRoot: string,
|
|
137
|
+
state: BoundTask['state']
|
|
138
|
+
): { kind: 'bound'; task: BoundTask } | { kind: 'error'; result: CommitOperationResult } {
|
|
139
|
+
let branch: string;
|
|
140
|
+
try {
|
|
141
|
+
const frontmatter = parseTaskFrontmatter(fs.readFileSync(taskMdPath, 'utf8'));
|
|
142
|
+
branch = frontmatter.branch ?? '';
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
kind: 'error',
|
|
146
|
+
result: failure(input, input.mode ?? 'direct', taskId, 'TASK_CONTEXT_UNREADABLE', error instanceof Error ? error.message : String(error))
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (!branch) return {
|
|
150
|
+
kind: 'error',
|
|
151
|
+
result: failure(input, input.mode ?? 'direct', taskId, 'TASK_CONTEXT_UNREADABLE', 'task branch is missing')
|
|
152
|
+
};
|
|
153
|
+
return { kind: 'bound', task: { repoRoot, taskId, taskDir, taskMdPath, state, branch } };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateCommon(input: CommitOperationInput, branch: string | null): Readonly<{ code: string; message: string }> | null {
|
|
157
|
+
if (!input.message.trim()) return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Commit message is required' };
|
|
158
|
+
if (input.paths.length === 0 && !input.push) return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'At least one explicit path is required unless this is a push-only retry' };
|
|
159
|
+
if (!/^[a-f0-9]{40,64}$/.test(input.expectedHead)) return { code: 'GIT_HEAD_EXPECTATION_REQUIRED', message: 'A valid expected HEAD is required' };
|
|
160
|
+
if (!/^[a-f0-9]{40,64}$/.test(input.expectedTree)) return { code: 'GIT_TREE_EXPECTATION_REQUIRED', message: 'A valid expected tree is required' };
|
|
161
|
+
if (input.paths.some((candidate) => !candidate || candidate.startsWith('/') || candidate.split(/[\\/]/).includes('..'))) {
|
|
162
|
+
return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Commit paths must stay inside the repository' };
|
|
163
|
+
}
|
|
164
|
+
if (input.paths.some((candidate) => /(^|\/)(?:\.env|credentials\.json|id_rsa)(?:$|\/)/i.test(candidate))) {
|
|
165
|
+
return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Sensitive paths cannot be committed' };
|
|
166
|
+
}
|
|
167
|
+
if (branch !== null && branch === '') return { code: 'GIT_BRANCH_INVALID', message: 'A named branch is required' };
|
|
168
|
+
if (input.push) {
|
|
169
|
+
if (!input.push.remote || input.push.refs.length !== 1) return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Push requires one remote and one ref' };
|
|
170
|
+
const ref = input.push.refs[0] ?? '';
|
|
171
|
+
if (!HEAD_REF.test(ref) || HEAD_REF.exec(ref)?.[1] !== branch) {
|
|
172
|
+
return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Push ref must be the current full heads ref' };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validateOrchestrated(
|
|
179
|
+
input: CommitOperationInput,
|
|
180
|
+
task: BoundTask
|
|
181
|
+
): { plan: OrchestrationStageCompletion } | { error: Readonly<{ code: string; message: string }> } {
|
|
182
|
+
if (!input.agent) return { error: { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'orchestrated commit requires an agent' } };
|
|
183
|
+
const planned = planOrchestrationStageCompletion(task.taskId, {
|
|
184
|
+
stage: 'commit',
|
|
185
|
+
round: 1,
|
|
186
|
+
artifact: 'commit',
|
|
187
|
+
role: 'executor',
|
|
188
|
+
agent: input.agent
|
|
189
|
+
}, { repoRoot: task.repoRoot });
|
|
190
|
+
const run = planned.result.run;
|
|
191
|
+
if (
|
|
192
|
+
!planned.plan
|
|
193
|
+
|| !run
|
|
194
|
+
|| run.status !== 'running'
|
|
195
|
+
|| !run.commitAuthorization.issuedAt
|
|
196
|
+
|| run.commitAuthorization.consumedAt
|
|
197
|
+
) return {
|
|
198
|
+
error: {
|
|
199
|
+
code: planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
200
|
+
message: planned.result.error?.message ?? 'orchestrated commit requires one matching activated commit delegation'
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
return { plan: planned.plan };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function syncTaskCommit(
|
|
207
|
+
input: CommitOperationInput,
|
|
208
|
+
task: BoundTask,
|
|
209
|
+
committedHead: string
|
|
210
|
+
): OperationWarning | null {
|
|
211
|
+
let content: string;
|
|
212
|
+
try {
|
|
213
|
+
content = fs.readFileSync(task.taskMdPath, 'utf8');
|
|
214
|
+
const activity = locateActivityLog(content);
|
|
215
|
+
if (!activity) return warning(
|
|
216
|
+
'TASK_STATUS_SYNC_FAILED',
|
|
217
|
+
'task has no unique Activity Log section',
|
|
218
|
+
true,
|
|
219
|
+
'task',
|
|
220
|
+
task.taskId,
|
|
221
|
+
'ACTION_REQUIRED'
|
|
222
|
+
);
|
|
223
|
+
const metadata = captureTaskWriteMetadata();
|
|
224
|
+
const commitNote = gitText(task.repoRoot, ['show', '-s', '--format=%h %s', committedHead]);
|
|
225
|
+
const alreadyLogged = activity.entries.some((entry) => entry.step === 'Commit' && entry.note === commitNote);
|
|
226
|
+
const mutations: TaskMutation[] = [];
|
|
227
|
+
if (!alreadyLogged) mutations.push({
|
|
228
|
+
kind: 'section',
|
|
229
|
+
aliases: ['活动日志', 'Activity Log'],
|
|
230
|
+
heading: activity.heading,
|
|
231
|
+
body: appendActivityEntry(activity, { time: metadata.timestamp, step: 'Commit', agent: input.agent!, note: commitNote })
|
|
232
|
+
});
|
|
233
|
+
const frontmatter: Record<string, string> = { assigned_to: input.agent! };
|
|
234
|
+
mutations.push({ kind: 'frontmatter', set: frontmatter });
|
|
235
|
+
if (mutations.length === 0) return null;
|
|
236
|
+
const written = writeTask({
|
|
237
|
+
taskRef: task.taskId,
|
|
238
|
+
expectedState: 'active',
|
|
239
|
+
mutations
|
|
240
|
+
}, { repoRoot: task.repoRoot, metadataProvider: () => metadata });
|
|
241
|
+
if (written.status === 'failed') return warning(
|
|
242
|
+
'TASK_STATUS_SYNC_FAILED',
|
|
243
|
+
written.error.message,
|
|
244
|
+
true,
|
|
245
|
+
'task',
|
|
246
|
+
task.taskId,
|
|
247
|
+
'ACTION_REQUIRED'
|
|
248
|
+
);
|
|
249
|
+
return null;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return warning(
|
|
252
|
+
'TASK_STATUS_SYNC_FAILED',
|
|
253
|
+
error instanceof Error ? error.message : String(error),
|
|
254
|
+
true,
|
|
255
|
+
'task',
|
|
256
|
+
task.taskId,
|
|
257
|
+
'ACTION_REQUIRED'
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function executeUnlocked(input: CommitOperationInput, task: BoundTask | null, mode: CommitExecutionMode): CommitOperationResult {
|
|
263
|
+
const inspected = inspectGitWorkflow(input.cwd);
|
|
264
|
+
if (!inspected.snapshot) return failure(input, mode, task?.taskId ?? null, 'GIT_INSPECT_FAILED', 'Unable to inspect Git repository');
|
|
265
|
+
const branch = inspected.snapshot.branch;
|
|
266
|
+
const commonError = validateCommon(input, branch);
|
|
267
|
+
if (commonError) return failure(input, mode, task?.taskId ?? null, commonError.code, commonError.message, 'blocked', inspected.snapshot);
|
|
268
|
+
if (task && task.state !== 'active') return failure(input, mode, task.taskId, 'TASK_STATE_INVALID', 'task-bound commit requires an active task', 'blocked', inspected.snapshot);
|
|
269
|
+
if (task && task.branch !== branch) return failure(input, mode, task.taskId, 'GIT_BRANCH_MISMATCH', `Task branch '${task.branch}' does not match current branch '${branch}'`, 'blocked', inspected.snapshot);
|
|
270
|
+
if (task && !input.agent) return failure(input, mode, task.taskId, 'COMMIT_AGENT_REQUIRED', 'task-bound commit requires an agent', 'blocked', inspected.snapshot);
|
|
271
|
+
const orchestrationValidation = mode === 'orchestrated'
|
|
272
|
+
? (task
|
|
273
|
+
? validateOrchestrated(input, task)
|
|
274
|
+
: { error: { code: 'ORCHESTRATION_TASK_REQUIRED', message: 'orchestrated commit requires a task context' } })
|
|
275
|
+
: null;
|
|
276
|
+
if (orchestrationValidation && 'error' in orchestrationValidation) {
|
|
277
|
+
const error = orchestrationValidation.error;
|
|
278
|
+
return failure(input, mode, task?.taskId ?? null, error.code, error.message, 'blocked', inspected.snapshot);
|
|
279
|
+
}
|
|
280
|
+
const orchestrationCompletion = orchestrationValidation && 'plan' in orchestrationValidation
|
|
281
|
+
? orchestrationValidation.plan
|
|
282
|
+
: null;
|
|
283
|
+
|
|
284
|
+
const committed = input.paths.length === 0
|
|
285
|
+
? (() => {
|
|
286
|
+
if (inspected.snapshot.worktree.length > 0 || inspected.snapshot.staged.length > 0) return {
|
|
287
|
+
status: 'failed' as const,
|
|
288
|
+
changed: false,
|
|
289
|
+
snapshot: inspected.snapshot,
|
|
290
|
+
operations: [],
|
|
291
|
+
error: { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Push-only retry requires a clean working tree' }
|
|
292
|
+
};
|
|
293
|
+
if (inspected.snapshot.head !== input.expectedHead) return {
|
|
294
|
+
status: 'failed' as const,
|
|
295
|
+
changed: false,
|
|
296
|
+
snapshot: inspected.snapshot,
|
|
297
|
+
operations: [],
|
|
298
|
+
error: { code: 'GIT_HEAD_MISMATCH', message: `Expected HEAD ${input.expectedHead}, received ${inspected.snapshot.head}` }
|
|
299
|
+
};
|
|
300
|
+
const currentTree = gitText(input.cwd, ['rev-parse', 'HEAD^{tree}']);
|
|
301
|
+
if (currentTree !== input.expectedTree) return {
|
|
302
|
+
status: 'failed' as const,
|
|
303
|
+
changed: false,
|
|
304
|
+
snapshot: inspected.snapshot,
|
|
305
|
+
operations: [],
|
|
306
|
+
error: { code: 'GIT_TREE_MISMATCH', message: `Expected current tree ${input.expectedTree}, received ${currentTree}` }
|
|
307
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
status: 'no-op' as const,
|
|
310
|
+
changed: false,
|
|
311
|
+
snapshot: inspected.snapshot,
|
|
312
|
+
operations: [{ name: 'commit', status: 'no-op' as const }],
|
|
313
|
+
error: null
|
|
314
|
+
};
|
|
315
|
+
})()
|
|
316
|
+
: commitExplicitPaths({
|
|
317
|
+
cwd: input.cwd,
|
|
318
|
+
paths: input.paths,
|
|
319
|
+
message: input.message,
|
|
320
|
+
expectedHead: input.expectedHead,
|
|
321
|
+
expectedTree: input.expectedTree
|
|
322
|
+
});
|
|
323
|
+
if (committed.status === 'failed') return {
|
|
324
|
+
...committed,
|
|
325
|
+
result: 'failed',
|
|
326
|
+
taskId: task?.taskId ?? null,
|
|
327
|
+
mode,
|
|
328
|
+
warnings: [],
|
|
329
|
+
snapshot: committed.snapshot
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const warnings: OperationWarning[] = [];
|
|
333
|
+
if (orchestrationCompletion) {
|
|
334
|
+
try {
|
|
335
|
+
commitOrchestrationStageCompletion(orchestrationCompletion);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
warnings.push(warning(
|
|
338
|
+
'ORCHESTRATION_COMPLETION_FAILED',
|
|
339
|
+
error instanceof Error ? error.message : String(error),
|
|
340
|
+
true,
|
|
341
|
+
'orchestration',
|
|
342
|
+
task?.taskId ?? 'orchestration',
|
|
343
|
+
'ACTION_REQUIRED'
|
|
344
|
+
));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
let status: CommitOperationResult['status'] = committed.status;
|
|
348
|
+
let result: CommitOperationResult['result'] = committed.status === 'applied' ? 'committed' : 'no_op';
|
|
349
|
+
if (input.push) {
|
|
350
|
+
const target = `${input.push.remote}:${input.push.refs[0]}`;
|
|
351
|
+
const decision = commitPushDecision({ branch, automatic: input.push.automatic }, target);
|
|
352
|
+
if (!decision.shouldPush) {
|
|
353
|
+
warnings.push(decision.warning!);
|
|
354
|
+
} else {
|
|
355
|
+
const pushed = pushGitRefs({
|
|
356
|
+
cwd: input.cwd,
|
|
357
|
+
remote: input.push.remote,
|
|
358
|
+
refs: input.push.refs,
|
|
359
|
+
expectedSha: committed.snapshot?.head ?? gitText(input.cwd, ['rev-parse', 'HEAD'])
|
|
360
|
+
});
|
|
361
|
+
if (pushed.status !== 'applied') warnings.push(warning(
|
|
362
|
+
'COMMIT_PUSH_FAILED',
|
|
363
|
+
pushed.error?.message ?? 'Git push failed',
|
|
364
|
+
true,
|
|
365
|
+
'push',
|
|
366
|
+
target,
|
|
367
|
+
'ACTION_REQUIRED'
|
|
368
|
+
));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (warnings.length > 0) {
|
|
372
|
+
status = 'applied';
|
|
373
|
+
result = 'committed_with_warnings';
|
|
374
|
+
}
|
|
375
|
+
if (task && (committed.status === 'applied' || committed.status === 'no-op')) {
|
|
376
|
+
const taskWarning = syncTaskCommit(input, task, committed.snapshot?.head ?? gitText(task.repoRoot, ['rev-parse', 'HEAD']));
|
|
377
|
+
if (taskWarning) {
|
|
378
|
+
warnings.push(taskWarning);
|
|
379
|
+
status = 'applied';
|
|
380
|
+
result = 'committed_with_warnings';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
status,
|
|
385
|
+
changed: committed.changed,
|
|
386
|
+
result,
|
|
387
|
+
taskId: task?.taskId ?? null,
|
|
388
|
+
mode,
|
|
389
|
+
warnings: mergeOperationWarnings(warnings),
|
|
390
|
+
snapshot: committed.snapshot,
|
|
391
|
+
operations: committed.operations,
|
|
392
|
+
error: null
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function executeCommitOperation(input: CommitOperationInput): CommitOperationResult {
|
|
397
|
+
if (input.mode !== undefined && !isCommitExecutionMode(input.mode)) return {
|
|
398
|
+
status: 'blocked',
|
|
399
|
+
changed: false,
|
|
400
|
+
result: 'blocked',
|
|
401
|
+
taskId: null,
|
|
402
|
+
mode: 'direct',
|
|
403
|
+
warnings: [],
|
|
404
|
+
snapshot: null,
|
|
405
|
+
operations: [],
|
|
406
|
+
error: { code: 'COMMIT_MODE_INVALID', message: 'commit mode must be direct or orchestrated' }
|
|
407
|
+
};
|
|
408
|
+
const mode: CommitExecutionMode = input.mode ?? 'direct';
|
|
409
|
+
let normalizedInput: CommitOperationInput;
|
|
410
|
+
try {
|
|
411
|
+
normalizedInput = { ...input, cwd: canonicalRepositoryRoot(input.cwd) };
|
|
412
|
+
} catch (error) {
|
|
413
|
+
return failure(input, mode, null, 'GIT_INSPECT_FAILED', error instanceof Error ? error.message : 'Unable to resolve Git repository root', 'blocked');
|
|
414
|
+
}
|
|
415
|
+
const resolved = resolveBoundTask(normalizedInput);
|
|
416
|
+
if (resolved.kind === 'error') return resolved.result;
|
|
417
|
+
const task = resolved.kind === 'bound' ? resolved.task : null;
|
|
418
|
+
if (mode === 'orchestrated' && !task) return failure(normalizedInput, mode, null, 'ORCHESTRATION_TASK_REQUIRED', 'orchestrated commit requires a task context', 'blocked');
|
|
419
|
+
try {
|
|
420
|
+
return task
|
|
421
|
+
? withRepositoryMutationLock(normalizedInput.cwd, () => withTaskExecutionLock(normalizedInput.cwd, task.taskId, 'commit-operation', () => executeUnlocked(normalizedInput, task, mode)))
|
|
422
|
+
: withRepositoryMutationLock(normalizedInput.cwd, () => executeUnlocked(normalizedInput, null, mode));
|
|
423
|
+
} catch (error) {
|
|
424
|
+
const code = error instanceof TaskExecutionLockError ? error.code : 'COMMIT_OPERATION_FAILED';
|
|
425
|
+
return failure(normalizedInput, mode, task?.taskId ?? null, code, error instanceof Error ? error.message : String(error), 'blocked');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export { executeCommitOperation };
|
|
430
|
+
export type { CommitExecutionMode, CommitOperationInput, CommitOperationResult };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { OperationWarning } from './operation-outcome.ts';
|
|
2
|
+
|
|
3
|
+
type CommitPushPolicy = Readonly<{
|
|
4
|
+
branch: string;
|
|
5
|
+
automatic: boolean;
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
type CommitPushDecision = Readonly<{
|
|
9
|
+
shouldPush: boolean;
|
|
10
|
+
warning: OperationWarning | null;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
function commitPushDecision(policy: CommitPushPolicy, target: string): CommitPushDecision {
|
|
14
|
+
const branch = policy.branch.trim();
|
|
15
|
+
if (policy.automatic && (branch === 'main' || branch === 'master')) return {
|
|
16
|
+
shouldPush: false,
|
|
17
|
+
warning: {
|
|
18
|
+
code: 'COMMIT_AUTOPUSH_PROTECTED_BRANCH',
|
|
19
|
+
message: `Automatic commit push is disabled for protected branch '${branch}'`,
|
|
20
|
+
retryable: false,
|
|
21
|
+
step: 'push',
|
|
22
|
+
target,
|
|
23
|
+
severity: 'IMPORTANT'
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return { shouldPush: true, warning: null };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { commitPushDecision };
|
|
30
|
+
export type { CommitPushDecision, CommitPushPolicy };
|