gentle-pi 2.5.0 → 2.6.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/README.md +139 -30
- package/assets/agents/gentle-ai-worker.md +4 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +21 -3
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/telemetry.md +57 -1
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +143 -15
- package/extensions/codegraph-tools.ts +1 -0
- package/extensions/gentle-agents.ts +799 -50
- package/extensions/gentle-ai.ts +2033 -322
- package/extensions/gentle-shell.ts +145 -42
- package/extensions/gentle-todo.ts +47 -12
- package/extensions/quiet-tools.ts +1 -0
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +7 -10
- package/lib/agents-history.ts +9 -1
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +77 -5
- package/lib/agents-runner.ts +548 -26
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +548 -191
- package/lib/agents-widget.ts +33 -14
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +8 -6
- package/lib/native-review-cli.ts +283 -1
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +296 -46
- package/lib/review-candidate-view.ts +30 -20
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +53 -8
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +187 -5
- package/lib/review-last-event-controller.ts +7 -4
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-session-standing-permission.ts +27 -6
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +47 -1
- package/lib/shell-card.ts +1 -4
- package/lib/shell-changes-view.ts +362 -37
- package/lib/shell-changes.ts +81 -1
- package/lib/shell-prompt.ts +11 -15
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +28 -11
- package/lib/telemetry-trigger.ts +2 -0
- package/package.json +6 -3
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +283 -1
- package/runtime/review-integration-v2.mjs +187 -5
- package/runtime/telemetry-trigger.mjs +2 -0
- package/scripts/build-runtime-modules.mjs +9 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +4 -2
- package/skills/_shared/review-ledger-contract.md +17 -1
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +62 -0
- package/tests/agents-fake-child.ts +15 -1
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +45 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner.test.ts +571 -14
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-view.test.ts +476 -65
- package/tests/agents-widget.test.ts +31 -1
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +169 -3
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +2 -1
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +2 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +1460 -33
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +38 -0
- package/tests/gentle-ai.test.ts +944 -4
- package/tests/gentle-shell.test.ts +303 -12
- package/tests/gentle-todo.test.ts +54 -10
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-review-capability-contract.test.ts +12 -1
- package/tests/native-review-cli.test.ts +277 -3
- package/tests/native-review-parity.test.ts +14 -7
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/package-manifest.test.ts +232 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +1 -0
- package/tests/rdd-aware-verification-contract.test.ts +10 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +304 -6
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +14 -0
- package/tests/review-controller-native-routing.test.ts +563 -3
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +364 -4
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +164 -0
- package/tests/review-last-event-closure.test.ts +105 -1
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-session-standing-permission-controller.test.ts +52 -4
- package/tests/review-session-standing-permission.test.ts +30 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-card.test.ts +24 -3
- package/tests/shell-changes-view.test.ts +471 -8
- package/tests/shell-changes.test.ts +168 -0
- package/tests/shell-prompt.test.ts +28 -6
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +100 -11
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +3 -1
|
@@ -560,6 +560,29 @@ export interface ReviewManagedAssetsContinuationV1 {
|
|
|
560
560
|
staleAssets?: readonly string[];
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
+
// gentle-pi#638: one selected lens slot the host declared unachievable
|
|
564
|
+
// through the native capture-unachievable verb, mirrored from Go's
|
|
565
|
+
// ReviewUnachievableLensSlot (internal/cli/review_next_transition.go). The
|
|
566
|
+
// withdraw form is deliberately narrower than a full execute transition:
|
|
567
|
+
// it is the literally runnable retraction command for exactly this slot, so
|
|
568
|
+
// a restart that never saw the collect offer can still take the declaration
|
|
569
|
+
// back from the stop alone.
|
|
570
|
+
export interface ReviewUnachievableLensWithdrawV3 {
|
|
571
|
+
operation: "review.capture-unachievable";
|
|
572
|
+
command: string;
|
|
573
|
+
arguments: readonly ReviewTransitionArgumentV3[];
|
|
574
|
+
binding: { targetIdentity: string; lineageId?: string; revision?: string };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export interface ReviewUnachievableLensSlotV3 {
|
|
578
|
+
lens: string;
|
|
579
|
+
selectedOrder: number;
|
|
580
|
+
subjectHash: string;
|
|
581
|
+
reason: string;
|
|
582
|
+
detail?: string;
|
|
583
|
+
withdraw: ReviewUnachievableLensWithdrawV3;
|
|
584
|
+
}
|
|
585
|
+
|
|
563
586
|
export interface ReviewNextTransitionV3 {
|
|
564
587
|
kind: "execute" | "collect" | "stop";
|
|
565
588
|
reasonCode: string;
|
|
@@ -569,9 +592,26 @@ export interface ReviewNextTransitionV3 {
|
|
|
569
592
|
correctionRequest?: ReviewCorrectionPlanRequestV1;
|
|
570
593
|
/** stop only, reason_code managed_assets_outdated: the sync continuation. */
|
|
571
594
|
continuation?: ReviewManagedAssetsContinuationV1;
|
|
595
|
+
/** stop only, reason_code unachievable_lens_slot: one entry per declared slot. */
|
|
596
|
+
unachievableLensSlots?: readonly ReviewUnachievableLensSlotV3[];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const ESCALATION_CAUSES = ["unknown_causality", "insufficient_evidence", "missing_refuter_outcome", "targeted_validator_rejected", "correction_budget_exceeded", "unresolved_severe_findings"] as const;
|
|
600
|
+
const ESCALATION_REFUTER_OUTCOMES = ["corroborated", "refuted", "inconclusive"] as const;
|
|
601
|
+
export interface ReviewEscalationRefuterOutcomeV1 {
|
|
602
|
+
findingId: string;
|
|
603
|
+
outcome: (typeof ESCALATION_REFUTER_OUTCOMES)[number];
|
|
604
|
+
proof: string;
|
|
605
|
+
}
|
|
606
|
+
export interface ReviewEscalationV1 {
|
|
607
|
+
cause: (typeof ESCALATION_CAUSES)[number];
|
|
608
|
+
findingIds: readonly string[];
|
|
609
|
+
refuterOutcomes?: readonly ReviewEscalationRefuterOutcomeV1[];
|
|
572
610
|
}
|
|
573
611
|
|
|
574
612
|
export interface ReviewStatusV3 {
|
|
613
|
+
/** Optional v7 diagnostic evidence, never routing or reconstructed authority. */
|
|
614
|
+
escalation?: ReviewEscalationV1;
|
|
575
615
|
contract: typeof REVIEW_INTEGRATION_CONTRACT;
|
|
576
616
|
applicability: Exclude<ReviewAuthorityApplicability, "not_evaluated">;
|
|
577
617
|
authority?: ReviewStatusAuthorityV1;
|
|
@@ -1712,7 +1752,7 @@ export function decodeReviewManagedAssetsContinuationV1(value: unknown, label: s
|
|
|
1712
1752
|
export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boolean; v6?: boolean } = {}): ReviewNextTransitionV3 {
|
|
1713
1753
|
const v6 = options.v6 === true;
|
|
1714
1754
|
const v5 = options.v5 === true || v6;
|
|
1715
|
-
const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : []), "continuation"]);
|
|
1755
|
+
const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : []), "continuation", "unachievable_lens_slots"]);
|
|
1716
1756
|
const kind = enumeration(transition.kind, ["execute", "collect", "stop"] as const, "next_transition.kind");
|
|
1717
1757
|
const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
1718
1758
|
const continuation = transition.continuation === undefined ? undefined : decodeReviewManagedAssetsContinuationV1(transition.continuation, "next_transition.continuation");
|
|
@@ -1722,6 +1762,13 @@ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boo
|
|
|
1722
1762
|
// it is only valid on a stop with this exact reason_code.
|
|
1723
1763
|
if (continuation !== undefined && !(kind === "stop" && reasonCode === "managed_assets_outdated")) throw new TypeError("next_transition.continuation is only valid for a stop transition with reason_code managed_assets_outdated");
|
|
1724
1764
|
|
|
1765
|
+
// gentle-pi#638: the unachievable stop names one entry per declared slot, each carrying its complete withdraw command (schemas/status-v7.schema.json, the stop variant). Like the continuation, the field is only valid on its own exact stop reason code, and an entry is refused rather than silently dropped when the provider drifts.
|
|
1766
|
+
let unachievableLensSlots: readonly ReviewUnachievableLensSlotV3[] | undefined;
|
|
1767
|
+
if (transition.unachievable_lens_slots !== undefined) {
|
|
1768
|
+
if (!(kind === "stop" && reasonCode === "unachievable_lens_slot")) throw new TypeError("next_transition.unachievable_lens_slots is only valid for a stop transition with reason_code unachievable_lens_slot");
|
|
1769
|
+
unachievableLensSlots = array(transition.unachievable_lens_slots, "next_transition.unachievable_lens_slots", decodeUnachievableLensSlot, { minimum: 1 });
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1725
1772
|
// status/v5: the bounded correction plan request rides exactly its two
|
|
1726
1773
|
// reason codes and never any other (vendored status-v5.schema.json).
|
|
1727
1774
|
let correctionRequest: ReviewCorrectionPlanRequestV1 | undefined;
|
|
@@ -1770,7 +1817,64 @@ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boo
|
|
|
1770
1817
|
return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1771
1818
|
}
|
|
1772
1819
|
if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
|
|
1773
|
-
return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }), ...(continuation === undefined ? {} : { continuation }) };
|
|
1820
|
+
return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }), ...(continuation === undefined ? {} : { continuation }), ...(unachievableLensSlots === undefined ? {} : { unachievableLensSlots }) };
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Mirrors NATIVE_REVIEW_UNACHIEVABLE_LENS_DETAIL_LIMIT (lib/native-review-cli.ts) and Go's 512-byte CAPTURE_UNACHIEVABLE detail bound.
|
|
1824
|
+
export const REVIEW_INTEGRATION_UNACHIEVABLE_LENS_DETAIL_LIMIT = 512;
|
|
1825
|
+
|
|
1826
|
+
function decodeUnachievableLensSlot(value: unknown, label: string): ReviewUnachievableLensSlotV3 {
|
|
1827
|
+
const body = exactRecord(value, label, ["lens", "selected_order", "subject_hash", "reason", "withdraw"], ["detail"]);
|
|
1828
|
+
const lens = nonempty(body.lens, `${label}.lens`);
|
|
1829
|
+
const selectedOrder = integer(body.selected_order, `${label}.selected_order`);
|
|
1830
|
+
const subjectHash = sha256(body.subject_hash, `${label}.subject_hash`);
|
|
1831
|
+
const reason = nonempty(body.reason, `${label}.reason`);
|
|
1832
|
+
const detail = body.detail === undefined ? undefined : nonempty(body.detail, `${label}.detail`);
|
|
1833
|
+
// gentle-pi#822: Go refuses a CAPTURE_UNACHIEVABLE detail above 512 UTF-8 bytes, so the stop decoder mirrors the same bound — measured in bytes, not UTF-16 code units — and a STATUS stop can never carry a detail this client would have refused to declare.
|
|
1834
|
+
if (detail !== undefined && Buffer.byteLength(detail, "utf8") > REVIEW_INTEGRATION_UNACHIEVABLE_LENS_DETAIL_LIMIT) throw new TypeError(`${label}.detail exceeds ${REVIEW_INTEGRATION_UNACHIEVABLE_LENS_DETAIL_LIMIT} bytes`);
|
|
1835
|
+
const withdraw = exactRecord(body.withdraw, `${label}.withdraw`, ["operation", "command", "arguments", "binding"]);
|
|
1836
|
+
const operation = enumeration(withdraw.operation, ["review.capture-unachievable"] as const, `${label}.withdraw.operation`);
|
|
1837
|
+
const command = nonempty(withdraw.command, `${label}.withdraw.command`);
|
|
1838
|
+
const arguments_ = decodeTransitionArguments(withdraw.arguments, `${label}.withdraw.arguments`);
|
|
1839
|
+
// Withdrawal is a single affirmative native action, never an optional or
|
|
1840
|
+
// negatable provider argument. The general token and command checks below
|
|
1841
|
+
// then prove its exact --withdraw=true rendering is the complete vector.
|
|
1842
|
+
const withdrawArguments = arguments_.filter((argument) => argument.name === "withdraw");
|
|
1843
|
+
if (withdrawArguments.length !== 1) throw new TypeError(`${label}.withdraw.arguments withdraw must appear exactly once`);
|
|
1844
|
+
if (withdrawArguments[0]!.value !== "true") throw new TypeError(`${label}.withdraw.arguments withdraw must be true`);
|
|
1845
|
+
// The binding keeps the execute branch's open-record discipline: the Go shape is target_identity plus optional lineage_id, revision, and repository_context, and closing it here would make Pi stricter than the contract it implements.
|
|
1846
|
+
const binding = exactRecord(withdraw.binding, `${label}.withdraw.binding`, ["target_identity"], ["lineage_id", "revision", "repository_context"], true);
|
|
1847
|
+
const targetIdentity = sha256(binding.target_identity, `${label}.withdraw.binding.target_identity`);
|
|
1848
|
+
const lineageId = binding.lineage_id === undefined ? undefined : lineage(binding.lineage_id, `${label}.withdraw.binding.lineage_id`);
|
|
1849
|
+
const revision = binding.revision === undefined ? undefined : sha256(binding.revision, `${label}.withdraw.binding.revision`);
|
|
1850
|
+
// gentle-pi#822: the withdraw form names the slot identity twice — as named arguments and as the binding object — and the two renderings must agree before the slot decodes. Strict equality also enforces both-or-neither on the optional lineage and revision fields, so a partially rendered binding never slips through and restart cannot withdraw a different slot. Each identity argument must appear EXACTLY once: a first-match lookup let a duplicate {name} entry smuggle a second value past the identity checks.
|
|
1851
|
+
const withdrawIdentityArgument = (name: string): ReviewTransitionArgumentV3 | undefined => {
|
|
1852
|
+
const matches = arguments_.filter((argument) => argument.name === name);
|
|
1853
|
+
if (matches.length > 1) throw new TypeError(`${label}.withdraw.arguments ${name} must appear exactly once`);
|
|
1854
|
+
return matches[0];
|
|
1855
|
+
};
|
|
1856
|
+
const withdrawRequestHash = withdrawIdentityArgument("request-hash");
|
|
1857
|
+
if (withdrawRequestHash?.value !== subjectHash) throw new TypeError(`${label}.withdraw.arguments request-hash does not match the slot subject_hash`);
|
|
1858
|
+
const withdrawTarget = withdrawIdentityArgument("target");
|
|
1859
|
+
if (withdrawTarget?.value !== targetIdentity) throw new TypeError(`${label}.withdraw.arguments target does not match the withdraw binding target_identity`);
|
|
1860
|
+
const withdrawLineage = withdrawIdentityArgument("lineage");
|
|
1861
|
+
if (withdrawLineage?.value !== lineageId) throw new TypeError(`${label}.withdraw.arguments lineage does not match the withdraw binding lineage_id`);
|
|
1862
|
+
const withdrawExpectedRevision = withdrawIdentityArgument("expected-revision");
|
|
1863
|
+
if (withdrawExpectedRevision?.value !== revision) throw new TypeError(`${label}.withdraw.arguments expected-revision does not match the withdraw binding revision`);
|
|
1864
|
+
// gentle-pi#822: command is executable authority, not a display hint. Every
|
|
1865
|
+
// provider-issued argument token must be the canonical --name=value rendering,
|
|
1866
|
+
// and command must render the canonical operation plus those tokens exactly in
|
|
1867
|
+
// order. This rejects omitted tokens, a second command, suffixes, and shell
|
|
1868
|
+
// payloads rather than merely finding identity-token substrings.
|
|
1869
|
+
const tokens = arguments_.map((argument, index) => {
|
|
1870
|
+
if (argument.token === undefined) throw new TypeError(`${label}.withdraw.arguments[${index}].token is required`);
|
|
1871
|
+
const expectedToken = `--${argument.name}=${argument.value}`;
|
|
1872
|
+
if (argument.token !== expectedToken) throw new TypeError(`${label}.withdraw.arguments[${index}].token must exactly render its name and value`);
|
|
1873
|
+
return argument.token;
|
|
1874
|
+
});
|
|
1875
|
+
const expectedCommand = `gentle-ai review capture-unachievable ${tokens.join(" ")}`;
|
|
1876
|
+
if (command !== expectedCommand) throw new TypeError(`${label}.withdraw.command must exactly render the canonical operation and withdraw arguments`);
|
|
1877
|
+
return { lens, selectedOrder, subjectHash, reason, ...(detail === undefined ? {} : { detail }), withdraw: { operation, command, arguments: arguments_, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) } } };
|
|
1774
1878
|
}
|
|
1775
1879
|
|
|
1776
1880
|
// ---------------------------------------------------------------------------
|
|
@@ -1820,7 +1924,7 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1820
1924
|
// key set plus the optional forecast and the v5-only next_transition
|
|
1821
1925
|
// surfaces. status/v6 adds the intended-untracked selection; status/v7
|
|
1822
1926
|
// (gentle-ai v2.6.0, advertised through capabilities/v2.5 alongside v6)
|
|
1823
|
-
// adds
|
|
1927
|
+
// adds optional `eligible_untracked_inventory` and escalation metadata,
|
|
1824
1928
|
// so it is decoded on the v6 surface. v3 keeps rejecting every v5/v6/v7-only
|
|
1825
1929
|
// field.
|
|
1826
1930
|
const schema = typeof value === "object" && value !== null ? (value as Record<string, unknown>).schema : undefined;
|
|
@@ -1829,7 +1933,7 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1829
1933
|
const v5 = v6 || schema === "gentle-ai.review-integration.status/v5";
|
|
1830
1934
|
const body = exactRecord(value, "status", [
|
|
1831
1935
|
"schema", "contract", "operation", "applicability", "action", "replayability", "target_identity", "projection", "repair", "candidates",
|
|
1832
|
-
], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : []), ...(v7 ? ["eligible_untracked_inventory"] : [])]);
|
|
1936
|
+
], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : []), ...(v7 ? ["eligible_untracked_inventory", "escalation"] : [])]);
|
|
1833
1937
|
requireIdentity(body, v7 ? "gentle-ai.review-integration.status/v7" : v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
|
|
1834
1938
|
|
|
1835
1939
|
const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] as const, "status.applicability");
|
|
@@ -1913,6 +2017,24 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1913
2017
|
};
|
|
1914
2018
|
}
|
|
1915
2019
|
|
|
2020
|
+
let escalation: ReviewEscalationV1 | undefined;
|
|
2021
|
+
if (Object.hasOwn(body, "escalation")) {
|
|
2022
|
+
const label = "status.escalation";
|
|
2023
|
+
const source = exactRecord(body.escalation, label, ["cause", "finding_ids"], ["refuter_outcomes"]);
|
|
2024
|
+
escalation = {
|
|
2025
|
+
cause: enumeration(source.cause, ESCALATION_CAUSES, `${label}.cause`),
|
|
2026
|
+
findingIds: stringArray(source.finding_ids, `${label}.finding_ids`),
|
|
2027
|
+
};
|
|
2028
|
+
if (Object.hasOwn(source, "refuter_outcomes")) escalation.refuterOutcomes = array(source.refuter_outcomes, `${label}.refuter_outcomes`, (entry, itemLabel) => {
|
|
2029
|
+
const row = exactRecord(entry, itemLabel, ["finding_id", "outcome", "proof"]);
|
|
2030
|
+
return {
|
|
2031
|
+
findingId: nonempty(row.finding_id, `${itemLabel}.finding_id`),
|
|
2032
|
+
outcome: enumeration(row.outcome, ESCALATION_REFUTER_OUTCOMES, `${itemLabel}.outcome`),
|
|
2033
|
+
proof: nonempty(row.proof, `${itemLabel}.proof`),
|
|
2034
|
+
};
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
|
|
1916
2038
|
// status/v7 top-level optional digest (gentle-ai v2.6.0): resolves #4066's
|
|
1917
2039
|
// closed loop where `sdd-attempt finish` named a digest status never
|
|
1918
2040
|
// published. Absent on the `staged` projection, which never resolves an
|
|
@@ -1940,6 +2062,7 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1940
2062
|
...(forecast === undefined ? {} : { forecast }),
|
|
1941
2063
|
...(repositoryContext === undefined ? {} : { repositoryContext }),
|
|
1942
2064
|
...(validationRequest === undefined ? {} : { validationRequest }),
|
|
2065
|
+
...(escalation === undefined ? {} : { escalation }),
|
|
1943
2066
|
...(eligibleUntrackedInventory === undefined ? {} : { eligibleUntrackedInventory }),
|
|
1944
2067
|
raw: body,
|
|
1945
2068
|
};
|
|
@@ -2450,6 +2573,28 @@ export function decodeReviewAcknowledgedV1(value: unknown, expected: ReviewAckno
|
|
|
2450
2573
|
return decoded;
|
|
2451
2574
|
}
|
|
2452
2575
|
|
|
2576
|
+
const REVIEW_LAST_EVENT_FINDING_SEVERITIES = ["BLOCKER", "CRITICAL", "WARNING", "SUGGESTION"] as const;
|
|
2577
|
+
const REVIEW_LAST_EVENT_EVIDENCE_CLASSES = ["deterministic", "inferential", "insufficient"] as const;
|
|
2578
|
+
const REVIEW_LAST_EVENT_CAUSAL_DISPOSITIONS = ["introduced", "behavior-activated", "worsened", "pre-existing", "base-only", "unknown"] as const;
|
|
2579
|
+
|
|
2580
|
+
export interface ReviewLastEventReviewerFindingV1 {
|
|
2581
|
+
id: string;
|
|
2582
|
+
lens: string;
|
|
2583
|
+
location: string;
|
|
2584
|
+
severity: (typeof REVIEW_LAST_EVENT_FINDING_SEVERITIES)[number];
|
|
2585
|
+
claim: string;
|
|
2586
|
+
proofRefs: readonly string[];
|
|
2587
|
+
evidenceClass?: (typeof REVIEW_LAST_EVENT_EVIDENCE_CLASSES)[number];
|
|
2588
|
+
causalDisposition?: (typeof REVIEW_LAST_EVENT_CAUSAL_DISPOSITIONS)[number];
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
export interface ReviewLastEventReviewerResultV1 {
|
|
2592
|
+
lens: string;
|
|
2593
|
+
findings: readonly ReviewLastEventReviewerFindingV1[];
|
|
2594
|
+
evidence: readonly string[];
|
|
2595
|
+
resultHash: string;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2453
2598
|
export interface ReviewLastEventClosureV1 {
|
|
2454
2599
|
schema: typeof REVIEW_LAST_EVENT_CLOSURE_SCHEMA;
|
|
2455
2600
|
operation: ReviewLastEventClosureOperation;
|
|
@@ -2461,6 +2606,7 @@ export interface ReviewLastEventClosureV1 {
|
|
|
2461
2606
|
requestHash?: string;
|
|
2462
2607
|
correctionLines?: number;
|
|
2463
2608
|
advisoryFindings?: ReviewAdvisoryFindingsV1;
|
|
2609
|
+
reviewerResults?: readonly ReviewLastEventReviewerResultV1[];
|
|
2464
2610
|
statusContinuation?: ReviewStatusContinuationV1;
|
|
2465
2611
|
acknowledgement?: ReviewApprovedAcknowledgementV1;
|
|
2466
2612
|
// Set when the provider sent an acknowledgement this decoder could not
|
|
@@ -2572,8 +2718,38 @@ function decodeReviewStatusContinuationV1(value: unknown): ReviewStatusContinuat
|
|
|
2572
2718
|
};
|
|
2573
2719
|
}
|
|
2574
2720
|
|
|
2721
|
+
function decodeReviewLastEventReviewerFindingV1(value: unknown, label: string): ReviewLastEventReviewerFindingV1 {
|
|
2722
|
+
const finding = exactRecord(value, label, ["id", "lens", "location", "severity", "claim", "proof_refs"], ["evidence_class", "causal_disposition"]);
|
|
2723
|
+
const evidenceClass = finding.evidence_class === undefined
|
|
2724
|
+
? undefined
|
|
2725
|
+
: enumeration(finding.evidence_class, REVIEW_LAST_EVENT_EVIDENCE_CLASSES, `${label}.evidence_class`);
|
|
2726
|
+
const causalDisposition = finding.causal_disposition === undefined
|
|
2727
|
+
? undefined
|
|
2728
|
+
: enumeration(finding.causal_disposition, REVIEW_LAST_EVENT_CAUSAL_DISPOSITIONS, `${label}.causal_disposition`);
|
|
2729
|
+
return {
|
|
2730
|
+
id: nonempty(finding.id, `${label}.id`),
|
|
2731
|
+
lens: nonempty(finding.lens, `${label}.lens`),
|
|
2732
|
+
location: nonempty(finding.location, `${label}.location`),
|
|
2733
|
+
severity: enumeration(finding.severity, REVIEW_LAST_EVENT_FINDING_SEVERITIES, `${label}.severity`),
|
|
2734
|
+
claim: nonempty(finding.claim, `${label}.claim`),
|
|
2735
|
+
proofRefs: stringArray(finding.proof_refs, `${label}.proof_refs`, { minimum: 1 }),
|
|
2736
|
+
...(evidenceClass === undefined ? {} : { evidenceClass }),
|
|
2737
|
+
...(causalDisposition === undefined ? {} : { causalDisposition }),
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
function decodeReviewLastEventReviewerResultV1(value: unknown, label: string): ReviewLastEventReviewerResultV1 {
|
|
2742
|
+
const result = exactRecord(value, label, ["lens", "findings", "evidence", "result_hash"]);
|
|
2743
|
+
return {
|
|
2744
|
+
lens: nonempty(result.lens, `${label}.lens`),
|
|
2745
|
+
findings: array(result.findings, `${label}.findings`, decodeReviewLastEventReviewerFindingV1),
|
|
2746
|
+
evidence: stringArray(result.evidence, `${label}.evidence`),
|
|
2747
|
+
resultHash: sha256(result.result_hash, `${label}.result_hash`),
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2575
2751
|
export function decodeReviewLastEventClosureV1(value: unknown): ReviewLastEventClosureV1 {
|
|
2576
|
-
const body = exactRecord(value, "last_event_closure", ["schema", "operation", "lineage_id", "state", "store_revision"], ["target_identity", "request_hash", "correction_lines", "action", "advisory_findings", "status_continuation", "acknowledgement"]);
|
|
2752
|
+
const body = exactRecord(value, "last_event_closure", ["schema", "operation", "lineage_id", "state", "store_revision"], ["target_identity", "request_hash", "correction_lines", "action", "advisory_findings", "reviewer_results", "status_continuation", "acknowledgement"]);
|
|
2577
2753
|
if (body.schema !== REVIEW_LAST_EVENT_CLOSURE_SCHEMA) throw new TypeError(`last_event_closure.schema must be ${REVIEW_LAST_EVENT_CLOSURE_SCHEMA}`);
|
|
2578
2754
|
const operation = enumeration(body.operation, Object.values(REVIEW_LAST_EVENT_CLOSURE_OPERATION), "last_event_closure.operation") as ReviewLastEventClosureOperation;
|
|
2579
2755
|
const state = enumeration(body.state, REVIEW_LAST_EVENT_TERMINAL_STATES, "last_event_closure.state") as ReviewLastEventClosureState;
|
|
@@ -2585,6 +2761,7 @@ export function decodeReviewLastEventClosureV1(value: unknown): ReviewLastEventC
|
|
|
2585
2761
|
storeRevision: sha256(body.store_revision, "last_event_closure.store_revision"),
|
|
2586
2762
|
};
|
|
2587
2763
|
if (operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_CORRECTION_PLAN) {
|
|
2764
|
+
if (body.reviewer_results !== undefined) throw new TypeError("last_event_closure reviewer_results requires approved state");
|
|
2588
2765
|
if (body.action !== undefined || body.advisory_findings !== undefined || body.status_continuation !== undefined) throw new TypeError("last_event_closure correction-plan cannot carry action, advisory_findings, or status_continuation");
|
|
2589
2766
|
if (state !== "correction_required") throw new TypeError("last_event_closure correction-plan requires correction_required state");
|
|
2590
2767
|
return {
|
|
@@ -2652,10 +2829,15 @@ export function decodeReviewLastEventClosureV1(value: unknown): ReviewLastEventC
|
|
|
2652
2829
|
? undefined
|
|
2653
2830
|
: decodeReviewAdvisoryFindingsV1(body.advisory_findings, "last_event_closure.advisory_findings");
|
|
2654
2831
|
if (advisoryFindings !== undefined && state !== "approved") throw new TypeError("last_event_closure advisory_findings requires approved state");
|
|
2832
|
+
const reviewerResults = body.reviewer_results === undefined
|
|
2833
|
+
? undefined
|
|
2834
|
+
: array(body.reviewer_results, "last_event_closure.reviewer_results", decodeReviewLastEventReviewerResultV1);
|
|
2835
|
+
if (reviewerResults !== undefined && state !== "approved") throw new TypeError("last_event_closure reviewer_results requires approved state");
|
|
2655
2836
|
return {
|
|
2656
2837
|
...shared,
|
|
2657
2838
|
action,
|
|
2658
2839
|
...(advisoryFindings === undefined ? {} : { advisoryFindings }),
|
|
2840
|
+
...(reviewerResults === undefined ? {} : { reviewerResults }),
|
|
2659
2841
|
...(statusContinuation === undefined ? {} : { statusContinuation }),
|
|
2660
2842
|
...(acknowledgement === undefined ? {} : { acknowledgement }),
|
|
2661
2843
|
...(acknowledgementUndecodable ? { acknowledgementUndecodable: true as const } : {}),
|
|
@@ -26,11 +26,14 @@ export async function reconcileUnknownReviewLastEventCapture(
|
|
|
26
26
|
lineageId: binding.lineageId,
|
|
27
27
|
...(selector === undefined ? {} : selector),
|
|
28
28
|
});
|
|
29
|
-
if (
|
|
30
|
-
throw new TypeError("capture reconciliation
|
|
29
|
+
if (status.applicability !== "current_target") {
|
|
30
|
+
throw new TypeError("capture reconciliation STATUS is not current for the bound target");
|
|
31
31
|
}
|
|
32
|
-
if (status.authority?.lineageId !==
|
|
33
|
-
throw new TypeError("capture reconciliation returned
|
|
32
|
+
if (status.authority?.lineageId !== binding.lineageId) {
|
|
33
|
+
throw new TypeError("capture reconciliation returned missing or different lineage authority");
|
|
34
|
+
}
|
|
35
|
+
if (binding.targetIdentity === undefined || status.targetIdentity !== binding.targetIdentity) {
|
|
36
|
+
throw new TypeError("capture reconciliation returned missing or different target");
|
|
34
37
|
}
|
|
35
38
|
return status;
|
|
36
39
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
|
|
3
|
+
// Reminder bookkeeping only: these entries neither define review scope nor grant
|
|
4
|
+
// authority. Both extensions append to the parent's active Pi session branch.
|
|
5
|
+
export const REVIEW_REMINDER_RECEIPT = "gentle-pi.review-reminder-receipt/v1";
|
|
6
|
+
export interface ReceiptSession {
|
|
7
|
+
getSessionId(): string;
|
|
8
|
+
getBranch(): readonly { type: string; customType?: string; data?: unknown }[];
|
|
9
|
+
}
|
|
10
|
+
interface ReceiptHost { appendEntry(type: string, data: unknown): void }
|
|
11
|
+
export interface MutationEvidence {
|
|
12
|
+
source: "direct" | "subagent";
|
|
13
|
+
toolName: "write" | "edit";
|
|
14
|
+
toolCallId: string;
|
|
15
|
+
taskId?: string;
|
|
16
|
+
}
|
|
17
|
+
type Mutation = MutationEvidence & { kind: "mutation"; sessionId: string; root: string; id: string };
|
|
18
|
+
type Consumption = { kind: "nudged" | "acknowledged"; sessionId: string; root: string; through: string; targetIdentity: string };
|
|
19
|
+
type Receipt = Mutation | Consumption;
|
|
20
|
+
const text = (value: unknown): value is string => typeof value === "string" && value.trim().length > 0;
|
|
21
|
+
|
|
22
|
+
function valid(value: unknown): value is Receipt {
|
|
23
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
24
|
+
const data = value as Record<string, unknown>;
|
|
25
|
+
if (!text(data.sessionId) || !text(data.root) || !isAbsolute(data.root)) return false;
|
|
26
|
+
let fields: string[];
|
|
27
|
+
if (data.kind === "mutation") {
|
|
28
|
+
if (!text(data.id) || !text(data.toolCallId) || (data.toolName !== "write" && data.toolName !== "edit")) return false;
|
|
29
|
+
if (data.source !== "direct" && data.source !== "subagent") return false;
|
|
30
|
+
if (data.source === "subagent" ? !text(data.taskId) : data.taskId !== undefined) return false;
|
|
31
|
+
if (data.id !== JSON.stringify([data.source, data.taskId ?? "", data.toolCallId])) return false;
|
|
32
|
+
fields = ["kind", "sessionId", "root", "id", "source", "toolName", "toolCallId", ...(data.source === "subagent" ? ["taskId"] : [])];
|
|
33
|
+
} else {
|
|
34
|
+
if (data.kind !== "nudged" && data.kind !== "acknowledged") return false;
|
|
35
|
+
if (!text(data.through) || !text(data.targetIdentity)) return false;
|
|
36
|
+
fields = ["kind", "sessionId", "root", "through", "targetIdentity"];
|
|
37
|
+
}
|
|
38
|
+
return Object.keys(data).length === fields.length && fields.every((field) => Object.hasOwn(data, field));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function receipts(session: ReceiptSession, root: string): Receipt[] {
|
|
42
|
+
const sessionId = session.getSessionId();
|
|
43
|
+
return session.getBranch().flatMap((entry) => entry.type === "custom" && entry.customType === REVIEW_REMINDER_RECEIPT &&
|
|
44
|
+
valid(entry.data) && entry.data.sessionId === sessionId && entry.data.root === root ? [entry.data] : []);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function pendingReviewMutation(session: ReceiptSession, root: string, captured?: string): string | undefined {
|
|
48
|
+
const mutations: string[] = [];
|
|
49
|
+
let consumed = -1;
|
|
50
|
+
for (const receipt of receipts(session, root)) {
|
|
51
|
+
if (receipt.kind === "mutation") {
|
|
52
|
+
if (!mutations.includes(receipt.id)) mutations.push(receipt.id);
|
|
53
|
+
} else {
|
|
54
|
+
// Only consume the captured prefix, not writes arriving while native
|
|
55
|
+
// STATUS/ACK was awaited. Unknown or off-branch watermarks do nothing.
|
|
56
|
+
consumed = Math.max(consumed, mutations.indexOf(receipt.through));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (captured !== undefined) return mutations.indexOf(captured) > consumed ? captured : undefined;
|
|
60
|
+
return mutations.length - 1 > consumed ? mutations.at(-1) : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function recordReviewMutation(host: ReceiptHost, session: ReceiptSession, root: string, evidence: MutationEvidence): void {
|
|
64
|
+
const id = JSON.stringify([evidence.source, evidence.taskId ?? "", evidence.toolCallId]);
|
|
65
|
+
const receipt: Mutation = { kind: "mutation", sessionId: session.getSessionId(), root, id, ...evidence };
|
|
66
|
+
if (!valid(receipt) || receipts(session, root).some((entry) => entry.kind === "mutation" && entry.id === id)) return;
|
|
67
|
+
host.appendEntry(REVIEW_REMINDER_RECEIPT, receipt);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function consumeReviewMutation(host: ReceiptHost, session: ReceiptSession, root: string, through: string | undefined, kind: Consumption["kind"], targetIdentity: string): void {
|
|
71
|
+
if (through === undefined) return;
|
|
72
|
+
const receipt: Consumption = { kind, sessionId: session.getSessionId(), root, through, targetIdentity };
|
|
73
|
+
if (valid(receipt)) host.appendEntry(REVIEW_REMINDER_RECEIPT, receipt);
|
|
74
|
+
}
|
|
@@ -10,6 +10,16 @@ export const REVIEW_SESSION_PERMISSION_REGISTRY_SYMBOL = Symbol.for(REVIEW_SESSI
|
|
|
10
10
|
|
|
11
11
|
const execFileAsync = promisify(execFile);
|
|
12
12
|
|
|
13
|
+
export interface GitCommandOptions {
|
|
14
|
+
encoding: "utf8";
|
|
15
|
+
timeout: number;
|
|
16
|
+
maxBuffer: number;
|
|
17
|
+
windowsHide?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type GitAsyncRunner = (command: string, args: readonly string[], options: GitCommandOptions) => Promise<{ stdout: string }>;
|
|
21
|
+
export type GitSyncRunner = (command: string, args: readonly string[], options: GitCommandOptions) => string;
|
|
22
|
+
|
|
13
23
|
export interface ReviewSessionManager {
|
|
14
24
|
getSessionId(): unknown;
|
|
15
25
|
}
|
|
@@ -78,12 +88,16 @@ function validAbsoluteGitPath(value: string): boolean {
|
|
|
78
88
|
return isAbsolute(value) && value.length > 0 && !value.includes("\n") && !value.includes("\r");
|
|
79
89
|
}
|
|
80
90
|
|
|
81
|
-
export async function resolveCanonicalGitWorktreeRoot(
|
|
91
|
+
export async function resolveCanonicalGitWorktreeRoot(
|
|
92
|
+
cwd: string,
|
|
93
|
+
run: GitAsyncRunner = execFileAsync as unknown as GitAsyncRunner,
|
|
94
|
+
): Promise<string | undefined> {
|
|
82
95
|
try {
|
|
83
|
-
const result = await
|
|
96
|
+
const result = await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
84
97
|
encoding: "utf8",
|
|
85
98
|
timeout: 5_000,
|
|
86
99
|
maxBuffer: 64 * 1024,
|
|
100
|
+
windowsHide: true,
|
|
87
101
|
});
|
|
88
102
|
const output = result.stdout.trim();
|
|
89
103
|
if (!validAbsoluteGitPath(output)) return undefined;
|
|
@@ -94,12 +108,16 @@ export async function resolveCanonicalGitWorktreeRoot(cwd: string): Promise<stri
|
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
/** Resolves a non-secret, clone-stable identity from Git's canonical common dir. */
|
|
97
|
-
export async function resolveCanonicalGitRepositoryIdentity(
|
|
111
|
+
export async function resolveCanonicalGitRepositoryIdentity(
|
|
112
|
+
cwd: string,
|
|
113
|
+
run: GitAsyncRunner = execFileAsync as unknown as GitAsyncRunner,
|
|
114
|
+
): Promise<string | undefined> {
|
|
98
115
|
try {
|
|
99
|
-
const result = await
|
|
116
|
+
const result = await run("git", ["-C", cwd, "rev-parse", "--git-common-dir"], {
|
|
100
117
|
encoding: "utf8",
|
|
101
118
|
timeout: 5_000,
|
|
102
119
|
maxBuffer: 64 * 1024,
|
|
120
|
+
windowsHide: true,
|
|
103
121
|
});
|
|
104
122
|
const output = result.stdout.trim();
|
|
105
123
|
if (output.length === 0 || output.includes("\n") || output.includes("\r")) return undefined;
|
|
@@ -110,9 +128,12 @@ export async function resolveCanonicalGitRepositoryIdentity(cwd: string): Promis
|
|
|
110
128
|
}
|
|
111
129
|
|
|
112
130
|
/** The parent AgentRunner binds a child task to this same digest at spawn time. */
|
|
113
|
-
export function resolveCanonicalGitRepositoryIdentitySync(
|
|
131
|
+
export function resolveCanonicalGitRepositoryIdentitySync(
|
|
132
|
+
cwd: string,
|
|
133
|
+
run: GitSyncRunner = execFileSync as unknown as GitSyncRunner,
|
|
134
|
+
): string | undefined {
|
|
114
135
|
try {
|
|
115
|
-
const output =
|
|
136
|
+
const output = run("git", ["-C", cwd, "rev-parse", "--git-common-dir"], { encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024, windowsHide: true }).trim();
|
|
116
137
|
if (output.length === 0 || output.includes("\n") || output.includes("\r")) return undefined;
|
|
117
138
|
return canonicalRepositoryIdentity(realpathSync(resolve(cwd, output)));
|
|
118
139
|
} catch {
|