ultimate-pi 0.16.0 → 0.18.0
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/.agents/skills/harness-context/SKILL.md +13 -6
- package/.agents/skills/harness-debate-plan/SKILL.md +37 -20
- package/.agents/skills/harness-eval/SKILL.md +6 -21
- package/.agents/skills/harness-governor/SKILL.md +4 -3
- package/.agents/skills/harness-orchestration/SKILL.md +39 -51
- package/.agents/skills/harness-plan/SKILL.md +23 -12
- package/.agents/skills/harness-review/SKILL.md +52 -0
- package/.agents/skills/harness-sentrux-setup/SKILL.md +13 -1
- package/.agents/skills/harness-steer/SKILL.md +14 -0
- package/.pi/agents/harness/adversary.md +3 -10
- package/.pi/agents/harness/evaluator.md +3 -12
- package/.pi/agents/harness/executor.md +12 -14
- package/.pi/agents/harness/planning/decompose.md +7 -4
- package/.pi/agents/harness/planning/hypothesis-validator.md +2 -0
- package/.pi/agents/harness/planning/hypothesis.md +4 -2
- package/.pi/agents/harness/planning/implementation-researcher.md +1 -1
- package/.pi/agents/harness/planning/plan-adversary.md +2 -0
- package/.pi/agents/harness/planning/plan-evaluator.md +2 -0
- package/.pi/agents/harness/planning/plan-synthesizer.md +25 -0
- package/.pi/agents/harness/planning/planning-context.md +48 -0
- package/.pi/agents/harness/planning/review-integrator.md +2 -0
- package/.pi/agents/harness/planning/scout-graphify.md +3 -1
- package/.pi/agents/harness/planning/scout-semantic.md +3 -1
- package/.pi/agents/harness/planning/scout-structure.md +3 -1
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
- package/.pi/agents/harness/sentrux-steward.md +51 -0
- package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
- package/.pi/extensions/harness-debate-tools.ts +12 -3
- package/.pi/extensions/harness-live-widget.ts +27 -1
- package/.pi/extensions/harness-plan-approval.ts +62 -56
- package/.pi/extensions/harness-run-context.ts +553 -84
- package/.pi/extensions/harness-subagent-submit.ts +43 -33
- package/.pi/extensions/harness-telemetry.ts +29 -4
- package/.pi/extensions/lib/debate-bus-core.ts +15 -9
- package/.pi/extensions/lib/harness-artifact-gate.ts +182 -0
- package/.pi/extensions/lib/harness-posthog.ts +9 -5
- package/.pi/extensions/lib/harness-spawn-topology.ts +188 -0
- package/.pi/extensions/lib/harness-subagent-auth.ts +105 -19
- package/.pi/extensions/lib/harness-subagent-policy.ts +37 -19
- package/.pi/extensions/lib/harness-subagent-precheck.ts +35 -9
- package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
- package/.pi/extensions/lib/harness-subagent-submit-registry.ts +21 -3
- package/.pi/extensions/lib/harness-subagents-bridge.ts +91 -28
- package/.pi/extensions/lib/harness-subprocess-bootstrap.ts +73 -0
- package/.pi/extensions/lib/plan-approval/create-plan.ts +2 -3
- package/.pi/extensions/lib/plan-approval/resolve-disk.ts +102 -0
- package/.pi/extensions/lib/plan-approval/schema.ts +22 -8
- package/.pi/extensions/lib/plan-approval/types.ts +1 -1
- package/.pi/extensions/lib/plan-approval/validate.ts +2 -2
- package/.pi/extensions/lib/plan-approval-readiness.ts +241 -0
- package/.pi/extensions/lib/plan-debate-eligibility.ts +67 -7
- package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
- package/.pi/extensions/lib/plan-debate-gate.ts +101 -17
- package/.pi/extensions/lib/plan-debate-lanes.ts +57 -3
- package/.pi/extensions/lib/plan-debate-round-status.ts +18 -7
- package/.pi/extensions/lib/plan-messenger.ts +4 -0
- package/.pi/extensions/lib/plan-review-gate.ts +59 -0
- package/.pi/extensions/lib/posthog-client.ts +76 -0
- package/.pi/extensions/policy-gate.ts +24 -19
- package/.pi/extensions/trace-recorder.ts +1 -0
- package/.pi/harness/agents.manifest.json +24 -16
- package/.pi/harness/corpus/cron.example +8 -0
- package/.pi/harness/corpus/graphify-kb-updater.config.json +159 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.env.template +4 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.service +17 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.timer +11 -0
- package/.pi/harness/docs/adrs/0001-harness-constitution.md +2 -1
- package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +7 -6
- package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +6 -1
- package/.pi/harness/docs/adrs/0031-harness-run-context.md +1 -1
- package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -0
- package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +3 -3
- package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +8 -5
- package/.pi/harness/docs/adrs/0039-harness-post-run-review-gate.md +47 -0
- package/.pi/harness/docs/adrs/0040-practice-grounded-orchestration.md +40 -0
- package/.pi/harness/docs/adrs/0041-intelligent-planning-reconnaissance.md +39 -0
- package/.pi/harness/docs/adrs/0042-agent-native-orchestration.md +35 -0
- package/.pi/harness/docs/adrs/0043-path-first-harness-tools.md +38 -0
- package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +36 -0
- package/.pi/harness/docs/adrs/README.md +10 -0
- package/.pi/harness/docs/graphify-kb-updater-runbook.md +157 -0
- package/.pi/harness/docs/practice-map.md +110 -0
- package/.pi/harness/env.harness.template +5 -3
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/implementation-research.yaml +28 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/review-round-consolidated.yaml +25 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-packet.yaml +196 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-review.md +14 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/research-brief.yaml +62 -0
- package/.pi/harness/evals/smoke/sentrux-stub.json +1 -1
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +43 -17
- package/.pi/harness/specs/README.md +1 -1
- package/.pi/harness/specs/harness-run-context.schema.json +11 -0
- package/.pi/harness/specs/harness-spawn-context.schema.json +14 -0
- package/.pi/harness/specs/plan-execution-plan.schema.json +39 -1
- package/.pi/harness/specs/plan-packet.schema.json +4 -0
- package/.pi/harness/specs/plan-phase-status.schema.json +17 -0
- package/.pi/harness/specs/plan-phase-waiver.schema.json +25 -0
- package/.pi/harness/specs/plan-planning-context.schema.json +50 -0
- package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
- package/.pi/harness/specs/repair-brief.schema.json +45 -0
- package/.pi/harness/specs/review-outcome.schema.json +46 -0
- package/.pi/harness/specs/sentrux-manifest-proposal.schema.json +80 -0
- package/.pi/harness/specs/sentrux-signal.schema.json +43 -0
- package/.pi/harness/specs/steer-state.schema.json +20 -0
- package/.pi/lib/harness-context-mode-policy.ts +256 -0
- package/.pi/lib/harness-repair-brief.ts +145 -0
- package/.pi/lib/harness-run-context.ts +591 -32
- package/.pi/lib/harness-ui-state.ts +87 -9
- package/.pi/model-router.example.json +13 -4
- package/.pi/prompts/harness-auto.md +9 -9
- package/.pi/prompts/harness-critic.md +3 -30
- package/.pi/prompts/harness-eval.md +4 -37
- package/.pi/prompts/harness-plan.md +139 -57
- package/.pi/prompts/harness-review.md +150 -15
- package/.pi/prompts/harness-run.md +62 -10
- package/.pi/prompts/harness-sentrux-steward.md +55 -0
- package/.pi/prompts/harness-setup.md +4 -4
- package/.pi/prompts/harness-steer.md +30 -0
- package/.pi/scripts/graphify-kb-updater.mjs +358 -0
- package/.pi/scripts/harness-generate-model-router.mjs +118 -36
- package/.pi/scripts/harness-model-router-routing.test.mjs +97 -0
- package/.pi/scripts/harness-sync-model-router.mjs +15 -2
- package/.pi/scripts/harness-verify.mjs +51 -6
- package/.pi/scripts/harness-web-policy-guard.mjs +68 -0
- package/.pi/scripts/validate-plan-dag.mjs +3 -3
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +22 -0
- package/package.json +5 -4
- package/vendor/pi-model-router/UPSTREAM_PIN.md +3 -1
- package/vendor/pi-model-router/extensions/commands.ts +4 -4
- package/vendor/pi-model-router/extensions/index.ts +21 -0
- package/vendor/pi-model-router/extensions/provider.ts +130 -79
- package/vendor/pi-model-router/extensions/routing.ts +148 -0
- package/vendor/pi-model-router/extensions/state.ts +3 -0
- package/vendor/pi-model-router/extensions/types.ts +9 -0
- package/vendor/pi-model-router/extensions/ui.ts +16 -2
- package/.pi/prompts/git-sync.md +0 -124
|
@@ -9,12 +9,13 @@ import { parse as parseYaml } from "yaml";
|
|
|
9
9
|
|
|
10
10
|
export const PLAN_FOCUS_AREAS = ["spec", "wbs", "schedule", "quality"] as const;
|
|
11
11
|
export type PlanDebateFocus = (typeof PLAN_FOCUS_AREAS)[number];
|
|
12
|
+
export type PlanDebateRoundFocus = PlanDebateFocus | "all";
|
|
12
13
|
|
|
13
14
|
export interface PlanFocusCoverage {
|
|
14
15
|
covered: PlanDebateFocus[];
|
|
15
16
|
missing: PlanDebateFocus[];
|
|
16
17
|
rounds_by_focus: Partial<Record<PlanDebateFocus, number>>;
|
|
17
|
-
focus_by_round: Partial<Record<number,
|
|
18
|
+
focus_by_round: Partial<Record<number, PlanDebateRoundFocus>>;
|
|
18
19
|
last_review_gate_ready: boolean;
|
|
19
20
|
last_round_index: number;
|
|
20
21
|
}
|
|
@@ -34,8 +35,9 @@ async function fileExists(path: string): Promise<boolean> {
|
|
|
34
35
|
|
|
35
36
|
function focusFromDraft(
|
|
36
37
|
draft: Record<string, unknown>,
|
|
37
|
-
):
|
|
38
|
+
): PlanDebateRoundFocus | null {
|
|
38
39
|
const focus = String(draft.debate_round_focus ?? "").trim();
|
|
40
|
+
if (focus === "all") return "all";
|
|
39
41
|
if ((PLAN_FOCUS_AREAS as readonly string[]).includes(focus)) {
|
|
40
42
|
return focus as PlanDebateFocus;
|
|
41
43
|
}
|
|
@@ -56,14 +58,14 @@ export async function getPlanFocusCoverage(
|
|
|
56
58
|
const artifactsDir = join(runDir, "artifacts");
|
|
57
59
|
const covered = new Set<PlanDebateFocus>();
|
|
58
60
|
const rounds_by_focus: Partial<Record<PlanDebateFocus, number>> = {};
|
|
59
|
-
const focus_by_round: Partial<Record<number,
|
|
61
|
+
const focus_by_round: Partial<Record<number, PlanDebateRoundFocus>> = {};
|
|
60
62
|
let last_review_gate_ready = false;
|
|
61
63
|
let last_round_index = 0;
|
|
62
64
|
|
|
63
65
|
let files: string[] = [];
|
|
64
66
|
try {
|
|
65
67
|
files = (await readdir(artifactsDir)).filter((f) =>
|
|
66
|
-
/^review-round
|
|
68
|
+
/^review-round(?:-r\d+|-consolidated)\.yaml$/i.test(f),
|
|
67
69
|
);
|
|
68
70
|
} catch {
|
|
69
71
|
return {
|
|
@@ -77,9 +79,12 @@ export async function getPlanFocusCoverage(
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
for (const name of files.sort()) {
|
|
80
|
-
const
|
|
82
|
+
const consolidated = /^review-round-consolidated\.yaml$/i.test(name);
|
|
83
|
+
const m = consolidated
|
|
84
|
+
? ["review-round-consolidated.yaml", "1"]
|
|
85
|
+
: /^review-round-r(\d+)\.yaml$/i.exec(name);
|
|
81
86
|
if (!m) continue;
|
|
82
|
-
const roundIndex = Number(m[1]);
|
|
87
|
+
const roundIndex = consolidated ? 1 : Number(m[1]);
|
|
83
88
|
if (roundIndex > last_round_index) last_round_index = roundIndex;
|
|
84
89
|
const raw = await readFile(join(artifactsDir, name), "utf-8");
|
|
85
90
|
let draft: Record<string, unknown>;
|
|
@@ -90,8 +95,15 @@ export async function getPlanFocusCoverage(
|
|
|
90
95
|
}
|
|
91
96
|
const focus = focusFromDraft(draft);
|
|
92
97
|
if (focus) {
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
if (focus === "all") {
|
|
99
|
+
for (const requiredFocus of required) {
|
|
100
|
+
covered.add(requiredFocus);
|
|
101
|
+
rounds_by_focus[requiredFocus] = roundIndex;
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
covered.add(focus);
|
|
105
|
+
rounds_by_focus[focus] = roundIndex;
|
|
106
|
+
}
|
|
95
107
|
focus_by_round[roundIndex] = focus;
|
|
96
108
|
}
|
|
97
109
|
if (roundIndex === last_round_index) {
|
|
@@ -138,7 +150,7 @@ export function planDebateOutcomeComplete(
|
|
|
138
150
|
export async function readDebateRoundFocus(
|
|
139
151
|
runDir: string,
|
|
140
152
|
roundIndex: number,
|
|
141
|
-
): Promise<
|
|
153
|
+
): Promise<PlanDebateRoundFocus | null> {
|
|
142
154
|
const path = join(runDir, "artifacts", `review-round-r${roundIndex}.yaml`);
|
|
143
155
|
if (!(await fileExists(path))) return null;
|
|
144
156
|
try {
|
|
@@ -7,18 +7,29 @@ import { access, readFile } from "node:fs/promises";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { isHarnessBudgetEnforceOn } from "../../lib/harness-budget-enforce.js";
|
|
9
9
|
import { capsForDebate } from "./debate-bus-core.js";
|
|
10
|
+
import type { DebateEligibilityResult } from "./plan-debate-eligibility.js";
|
|
10
11
|
import {
|
|
11
12
|
getPlanFocusCoverage,
|
|
12
13
|
type PlanDebateFocus,
|
|
13
14
|
planDebateOutcomeComplete,
|
|
14
15
|
} from "./plan-debate-focus.js";
|
|
15
16
|
import { planDebateIdForRun } from "./plan-debate-id.js";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
laneArtifactPathsForConsolidatedRound,
|
|
19
|
+
laneArtifactPathsForParallelProbesRound,
|
|
20
|
+
laneArtifactPathsForRound,
|
|
21
|
+
} from "./plan-debate-lanes.js";
|
|
17
22
|
import {
|
|
18
23
|
getMessengerRoundState,
|
|
19
24
|
loadMessengerState,
|
|
20
25
|
messengerRoundDebateReady,
|
|
21
26
|
} from "./plan-messenger.js";
|
|
27
|
+
import {
|
|
28
|
+
CONSOLIDATED_REVIEW_ARTIFACT,
|
|
29
|
+
isConsolidatedReviewStrategy,
|
|
30
|
+
isParallelProbesReviewStrategy,
|
|
31
|
+
planReviewGateStrategyFromEligibility,
|
|
32
|
+
} from "./plan-review-gate.js";
|
|
22
33
|
|
|
23
34
|
async function fileExists(path: string): Promise<boolean> {
|
|
24
35
|
try {
|
|
@@ -64,6 +75,7 @@ export interface PlanDebateGateResult {
|
|
|
64
75
|
export async function validatePlanDebateGate(
|
|
65
76
|
projectRoot: string,
|
|
66
77
|
runId: string,
|
|
78
|
+
eligibility?: DebateEligibilityResult,
|
|
67
79
|
): Promise<PlanDebateGateResult> {
|
|
68
80
|
const errors: string[] = [];
|
|
69
81
|
const warnings: string[] = [];
|
|
@@ -77,6 +89,34 @@ export async function validatePlanDebateGate(
|
|
|
77
89
|
? messenger.required_focuses
|
|
78
90
|
: (["spec", "wbs", "schedule", "quality"] as const);
|
|
79
91
|
const caps = capsForDebate(debateId, debateProfile);
|
|
92
|
+
const reviewStrategy =
|
|
93
|
+
eligibility != null
|
|
94
|
+
? planReviewGateStrategyFromEligibility(eligibility)
|
|
95
|
+
: messenger?.review_gate_mode === "consolidated"
|
|
96
|
+
? {
|
|
97
|
+
mode: "consolidated" as const,
|
|
98
|
+
profile: debateProfile as DebateEligibilityResult["profile"],
|
|
99
|
+
required_focuses: [...requiredFocuses],
|
|
100
|
+
min_focus_rounds: caps.min_focus_rounds,
|
|
101
|
+
max_rounds: caps.max_rounds,
|
|
102
|
+
max_exchanges_per_round: caps.max_exchanges_per_round,
|
|
103
|
+
round_token_cap: caps.round_token_cap,
|
|
104
|
+
debate_global_cap: caps.debate_global_cap,
|
|
105
|
+
rationale: ["messenger review_gate_mode=consolidated"],
|
|
106
|
+
}
|
|
107
|
+
: {
|
|
108
|
+
mode: "threaded" as const,
|
|
109
|
+
profile: debateProfile as DebateEligibilityResult["profile"],
|
|
110
|
+
required_focuses: [...requiredFocuses],
|
|
111
|
+
min_focus_rounds: caps.min_focus_rounds,
|
|
112
|
+
max_rounds: caps.max_rounds,
|
|
113
|
+
max_exchanges_per_round: caps.max_exchanges_per_round,
|
|
114
|
+
round_token_cap: caps.round_token_cap,
|
|
115
|
+
debate_global_cap: caps.debate_global_cap,
|
|
116
|
+
rationale: [],
|
|
117
|
+
};
|
|
118
|
+
const consolidated = isConsolidatedReviewStrategy(reviewStrategy);
|
|
119
|
+
const parallelProbes = isParallelProbesReviewStrategy(reviewStrategy);
|
|
80
120
|
const coverage = await getPlanFocusCoverage(runDir, { requiredFocuses });
|
|
81
121
|
const dialogueOpts = {
|
|
82
122
|
max_exchanges_per_round: caps.max_exchanges_per_round,
|
|
@@ -89,31 +129,73 @@ export async function validatePlanDebateGate(
|
|
|
89
129
|
errors.push("last submitted review round has review_gate_ready !== true");
|
|
90
130
|
}
|
|
91
131
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
132
|
+
if (parallelProbes) {
|
|
133
|
+
for (const rel of laneArtifactPathsForParallelProbesRound()) {
|
|
134
|
+
const abs = join(runDir, rel);
|
|
135
|
+
if (!(await fileExists(abs))) {
|
|
136
|
+
errors.push(`missing ${rel}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const roundState = await getMessengerRoundState(runDir, 1);
|
|
140
|
+
const messengerCheck = messengerRoundDebateReady(
|
|
141
|
+
roundState,
|
|
142
|
+
false,
|
|
143
|
+
dialogueOpts,
|
|
144
|
+
);
|
|
145
|
+
if (!messengerCheck.ok) {
|
|
146
|
+
for (const e of messengerCheck.errors) {
|
|
147
|
+
errors.push(`parallel_probes round messenger: ${e}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else if (consolidated) {
|
|
151
|
+
const absConsolidated = join(runDir, CONSOLIDATED_REVIEW_ARTIFACT);
|
|
152
|
+
if (!(await fileExists(absConsolidated))) {
|
|
153
|
+
errors.push(`missing ${CONSOLIDATED_REVIEW_ARTIFACT}`);
|
|
154
|
+
}
|
|
155
|
+
for (const rel of laneArtifactPathsForConsolidatedRound()) {
|
|
102
156
|
const abs = join(runDir, rel);
|
|
103
157
|
if (!(await fileExists(abs))) {
|
|
104
158
|
errors.push(`missing ${rel}`);
|
|
105
159
|
}
|
|
106
160
|
}
|
|
107
|
-
const roundState = await getMessengerRoundState(runDir,
|
|
108
|
-
const requireSprint = focus === "quality" || r >= 4;
|
|
161
|
+
const roundState = await getMessengerRoundState(runDir, 1);
|
|
109
162
|
const messengerCheck = messengerRoundDebateReady(
|
|
110
163
|
roundState,
|
|
111
|
-
|
|
164
|
+
true,
|
|
112
165
|
dialogueOpts,
|
|
113
166
|
);
|
|
114
167
|
if (!messengerCheck.ok) {
|
|
115
168
|
for (const e of messengerCheck.errors) {
|
|
116
|
-
errors.push(`round
|
|
169
|
+
errors.push(`consolidated round messenger: ${e}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
const roundIndices = [
|
|
174
|
+
...new Set(
|
|
175
|
+
Object.values(coverage.rounds_by_focus).filter(
|
|
176
|
+
(v): v is number => typeof v === "number",
|
|
177
|
+
),
|
|
178
|
+
),
|
|
179
|
+
];
|
|
180
|
+
for (const r of roundIndices) {
|
|
181
|
+
const focus = coverage.focus_by_round[r] ?? null;
|
|
182
|
+
for (const rel of laneArtifactPathsForRound(r, focus)) {
|
|
183
|
+
const abs = join(runDir, rel);
|
|
184
|
+
if (!(await fileExists(abs))) {
|
|
185
|
+
errors.push(`missing ${rel}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const roundState = await getMessengerRoundState(runDir, r);
|
|
189
|
+
const requireSprint = focus === "quality" || r >= 4;
|
|
190
|
+
const messengerCheck = messengerRoundDebateReady(
|
|
191
|
+
roundState,
|
|
192
|
+
requireSprint,
|
|
193
|
+
dialogueOpts,
|
|
194
|
+
);
|
|
195
|
+
if (!messengerCheck.ok) {
|
|
196
|
+
for (const e of messengerCheck.errors) {
|
|
197
|
+
errors.push(`round ${r} messenger: ${e}`);
|
|
198
|
+
}
|
|
117
199
|
}
|
|
118
200
|
}
|
|
119
201
|
}
|
|
@@ -203,7 +285,9 @@ export async function validatePlanDebateGate(
|
|
|
203
285
|
}
|
|
204
286
|
|
|
205
287
|
export function isReviewRoundArtifactPath(relPath: string): boolean {
|
|
206
|
-
|
|
207
|
-
|
|
288
|
+
const norm = relPath.replace(/\\/g, "/");
|
|
289
|
+
return (
|
|
290
|
+
/^artifacts\/review-round-r\d+\.yaml$/i.test(norm) ||
|
|
291
|
+
norm === CONSOLIDATED_REVIEW_ARTIFACT
|
|
208
292
|
);
|
|
209
293
|
}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Shared Review Gate lane list for a round (gate + round-status).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { PlanDebateRoundFocus } from "./plan-debate-focus.js";
|
|
6
6
|
import type { DebateLaneKind } from "./plan-debate-lane.js";
|
|
7
7
|
|
|
8
8
|
/** Lanes required before review-integrator for this round. */
|
|
9
9
|
export function lanesForRound(
|
|
10
10
|
roundIndex: number,
|
|
11
|
-
debateRoundFocus?:
|
|
11
|
+
debateRoundFocus?: PlanDebateRoundFocus | null,
|
|
12
12
|
): DebateLaneKind[] {
|
|
13
13
|
const lanes: DebateLaneKind[] = ["validation-turn", "adversary-brief"];
|
|
14
14
|
if (roundIndex === 1) {
|
|
@@ -23,7 +23,7 @@ export function lanesForRound(
|
|
|
23
23
|
/** Relative artifact paths for lane YAML + review-round. */
|
|
24
24
|
export function laneArtifactPathsForRound(
|
|
25
25
|
roundIndex: number,
|
|
26
|
-
debateRoundFocus?:
|
|
26
|
+
debateRoundFocus?: PlanDebateRoundFocus | null,
|
|
27
27
|
): string[] {
|
|
28
28
|
const paths = lanesForRound(roundIndex, debateRoundFocus).map((lane) => {
|
|
29
29
|
switch (lane) {
|
|
@@ -42,3 +42,57 @@ export function laneArtifactPathsForRound(
|
|
|
42
42
|
paths.push(`artifacts/review-round-r${roundIndex}.yaml`);
|
|
43
43
|
return paths;
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/** Lanes for consolidated Review Gate (single round; blind verifier first). */
|
|
47
|
+
export function lanesForConsolidatedRound(): DebateLaneKind[] {
|
|
48
|
+
return [
|
|
49
|
+
"hypothesis-validation",
|
|
50
|
+
"validation-turn",
|
|
51
|
+
"adversary-brief",
|
|
52
|
+
"sprint-audit",
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const PARALLEL_PROBES_REVIEW_ARTIFACT =
|
|
57
|
+
"artifacts/review-round-parallel-probes.yaml";
|
|
58
|
+
|
|
59
|
+
/** Parallel plan-verify: inspector ∥ adversary (round 1), then integrator. */
|
|
60
|
+
export function lanesForParallelProbesRound(): DebateLaneKind[] {
|
|
61
|
+
return ["hypothesis-validation", "validation-turn", "adversary-brief"];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function laneArtifactPathsForParallelProbesRound(): string[] {
|
|
65
|
+
const roundIndex = 1;
|
|
66
|
+
return [
|
|
67
|
+
...lanesForParallelProbesRound().map((lane) => {
|
|
68
|
+
switch (lane) {
|
|
69
|
+
case "validation-turn":
|
|
70
|
+
return `artifacts/validation-turn-r${roundIndex}.yaml`;
|
|
71
|
+
case "adversary-brief":
|
|
72
|
+
return `artifacts/adversary-brief-r${roundIndex}.yaml`;
|
|
73
|
+
default:
|
|
74
|
+
return `artifacts/${lane}-r${roundIndex}.yaml`;
|
|
75
|
+
}
|
|
76
|
+
}),
|
|
77
|
+
PARALLEL_PROBES_REVIEW_ARTIFACT,
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function laneArtifactPathsForConsolidatedRound(): string[] {
|
|
82
|
+
const roundIndex = 1;
|
|
83
|
+
return [
|
|
84
|
+
...lanesForConsolidatedRound().map((lane) => {
|
|
85
|
+
switch (lane) {
|
|
86
|
+
case "validation-turn":
|
|
87
|
+
return `artifacts/validation-turn-r${roundIndex}.yaml`;
|
|
88
|
+
case "adversary-brief":
|
|
89
|
+
return `artifacts/adversary-brief-r${roundIndex}.yaml`;
|
|
90
|
+
case "sprint-audit":
|
|
91
|
+
return `artifacts/sprint-audit-r${roundIndex}.yaml`;
|
|
92
|
+
default:
|
|
93
|
+
return `artifacts/${lane}-r${roundIndex}.yaml`;
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
96
|
+
"artifacts/review-round-consolidated.yaml",
|
|
97
|
+
];
|
|
98
|
+
}
|
|
@@ -7,12 +7,15 @@ import { access } from "node:fs/promises";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { capsForDebate } from "./debate-bus-core.js";
|
|
9
9
|
import {
|
|
10
|
-
type
|
|
10
|
+
type PlanDebateRoundFocus,
|
|
11
11
|
readDebateRoundFocus,
|
|
12
12
|
} from "./plan-debate-focus.js";
|
|
13
13
|
import { planDebateIdForRun } from "./plan-debate-id.js";
|
|
14
14
|
import { laneArtifactPath } from "./plan-debate-lane.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
lanesForConsolidatedRound,
|
|
17
|
+
lanesForRound,
|
|
18
|
+
} from "./plan-debate-lanes.js";
|
|
16
19
|
import {
|
|
17
20
|
getMessengerRoundState,
|
|
18
21
|
loadMessengerState,
|
|
@@ -40,26 +43,32 @@ export interface RoundStatusResult {
|
|
|
40
43
|
dialogue: { ok: boolean; errors: string[] };
|
|
41
44
|
unresolved_claim_ids: string[];
|
|
42
45
|
exchange_count: number;
|
|
43
|
-
debate_round_focus?:
|
|
46
|
+
debate_round_focus?: PlanDebateRoundFocus | null;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export async function getPlanDebateRoundStatus(
|
|
47
50
|
runDir: string,
|
|
48
51
|
roundIndex: number,
|
|
49
52
|
runId?: string,
|
|
50
|
-
opts?: { debate_round_focus?:
|
|
53
|
+
opts?: { debate_round_focus?: PlanDebateRoundFocus },
|
|
51
54
|
): Promise<RoundStatusResult> {
|
|
55
|
+
const messengerState = await loadMessengerState(runDir);
|
|
56
|
+
const consolidated =
|
|
57
|
+
messengerState?.review_gate_mode === "consolidated" && roundIndex === 1;
|
|
52
58
|
const focus =
|
|
53
59
|
opts?.debate_round_focus ??
|
|
60
|
+
(consolidated ? ("all" as PlanDebateRoundFocus) : null) ??
|
|
54
61
|
(await readDebateRoundFocus(runDir, roundIndex));
|
|
55
62
|
const missing: string[] = [];
|
|
56
|
-
|
|
63
|
+
const laneList = consolidated
|
|
64
|
+
? lanesForConsolidatedRound()
|
|
65
|
+
: lanesForRound(roundIndex, focus);
|
|
66
|
+
for (const lane of laneList) {
|
|
57
67
|
const rel = laneArtifactPath(lane, roundIndex);
|
|
58
68
|
if (!(await exists(join(runDir, rel)))) {
|
|
59
69
|
missing.push(rel);
|
|
60
70
|
}
|
|
61
71
|
}
|
|
62
|
-
const messengerState = await loadMessengerState(runDir);
|
|
63
72
|
const profile = messengerState?.debate_profile;
|
|
64
73
|
const caps = capsForDebate(
|
|
65
74
|
runId ? planDebateIdForRun(runId) : `plan-${runId ?? "unknown"}`,
|
|
@@ -73,7 +82,9 @@ export async function getPlanDebateRoundStatus(
|
|
|
73
82
|
if (!dialogue.ok) {
|
|
74
83
|
missing.push(...dialogue.errors.map((e) => `messenger: ${e}`));
|
|
75
84
|
}
|
|
76
|
-
const reviewRound =
|
|
85
|
+
const reviewRound = consolidated
|
|
86
|
+
? "artifacts/review-round-consolidated.yaml"
|
|
87
|
+
: `artifacts/review-round-r${roundIndex}.yaml`;
|
|
77
88
|
const reviewRoundOnDisk = await exists(join(runDir, reviewRound));
|
|
78
89
|
|
|
79
90
|
let next_tool: string | undefined;
|
|
@@ -63,6 +63,8 @@ export interface MessengerState {
|
|
|
63
63
|
rounds: Record<string, MessengerRoundState>;
|
|
64
64
|
debate_profile?: DebateProfile;
|
|
65
65
|
required_focuses?: PlanDebateFocus[];
|
|
66
|
+
/** consolidated = single Review Gate round; threaded = per-focus rounds */
|
|
67
|
+
review_gate_mode?: "consolidated" | "threaded";
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
function messengerRoot(runDir: string): string {
|
|
@@ -84,6 +86,7 @@ export async function initPlanMessenger(
|
|
|
84
86
|
debateId: string;
|
|
85
87
|
debate_profile?: DebateProfile;
|
|
86
88
|
required_focuses?: PlanDebateFocus[];
|
|
89
|
+
review_gate_mode?: "consolidated" | "threaded";
|
|
87
90
|
},
|
|
88
91
|
): Promise<string> {
|
|
89
92
|
const root = messengerRoot(runDir);
|
|
@@ -97,6 +100,7 @@ export async function initPlanMessenger(
|
|
|
97
100
|
rounds: {},
|
|
98
101
|
debate_profile: opts.debate_profile,
|
|
99
102
|
required_focuses: opts.required_focuses,
|
|
103
|
+
review_gate_mode: opts.review_gate_mode,
|
|
100
104
|
};
|
|
101
105
|
await writeFile(
|
|
102
106
|
join(root, "state.json"),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidated vs threaded Review Gate strategy for plan-phase debate.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
DebateEligibilityResult,
|
|
7
|
+
PlanReviewGateStrategy,
|
|
8
|
+
} from "./plan-debate-eligibility.js";
|
|
9
|
+
import type { PlanDebateFocus } from "./plan-debate-focus.js";
|
|
10
|
+
|
|
11
|
+
export type { PlanReviewGateStrategy };
|
|
12
|
+
|
|
13
|
+
export const CONSOLIDATED_REVIEW_ROUND = 1;
|
|
14
|
+
export const CONSOLIDATED_REVIEW_ARTIFACT =
|
|
15
|
+
"artifacts/review-round-consolidated.yaml";
|
|
16
|
+
|
|
17
|
+
export function planReviewGateStrategyFromEligibility(
|
|
18
|
+
eligibility: DebateEligibilityResult,
|
|
19
|
+
): PlanReviewGateStrategy {
|
|
20
|
+
return (
|
|
21
|
+
eligibility.review_gate_strategy ?? {
|
|
22
|
+
mode: eligibility.profile === "fast" ? "consolidated" : "threaded",
|
|
23
|
+
profile: eligibility.profile,
|
|
24
|
+
required_focuses: [...eligibility.required_focuses],
|
|
25
|
+
min_focus_rounds: eligibility.min_focus_rounds,
|
|
26
|
+
max_rounds: eligibility.max_rounds,
|
|
27
|
+
max_exchanges_per_round: eligibility.max_exchanges_per_round,
|
|
28
|
+
round_token_cap: eligibility.round_token_cap,
|
|
29
|
+
debate_global_cap: eligibility.debate_global_cap,
|
|
30
|
+
rationale: [...eligibility.rationale],
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isConsolidatedReviewStrategy(
|
|
36
|
+
strategy: PlanReviewGateStrategy,
|
|
37
|
+
): boolean {
|
|
38
|
+
return strategy.mode === "consolidated";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { PARALLEL_PROBES_REVIEW_ARTIFACT } from "./plan-debate-lanes.js";
|
|
42
|
+
|
|
43
|
+
export function isParallelProbesReviewStrategy(
|
|
44
|
+
strategy: PlanReviewGateStrategy,
|
|
45
|
+
): boolean {
|
|
46
|
+
return strategy.mode === "parallel_probes";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Focus areas covered in a single consolidated review round (spec + quality gate). */
|
|
50
|
+
export const CONSOLIDATED_REVIEW_FOCUS_AREAS: readonly PlanDebateFocus[] = [
|
|
51
|
+
"spec",
|
|
52
|
+
"quality",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export function consolidatedReviewFocusesSatisfied(
|
|
56
|
+
covered: readonly string[],
|
|
57
|
+
): boolean {
|
|
58
|
+
return CONSOLIDATED_REVIEW_FOCUS_AREAS.every((f) => covered.includes(f));
|
|
59
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostHog client helpers — IPv4-first fetch for WSL2 / broken dual-stack DNS.
|
|
3
|
+
*
|
|
4
|
+
* Node's default fetch can ETIMEDOUT against *.posthog.com while curl succeeds.
|
|
5
|
+
* Use createPostHogFetch() (undici, family 4) for all posthog-node clients.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
9
|
+
|
|
10
|
+
const POSTHOG_HOST_RE = /(^https?:\/\/)?([^.]+\.)*posthog\.com(\/|$)/i;
|
|
11
|
+
|
|
12
|
+
const ipv4Agent = new Agent({ connect: { family: 4 } });
|
|
13
|
+
|
|
14
|
+
let fetchPatchInstalled = false;
|
|
15
|
+
|
|
16
|
+
export function isPostHogHostUrl(url: string): boolean {
|
|
17
|
+
return POSTHOG_HOST_RE.test(url);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolvePostHogHost(): string {
|
|
21
|
+
return process.env.POSTHOG_HOST?.trim() || "https://us.i.posthog.com";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Fetch that prefers IPv4 — fixes WSL2 ETIMEDOUT on us.i.posthog.com. */
|
|
25
|
+
export function createPostHogFetch(): typeof fetch {
|
|
26
|
+
return ((input: Parameters<typeof fetch>[0], init?: RequestInit) =>
|
|
27
|
+
undiciFetch(
|
|
28
|
+
input as Parameters<typeof undiciFetch>[0],
|
|
29
|
+
{
|
|
30
|
+
...init,
|
|
31
|
+
dispatcher: ipv4Agent,
|
|
32
|
+
} as Parameters<typeof undiciFetch>[1],
|
|
33
|
+
)) as typeof fetch;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getPostHogClientOptions(): {
|
|
37
|
+
host: string;
|
|
38
|
+
fetch: typeof fetch;
|
|
39
|
+
requestTimeout: number;
|
|
40
|
+
} {
|
|
41
|
+
return {
|
|
42
|
+
host: resolvePostHogHost(),
|
|
43
|
+
fetch: createPostHogFetch(),
|
|
44
|
+
requestTimeout: 30_000,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Patch global fetch so @posthog/pi (which uses default fetch) reaches PostHog on WSL2.
|
|
50
|
+
* Only PostHog hostnames are routed through the IPv4 agent.
|
|
51
|
+
*/
|
|
52
|
+
export function installPostHogFetchPatch(): void {
|
|
53
|
+
if (fetchPatchInstalled) return;
|
|
54
|
+
fetchPatchInstalled = true;
|
|
55
|
+
|
|
56
|
+
const nativeFetch = globalThis.fetch.bind(globalThis);
|
|
57
|
+
const posthogFetch = createPostHogFetch();
|
|
58
|
+
|
|
59
|
+
globalThis.fetch = ((
|
|
60
|
+
input: Parameters<typeof fetch>[0],
|
|
61
|
+
init?: RequestInit,
|
|
62
|
+
) => {
|
|
63
|
+
const url =
|
|
64
|
+
typeof input === "string"
|
|
65
|
+
? input
|
|
66
|
+
: input instanceof URL
|
|
67
|
+
? input.href
|
|
68
|
+
: typeof input === "object" && input !== null && "url" in input
|
|
69
|
+
? String((input as { url: string }).url)
|
|
70
|
+
: "";
|
|
71
|
+
if (url && isPostHogHostUrl(url)) {
|
|
72
|
+
return posthogFetch(input, init);
|
|
73
|
+
}
|
|
74
|
+
return nativeFetch(input, init);
|
|
75
|
+
}) as typeof fetch;
|
|
76
|
+
}
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import {
|
|
13
|
+
evaluateContextModeMutation,
|
|
14
|
+
isMutatingBash,
|
|
15
|
+
} from "../lib/harness-context-mode-policy.js";
|
|
12
16
|
import {
|
|
13
17
|
extractWritePathFromToolInput,
|
|
14
18
|
getLatestRunContext,
|
|
@@ -27,6 +31,7 @@ import {
|
|
|
27
31
|
userVisiblePromptSlice,
|
|
28
32
|
validatePlanPacket,
|
|
29
33
|
} from "../lib/harness-run-context.js";
|
|
34
|
+
import { bootstrapHarnessSubprocessFromEnv } from "./lib/harness-subprocess-bootstrap.js";
|
|
30
35
|
|
|
31
36
|
type HarnessPhase = "plan" | "execute" | "evaluate" | "adversary" | "merge";
|
|
32
37
|
|
|
@@ -56,20 +61,6 @@ const PHASE_ORDER: HarnessPhase[] = [
|
|
|
56
61
|
];
|
|
57
62
|
|
|
58
63
|
const MUTATING_TOOLS = new Set(["write", "edit"]);
|
|
59
|
-
const BASH_MUTATION_PATTERNS = [
|
|
60
|
-
/\bgit\s+commit\b/i,
|
|
61
|
-
/\bgit\s+push\b/i,
|
|
62
|
-
/\bgit\s+merge\b/i,
|
|
63
|
-
/\bgit\s+rebase\b/i,
|
|
64
|
-
/\brm\s+(-rf?|--recursive)\b/i,
|
|
65
|
-
/\bmv\b/i,
|
|
66
|
-
/\bcp\b/i,
|
|
67
|
-
/\bmkdir\b/i,
|
|
68
|
-
/\bchmod\b/i,
|
|
69
|
-
/\bchown\b/i,
|
|
70
|
-
/\bsed\s+-i\b/i,
|
|
71
|
-
/\bperl\s+-i\b/i,
|
|
72
|
-
];
|
|
73
64
|
|
|
74
65
|
function nowIso(): string {
|
|
75
66
|
return new Date().toISOString();
|
|
@@ -94,10 +85,6 @@ function hasApprovedPlanSignal(prompt: string, entries: unknown[]): boolean {
|
|
|
94
85
|
return hasApprovedPlanSignalFromUserPrompt(prompt);
|
|
95
86
|
}
|
|
96
87
|
|
|
97
|
-
function isMutatingBash(command: string): boolean {
|
|
98
|
-
return BASH_MUTATION_PATTERNS.some((pattern) => pattern.test(command));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
88
|
function getLatestPolicyStateFull(ctx: {
|
|
102
89
|
sessionManager: { getEntries(): unknown[] };
|
|
103
90
|
}): PolicyState {
|
|
@@ -148,10 +135,15 @@ export default function policyGate(pi: ExtensionAPI) {
|
|
|
148
135
|
|
|
149
136
|
pi.on("session_start", async (_event, ctx) => {
|
|
150
137
|
state = getLatestPolicyStateFull(ctx);
|
|
138
|
+
const booted = await bootstrapHarnessSubprocessFromEnv(pi, ctx);
|
|
139
|
+
if (booted) {
|
|
140
|
+
state = getLatestPolicyStateFull(ctx);
|
|
141
|
+
}
|
|
151
142
|
});
|
|
152
143
|
|
|
153
144
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
154
145
|
const userPrompt = userVisiblePromptSlice(event.prompt);
|
|
146
|
+
await bootstrapHarnessSubprocessFromEnv(pi, ctx);
|
|
155
147
|
const entries = ctx.sessionManager.getEntries();
|
|
156
148
|
state = getLatestPolicyStateFull(ctx);
|
|
157
149
|
const bootstrapPrompt = isHarnessBootstrapPrompt(userPrompt);
|
|
@@ -243,7 +235,7 @@ export default function policyGate(pi: ExtensionAPI) {
|
|
|
243
235
|
|
|
244
236
|
const planPhaseHint =
|
|
245
237
|
state.phase === "plan"
|
|
246
|
-
? "\nPlan phase: scouts → decompose → hypothesis → implementation-researcher + stack-researcher → execution-plan-author → validate-plan-dag → debate eligibility + Review Gate → approve_plan → create_plan (YAML plan-packet.yaml). Post-execute: /harness-
|
|
238
|
+
? "\nPlan phase: scouts (parallel) → decompose → hypothesis (sequential) → implementation-researcher + stack-researcher (parallel) → execution-plan-author → validate-plan-dag → debate eligibility + Review Gate → approve_plan → create_plan (YAML plan-packet.yaml). Post-execute: /harness-review."
|
|
247
239
|
: "";
|
|
248
240
|
|
|
249
241
|
return {
|
|
@@ -296,6 +288,19 @@ export default function policyGate(pi: ExtensionAPI) {
|
|
|
296
288
|
}
|
|
297
289
|
}
|
|
298
290
|
|
|
291
|
+
const ctxDecision = evaluateContextModeMutation(
|
|
292
|
+
event.toolName,
|
|
293
|
+
event.input as Record<string, unknown>,
|
|
294
|
+
state.phase,
|
|
295
|
+
{
|
|
296
|
+
aborted: state.aborted,
|
|
297
|
+
budgetBypass: state.budgetBypass,
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
if (ctxDecision.blocked) {
|
|
301
|
+
return { block: true, reason: ctxDecision.reason };
|
|
302
|
+
}
|
|
303
|
+
|
|
299
304
|
return undefined;
|
|
300
305
|
});
|
|
301
306
|
|
|
@@ -235,6 +235,7 @@ export default function traceRecorder(pi: ExtensionAPI) {
|
|
|
235
235
|
if (shouldEmitStarted) {
|
|
236
236
|
captureHarnessEvent(sessionId, "harness_run_started", {
|
|
237
237
|
harness_run_id: runId,
|
|
238
|
+
run_id: runId,
|
|
238
239
|
harness_plan_id: activeRun.planId,
|
|
239
240
|
harness_phase: activeRun.phase,
|
|
240
241
|
pi_session_id: sessionId,
|