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.
@@ -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.substages.map((substage, substageIndex) => ({
775
- ...substage,
776
- status: substageIndex === 0 && status === "active" ? "active" : status === "done" ? "done" : "pending",
777
- evidenceCounts: counts,
778
- latestEvidence: stageEvidence.slice(0, 3),
779
- requiredEvidence: []
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
- cleanupCheck("pr_merged", "PR merged", "GitHub", evidence, input.pr?.state === "MERGED", input.pr?.state ?? "no PR link"),
1349
- cleanupCheck("switched_main", "Switched to main", "Codex", evidence),
1350
- cleanupCheck("pulled_latest", "Pulled latest", "Codex", evidence),
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
- if (!row) {
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
- if (!row) {
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
- if (!row) {
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
- if (!row) {
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
- if (!row) {
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 command = unwrapCommand(normalizeCommand(input2.command));
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({ repoRoot: route.binding.repoRoot, command, storage, protectedPaths: config.protectedPaths });
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: "deny",
3184
- permissionDecision: "deny",
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 normalized = unwrapCommand(normalizeCommand(command));
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 normalized = unwrapCommand(normalizeCommand(command));
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 state = current.run?.currentState;
3267
- if (command.file === "git" && (args[0] === "commit" || args[0] === "push") && state !== "COMMIT_PUSH_PR") {
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((arg) => ["-f", "--force", "--force-with-lease"].includes(arg))) {
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] === "add" && args[1] === "--" || args[0] === "commit" && args[1] === "-m" || args[0] === "push" && args[1] === "-u";
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] === "agent-loop" && ["status", "doctor", "logs"].includes(command.args[1] ?? "");
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
- return storage.hasRunCheck(run.id, "self_check") && storage.hasRunCheck(run.id, "gitnexus_detect_changes");
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
- if (!row) {
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
- if (!row) {
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
- if (!row) {
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(