agentxchain 2.154.8 → 2.154.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.154.8",
3
+ "version": "2.154.9",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -65,31 +65,59 @@ function entrySatisfiesSyntheticGateVerification(gate, entry) {
65
65
  return verificationStatus === 'pass' || verificationStatus === 'attested_pass';
66
66
  }
67
67
 
68
- function turnContributedToHumanApprovalGateArtifacts(root, state, config, entry) {
69
- // Returns true only when the accepted turn itself produced at least one of
70
- // the phase exit gate's required_files AND all required_files are present
71
- // on disk. This distinguishes a PM turn that finished phase work and
72
- // escalated for final sign-off (BUG-52 third variant) from a generic
73
- // escalation where the agent blocked BEFORE writing gate artifacts
74
- // (schedule-daemon `needs_decision` fixture).
68
+ function evaluateHumanApprovalGateArtifacts(root, state, config, entry) {
69
+ // Returns the artifact readiness shape for a human-required phase exit gate.
70
+ // A turn can be a valid standing-gate source either by producing one of the
71
+ // required files, or by re-verifying already-complete gate artifacts as the
72
+ // phase entry role before escalating to the human reviewer.
75
73
  const phase = state?.phase;
76
74
  const gateId = phase ? config?.routing?.[phase]?.exit_gate : null;
77
- if (!gateId) return false;
75
+ if (!gateId) return { ready: false, contributed: false, entryRole: false };
78
76
  const gate = config?.gates?.[gateId];
79
77
  if (!gate || !Array.isArray(gate.requires_files) || gate.requires_files.length === 0) {
80
- return false;
78
+ return { ready: false, contributed: false, entryRole: false };
81
79
  }
82
- if (!gate.requires_human_approval) return false;
80
+ if (!gate.requires_human_approval) return { ready: false, contributed: false, entryRole: false };
83
81
  const filesChanged = Array.isArray(entry?.files_changed) ? entry.files_changed : [];
84
82
  const required = gate.requires_files.filter((p) => typeof p === 'string' && p.trim());
85
- if (required.length === 0) return false;
83
+ if (required.length === 0) return { ready: false, contributed: false, entryRole: false };
86
84
  const changedSet = new Set(filesChanged.filter((p) => typeof p === 'string'));
87
85
  const contributed = required.some((relPath) => changedSet.has(relPath));
88
- if (!contributed) return false;
89
- for (const relPath of required) {
90
- if (!existsSync(join(root, relPath))) return false;
91
- }
92
- return true;
86
+ const ready = required.every((relPath) => existsSync(join(root, relPath)));
87
+ const entryRole = Boolean(config?.routing?.[phase]?.entry_role)
88
+ && (entry?.role === config.routing[phase].entry_role || entry?.assigned_role === config.routing[phase].entry_role);
89
+ return { ready, contributed, entryRole };
90
+ }
91
+
92
+ function turnCanApproveHumanGateFromEscalation(root, state, config, entry, proposed) {
93
+ const artifacts = evaluateHumanApprovalGateArtifacts(root, state, config, entry);
94
+ if (!artifacts.ready) return false;
95
+ if (artifacts.contributed) return true;
96
+ const phase = state?.phase;
97
+ const gateId = phase ? config?.routing?.[phase]?.exit_gate : null;
98
+ const gateApprovalText = [
99
+ entry?.summary,
100
+ entry?.needs_human_reason,
101
+ entry?.verification?.evidence_summary,
102
+ state?.blocked_on,
103
+ state?.blocked_reason?.recovery?.detail,
104
+ ...(Array.isArray(entry?.decisions)
105
+ ? entry.decisions.flatMap((decision) => [decision?.statement, decision?.rationale])
106
+ : []),
107
+ ]
108
+ .filter((value) => typeof value === 'string' && value.trim())
109
+ .join('\n')
110
+ .toLowerCase();
111
+ const referencesHumanGateApproval = Boolean(gateId)
112
+ && (
113
+ gateApprovalText.includes(gateId.toLowerCase())
114
+ || (gateApprovalText.includes('gate') && gateApprovalText.includes('human approval'))
115
+ || (gateApprovalText.includes('gate') && gateApprovalText.includes('human-required'))
116
+ );
117
+ // tusq.dev BUG-52 real shape: PM re-verified already-complete planning
118
+ // artifacts, changed no files by design, and escalated to human because the
119
+ // gate requires human approval. The human unblock is the gate approval.
120
+ return proposed === 'human' && artifacts.entryRole && referencesHumanGateApproval;
93
121
  }
94
122
 
95
123
  function latestCompletedTurnWantsPhaseContinuation(root, state, config, opts = {}) {
@@ -130,7 +158,7 @@ function latestCompletedTurnWantsPhaseContinuation(root, state, config, opts = {
130
158
  if (
131
159
  entry.status === 'needs_human'
132
160
  && entrySatisfiesSyntheticGateVerification(standingGate, entry)
133
- && turnContributedToHumanApprovalGateArtifacts(root, state, config, entry)
161
+ && turnCanApproveHumanGateFromEscalation(root, state, config, entry, proposed)
134
162
  ) {
135
163
  return true;
136
164
  }