holo-codex 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/docs/release-checklist.md +29 -3
- package/package.json +3 -2
- package/plugins/autonomous-pr-loop/.codex-plugin/plugin.json +1 -1
- package/plugins/autonomous-pr-loop/.mcp.json +2 -4
- package/plugins/autonomous-pr-loop/core/cli.ts +72 -0
- package/plugins/autonomous-pr-loop/core/hook-policy.ts +230 -23
- package/plugins/autonomous-pr-loop/core/storage.ts +5 -5
- package/plugins/autonomous-pr-loop/core/types.ts +2 -0
- package/plugins/autonomous-pr-loop/core/workflow-board.ts +78 -14
- package/plugins/autonomous-pr-loop/hooks/dist/permission-request.js +4 -4
- package/plugins/autonomous-pr-loop/hooks/dist/post-compact.js +4 -4
- package/plugins/autonomous-pr-loop/hooks/dist/post-tool-use.js +4 -4
- package/plugins/autonomous-pr-loop/hooks/dist/pre-compact.js +4 -4
- package/plugins/autonomous-pr-loop/hooks/dist/pre-tool-use.js +191 -27
- package/plugins/autonomous-pr-loop/hooks/dist/session-start.js +4 -4
- package/plugins/autonomous-pr-loop/hooks/dist/stop.js +4 -4
- package/plugins/autonomous-pr-loop/hooks/dist/user-prompt-submit.js +4 -4
- package/plugins/autonomous-pr-loop/mcp-server/dist/index.js +10551 -0
- package/plugins/autonomous-pr-loop/mcp-server/src/index.ts +6 -3
- package/plugins/autonomous-pr-loop/package.json +1 -1
|
@@ -474,6 +474,8 @@ export interface AgentLoopStorage {
|
|
|
474
474
|
listRunChecks(runId: string): AgentLoopRunCheck[];
|
|
475
475
|
/** Return the latest run by update time, if any exists. */
|
|
476
476
|
getCurrentRun(): AgentLoopRun | undefined;
|
|
477
|
+
/** Fetch a run by stable id. */
|
|
478
|
+
getRun(runId: string): AgentLoopRun | undefined;
|
|
477
479
|
/** List persisted runs newest-first. */
|
|
478
480
|
listRuns(limit?: number): AgentLoopRun[];
|
|
479
481
|
/** Run a group of read queries against one consistent SQLite snapshot. */
|
|
@@ -771,13 +771,15 @@ function buildStage(input: {
|
|
|
771
771
|
status,
|
|
772
772
|
actorChips: actorChipsForStage(input.definition.id, status, input.profileRoleMapping, input.stageMetadata),
|
|
773
773
|
evidenceCounts: counts,
|
|
774
|
-
substages: input.definition.
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
774
|
+
substages: input.definition.id === "cleanup"
|
|
775
|
+
? cleanupSubstages(input.definition, input.input, input.evidenceRefs, status)
|
|
776
|
+
: input.definition.substages.map((substage, substageIndex) => ({
|
|
777
|
+
...substage,
|
|
778
|
+
status: substageIndex === 0 && status === "active" ? "active" : status === "done" ? "done" : "pending",
|
|
779
|
+
evidenceCounts: counts,
|
|
780
|
+
latestEvidence: stageEvidence.slice(0, 3),
|
|
781
|
+
requiredEvidence: []
|
|
782
|
+
})),
|
|
781
783
|
latestAction: { label: status === "blocked" ? "Resolve blocker" : input.definition.nextAction, safeToRunFromDashboard: false, requiresConfirmation: false },
|
|
782
784
|
blockers: [],
|
|
783
785
|
nextAction: input.definition.nextAction
|
|
@@ -1343,14 +1345,36 @@ function satisfiedReviewEvidence(events: AgentLoopEvent[]): string | undefined {
|
|
|
1343
1345
|
}
|
|
1344
1346
|
|
|
1345
1347
|
function cleanupRows(input: WorkflowBoardInput): WorkflowCheckRow[] {
|
|
1348
|
+
return cleanupSubstageRows(input);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function cleanupSubstages(
|
|
1352
|
+
definition: (typeof WORKFLOW_STAGE_DEFINITIONS)[number],
|
|
1353
|
+
input: WorkflowBoardInput,
|
|
1354
|
+
refs: WorkflowEvidenceRef[],
|
|
1355
|
+
stageStatus: WorkflowStageStatus
|
|
1356
|
+
): WorkflowBoardSubstage[] {
|
|
1357
|
+
const rows = cleanupSubstageRows(input);
|
|
1358
|
+
const firstIncompleteIndex = rows.findIndex((row) => row.status !== "passed" && row.status !== "skipped");
|
|
1359
|
+
return definition.substages.map((substage, index) => {
|
|
1360
|
+
const row = rows.find((item) => item.id === substage.id);
|
|
1361
|
+
const latestEvidence = cleanupEvidenceRefs(input.events, refs, substage.id);
|
|
1362
|
+
return {
|
|
1363
|
+
...substage,
|
|
1364
|
+
status: row ? cleanupSubstageStatus(row, stageStatus, index === firstIncompleteIndex) : "pending",
|
|
1365
|
+
evidenceCounts: evidenceCounts(latestEvidence),
|
|
1366
|
+
latestEvidence,
|
|
1367
|
+
requiredEvidence: []
|
|
1368
|
+
};
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function cleanupSubstageRows(input: WorkflowBoardInput): WorkflowCheckRow[] {
|
|
1346
1373
|
const evidence = cleanupEvidenceBySubstage(input.events);
|
|
1347
|
-
return
|
|
1348
|
-
|
|
1349
|
-
cleanupCheck(
|
|
1350
|
-
|
|
1351
|
-
cleanupCheck("gitnexus_reindexed", "GitNexus index rebuilt", "GitNexus", evidence),
|
|
1352
|
-
cleanupCheck("worktree_clean", "Worktree clean", "Codex", evidence, input.run?.worktreeClean === true, String(input.run?.worktreeClean ?? "unknown"))
|
|
1353
|
-
];
|
|
1374
|
+
return cleanupDefinition().substages.map((substage) => {
|
|
1375
|
+
const fallback = cleanupFallback(input, substage.id);
|
|
1376
|
+
return cleanupCheck(substage.id, substage.label, cleanupOwner(substage.id), evidence, fallback.passed, fallback.evidence);
|
|
1377
|
+
});
|
|
1354
1378
|
}
|
|
1355
1379
|
|
|
1356
1380
|
function cleanupCheck(
|
|
@@ -1368,6 +1392,46 @@ function cleanupCheck(
|
|
|
1368
1392
|
return { id, label, status: fallbackPassed ? "passed" : "pending", evidence: fallbackEvidence, owner };
|
|
1369
1393
|
}
|
|
1370
1394
|
|
|
1395
|
+
function cleanupSubstageStatus(row: WorkflowCheckRow, stageStatus: WorkflowStageStatus, isFirstIncomplete: boolean): WorkflowStageStatus {
|
|
1396
|
+
if (row.status === "passed") return "done";
|
|
1397
|
+
if (row.status === "failed") return "failed";
|
|
1398
|
+
if (row.status === "blocked") return "blocked";
|
|
1399
|
+
if (row.status === "skipped") return "skipped";
|
|
1400
|
+
if (stageStatus === "active" && isFirstIncomplete) return "active";
|
|
1401
|
+
return "pending";
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function cleanupFallback(input: WorkflowBoardInput, substageId: string): { passed: boolean; evidence: string } {
|
|
1405
|
+
if (substageId === "pr_merged") {
|
|
1406
|
+
return { passed: input.pr?.state === "MERGED", evidence: input.pr?.state ?? "no PR link" };
|
|
1407
|
+
}
|
|
1408
|
+
if (substageId === "worktree_clean") {
|
|
1409
|
+
return { passed: input.run?.worktreeClean === true, evidence: String(input.run?.worktreeClean ?? "unknown") };
|
|
1410
|
+
}
|
|
1411
|
+
return { passed: false, evidence: "no appended evidence" };
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function cleanupOwner(substageId: string): string {
|
|
1415
|
+
if (substageId === "pr_merged") return "GitHub";
|
|
1416
|
+
if (substageId === "gitnexus_reindexed") return "GitNexus";
|
|
1417
|
+
return "Codex";
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function cleanupEvidenceRefs(events: AgentLoopEvent[], refs: WorkflowEvidenceRef[], substageId: string): WorkflowEvidenceRef[] {
|
|
1421
|
+
const eventIds = new Set(events
|
|
1422
|
+
.filter((event) => event.kind === WORKFLOW_EVIDENCE_KIND && payloadStage(event) === "cleanup" && payloadString(event, "substageId") === substageId)
|
|
1423
|
+
.map((event) => event.id));
|
|
1424
|
+
return refs.filter((ref) => eventIds.has(ref.id)).slice(0, 3);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function cleanupDefinition(): (typeof WORKFLOW_STAGE_DEFINITIONS)[number] {
|
|
1428
|
+
const definition = STAGE_BY_ID.get("cleanup");
|
|
1429
|
+
if (!definition) {
|
|
1430
|
+
throw new AgentLoopError("invalid_config", "cleanup workflow stage definition is missing.");
|
|
1431
|
+
}
|
|
1432
|
+
return definition;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1371
1435
|
function cleanupEvidenceBySubstage(events: AgentLoopEvent[]): Map<string, AgentLoopEvent> {
|
|
1372
1436
|
const bySubstage = new Map<string, AgentLoopEvent>();
|
|
1373
1437
|
for (const event of [...events].sort((left, right) => right.seq - left.seq)) {
|
|
@@ -923,6 +923,9 @@ var SqliteAgentLoopStorage = class {
|
|
|
923
923
|
);
|
|
924
924
|
}
|
|
925
925
|
const run = this.getRun(runId);
|
|
926
|
+
if (!run) {
|
|
927
|
+
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
928
|
+
}
|
|
926
929
|
this.db.prepare(
|
|
927
930
|
`insert into states (run_id, status, state, version, payload_json, created_at)
|
|
928
931
|
values (?, ?, ?, ?, null, ?)`
|
|
@@ -1696,10 +1699,7 @@ var SqliteAgentLoopStorage = class {
|
|
|
1696
1699
|
from runs
|
|
1697
1700
|
where id = ?`
|
|
1698
1701
|
).get(runId);
|
|
1699
|
-
|
|
1700
|
-
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
1701
|
-
}
|
|
1702
|
-
return fromRunRow(row);
|
|
1702
|
+
return row ? fromRunRow(row) : void 0;
|
|
1703
1703
|
}
|
|
1704
1704
|
getActiveRun() {
|
|
1705
1705
|
const row = this.db.prepare(
|
|
@@ -923,6 +923,9 @@ var SqliteAgentLoopStorage = class {
|
|
|
923
923
|
);
|
|
924
924
|
}
|
|
925
925
|
const run = this.getRun(runId);
|
|
926
|
+
if (!run) {
|
|
927
|
+
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
928
|
+
}
|
|
926
929
|
this.db.prepare(
|
|
927
930
|
`insert into states (run_id, status, state, version, payload_json, created_at)
|
|
928
931
|
values (?, ?, ?, ?, null, ?)`
|
|
@@ -1696,10 +1699,7 @@ var SqliteAgentLoopStorage = class {
|
|
|
1696
1699
|
from runs
|
|
1697
1700
|
where id = ?`
|
|
1698
1701
|
).get(runId);
|
|
1699
|
-
|
|
1700
|
-
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
1701
|
-
}
|
|
1702
|
-
return fromRunRow(row);
|
|
1702
|
+
return row ? fromRunRow(row) : void 0;
|
|
1703
1703
|
}
|
|
1704
1704
|
getActiveRun() {
|
|
1705
1705
|
const row = this.db.prepare(
|
|
@@ -923,6 +923,9 @@ var SqliteAgentLoopStorage = class {
|
|
|
923
923
|
);
|
|
924
924
|
}
|
|
925
925
|
const run = this.getRun(runId);
|
|
926
|
+
if (!run) {
|
|
927
|
+
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
928
|
+
}
|
|
926
929
|
this.db.prepare(
|
|
927
930
|
`insert into states (run_id, status, state, version, payload_json, created_at)
|
|
928
931
|
values (?, ?, ?, ?, null, ?)`
|
|
@@ -1696,10 +1699,7 @@ var SqliteAgentLoopStorage = class {
|
|
|
1696
1699
|
from runs
|
|
1697
1700
|
where id = ?`
|
|
1698
1701
|
).get(runId);
|
|
1699
|
-
|
|
1700
|
-
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
1701
|
-
}
|
|
1702
|
-
return fromRunRow(row);
|
|
1702
|
+
return row ? fromRunRow(row) : void 0;
|
|
1703
1703
|
}
|
|
1704
1704
|
getActiveRun() {
|
|
1705
1705
|
const row = this.db.prepare(
|
|
@@ -923,6 +923,9 @@ var SqliteAgentLoopStorage = class {
|
|
|
923
923
|
);
|
|
924
924
|
}
|
|
925
925
|
const run = this.getRun(runId);
|
|
926
|
+
if (!run) {
|
|
927
|
+
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
928
|
+
}
|
|
926
929
|
this.db.prepare(
|
|
927
930
|
`insert into states (run_id, status, state, version, payload_json, created_at)
|
|
928
931
|
values (?, ?, ?, ?, null, ?)`
|
|
@@ -1696,10 +1699,7 @@ var SqliteAgentLoopStorage = class {
|
|
|
1696
1699
|
from runs
|
|
1697
1700
|
where id = ?`
|
|
1698
1701
|
).get(runId);
|
|
1699
|
-
|
|
1700
|
-
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
1701
|
-
}
|
|
1702
|
-
return fromRunRow(row);
|
|
1702
|
+
return row ? fromRunRow(row) : void 0;
|
|
1703
1703
|
}
|
|
1704
1704
|
getActiveRun() {
|
|
1705
1705
|
const row = this.db.prepare(
|
|
@@ -1629,6 +1629,9 @@ var SqliteAgentLoopStorage = class {
|
|
|
1629
1629
|
);
|
|
1630
1630
|
}
|
|
1631
1631
|
const run = this.getRun(runId);
|
|
1632
|
+
if (!run) {
|
|
1633
|
+
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
1634
|
+
}
|
|
1632
1635
|
this.db.prepare(
|
|
1633
1636
|
`insert into states (run_id, status, state, version, payload_json, created_at)
|
|
1634
1637
|
values (?, ?, ?, ?, null, ?)`
|
|
@@ -2402,10 +2405,7 @@ var SqliteAgentLoopStorage = class {
|
|
|
2402
2405
|
from runs
|
|
2403
2406
|
where id = ?`
|
|
2404
2407
|
).get(runId);
|
|
2405
|
-
|
|
2406
|
-
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
2407
|
-
}
|
|
2408
|
-
return fromRunRow(row);
|
|
2408
|
+
return row ? fromRunRow(row) : void 0;
|
|
2409
2409
|
}
|
|
2410
2410
|
getActiveRun() {
|
|
2411
2411
|
const row = this.db.prepare(
|
|
@@ -3073,6 +3073,7 @@ function toStorageError(error, message) {
|
|
|
3073
3073
|
}
|
|
3074
3074
|
|
|
3075
3075
|
// plugins/autonomous-pr-loop/core/hook-policy.ts
|
|
3076
|
+
var REQUIRED_PUBLISH_EVIDENCE_SUBSTAGES = ["lint", "full_tests", "gitnexus_detect"];
|
|
3076
3077
|
function commandFromHookPayload(payload) {
|
|
3077
3078
|
if (!isRecord(payload)) {
|
|
3078
3079
|
return void 0;
|
|
@@ -3090,7 +3091,12 @@ function commandFromHookPayload(payload) {
|
|
|
3090
3091
|
return tokenizeCommand(command);
|
|
3091
3092
|
}
|
|
3092
3093
|
function evaluateHookPolicy(input2) {
|
|
3093
|
-
const
|
|
3094
|
+
const normalized = normalizeCommand(input2.command);
|
|
3095
|
+
const shellControl = shellControlPolicy(normalized);
|
|
3096
|
+
if (shellControl) {
|
|
3097
|
+
return deny(renderCommand(normalized), shellControl, "policy_violation", "Run one allowlisted command at a time without shell control operators.");
|
|
3098
|
+
}
|
|
3099
|
+
const command = unwrapCommand(normalized);
|
|
3094
3100
|
const blockedCommand = renderCommand(command);
|
|
3095
3101
|
const destructive = destructivePolicy(command);
|
|
3096
3102
|
if (destructive) {
|
|
@@ -3105,10 +3111,25 @@ function evaluateHookPolicy(input2) {
|
|
|
3105
3111
|
if (protectedPath) {
|
|
3106
3112
|
return deny(blockedCommand, protectedPath, "policy_violation", "Remove protected path changes from the command.");
|
|
3107
3113
|
}
|
|
3108
|
-
const gate = gatedLifecyclePolicy(command, input2.storage);
|
|
3114
|
+
const gate = gatedLifecyclePolicy(command, input2.storage, input2.runId);
|
|
3109
3115
|
if (gate) {
|
|
3110
3116
|
return deny(blockedCommand, gate.policy, gate.gate, gate.nextAction);
|
|
3111
3117
|
}
|
|
3118
|
+
const override = activeMaintainerOverride(input2.storage, lifecycleOverrideScope(command), input2.runId);
|
|
3119
|
+
if (override && matchesHookAllowlist(command)) {
|
|
3120
|
+
return {
|
|
3121
|
+
allow: true,
|
|
3122
|
+
matchedPolicy: `maintainer_override:${override.scope}`,
|
|
3123
|
+
blockedCommand,
|
|
3124
|
+
nextAction: "Continue.",
|
|
3125
|
+
reason: `Maintainer override ${override.decisionId} allows ${blockedCommand} until ${override.expiresAt}.`,
|
|
3126
|
+
auditDetails: {
|
|
3127
|
+
overrideDecisionId: override.decisionId,
|
|
3128
|
+
overrideScope: override.scope,
|
|
3129
|
+
overrideExpiresAt: override.expiresAt
|
|
3130
|
+
}
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3112
3133
|
if (!matchesHookAllowlist(command)) {
|
|
3113
3134
|
return deny(blockedCommand, "command_not_in_hook_allowlist", "policy_violation", "Use agent-loop MCP/CLI control surfaces or an allowlisted read/check command.");
|
|
3114
3135
|
}
|
|
@@ -3158,7 +3179,13 @@ function evaluatePreToolUseHook(payload, repoRoot2) {
|
|
|
3158
3179
|
try {
|
|
3159
3180
|
const config = loadConfig(route.binding.repoRoot).config;
|
|
3160
3181
|
storage = new SqliteAgentLoopStorage(statePath(route.binding.repoRoot));
|
|
3161
|
-
const decision2 = evaluateHookPolicy({
|
|
3182
|
+
const decision2 = evaluateHookPolicy({
|
|
3183
|
+
repoRoot: route.binding.repoRoot,
|
|
3184
|
+
command,
|
|
3185
|
+
storage,
|
|
3186
|
+
...route.binding.runId ? { runId: route.binding.runId } : {},
|
|
3187
|
+
protectedPaths: config.protectedPaths
|
|
3188
|
+
});
|
|
3162
3189
|
recordHookDecision(storage, decision2, route.binding.runId);
|
|
3163
3190
|
return decision2;
|
|
3164
3191
|
} catch (error) {
|
|
@@ -3180,10 +3207,8 @@ function toCodexHookResponse(decision2) {
|
|
|
3180
3207
|
return { continue: true };
|
|
3181
3208
|
}
|
|
3182
3209
|
return {
|
|
3183
|
-
decision: "
|
|
3184
|
-
|
|
3185
|
-
continue: false,
|
|
3186
|
-
stopReason: decision2.reason,
|
|
3210
|
+
decision: "block",
|
|
3211
|
+
reason: decision2.reason,
|
|
3187
3212
|
systemMessage: formatHookMessage(decision2)
|
|
3188
3213
|
};
|
|
3189
3214
|
}
|
|
@@ -3198,6 +3223,7 @@ function recordHookDecision(storage, decision2, runId) {
|
|
|
3198
3223
|
allow: decision2.allow,
|
|
3199
3224
|
matchedPolicy: decision2.matchedPolicy,
|
|
3200
3225
|
...decision2.gate ? { gate: decision2.gate } : {},
|
|
3226
|
+
...decision2.auditDetails ? { auditDetails: decision2.auditDetails } : {},
|
|
3201
3227
|
nextAction: decision2.nextAction,
|
|
3202
3228
|
commandLength: command.length,
|
|
3203
3229
|
commandSha256: createHash2("sha256").update(command).digest("hex"),
|
|
@@ -3206,9 +3232,11 @@ function recordHookDecision(storage, decision2, runId) {
|
|
|
3206
3232
|
});
|
|
3207
3233
|
}
|
|
3208
3234
|
function routeErrorDecision(command, reason) {
|
|
3209
|
-
const
|
|
3235
|
+
const baseCommand = normalizeCommand(command);
|
|
3236
|
+
const shellControl = shellControlPolicy(baseCommand);
|
|
3237
|
+
const normalized = shellControl ? baseCommand : unwrapCommand(baseCommand);
|
|
3210
3238
|
const blockedCommand = renderCommand(normalized);
|
|
3211
|
-
const destructive = destructivePolicy(normalized);
|
|
3239
|
+
const destructive = shellControl ?? destructivePolicy(normalized);
|
|
3212
3240
|
if (destructive || lifecycleCommand(normalized)) {
|
|
3213
3241
|
return deny(
|
|
3214
3242
|
blockedCommand,
|
|
@@ -3226,9 +3254,11 @@ function routeErrorDecision(command, reason) {
|
|
|
3226
3254
|
};
|
|
3227
3255
|
}
|
|
3228
3256
|
function routeSessionMismatchDecision(command, reason) {
|
|
3229
|
-
const
|
|
3257
|
+
const baseCommand = normalizeCommand(command);
|
|
3258
|
+
const shellControl = shellControlPolicy(baseCommand);
|
|
3259
|
+
const normalized = shellControl ? baseCommand : unwrapCommand(baseCommand);
|
|
3230
3260
|
const blockedCommand = renderCommand(normalized);
|
|
3231
|
-
const destructive = destructivePolicy(normalized);
|
|
3261
|
+
const destructive = shellControl ?? destructivePolicy(normalized);
|
|
3232
3262
|
if (destructive || lifecycleCommand(normalized)) {
|
|
3233
3263
|
return deny(
|
|
3234
3264
|
blockedCommand,
|
|
@@ -3249,7 +3279,7 @@ function lifecycleCommand(command) {
|
|
|
3249
3279
|
const args = stripGitGlobalOptions(command.args);
|
|
3250
3280
|
return command.file === "git" && ["commit", "push", "merge"].includes(args[0] ?? "") || command.file === "gh" && command.args[0] === "pr" && ["create", "ready", "merge"].includes(command.args[1] ?? "");
|
|
3251
3281
|
}
|
|
3252
|
-
function gatedLifecyclePolicy(command, storage) {
|
|
3282
|
+
function gatedLifecyclePolicy(command, storage, runId) {
|
|
3253
3283
|
const args = stripGitGlobalOptions(command.args);
|
|
3254
3284
|
const lifecycleCommand2 = command.file === "git" && args[0] === "commit" || command.file === "git" && args[0] === "push" || command.file === "gh" && command.args[0] === "pr" && command.args[1] === "merge";
|
|
3255
3285
|
if (!lifecycleCommand2) {
|
|
@@ -3263,22 +3293,24 @@ function gatedLifecyclePolicy(command, storage) {
|
|
|
3263
3293
|
};
|
|
3264
3294
|
}
|
|
3265
3295
|
const current = storage.getCurrentStatus();
|
|
3266
|
-
const
|
|
3267
|
-
|
|
3296
|
+
const run = runId ? storage.getRun(runId) : current.run;
|
|
3297
|
+
const state = run?.currentState;
|
|
3298
|
+
const override = activeMaintainerOverride(storage, lifecycleOverrideScope(command), runId);
|
|
3299
|
+
if (command.file === "git" && (args[0] === "commit" || args[0] === "push") && state !== "COMMIT_PUSH_PR" && !override) {
|
|
3268
3300
|
return {
|
|
3269
3301
|
policy: "commit_push_state_gate",
|
|
3270
3302
|
gate: current.gate?.kind ?? "policy_violation",
|
|
3271
3303
|
nextAction: "Resume agent-loop until COMMIT_PUSH_PR owns publishing."
|
|
3272
3304
|
};
|
|
3273
3305
|
}
|
|
3274
|
-
if (command.file === "git" && (args[0] === "commit" || args[0] === "push") && !publishPrerequisitesSatisfied(storage)) {
|
|
3306
|
+
if (command.file === "git" && (args[0] === "commit" || args[0] === "push") && !publishPrerequisitesSatisfied(storage, runId)) {
|
|
3275
3307
|
return {
|
|
3276
3308
|
policy: "commit_push_prerequisite_gate",
|
|
3277
3309
|
gate: "policy_violation",
|
|
3278
3310
|
nextAction: "Run SELF_CHECK and GitNexus detect_changes through agent-loop before publishing."
|
|
3279
3311
|
};
|
|
3280
3312
|
}
|
|
3281
|
-
if (command.file === "gh" && command.args[0] === "pr" && command.args[1] === "merge" && state !== "MERGE") {
|
|
3313
|
+
if (command.file === "gh" && command.args[0] === "pr" && command.args[1] === "merge" && state !== "MERGE" && !override) {
|
|
3282
3314
|
return {
|
|
3283
3315
|
policy: "merge_state_gate",
|
|
3284
3316
|
gate: current.gate?.kind ?? "merge_requires_confirmation",
|
|
@@ -3287,6 +3319,41 @@ function gatedLifecyclePolicy(command, storage) {
|
|
|
3287
3319
|
}
|
|
3288
3320
|
return void 0;
|
|
3289
3321
|
}
|
|
3322
|
+
function lifecycleOverrideScope(command) {
|
|
3323
|
+
const args = stripGitGlobalOptions(command.args);
|
|
3324
|
+
if (command.file === "git" && (args[0] === "commit" || args[0] === "push")) {
|
|
3325
|
+
return "publish";
|
|
3326
|
+
}
|
|
3327
|
+
if (command.file === "gh" && command.args[0] === "pr" && command.args[1] === "merge") {
|
|
3328
|
+
return "merge";
|
|
3329
|
+
}
|
|
3330
|
+
return void 0;
|
|
3331
|
+
}
|
|
3332
|
+
function activeMaintainerOverride(storage, scope, runId) {
|
|
3333
|
+
if (!storage || !scope) {
|
|
3334
|
+
return void 0;
|
|
3335
|
+
}
|
|
3336
|
+
const run = runId ? storage.getRun(runId) : storage.getCurrentRun();
|
|
3337
|
+
if (!run) {
|
|
3338
|
+
return void 0;
|
|
3339
|
+
}
|
|
3340
|
+
return storage.listDecisions(run.id).map((decision2) => {
|
|
3341
|
+
const details = objectDetails(decision2.details);
|
|
3342
|
+
const overrideScope = stringValue2(details?.scope);
|
|
3343
|
+
const expiresAt = stringValue2(details?.expiresAt);
|
|
3344
|
+
if (decision2.kind !== "maintainer_override_approved" || !overrideScope || !expiresAt) {
|
|
3345
|
+
return void 0;
|
|
3346
|
+
}
|
|
3347
|
+
if (overrideScope !== scope) {
|
|
3348
|
+
return void 0;
|
|
3349
|
+
}
|
|
3350
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
3351
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
|
|
3352
|
+
return void 0;
|
|
3353
|
+
}
|
|
3354
|
+
return { decisionId: decision2.id, scope, expiresAt };
|
|
3355
|
+
}).find((override) => override !== void 0);
|
|
3356
|
+
}
|
|
3290
3357
|
function destructivePolicy(command) {
|
|
3291
3358
|
const args = stripGitGlobalOptions(command.args);
|
|
3292
3359
|
if (command.file === "git" && args[0] === "reset" && args.includes("--hard")) {
|
|
@@ -3295,7 +3362,9 @@ function destructivePolicy(command) {
|
|
|
3295
3362
|
if (command.file === "git" && args[0] === "clean" && args.some((arg) => /^-.*f/.test(arg))) {
|
|
3296
3363
|
return "destructive_git_clean";
|
|
3297
3364
|
}
|
|
3298
|
-
if (command.file === "git" && args[0] === "push" && args.some(
|
|
3365
|
+
if (command.file === "git" && args[0] === "push" && args.some(
|
|
3366
|
+
(arg) => ["-f", "-d", "--force", "--force-with-lease", "--mirror", "--delete"].includes(arg) || arg.startsWith("+") || /^:[^:]+/.test(arg)
|
|
3367
|
+
)) {
|
|
3299
3368
|
return "destructive_git_force_push";
|
|
3300
3369
|
}
|
|
3301
3370
|
if (command.file === "gh" && command.args[0] === "repo" && command.args[1] === "delete") {
|
|
@@ -3325,14 +3394,17 @@ function protectedPathPolicy(command, protectedPaths) {
|
|
|
3325
3394
|
}
|
|
3326
3395
|
function matchesHookAllowlist(command) {
|
|
3327
3396
|
const args = stripGitGlobalOptions(command.args);
|
|
3397
|
+
if (command.file === "rg" && matchesRipgrepAllowlist(command.args) || isApplyPatchCommand(command)) {
|
|
3398
|
+
return true;
|
|
3399
|
+
}
|
|
3328
3400
|
if (command.file === "git") {
|
|
3329
|
-
return args[0] === "status" || args[0] === "branch" && args[1] === "--show-current" || args[0] === "rev-parse" || args[0] === "diff" || args[0] === "
|
|
3401
|
+
return args[0] === "status" || args[0] === "branch" && args[1] === "--show-current" || args[0] === "rev-parse" || args[0] === "diff" || ["log", "show"].includes(args[0] ?? "") || args[0] === "grep" && matchesGitGrepAllowlist(args.slice(1)) || args[0] === "switch" && args.length === 2 && typeof args[1] === "string" && !args[1].startsWith("-") || args[0] === "add" && args[1] === "--" || args[0] === "commit" && args[1] === "-m" || args[0] === "push" && matchesGitPushAllowlist(args.slice(1));
|
|
3330
3402
|
}
|
|
3331
3403
|
if (command.file === "gh") {
|
|
3332
|
-
return command.args[0] === "auth" && command.args[1] === "status" || command.args[0] === "pr" && ["list", "view"].includes(command.args[1] ?? "") || command.args[0] === "api" && command.args[1] === "graphql";
|
|
3404
|
+
return command.args[0] === "auth" && command.args[1] === "status" || command.args[0] === "pr" && ["list", "view", "checks"].includes(command.args[1] ?? "") || command.args[0] === "pr" && command.args[1] === "merge" && matchesGhPrMergeAllowlist(command.args.slice(2)) || command.args[0] === "api" && command.args[1] === "graphql";
|
|
3333
3405
|
}
|
|
3334
3406
|
if (command.file === "pnpm") {
|
|
3335
|
-
return command.args[0] === "test" || command.args[0] === "lint" || command.args[0] === "
|
|
3407
|
+
return command.args[0] === "test" || command.args[0] === "lint" || command.args[0] === "build:hooks" || command.args[0] === "build:mcp" || command.args[0] === "agent-loop" && matchesAgentLoopAllowlist(command.args.slice(1));
|
|
3336
3408
|
}
|
|
3337
3409
|
if (command.file === "npx") {
|
|
3338
3410
|
return command.args[0] === "gitnexus" && ["--version", "status", "analyze", "detect_changes", "impact"].includes(command.args[1] ?? "");
|
|
@@ -3342,6 +3414,75 @@ function matchesHookAllowlist(command) {
|
|
|
3342
3414
|
}
|
|
3343
3415
|
return false;
|
|
3344
3416
|
}
|
|
3417
|
+
function matchesRipgrepAllowlist(args) {
|
|
3418
|
+
return !args.some((arg) => arg === "--pre" || arg.startsWith("--pre="));
|
|
3419
|
+
}
|
|
3420
|
+
function matchesGitGrepAllowlist(args) {
|
|
3421
|
+
return !args.some(
|
|
3422
|
+
(arg) => arg === "-O" || arg.startsWith("-O") || arg === "--open-files-in-pager" || arg.startsWith("--open-files-in-pager=")
|
|
3423
|
+
);
|
|
3424
|
+
}
|
|
3425
|
+
function matchesGitPushAllowlist(args) {
|
|
3426
|
+
return args.length >= 3 && args[0] === "-u" && args.every((arg) => !["-f", "-d", "--force", "--force-with-lease", "--mirror", "--delete"].includes(arg) && !arg.startsWith("+") && !/^:[^:]+/.test(arg));
|
|
3427
|
+
}
|
|
3428
|
+
function matchesGhPrMergeAllowlist(args) {
|
|
3429
|
+
const allowedFlags = /* @__PURE__ */ new Set(["--merge", "--squash", "--rebase", "--body", "--subject"]);
|
|
3430
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3431
|
+
const arg = args[index] ?? "";
|
|
3432
|
+
if (["--admin", "--auto", "--delete-branch", "-d"].includes(arg)) {
|
|
3433
|
+
return false;
|
|
3434
|
+
}
|
|
3435
|
+
if (arg.startsWith("--") && !allowedFlags.has(arg)) {
|
|
3436
|
+
return false;
|
|
3437
|
+
}
|
|
3438
|
+
if ((arg === "--body" || arg === "--subject") && args[index + 1]) {
|
|
3439
|
+
index += 1;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
return args.some((arg) => ["--merge", "--squash", "--rebase"].includes(arg));
|
|
3443
|
+
}
|
|
3444
|
+
function isApplyPatchCommand(command) {
|
|
3445
|
+
return command.file === "apply_patch" || command.raw?.startsWith("*** Begin Patch") === true;
|
|
3446
|
+
}
|
|
3447
|
+
function matchesAgentLoopAllowlist(args) {
|
|
3448
|
+
if (["status", "doctor", "logs", "observe", "timeline", "workers", "stop"].includes(args[0] ?? "")) {
|
|
3449
|
+
return true;
|
|
3450
|
+
}
|
|
3451
|
+
if (args[0] === "local") {
|
|
3452
|
+
return args[1] === "doctor";
|
|
3453
|
+
}
|
|
3454
|
+
if (args[0] === "hooks") {
|
|
3455
|
+
return ["doctor", "list"].includes(args[1] ?? "");
|
|
3456
|
+
}
|
|
3457
|
+
if (args[0] === "delivery") {
|
|
3458
|
+
return ["bind", "stage"].includes(args[1] ?? "");
|
|
3459
|
+
}
|
|
3460
|
+
if (args[0] === "evidence") {
|
|
3461
|
+
return args[1] === "append";
|
|
3462
|
+
}
|
|
3463
|
+
if (args[0] === "maintainer-override") {
|
|
3464
|
+
return args[1] === "approve";
|
|
3465
|
+
}
|
|
3466
|
+
return false;
|
|
3467
|
+
}
|
|
3468
|
+
function shellControlPolicy(command) {
|
|
3469
|
+
if (isApplyPatchCommand(command)) {
|
|
3470
|
+
return void 0;
|
|
3471
|
+
}
|
|
3472
|
+
if (command.raw && hasShellControlOperator(command.raw)) {
|
|
3473
|
+
return "shell_control_operator_forbidden";
|
|
3474
|
+
}
|
|
3475
|
+
if (command.file === "env") {
|
|
3476
|
+
const index = command.args.findIndex((arg) => !arg.includes("="));
|
|
3477
|
+
if (index >= 0) {
|
|
3478
|
+
return shellControlPolicy({ file: basename(command.args[index] ?? ""), args: command.args.slice(index + 1) });
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
if ((command.file === "sh" || command.file === "bash") && command.args[0] === "-c" && command.args[1] && hasShellControlOperator(command.args[1])) {
|
|
3482
|
+
return "shell_control_operator_forbidden";
|
|
3483
|
+
}
|
|
3484
|
+
return void 0;
|
|
3485
|
+
}
|
|
3345
3486
|
function deny(blockedCommand, matchedPolicy, gate, nextAction) {
|
|
3346
3487
|
return {
|
|
3347
3488
|
allow: false,
|
|
@@ -3383,6 +3524,9 @@ function tokenizeCommand(command) {
|
|
|
3383
3524
|
const [file = "", ...args] = parts;
|
|
3384
3525
|
return { file: basename(file), args, raw: command };
|
|
3385
3526
|
}
|
|
3527
|
+
function hasShellControlOperator(value) {
|
|
3528
|
+
return /&&|\|\||[;|<>\n\r]/.test(value);
|
|
3529
|
+
}
|
|
3386
3530
|
function stripGitGlobalOptions(args) {
|
|
3387
3531
|
const result = [...args];
|
|
3388
3532
|
while (result.length > 0) {
|
|
@@ -3403,12 +3547,29 @@ function stripGitGlobalOptions(args) {
|
|
|
3403
3547
|
}
|
|
3404
3548
|
return result;
|
|
3405
3549
|
}
|
|
3406
|
-
function publishPrerequisitesSatisfied(storage) {
|
|
3407
|
-
const run = storage.getCurrentRun();
|
|
3550
|
+
function publishPrerequisitesSatisfied(storage, runId) {
|
|
3551
|
+
const run = runId ? storage.getRun(runId) : storage.getCurrentRun();
|
|
3408
3552
|
if (!run) {
|
|
3409
3553
|
return false;
|
|
3410
3554
|
}
|
|
3411
|
-
|
|
3555
|
+
if (storage.hasRunCheck(run.id, "self_check") && storage.hasRunCheck(run.id, "gitnexus_detect_changes")) {
|
|
3556
|
+
return true;
|
|
3557
|
+
}
|
|
3558
|
+
return publishWorkflowEvidenceSatisfied(storage, run.id);
|
|
3559
|
+
}
|
|
3560
|
+
function publishWorkflowEvidenceSatisfied(storage, runId) {
|
|
3561
|
+
const completed = /* @__PURE__ */ new Set();
|
|
3562
|
+
for (const event of storage.listEvents(200)) {
|
|
3563
|
+
const payload = objectDetails(event.payload);
|
|
3564
|
+
if (event.runId !== runId || event.kind !== "workflow_stage_evidence" || stringValue2(payload?.stageId) !== "verify" || stringValue2(payload?.status) !== "done") {
|
|
3565
|
+
continue;
|
|
3566
|
+
}
|
|
3567
|
+
const substageId = stringValue2(payload?.substageId);
|
|
3568
|
+
if (substageId) {
|
|
3569
|
+
completed.add(substageId);
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
return REQUIRED_PUBLISH_EVIDENCE_SUBSTAGES.every((substageId) => completed.has(substageId));
|
|
3412
3573
|
}
|
|
3413
3574
|
function basename(value) {
|
|
3414
3575
|
return value.replaceAll("\\", "/").split("/").at(-1) ?? value;
|
|
@@ -3416,6 +3577,9 @@ function basename(value) {
|
|
|
3416
3577
|
function stringValue2(value) {
|
|
3417
3578
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
3418
3579
|
}
|
|
3580
|
+
function objectDetails(value) {
|
|
3581
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
3582
|
+
}
|
|
3419
3583
|
|
|
3420
3584
|
// plugins/autonomous-pr-loop/hooks/pre-tool-use.ts
|
|
3421
3585
|
var repoRoot = process.env.AGENT_LOOP_REPO_ROOT;
|
|
@@ -923,6 +923,9 @@ var SqliteAgentLoopStorage = class {
|
|
|
923
923
|
);
|
|
924
924
|
}
|
|
925
925
|
const run = this.getRun(runId);
|
|
926
|
+
if (!run) {
|
|
927
|
+
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
928
|
+
}
|
|
926
929
|
this.db.prepare(
|
|
927
930
|
`insert into states (run_id, status, state, version, payload_json, created_at)
|
|
928
931
|
values (?, ?, ?, ?, null, ?)`
|
|
@@ -1696,10 +1699,7 @@ var SqliteAgentLoopStorage = class {
|
|
|
1696
1699
|
from runs
|
|
1697
1700
|
where id = ?`
|
|
1698
1701
|
).get(runId);
|
|
1699
|
-
|
|
1700
|
-
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
1701
|
-
}
|
|
1702
|
-
return fromRunRow(row);
|
|
1702
|
+
return row ? fromRunRow(row) : void 0;
|
|
1703
1703
|
}
|
|
1704
1704
|
getActiveRun() {
|
|
1705
1705
|
const row = this.db.prepare(
|
|
@@ -923,6 +923,9 @@ var SqliteAgentLoopStorage = class {
|
|
|
923
923
|
);
|
|
924
924
|
}
|
|
925
925
|
const run = this.getRun(runId);
|
|
926
|
+
if (!run) {
|
|
927
|
+
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
928
|
+
}
|
|
926
929
|
this.db.prepare(
|
|
927
930
|
`insert into states (run_id, status, state, version, payload_json, created_at)
|
|
928
931
|
values (?, ?, ?, ?, null, ?)`
|
|
@@ -1696,10 +1699,7 @@ var SqliteAgentLoopStorage = class {
|
|
|
1696
1699
|
from runs
|
|
1697
1700
|
where id = ?`
|
|
1698
1701
|
).get(runId);
|
|
1699
|
-
|
|
1700
|
-
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
1701
|
-
}
|
|
1702
|
-
return fromRunRow(row);
|
|
1702
|
+
return row ? fromRunRow(row) : void 0;
|
|
1703
1703
|
}
|
|
1704
1704
|
getActiveRun() {
|
|
1705
1705
|
const row = this.db.prepare(
|
|
@@ -923,6 +923,9 @@ var SqliteAgentLoopStorage = class {
|
|
|
923
923
|
);
|
|
924
924
|
}
|
|
925
925
|
const run = this.getRun(runId);
|
|
926
|
+
if (!run) {
|
|
927
|
+
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
928
|
+
}
|
|
926
929
|
this.db.prepare(
|
|
927
930
|
`insert into states (run_id, status, state, version, payload_json, created_at)
|
|
928
931
|
values (?, ?, ?, ?, null, ?)`
|
|
@@ -1696,10 +1699,7 @@ var SqliteAgentLoopStorage = class {
|
|
|
1696
1699
|
from runs
|
|
1697
1700
|
where id = ?`
|
|
1698
1701
|
).get(runId);
|
|
1699
|
-
|
|
1700
|
-
throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
|
|
1701
|
-
}
|
|
1702
|
-
return fromRunRow(row);
|
|
1702
|
+
return row ? fromRunRow(row) : void 0;
|
|
1703
1703
|
}
|
|
1704
1704
|
getActiveRun() {
|
|
1705
1705
|
const row = this.db.prepare(
|