@piagent/platform 1.3.0 → 1.4.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/CHANGELOG.md +56 -3
- package/README.md +3 -3
- package/architecture/layers.json +96 -1
- package/docs/README.md +1 -0
- package/docs/architecture.md +1 -0
- package/docs/authority-manifest.md +1 -0
- package/docs/auto-delegation-policy.md +1 -0
- package/docs/capability-packs.md +10 -2
- package/docs/command-reference-vietnamese.md +5 -4
- package/docs/context-engine.md +1 -0
- package/docs/context-window-policy.md +1 -0
- package/docs/distribution-standard.md +8 -7
- package/docs/herdr-workflow.md +1 -0
- package/docs/mcp-and-tools.md +14 -9
- package/docs/memory-policy.md +1 -0
- package/docs/model-options.md +1 -0
- package/docs/oauth-providers.md +1 -0
- package/docs/operator-manual-vietnamese.md +15 -12
- package/docs/package-architecture-notes.md +1 -0
- package/docs/project-adapters.md +1 -0
- package/docs/project-onboarding.md +1 -0
- package/docs/publishing-for-teams.md +1 -0
- package/docs/quality-benchmark.md +1 -0
- package/docs/quickstart-vietnamese.md +3 -2
- package/docs/release-install-policy.md +11 -7
- package/docs/runtime-harness-standard.md +1 -0
- package/docs/runtime-policy-design.md +1 -0
- package/docs/runtime-quality-baseline.md +1 -0
- package/docs/security-redaction-benchmark.md +1 -0
- package/docs/security-threat-model.md +2 -1
- package/docs/subagent-orchestration-capabilities.md +1 -0
- package/docs/subagents-and-multiagent.md +1 -0
- package/docs/task-implementation-contract.md +1 -0
- package/docs/task-lifecycle-vietnamese.md +1 -0
- package/docs/team-onboarding.md +3 -2
- package/docs/third-party-neutrality.md +26 -0
- package/docs/usage-observability.md +1 -0
- package/docs/vercel-docs-site.md +1 -0
- package/docs/workflow-recipes.md +1 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +1 -1
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/mutation-provenance-record.invalid.json +17 -0
- package/evals/fixtures/mutation-provenance-record.valid.json +27 -0
- package/evals/fixtures/piagent-webui/approval-v1.invalid.json +54 -0
- package/evals/fixtures/piagent-webui/approval-v1.valid.json +58 -0
- package/evals/fixtures/piagent-webui/attachment-v1.invalid.json +25 -0
- package/evals/fixtures/piagent-webui/attachment-v1.valid.json +25 -0
- package/evals/fixtures/piagent-webui/capabilities-v1.invalid.json +51 -0
- package/evals/fixtures/piagent-webui/capabilities-v1.valid.json +122 -0
- package/evals/fixtures/piagent-webui/commit-summary-v1.invalid.json +10 -0
- package/evals/fixtures/piagent-webui/commit-summary-v1.valid.json +25 -0
- package/evals/fixtures/piagent-webui/control-command-v1.invalid.json +37 -0
- package/evals/fixtures/piagent-webui/control-command-v1.valid.json +35 -0
- package/evals/fixtures/piagent-webui/diff-v1.invalid.json +13 -0
- package/evals/fixtures/piagent-webui/diff-v1.valid.json +141 -0
- package/evals/fixtures/piagent-webui/gateway-capabilities-v1.invalid.json +35 -0
- package/evals/fixtures/piagent-webui/gateway-capabilities-v1.valid.json +35 -0
- package/evals/fixtures/piagent-webui/gateway-protocol-v1.invalid.json +14 -0
- package/evals/fixtures/piagent-webui/gateway-protocol-v1.valid.json +14 -0
- package/evals/fixtures/piagent-webui/handoff-history-v1.invalid.json +16 -0
- package/evals/fixtures/piagent-webui/handoff-history-v1.valid.json +16 -0
- package/evals/fixtures/piagent-webui/model-catalog-v1.invalid.json +31 -0
- package/evals/fixtures/piagent-webui/model-catalog-v1.valid.json +43 -0
- package/evals/fixtures/piagent-webui/queue-v1.invalid.json +29 -0
- package/evals/fixtures/piagent-webui/queue-v1.valid.json +44 -0
- package/evals/fixtures/piagent-webui/recovery-history-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/recovery-history-v1.valid.json +20 -0
- package/evals/fixtures/piagent-webui/release-monitor-v1.invalid.json +12 -0
- package/evals/fixtures/piagent-webui/release-monitor-v1.valid.json +29 -0
- package/evals/fixtures/piagent-webui/review-state-v1.invalid.json +25 -0
- package/evals/fixtures/piagent-webui/review-state-v1.valid.json +38 -0
- package/evals/fixtures/piagent-webui/runtime-event-v2.invalid.json +49 -0
- package/evals/fixtures/piagent-webui/runtime-event-v2.valid.json +49 -0
- package/evals/fixtures/piagent-webui/session-catalog-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/session-catalog-v1.valid.json +58 -0
- package/evals/fixtures/piagent-webui/session-command-v1.invalid.json +19 -0
- package/evals/fixtures/piagent-webui/session-command-v1.valid.json +19 -0
- package/evals/fixtures/piagent-webui/snapshot-v1.invalid.json +5 -0
- package/evals/fixtures/piagent-webui/snapshot-v1.valid.json +191 -0
- package/evals/fixtures/piagent-webui/source-change-v1.invalid.json +57 -0
- package/evals/fixtures/piagent-webui/source-change-v1.valid.json +98 -0
- package/evals/fixtures/piagent-webui/source-mutation-v1.invalid.json +19 -0
- package/evals/fixtures/piagent-webui/source-mutation-v1.valid.json +38 -0
- package/evals/fixtures/piagent-webui/source-revert-v1.invalid.json +20 -0
- package/evals/fixtures/piagent-webui/source-revert-v1.valid.json +62 -0
- package/evals/fixtures/piagent-webui/subagent-tree-v1.invalid.json +26 -0
- package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +39 -0
- package/evals/fixtures/piagent-webui/task-index-v1.invalid.json +15 -0
- package/evals/fixtures/piagent-webui/task-index-v1.valid.json +15 -0
- package/evals/fixtures/piagent-webui/task-timeline-v1.invalid.json +14 -0
- package/evals/fixtures/piagent-webui/task-timeline-v1.valid.json +16 -0
- package/evals/fixtures/piagent-webui/transcript-v1.invalid.json +35 -0
- package/evals/fixtures/piagent-webui/transcript-v1.valid.json +55 -0
- package/evals/fixtures/task-baseline-manifest.invalid.json +16 -0
- package/evals/fixtures/task-baseline-manifest.valid.json +41 -0
- package/evals/fixtures/verifier-file-snapshot.invalid.json +21 -0
- package/evals/fixtures/verifier-file-snapshot.valid.json +21 -0
- package/package.json +30 -3
- package/packages/piagent-core/benchmark/benchmark-candidate.d.ts +1 -0
- package/packages/piagent-core/benchmark/benchmark-forensics.d.ts +1 -0
- package/packages/piagent-core/benchmark/benchmark-ledger.d.ts +4 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.d.ts +1 -0
- package/packages/piagent-core/benchmark/benchmark-runtime.d.ts +1 -0
- package/packages/piagent-core/capabilities/capability-core.js +2 -2
- package/packages/piagent-core/capabilities/capability-verification-cache.js +280 -0
- package/packages/piagent-core/capabilities/runtime-integrity.js +153 -1
- package/packages/piagent-core/extensions/document-intake.ts +24 -10
- package/packages/piagent-core/extensions/piagent-guard.ts +90 -73
- package/packages/piagent-core/extensions/redaction-core.d.ts +2 -0
- package/packages/piagent-core/extensions/state-retention.js +77 -44
- package/packages/piagent-core/extensions/task-journal.js +30 -1
- package/packages/piagent-core/extensions/task-state.js +33 -36
- package/packages/piagent-core/mcp/mcp-server-catalog.js +5 -5
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -2
- package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +5 -2
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +20 -19
- package/packages/piagent-core/runtime/hooks/tool-result-metadata.ts +13 -0
- package/packages/piagent-core/runtime/input/chat-images.ts +2 -2
- package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +94 -0
- package/packages/piagent-core/runtime/inspection/approval-broker.ts +318 -0
- package/packages/piagent-core/runtime/inspection/commit-summary-projection.ts +59 -0
- package/packages/piagent-core/runtime/inspection/context-telemetry-inspection.ts +37 -0
- package/packages/piagent-core/runtime/inspection/criteria-links.ts +295 -0
- package/packages/piagent-core/runtime/inspection/diff-projection.ts +499 -0
- package/packages/piagent-core/runtime/inspection/git-filter-safety.ts +38 -0
- package/packages/piagent-core/runtime/inspection/git-numstat-adapter.ts +71 -0
- package/packages/piagent-core/runtime/inspection/git-status-adapter.ts +493 -0
- package/packages/piagent-core/runtime/inspection/git-tree-adapter.ts +108 -0
- package/packages/piagent-core/runtime/inspection/mcp-control.ts +10 -0
- package/packages/piagent-core/runtime/inspection/mutation-provenance-contract.ts +131 -0
- package/packages/piagent-core/runtime/inspection/mutation-provenance-recorder.ts +48 -0
- package/packages/piagent-core/runtime/inspection/mutation-provenance-store.ts +161 -0
- package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +36 -0
- package/packages/piagent-core/runtime/inspection/review-state-contract.ts +108 -0
- package/packages/piagent-core/runtime/inspection/review-state-projection.ts +93 -0
- package/packages/piagent-core/runtime/inspection/review-state-store.ts +112 -0
- package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +262 -0
- package/packages/piagent-core/runtime/inspection/same-process-bridge-proof.ts +103 -0
- package/packages/piagent-core/runtime/inspection/source-change-projection.ts +471 -0
- package/packages/piagent-core/runtime/inspection/source-evidence-contract.ts +139 -0
- package/packages/piagent-core/runtime/inspection/source-evidence-store.ts +315 -0
- package/packages/piagent-core/runtime/inspection/source-handoff-store.ts +70 -0
- package/packages/piagent-core/runtime/inspection/source-inspection-plan.ts +34 -0
- package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +164 -0
- package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +168 -0
- package/packages/piagent-core/runtime/inspection/source-open-target.ts +38 -0
- package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +147 -0
- package/packages/piagent-core/runtime/inspection/task-baseline-start-capture.ts +22 -0
- package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +160 -0
- package/packages/piagent-core/runtime/inspection/task-control-journal.ts +131 -0
- package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +95 -0
- package/packages/piagent-core/runtime/inspection/task-provenance-projection.ts +60 -0
- package/packages/piagent-core/runtime/inspection/task-recovery-timeline.ts +88 -0
- package/packages/piagent-core/runtime/inspection/task-run-index.ts +87 -0
- package/packages/piagent-core/runtime/inspection/task-source-projection.ts +401 -0
- package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +101 -0
- package/packages/piagent-core/runtime/inspection/text-diff.ts +60 -0
- package/packages/piagent-core/runtime/inspection/verifier-snapshot-contract.ts +114 -0
- package/packages/piagent-core/runtime/inspection/verifier-snapshot-store.ts +189 -0
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +320 -0
- package/packages/piagent-core/runtime/inspection/workspace-file-reader.ts +132 -0
- package/packages/piagent-core/runtime/inspection/workspace-source-projection.ts +96 -0
- package/packages/piagent-core/runtime/inspection/zero-turn-conformance.ts +175 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +68 -4
- package/packages/piagent-core/runtime/policy/source-index-transaction.ts +173 -0
- package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +42 -0
- package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +184 -0
- package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +105 -0
- package/packages/piagent-core/runtime/product/activity-inspector-footer.ts +5 -3
- package/packages/piagent-core/runtime/product/activity-inspector.ts +85 -343
- package/packages/piagent-core/runtime/recovery/resume-state.ts +38 -1
- package/packages/piagent-core/runtime/registration/activity-inspector-command.ts +68 -14
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +13 -14
- package/packages/piagent-core/runtime/session/model-authorship-state.ts +31 -3
- package/packages/piagent-core/runtime/session/runtime-state.ts +11 -1
- package/packages/piagent-webui/dist/client/assets/index-DjMntaK5.js +172 -0
- package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +1 -0
- package/packages/piagent-webui/dist/client/index.html +16 -0
- package/packages/piagent-webui/extension/attachment-store.ts +323 -0
- package/packages/piagent-webui/extension/held-message-queue.ts +311 -0
- package/packages/piagent-webui/extension/lifecycle-controller.ts +408 -0
- package/packages/piagent-webui/extension/lifecycle-event-adapter.ts +23 -0
- package/packages/piagent-webui/extension/piagent-webui.ts +470 -0
- package/packages/piagent-webui/extension/review-controller.ts +160 -0
- package/packages/piagent-webui/extension/same-session-bridge.ts +500 -0
- package/packages/piagent-webui/extension/session-options-controller.ts +281 -0
- package/packages/piagent-webui/extension/session-stream-adapter.ts +249 -0
- package/packages/piagent-webui/extension/source-mutation-controller.ts +215 -0
- package/packages/piagent-webui/extension/source-open-controller.ts +136 -0
- package/packages/piagent-webui/extension/source-revert-controller.ts +189 -0
- package/packages/piagent-webui/extension/terminal-session-adapter.ts +73 -0
- package/packages/piagent-webui/extension/vscode-handoff.ts +51 -0
- package/packages/piagent-webui/gateway/control-socket.ts +92 -0
- package/packages/piagent-webui/gateway/gateway-events.ts +60 -0
- package/packages/piagent-webui/gateway/gateway-protocol-service.ts +64 -0
- package/packages/piagent-webui/gateway/gateway-service.ts +215 -0
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +110 -0
- package/packages/piagent-webui/gateway/mcp-auth-broker.ts +158 -0
- package/packages/piagent-webui/gateway/native-project-picker.ts +29 -0
- package/packages/piagent-webui/gateway/pi-host.ts +41 -0
- package/packages/piagent-webui/gateway/profile-state.ts +1 -0
- package/packages/piagent-webui/gateway/project-registry.ts +108 -0
- package/packages/piagent-webui/gateway/provider-auth-broker.ts +172 -0
- package/packages/piagent-webui/gateway/session-catalog.ts +128 -0
- package/packages/piagent-webui/gateway/session-command-controller.ts +237 -0
- package/packages/piagent-webui/gateway/session-command-store.ts +224 -0
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +204 -0
- package/packages/piagent-webui/gateway/session-lease-store.ts +1 -0
- package/packages/piagent-webui/gateway/session-metadata-store.ts +152 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +485 -0
- package/packages/piagent-webui/ownership/gateway-runtime-context.ts +8 -0
- package/packages/piagent-webui/ownership/profile-state.ts +117 -0
- package/packages/piagent-webui/ownership/session-lease-store.ts +329 -0
- package/packages/piagent-webui/ownership/session-refs.ts +8 -0
- package/packages/piagent-webui/package.json +33 -0
- package/packages/piagent-webui/server/README.md +4 -0
- package/packages/piagent-webui/server/benchmark-release-monitor.ts +230 -0
- package/packages/piagent-webui/server/core-inspection-provider.ts +403 -0
- package/packages/piagent-webui/server/gateway-websocket.ts +143 -0
- package/packages/piagent-webui/server/http-security.ts +27 -0
- package/packages/piagent-webui/server/ipc-read-model-client.ts +89 -0
- package/packages/piagent-webui/server/loopback-server.ts +368 -0
- package/packages/piagent-webui/server/read-model-provider.ts +37 -0
- package/packages/piagent-webui/server/read-only-router.ts +105 -0
- package/packages/piagent-webui/server/session-auth.ts +88 -0
- package/packages/piagent-webui/server/sidecar-main.ts +86 -0
- package/packages/piagent-webui/server/sse-hub.ts +76 -0
- package/packages/piagent-webui/server/static-bundle.ts +40 -0
- package/packages/piagent-webui/server/transcript-projection.ts +110 -0
- package/schemas/mutation-provenance-record.schema.json +100 -0
- package/schemas/piagent-webui/approval-v1.schema.json +654 -0
- package/schemas/piagent-webui/attachment-v1.schema.json +267 -0
- package/schemas/piagent-webui/capabilities-v1.schema.json +729 -0
- package/schemas/piagent-webui/catalog-v1.json +240 -0
- package/schemas/piagent-webui/commit-summary-v1.schema.json +63 -0
- package/schemas/piagent-webui/common-v1.schema.json +479 -0
- package/schemas/piagent-webui/control-command-v1.schema.json +1780 -0
- package/schemas/piagent-webui/diff-v1.schema.json +826 -0
- package/schemas/piagent-webui/gateway-capabilities-v1.schema.json +136 -0
- package/schemas/piagent-webui/gateway-protocol-v1.schema.json +456 -0
- package/schemas/piagent-webui/handoff-history-v1.schema.json +92 -0
- package/schemas/piagent-webui/model-catalog-v1.schema.json +95 -0
- package/schemas/piagent-webui/queue-v1.schema.json +99 -0
- package/schemas/piagent-webui/recovery-history-v1.schema.json +144 -0
- package/schemas/piagent-webui/release-monitor-v1.schema.json +173 -0
- package/schemas/piagent-webui/review-state-v1.schema.json +206 -0
- package/schemas/piagent-webui/runtime-event-v2.schema.json +5206 -0
- package/schemas/piagent-webui/session-catalog-v1.schema.json +222 -0
- package/schemas/piagent-webui/session-command-v1.schema.json +390 -0
- package/schemas/piagent-webui/snapshot-v1.schema.json +2622 -0
- package/schemas/piagent-webui/source-change-v1.schema.json +763 -0
- package/schemas/piagent-webui/source-mutation-v1.schema.json +96 -0
- package/schemas/piagent-webui/source-revert-v1.schema.json +130 -0
- package/schemas/piagent-webui/subagent-tree-v1.schema.json +88 -0
- package/schemas/piagent-webui/task-index-v1.schema.json +74 -0
- package/schemas/piagent-webui/task-timeline-v1.schema.json +49 -0
- package/schemas/piagent-webui/transcript-v1.schema.json +243 -0
- package/schemas/task-baseline-manifest.schema.json +122 -0
- package/schemas/verifier-file-snapshot.schema.json +81 -0
- package/scripts/check-architecture.mjs +1 -0
- package/scripts/check-doc-languages.mjs +19 -2
- package/scripts/check-third-party-neutrality.mjs +43 -0
- package/scripts/install-global.sh +1 -1
- package/scripts/piagent-cli.mjs +17 -3
- package/scripts/piagent-dashboard.mjs +208 -0
- package/scripts/piagent-webui-launcher.mjs +121 -0
- package/scripts/typescript-loader.mjs +26 -1
- package/scripts/verify-local.sh +253 -0
- package/scripts/verify-release-identity.mjs +4 -1
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
collectGitStatus,
|
|
6
|
+
projectGitPath,
|
|
7
|
+
readGitBlob,
|
|
8
|
+
type GitStatusRecord,
|
|
9
|
+
type GitStatusSnapshot
|
|
10
|
+
} from "./git-status-adapter.ts";
|
|
11
|
+
import { emptyGitTreeObjectId, listGitChangesAgainstTree, readGitTreeEntry } from "./git-tree-adapter.ts";
|
|
12
|
+
import {
|
|
13
|
+
decodeBaselineRepoPath,
|
|
14
|
+
readTaskBaselineBlob,
|
|
15
|
+
readTaskBaselineManifest,
|
|
16
|
+
taskBaselineManifestRef,
|
|
17
|
+
taskBaselineRetentionState
|
|
18
|
+
} from "./source-evidence-store.ts";
|
|
19
|
+
import type { TaskBaselineEntry, TaskBaselineManifest, TaskBaselineRoot } from "./source-evidence-contract.ts";
|
|
20
|
+
import type { SourceChangeDocument, SourceProjectionOptions } from "./source-change-projection.ts";
|
|
21
|
+
import { buffersEqual, diffTextBuffers } from "./text-diff.ts";
|
|
22
|
+
import { taskProvenanceResolver } from "./task-provenance-projection.ts";
|
|
23
|
+
import { hashWorkspaceFile, inspectWorkspaceEntry, readWorkspaceFile, readWorkspaceLink } from "./workspace-file-reader.ts";
|
|
24
|
+
|
|
25
|
+
const MAX_CONTENT_BYTES = 4 * 1024 * 1024;
|
|
26
|
+
|
|
27
|
+
type RootState = { manifest: TaskBaselineRoot; root: string; status: GitStatusSnapshot; records: Map<string, GitStatusRecord> };
|
|
28
|
+
type Content = { bytes: Buffer | null; kind: "text" | "binary" | "symlink" | "submodule" | "unknown"; access: "available" | "protected" | "oversized" | "unavailable"; reasonCode: string | null };
|
|
29
|
+
type BaselineItem = { entry: TaskBaselineEntry; root: TaskBaselineRoot; repoPath: string };
|
|
30
|
+
type TaskPathSelection = { paths: string[]; aliases: Map<string, Set<string>> };
|
|
31
|
+
|
|
32
|
+
function hash(value: string | Buffer): string {
|
|
33
|
+
return createHash("sha256").update(value).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function token(prefix: string, value: string | Buffer): string {
|
|
37
|
+
return `${prefix}.${hash(value)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function digest(value: Buffer | null): string | null {
|
|
41
|
+
return value === null ? null : `sha256:${hash(value)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rootPath(projectRoot: string, projectPath: string): string {
|
|
45
|
+
return projectPath === "." ? projectRoot : path.join(projectRoot, ...projectPath.split("/"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rootRelative(root: TaskBaselineRoot, projectPath: string): string | null {
|
|
49
|
+
if (root.projectPath === ".") return projectPath;
|
|
50
|
+
return projectPath.startsWith(`${root.projectPath}/`) ? projectPath.slice(root.projectPath.length + 1) : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function visibleRepoPath(repoPath: string): boolean {
|
|
54
|
+
return repoPath !== ".pi/piagent-state" && !repoPath.startsWith(".pi/piagent-state/");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function visibleRecord(record: GitStatusRecord): boolean {
|
|
58
|
+
const paths = [record.path.value, record.oldPath?.value].filter((value): value is string => Boolean(value));
|
|
59
|
+
return paths.length > 0 && paths.every(visibleRepoPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function recordMap(status: GitStatusSnapshot): Map<string, GitStatusRecord> {
|
|
63
|
+
const result = new Map<string, GitStatusRecord>();
|
|
64
|
+
for (const record of status.records) {
|
|
65
|
+
if (record.path.value) result.set(record.path.value, record);
|
|
66
|
+
if (record.oldPath?.value) result.set(record.oldPath.value, record);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function collectRoots(projectRoot: string, manifest: TaskBaselineManifest): Promise<RootState[]> {
|
|
72
|
+
return await Promise.all(manifest.roots.map(async (root) => {
|
|
73
|
+
const absolute = rootPath(projectRoot, root.projectPath);
|
|
74
|
+
const status = await collectGitStatus(absolute);
|
|
75
|
+
return { manifest: root, root: absolute, status, records: recordMap(status) };
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function entryMap(manifest: TaskBaselineManifest): Map<string, BaselineItem> {
|
|
80
|
+
const result = new Map<string, BaselineItem>();
|
|
81
|
+
for (const root of manifest.roots) {
|
|
82
|
+
for (const entry of root.entries) {
|
|
83
|
+
const repoPath = decodeBaselineRepoPath(entry);
|
|
84
|
+
if (!repoPath || !visibleRepoPath(repoPath)) continue;
|
|
85
|
+
const projectPath = root.projectPath === "." ? repoPath : `${root.projectPath}/${repoPath}`;
|
|
86
|
+
result.set(projectPath, { entry, root, repoPath });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function unavailableContent(access: Content["access"], reasonCode: string, kind: Content["kind"] = "unknown"): Content {
|
|
93
|
+
return { bytes: null, kind, access, reasonCode };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function contentKind(bytes: Buffer): "text" | "binary" {
|
|
97
|
+
return bytes.includes(0) ? "binary" : "text";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function baselineEntryContent(projectRoot: string, taskRunId: string, entry: TaskBaselineEntry): Content {
|
|
101
|
+
if (entry.state === "absent") return { bytes: null, kind: "unknown", access: "available", reasonCode: null };
|
|
102
|
+
if (entry.state === "submodule") return unavailableContent("available", "submodule-content", "submodule");
|
|
103
|
+
if (entry.state === "protected") return unavailableContent("protected", entry.reasonCode ?? "protected-path");
|
|
104
|
+
if (entry.state === "oversized") return unavailableContent("oversized", entry.reasonCode ?? "file-size-limit");
|
|
105
|
+
if (entry.state === "unavailable" || !entry.contentRef) return unavailableContent("unavailable", entry.reasonCode ?? "baseline-content-unavailable");
|
|
106
|
+
const bytes = readTaskBaselineBlob(projectRoot, taskRunId, entry.contentRef, MAX_CONTENT_BYTES);
|
|
107
|
+
return { bytes, kind: entry.state === "symlink" ? "symlink" : contentKind(bytes), access: "available", reasonCode: null };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function headContent(root: RootState, repoPath: string): Promise<Content> {
|
|
111
|
+
if (root.manifest.headState === "unborn" || !root.manifest.headOid) return { bytes: null, kind: "unknown", access: "available", reasonCode: null };
|
|
112
|
+
try {
|
|
113
|
+
const entry = await readGitTreeEntry(root.root, root.manifest.headOid, repoPath);
|
|
114
|
+
if (!entry) return { bytes: null, kind: "unknown", access: "available", reasonCode: null };
|
|
115
|
+
if (entry.mode === "160000" || entry.type === "commit") return unavailableContent("available", "submodule-content", "submodule");
|
|
116
|
+
const bytes = await readGitBlob(root.root, entry.objectId, { maxBytes: MAX_CONTENT_BYTES });
|
|
117
|
+
return { bytes, kind: entry.mode === "120000" ? "symlink" : contentKind(bytes), access: "available", reasonCode: null };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
const reason = (error as { code?: string }).code === "output-limit" ? "baseline-blob-limit" : "baseline-git-object-unavailable";
|
|
120
|
+
return unavailableContent((error as { code?: string }).code === "output-limit" ? "oversized" : "unavailable", reason);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function linkAliases(aliases: Map<string, Set<string>>, paths: string[]): void {
|
|
125
|
+
const linked = new Set(paths.flatMap((candidate) => [...(aliases.get(candidate) ?? [candidate])]));
|
|
126
|
+
for (const candidate of linked) aliases.set(candidate, linked);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function taskPaths(roots: RootState[], baseline: ReturnType<typeof entryMap>): Promise<TaskPathSelection> {
|
|
130
|
+
const values = new Set(baseline.keys());
|
|
131
|
+
const aliases = new Map<string, Set<string>>();
|
|
132
|
+
for (const root of roots) {
|
|
133
|
+
const projectPath = (repoPath: string) => root.manifest.projectPath === "." ? repoPath : `${root.manifest.projectPath}/${repoPath}`;
|
|
134
|
+
const hidden = new Set<string>();
|
|
135
|
+
for (const record of root.status.records) {
|
|
136
|
+
const recordPaths = [record.path.value, record.oldPath?.value].filter((value): value is string => Boolean(value));
|
|
137
|
+
const projected = recordPaths.map(projectPath);
|
|
138
|
+
linkAliases(aliases, projected);
|
|
139
|
+
projected.forEach((value) => values.add(value));
|
|
140
|
+
if (!visibleRecord(record)) projected.forEach((value) => hidden.add(value));
|
|
141
|
+
}
|
|
142
|
+
const trees = new Set([
|
|
143
|
+
root.manifest.headOid ?? await emptyGitTreeObjectId(root.root),
|
|
144
|
+
root.status.headOid ?? await emptyGitTreeObjectId(root.root)
|
|
145
|
+
]);
|
|
146
|
+
for (const tree of trees) {
|
|
147
|
+
for (const change of await listGitChangesAgainstTree(root.root, tree)) {
|
|
148
|
+
const repoPaths = [change.path, change.oldPath].filter((value): value is string => Boolean(value));
|
|
149
|
+
const projected = repoPaths.map(projectPath);
|
|
150
|
+
linkAliases(aliases, projected);
|
|
151
|
+
projected.forEach((value) => values.add(value));
|
|
152
|
+
if (repoPaths.some((value) => !visibleRepoPath(value))) projected.forEach((value) => hidden.add(value));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const candidate of [...hidden]) for (const alias of aliases.get(candidate) ?? []) hidden.add(alias);
|
|
156
|
+
for (const candidate of hidden) values.delete(candidate);
|
|
157
|
+
}
|
|
158
|
+
return { paths: [...values].sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right))), aliases };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function protectedRecordPath(options: SourceProjectionOptions, root: RootState, repoPath: string, record?: GitStatusRecord, historical: string[] = []): boolean {
|
|
162
|
+
const candidates = new Set([repoPath, record?.path.value, record?.oldPath?.value, ...historical].filter((value): value is string => Boolean(value)));
|
|
163
|
+
return [...candidates].some((candidate) => options.isProtectedPath?.(root.root, candidate) === true);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function workspaceEvidence(root: RootState, record: GitStatusRecord, options: SourceProjectionOptions): unknown {
|
|
167
|
+
const repoPath = record.path.value;
|
|
168
|
+
const identity = [record.path.digest, record.oldPath?.digest ?? null, record.indexStatus, record.worktreeStatus];
|
|
169
|
+
if (!repoPath) return [...identity, "unsafe-path"];
|
|
170
|
+
const protectedPath = protectedRecordPath(options, root, repoPath, record);
|
|
171
|
+
try {
|
|
172
|
+
const stat = inspectWorkspaceEntry(root.root, repoPath);
|
|
173
|
+
if (protectedPath) return [...identity, "protected", stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs];
|
|
174
|
+
if (stat.isSymbolicLink()) return [...identity, "symlink", readWorkspaceLink(root.root, repoPath)];
|
|
175
|
+
if (stat.isFile() && stat.size <= MAX_CONTENT_BYTES) return [...identity, "file", hashWorkspaceFile(root.root, repoPath)];
|
|
176
|
+
return [...identity, stat.isFile() ? "oversized" : "other", stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs];
|
|
177
|
+
} catch {
|
|
178
|
+
return [...identity, "absent"];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function observation(roots: RootState[], options: SourceProjectionOptions): string {
|
|
183
|
+
return token("workspace", JSON.stringify(roots.map((root) => [
|
|
184
|
+
root.manifest.repoRef,
|
|
185
|
+
root.status.headOid,
|
|
186
|
+
root.status.records.filter(visibleRecord).map((record) => workspaceEvidence(root, record, options))
|
|
187
|
+
])));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function workspacePathPresent(root: RootState, repoPath: string): boolean {
|
|
191
|
+
try { inspectWorkspaceEntry(root.root, repoPath); return true; }
|
|
192
|
+
catch { return false; }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function treeEntryIdentity(root: RootState, oid: string | null, repoPath: string): Promise<string | null> {
|
|
196
|
+
if (!oid) return null;
|
|
197
|
+
const entry = await readGitTreeEntry(root.root, oid, repoPath);
|
|
198
|
+
return entry ? `${entry.mode}:${entry.type}:${entry.objectId}` : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function protectedChangeState(
|
|
202
|
+
root: RootState,
|
|
203
|
+
repoPath: string,
|
|
204
|
+
record: GitStatusRecord | undefined,
|
|
205
|
+
baselineItem: BaselineItem | undefined
|
|
206
|
+
): Promise<{ changed: boolean; basePresent: boolean; currentPresent: boolean; exact: boolean }> {
|
|
207
|
+
const currentPresent = workspacePathPresent(root, repoPath);
|
|
208
|
+
if (baselineItem) {
|
|
209
|
+
const basePresent = baselineItem.entry.state !== "absent";
|
|
210
|
+
if (!basePresent || !currentPresent) return { changed: basePresent !== currentPresent, basePresent, currentPresent, exact: true };
|
|
211
|
+
return { changed: true, basePresent, currentPresent, exact: false };
|
|
212
|
+
}
|
|
213
|
+
const baseIdentity = await treeEntryIdentity(root, root.manifest.headOid, repoPath);
|
|
214
|
+
const currentIdentity = await treeEntryIdentity(root, root.status.headOid, repoPath);
|
|
215
|
+
return {
|
|
216
|
+
changed: Boolean(record) || baseIdentity !== currentIdentity,
|
|
217
|
+
basePresent: baseIdentity !== null,
|
|
218
|
+
currentPresent,
|
|
219
|
+
exact: true
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function currentContent(root: RootState, repoPath: string, record: GitStatusRecord | undefined): Content {
|
|
224
|
+
if (record?.headMode === "160000" || record?.indexMode === "160000" || record?.worktreeMode === "160000" || record?.submodule?.startsWith("S")) {
|
|
225
|
+
return unavailableContent("available", "submodule-content", "submodule");
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const stat = inspectWorkspaceEntry(root.root, repoPath);
|
|
229
|
+
if (stat.isSymbolicLink()) {
|
|
230
|
+
const bytes = Buffer.from(readWorkspaceLink(root.root, repoPath), "utf8");
|
|
231
|
+
return { bytes, kind: "symlink", access: "available", reasonCode: null };
|
|
232
|
+
}
|
|
233
|
+
if (!stat.isFile()) return unavailableContent("unavailable", "unsupported-file-type");
|
|
234
|
+
if (stat.size > MAX_CONTENT_BYTES) return unavailableContent("oversized", "file-size-limit");
|
|
235
|
+
const bytes = readWorkspaceFile(root.root, repoPath, MAX_CONTENT_BYTES);
|
|
236
|
+
return { bytes, kind: contentKind(bytes), access: "available", reasonCode: null };
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { bytes: null, kind: "unknown", access: "available", reasonCode: null };
|
|
239
|
+
return unavailableContent("unavailable", "content-read-failed");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function projectUnavailable(options: SourceProjectionOptions, manifest: TaskBaselineManifest, reasonCode: string, message: string, protectedCount = 0): SourceChangeDocument {
|
|
244
|
+
const repoRef = manifest.roots[0]?.repoRef ?? token("repo", options.cwd);
|
|
245
|
+
const basisRef = token("basis", `${manifest.integrityDigest}:unavailable`);
|
|
246
|
+
const viewRevision = token("task", `${manifest.integrityDigest}:unavailable`);
|
|
247
|
+
return {
|
|
248
|
+
schemaVersion: 1, version: "piagent-webui-source-change-v1", generatedAt: options.generatedAt ?? new Date().toISOString(), identity: options.identity,
|
|
249
|
+
view: "task", viewRevision, bases: [{ basisRef, repoRef, view: "task", state: "unavailable", reasonCode,
|
|
250
|
+
basisRevision: token("basis-rev", `${basisRef}:unavailable`), taskRunId: manifest.taskRunId,
|
|
251
|
+
taskRevision: options.taskRevision ?? "task-revision.unavailable", workspaceRevision: token("workspace", `${manifest.integrityDigest}:unavailable`),
|
|
252
|
+
baselineManifestRef: taskBaselineManifestRef(manifest), baselineTreeDigest: manifest.baselineTreeDigest }],
|
|
253
|
+
availability: { state: "unavailable", reasonCode, message }, files: [],
|
|
254
|
+
page: { cursor: null, nextCursor: null, total: 0, returned: 0, truncated: false }, truncationReason: null,
|
|
255
|
+
redaction: { applied: protectedCount > 0 || manifest.roots.some((root) => root.entries.some((entry) => entry.state === "protected")), valuesRemoved: protectedCount + manifest.roots.reduce((count, root) => count + root.entries.filter((entry) => entry.state === "protected").length, 0), truncated: false },
|
|
256
|
+
health: { state: "unavailable", reasonCode, message }, issues: []
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function stats(base: Content, current: Content, conflict: boolean) {
|
|
261
|
+
if (conflict) return { state: "unavailable", additions: null, deletions: null, reasonCode: "git-conflict" };
|
|
262
|
+
if (base.bytes === null && base.access === "available" && current.bytes !== null) {
|
|
263
|
+
const result = diffTextBuffers(Buffer.alloc(0), current.bytes);
|
|
264
|
+
return result.exact ? { state: "exact", additions: result.additions, deletions: 0, reasonCode: null } : { state: "unavailable", additions: null, deletions: null, reasonCode: result.reasonCode };
|
|
265
|
+
}
|
|
266
|
+
if (current.bytes === null && current.access === "available" && base.bytes !== null) {
|
|
267
|
+
const result = diffTextBuffers(base.bytes, Buffer.alloc(0));
|
|
268
|
+
return result.exact ? { state: "exact", additions: 0, deletions: result.deletions, reasonCode: null } : { state: "unavailable", additions: null, deletions: null, reasonCode: result.reasonCode };
|
|
269
|
+
}
|
|
270
|
+
if (base.bytes !== null && current.bytes !== null) {
|
|
271
|
+
const result = diffTextBuffers(base.bytes, current.bytes);
|
|
272
|
+
return result.exact ? { state: "exact", additions: result.additions, deletions: result.deletions, reasonCode: null } : { state: "unavailable", additions: null, deletions: null, reasonCode: result.reasonCode };
|
|
273
|
+
}
|
|
274
|
+
return { state: "unavailable", additions: null, deletions: null, reasonCode: base.reasonCode ?? current.reasonCode ?? "line-stats-unavailable" };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function changed(base: Content, current: Content, inCurrentSnapshot: boolean, inBaseline: boolean): boolean {
|
|
278
|
+
if (base.access !== "available" || current.access !== "available") return inCurrentSnapshot || inBaseline;
|
|
279
|
+
return !buffersEqual(base.bytes, current.bytes);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function collectTaskSourceChangeView(options: SourceProjectionOptions): Promise<SourceChangeDocument | null> {
|
|
283
|
+
if (!options.identity.taskId || !options.identity.taskRunId) return null;
|
|
284
|
+
let manifest: TaskBaselineManifest | undefined;
|
|
285
|
+
try { manifest = readTaskBaselineManifest(options.cwd, options.identity.taskRunId); }
|
|
286
|
+
catch { return null; }
|
|
287
|
+
if (!manifest) return null;
|
|
288
|
+
if (taskBaselineRetentionState(manifest, new Date(options.generatedAt ?? Date.now())) === "expired") {
|
|
289
|
+
return projectUnavailable(options, manifest, "task-baseline-retention-expired", "Task baseline evidence is outside its retention window");
|
|
290
|
+
}
|
|
291
|
+
if (manifest.taskId !== options.identity.taskId || manifest.captureState !== "current") {
|
|
292
|
+
return projectUnavailable(options, manifest, manifest.reasonCode ?? "task-baseline-content-unavailable", "Exact task baseline evidence is unavailable");
|
|
293
|
+
}
|
|
294
|
+
const roots = await collectRoots(options.cwd, manifest);
|
|
295
|
+
const baseline = entryMap(manifest);
|
|
296
|
+
const selection = await taskPaths(roots, baseline);
|
|
297
|
+
const beforeObservation = observation(roots, options);
|
|
298
|
+
const provenanceFor = taskProvenanceResolver(options.cwd, manifest);
|
|
299
|
+
const basisRefs = new Map(manifest.roots.map((root) => [root.repoRef, token("basis", `${manifest.integrityDigest}:${root.repoRef}`)]));
|
|
300
|
+
const files: Array<Record<string, unknown>> = [];
|
|
301
|
+
for (const projectPath of selection.paths) {
|
|
302
|
+
const root = roots.find((candidate) => rootRelative(candidate.manifest, projectPath) !== null);
|
|
303
|
+
if (!root) continue;
|
|
304
|
+
const repoPath = rootRelative(root.manifest, projectPath) as string;
|
|
305
|
+
const record = root.records.get(repoPath);
|
|
306
|
+
if (!visibleRepoPath(repoPath) || (record && !visibleRecord(record))) continue;
|
|
307
|
+
const baselineItem = baseline.get(projectPath);
|
|
308
|
+
const historical = [...(selection.aliases.get(projectPath) ?? [])]
|
|
309
|
+
.map((candidate) => rootRelative(root.manifest, candidate)).filter((candidate): candidate is string => candidate !== null);
|
|
310
|
+
const protectedPath = protectedRecordPath(options, root, repoPath, record, historical);
|
|
311
|
+
const protectedState = protectedPath ? await protectedChangeState(root, repoPath, record, baselineItem) : null;
|
|
312
|
+
if (protectedState && !protectedState.exact) {
|
|
313
|
+
return projectUnavailable(options, manifest, "protected-baseline-overlap", "Exact task delta overlaps content protected by the current policy", 1);
|
|
314
|
+
}
|
|
315
|
+
const base = protectedPath
|
|
316
|
+
? unavailableContent("protected", "protected-path")
|
|
317
|
+
: baselineItem ? baselineEntryContent(options.cwd, manifest.taskRunId, baselineItem.entry) : await headContent(root, repoPath);
|
|
318
|
+
const current = protectedPath ? unavailableContent("protected", "protected-path") : currentContent(root, repoPath, record);
|
|
319
|
+
const basePresent = protectedState?.basePresent
|
|
320
|
+
?? (baselineItem ? baselineItem.entry.state !== "absent" : await treeEntryIdentity(root, root.manifest.headOid, repoPath) !== null);
|
|
321
|
+
const currentPresent = protectedState?.currentPresent ?? workspacePathPresent(root, repoPath);
|
|
322
|
+
if (protectedState ? !protectedState.changed : !changed(base, current, currentPresent, basePresent)) continue;
|
|
323
|
+
const conflict = record?.kind === "unmerged" || record?.indexStatus === "U" || record?.worktreeStatus === "U";
|
|
324
|
+
const status = conflict ? "C" : !basePresent ? (record?.kind === "untracked" ? "U" : "A") : !currentPresent ? "D" : "M";
|
|
325
|
+
const display = projectGitPath(projectPath);
|
|
326
|
+
if (!display.display) continue;
|
|
327
|
+
const fileRef = token("file", `${root.status.repoDigest}\0${display.digest}`);
|
|
328
|
+
const recordState = record ? [record.kind, record.path.digest, record.oldPath?.digest ?? null, record.indexStatus, record.worktreeStatus,
|
|
329
|
+
record.headMode, record.indexMode, record.worktreeMode, record.headObject, record.indexObject] : null;
|
|
330
|
+
const fileRevision = token("file-rev", JSON.stringify([manifest.integrityDigest, projectPath, digest(base.bytes), digest(current.bytes),
|
|
331
|
+
root.status.headOid, recordState, protectedPath]));
|
|
332
|
+
const access = base.access !== "available" ? base.access : current.access;
|
|
333
|
+
const kind = current.bytes === null ? base.kind : current.kind;
|
|
334
|
+
const reasonCode = base.reasonCode ?? current.reasonCode;
|
|
335
|
+
files.push({
|
|
336
|
+
repoRef: root.manifest.repoRef, basisRef: basisRefs.get(root.manifest.repoRef), fileRef, fileRevision,
|
|
337
|
+
path: display.display, oldPath: null, pathDisplay: display.displayMode, status,
|
|
338
|
+
git: { indexStatus: record?.indexStatus ?? ".", worktreeStatus: record?.worktreeStatus ?? ".", conflict },
|
|
339
|
+
baseDigest: protectedPath || status === "A" || status === "U" ? null : digest(base.bytes), currentDigest: protectedPath || status === "D" ? null : digest(current.bytes),
|
|
340
|
+
content: access === "available" && kind !== "unknown" ? { kind, access, reasonCode: null } : { kind: kind === "unknown" ? "unknown" : kind, access, reasonCode: reasonCode ?? "content-unavailable" },
|
|
341
|
+
stats: protectedPath ? { state: "unavailable", additions: null, deletions: null, reasonCode: "protected-path" } : stats(base, current, conflict),
|
|
342
|
+
provenance: provenanceFor(projectPath, digest(current.bytes), baselineItem?.entry),
|
|
343
|
+
criterionIds: [], verifierAttemptIds: [], health: access === "available" ? { state: "ok", reasonCode: null, message: null } : { state: "degraded", reasonCode: reasonCode ?? "content-unavailable", message: "Exact task content is unavailable" }
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
const afterRoots = await collectRoots(options.cwd, manifest);
|
|
347
|
+
const afterObservation = observation(afterRoots, options);
|
|
348
|
+
if (beforeObservation !== afterObservation) return projectUnavailable(options, manifest, "git-race", "Workspace changed during task projection; retry");
|
|
349
|
+
const workspaceRevision = afterObservation;
|
|
350
|
+
const viewRevision = token("task", `${manifest.integrityDigest}:${workspaceRevision}`);
|
|
351
|
+
const bases = manifest.roots.map((root) => ({
|
|
352
|
+
basisRef: basisRefs.get(root.repoRef), repoRef: root.repoRef, view: "task", state: "current", reasonCode: null,
|
|
353
|
+
basisRevision: token("basis-rev", `${manifest.integrityDigest}:${root.repoRef}`), taskRunId: manifest.taskRunId,
|
|
354
|
+
taskRevision: options.taskRevision ?? "task-revision.unavailable", workspaceRevision,
|
|
355
|
+
baselineManifestRef: taskBaselineManifestRef(manifest), baselineTreeDigest: manifest.baselineTreeDigest
|
|
356
|
+
}));
|
|
357
|
+
const limit = Number.isInteger(options.pageLimit) ? Math.max(1, Math.min(2000, options.pageLimit as number)) : 300;
|
|
358
|
+
const returned = files.slice(0, limit);
|
|
359
|
+
const truncated = returned.length < files.length;
|
|
360
|
+
const protectedCount = files.filter((file) => (file.content as Record<string, unknown>)?.access === "protected").length;
|
|
361
|
+
return {
|
|
362
|
+
schemaVersion: 1, version: "piagent-webui-source-change-v1", generatedAt: options.generatedAt ?? new Date().toISOString(), identity: options.identity,
|
|
363
|
+
view: "task", viewRevision, bases, availability: { state: "current", reasonCode: null, message: null }, files: returned,
|
|
364
|
+
page: { cursor: null, nextCursor: truncated ? token("page", `${viewRevision}:${limit}`) : null, total: files.length, returned: returned.length, truncated }, truncationReason: truncated ? "page-limit" : null,
|
|
365
|
+
redaction: { applied: protectedCount > 0, valuesRemoved: protectedCount, truncated: false },
|
|
366
|
+
health: protectedCount > 0
|
|
367
|
+
? { state: "degraded", reasonCode: "source-content-restricted", message: "Some task content is protected by policy" }
|
|
368
|
+
: { state: "ok", reasonCode: null, message: null },
|
|
369
|
+
issues: []
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function readTaskFileContents(options: SourceProjectionOptions, fileRef: string): Promise<{ base: Buffer | null; current: Buffer | null } | null> {
|
|
374
|
+
if (!options.identity.taskRunId) return null;
|
|
375
|
+
let manifest: TaskBaselineManifest | undefined;
|
|
376
|
+
try { manifest = readTaskBaselineManifest(options.cwd, options.identity.taskRunId); }
|
|
377
|
+
catch { return null; }
|
|
378
|
+
if (!manifest || manifest.captureState !== "current"
|
|
379
|
+
|| taskBaselineRetentionState(manifest, new Date(options.generatedAt ?? Date.now())) === "expired") return null;
|
|
380
|
+
const roots = await collectRoots(options.cwd, manifest);
|
|
381
|
+
const baseline = entryMap(manifest);
|
|
382
|
+
const selection = await taskPaths(roots, baseline);
|
|
383
|
+
for (const projectPath of selection.paths) {
|
|
384
|
+
const root = roots.find((candidate) => rootRelative(candidate.manifest, projectPath) !== null);
|
|
385
|
+
if (!root) continue;
|
|
386
|
+
const repoPath = rootRelative(root.manifest, projectPath) as string;
|
|
387
|
+
const record = root.records.get(repoPath);
|
|
388
|
+
if (!visibleRepoPath(repoPath) || (record && !visibleRecord(record))) continue;
|
|
389
|
+
const display = projectGitPath(projectPath);
|
|
390
|
+
if (token("file", `${root.status.repoDigest}\0${display.digest}`) !== fileRef) continue;
|
|
391
|
+
const historical = [...(selection.aliases.get(projectPath) ?? [])]
|
|
392
|
+
.map((candidate) => rootRelative(root.manifest, candidate)).filter((candidate): candidate is string => candidate !== null);
|
|
393
|
+
if (protectedRecordPath(options, root, repoPath, record, historical)) return null;
|
|
394
|
+
const item = baseline.get(projectPath);
|
|
395
|
+
const base = item ? baselineEntryContent(options.cwd, manifest.taskRunId, item.entry) : await headContent(root, repoPath);
|
|
396
|
+
const current = currentContent(root, repoPath, record);
|
|
397
|
+
if (base.access !== "available" || current.access !== "available") return null;
|
|
398
|
+
return { base: base.bytes, current: current.bytes };
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { redactSensitiveText } from "../../extensions/redaction-core.js";
|
|
4
|
+
import { inspectOwnedWorkBudget, type OwnedWorkReservation } from "../orchestration/owned-work-budget.ts";
|
|
5
|
+
import { taskRunOpaqueRef } from "./task-run-index.ts";
|
|
6
|
+
|
|
7
|
+
type Task = { taskId: string; taskRunId: string; trace?: { outcome?: unknown }; orchestration?: Record<string, unknown>;
|
|
8
|
+
acceptanceReceipt?: { helperUsage?: { used?: unknown; helpers?: unknown[] } }; [key: string]: any };
|
|
9
|
+
type Identity = { projectRef: string; runtimeInstanceId: string; sessionRef: string; taskId: string; taskRunId: string;
|
|
10
|
+
agentOperationId: null; toolCallId: null };
|
|
11
|
+
|
|
12
|
+
function opaque(prefix: string, value: string): string {
|
|
13
|
+
return `${prefix}.${createHash("sha256").update(`piagent-webui-subagent-tree-v1\0${value}`).digest("hex").slice(0, 48)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function display(value: unknown, maximum = 120): string | null {
|
|
17
|
+
const text = redactSensitiveText(String(value ?? "")).text.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
18
|
+
return text ? text.slice(0, maximum) : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function helperReceipts(task: Task): Map<string, Record<string, any>> {
|
|
22
|
+
const helpers = task.acceptanceReceipt?.helperUsage?.helpers;
|
|
23
|
+
const map = new Map<string, Record<string, any>>();
|
|
24
|
+
if (!Array.isArray(helpers)) return map;
|
|
25
|
+
for (const helper of helpers.slice(0, 64)) {
|
|
26
|
+
if (!helper || typeof helper !== "object" || Array.isArray(helper)) continue;
|
|
27
|
+
const item = helper as Record<string, any>;
|
|
28
|
+
if (typeof item.requestRef === "string" && /^[a-f0-9]{64}$/.test(item.requestRef)) map.set(item.requestRef, item);
|
|
29
|
+
}
|
|
30
|
+
return map;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function child(taskRunId: string, parentRef: string, item: OwnedWorkReservation, receipt: Record<string, any> | undefined) {
|
|
34
|
+
const nodeRef = opaque("helper", `${taskRunId}\0${item.id}`);
|
|
35
|
+
const disposition = display(receipt?.disposition, 80);
|
|
36
|
+
const staleResult = disposition === "stale-result";
|
|
37
|
+
const resultState = staleResult ? "stale-result" : item.status === "succeeded" && item.usageRef ? "accepted"
|
|
38
|
+
: receipt ? "rejected" : "not-recorded";
|
|
39
|
+
return {
|
|
40
|
+
nodeRef, parentRef, role: item.role, authority: item.authority, lifecycleState: item.status,
|
|
41
|
+
reservedAt: item.reservedAt, expiresAt: item.expiresAt, completedAt: item.completedAt,
|
|
42
|
+
currentWriter: item.status === "active" && item.authority === "single-writer",
|
|
43
|
+
result: { state: resultState, disposition, calls: item.usageRef?.calls ?? null, tokens: item.usageRef?.tokens ?? null }
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function orchestration(task: Task) {
|
|
48
|
+
const mode = task.orchestration?.mode;
|
|
49
|
+
const subagents = task.orchestration?.subagents;
|
|
50
|
+
return {
|
|
51
|
+
mode: ["solo-first", "bounded-subagents", "parallel-readonly"].includes(String(mode)) ? mode : "unknown",
|
|
52
|
+
subagents: ["not-used", "optional", "used"].includes(String(subagents)) ? subagents : "unknown"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function unavailable(identity: Identity, runRef: string, generatedAt: string, reasonCode: string) {
|
|
57
|
+
return { schemaVersion: 1, version: "piagent-webui-subagent-tree-v1", generatedAt, identity: structuredClone(identity), runRef,
|
|
58
|
+
state: "unavailable", treeRevision: null, evidenceState: "unknown", orchestration: { mode: "unknown", subagents: "unknown" },
|
|
59
|
+
parent: null, children: [], writer: { state: "unknown", ownerNodeRef: null },
|
|
60
|
+
nestedLineage: { state: "unavailable", reasonCode: "no-durable-nested-lineage" },
|
|
61
|
+
summary: { total: 0, active: 0, completed: 0, staleResults: 0, readOnly: 0, singleWriter: 0 }, warnings: [],
|
|
62
|
+
health: { state: "error", reasonCode, message: "Helper/subagent evidence is unavailable." } };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function projectTaskSubagentTree(input: { cwd: string; task: Task; identity: Identity; generatedAt?: string }): Record<string, any> {
|
|
66
|
+
const generatedAt = input.generatedAt ?? new Date().toISOString(), runRef = taskRunOpaqueRef(input.task.taskRunId);
|
|
67
|
+
const inspected = inspectOwnedWorkBudget(input.cwd, input.task.taskId, input.task.taskRunId, generatedAt);
|
|
68
|
+
if (inspected.state === "corrupt") return unavailable(input.identity, runRef, generatedAt, inspected.reasonCode ?? "helper-budget-corrupt");
|
|
69
|
+
const policy = orchestration(input.task), parentRef = opaque("parent", input.task.taskRunId), receipts = helperReceipts(input.task);
|
|
70
|
+
const children = inspected.reservations.map((item) => child(input.task.taskRunId, parentRef, item, receipts.get(item.deduplicationKey)));
|
|
71
|
+
const matchedReceipts = new Set(inspected.reservations.map((item) => item.deduplicationKey).filter((key) => receipts.has(key)));
|
|
72
|
+
const unmatchedReceipts = Math.max(0, receipts.size - matchedReceipts.size);
|
|
73
|
+
const activeWriter = children.find((item) => item.currentWriter);
|
|
74
|
+
const outcome = String(input.task.trace?.outcome ?? "unknown");
|
|
75
|
+
const parentState = outcome === "pending" ? "active" : ["completed", "blocked", "partial", "failed"].includes(outcome) ? "terminal" : "unknown";
|
|
76
|
+
const aggregateEvidence = policy.mode !== "unknown" || policy.subagents !== "unknown" || receipts.size > 0;
|
|
77
|
+
const evidenceState = inspected.state === "ready" ? unmatchedReceipts ? "partial" : "complete" : aggregateEvidence ? "aggregate-only" : "missing";
|
|
78
|
+
const warnings: Array<{ code: string; count: number; message: string }> = [];
|
|
79
|
+
if (inspected.state === "missing") warnings.push({ code: "helper-budget-missing", count: 1,
|
|
80
|
+
message: "Only aggregate orchestration evidence is available; helper lifecycle detail cannot be reconstructed." });
|
|
81
|
+
if (inspected.derivedOrphans) warnings.push({ code: "expired-helper-derived-orphan", count: inspected.derivedOrphans,
|
|
82
|
+
message: `${inspected.derivedOrphans} expired active helper reservation(s) are displayed as orphaned without changing runtime state.` });
|
|
83
|
+
if (unmatchedReceipts) warnings.push({ code: "helper-receipt-unmatched", count: unmatchedReceipts,
|
|
84
|
+
message: `${unmatchedReceipts} helper receipt(s) cannot be linked to a durable reservation.` });
|
|
85
|
+
const writer = activeWriter ? { state: "helper", ownerNodeRef: activeWriter.nodeRef }
|
|
86
|
+
: inspected.state === "ready" || policy.subagents === "not-used" ? { state: "parent", ownerNodeRef: parentRef }
|
|
87
|
+
: { state: "unknown", ownerNodeRef: null };
|
|
88
|
+
const summary = { total: children.length, active: children.filter((item) => item.lifecycleState === "active").length,
|
|
89
|
+
completed: children.filter((item) => item.lifecycleState !== "active").length,
|
|
90
|
+
staleResults: children.filter((item) => item.result.state === "stale-result").length,
|
|
91
|
+
readOnly: children.filter((item) => item.authority === "read-only").length,
|
|
92
|
+
singleWriter: children.filter((item) => item.authority === "single-writer").length };
|
|
93
|
+
const treeRevision = `subagent-tree.${createHash("sha256").update(JSON.stringify({ policy, parentState, terminal: inspected.terminal,
|
|
94
|
+
children, writer, evidenceState })).digest("hex")}`;
|
|
95
|
+
return { schemaVersion: 1, version: "piagent-webui-subagent-tree-v1", generatedAt, identity: structuredClone(input.identity), runRef,
|
|
96
|
+
state: "ready", treeRevision, evidenceState, orchestration: policy,
|
|
97
|
+
parent: { nodeRef: parentRef, lifecycleState: parentState, budgetTerminal: inspected.terminal, mergeOwner: "parent" }, children, writer,
|
|
98
|
+
nestedLineage: { state: "unavailable", reasonCode: "no-durable-nested-lineage" }, summary, warnings,
|
|
99
|
+
health: warnings.length ? { state: "degraded", reasonCode: "subagent-tree-incomplete", message: "Some helper/subagent evidence is incomplete." }
|
|
100
|
+
: { state: "ok", reasonCode: null, message: null } };
|
|
101
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type TextDiffOperation = { kind: "context" | "added" | "deleted"; text: string };
|
|
2
|
+
|
|
3
|
+
export type TextDiffResult = {
|
|
4
|
+
operations: TextDiffOperation[];
|
|
5
|
+
additions: number;
|
|
6
|
+
deletions: number;
|
|
7
|
+
exact: boolean;
|
|
8
|
+
reasonCode: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function lines(content: Buffer): string[] | null {
|
|
12
|
+
if (content.includes(0)) return null;
|
|
13
|
+
const text = content.toString("utf8");
|
|
14
|
+
if (Buffer.from(text, "utf8").compare(content) !== 0) return null;
|
|
15
|
+
if (!text) return [];
|
|
16
|
+
const result = text.split("\n");
|
|
17
|
+
if (text.endsWith("\n")) result.pop();
|
|
18
|
+
return result.map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function diffTextBuffers(base: Buffer, current: Buffer, maxCells = 4_000_000): TextDiffResult {
|
|
22
|
+
const before = lines(base);
|
|
23
|
+
const after = lines(current);
|
|
24
|
+
if (!before || !after) return { operations: [], additions: 0, deletions: 0, exact: false, reasonCode: "binary-content" };
|
|
25
|
+
const cells = (before.length + 1) * (after.length + 1);
|
|
26
|
+
if (!Number.isSafeInteger(cells) || cells > maxCells) {
|
|
27
|
+
return { operations: [], additions: 0, deletions: 0, exact: false, reasonCode: "diff-complexity-limit" };
|
|
28
|
+
}
|
|
29
|
+
const matrix = Array.from({ length: before.length + 1 }, () => new Uint32Array(after.length + 1));
|
|
30
|
+
for (let left = before.length - 1; left >= 0; left -= 1) {
|
|
31
|
+
for (let right = after.length - 1; right >= 0; right -= 1) {
|
|
32
|
+
matrix[left][right] = before[left] === after[right]
|
|
33
|
+
? matrix[left + 1][right + 1] + 1
|
|
34
|
+
: Math.max(matrix[left + 1][right], matrix[left][right + 1]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const operations: TextDiffOperation[] = [];
|
|
38
|
+
let left = 0;
|
|
39
|
+
let right = 0;
|
|
40
|
+
let additions = 0;
|
|
41
|
+
let deletions = 0;
|
|
42
|
+
while (left < before.length || right < after.length) {
|
|
43
|
+
if (left < before.length && right < after.length && before[left] === after[right]) {
|
|
44
|
+
operations.push({ kind: "context", text: before[left] });
|
|
45
|
+
left += 1;
|
|
46
|
+
right += 1;
|
|
47
|
+
} else if (right < after.length && (left >= before.length || matrix[left][right + 1] >= matrix[left + 1][right])) {
|
|
48
|
+
operations.push({ kind: "added", text: after[right++] });
|
|
49
|
+
additions += 1;
|
|
50
|
+
} else {
|
|
51
|
+
operations.push({ kind: "deleted", text: before[left++] });
|
|
52
|
+
deletions += 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { operations, additions, deletions, exact: true, reasonCode: null };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buffersEqual(left: Buffer | null, right: Buffer | null): boolean {
|
|
59
|
+
return left === null ? right === null : right !== null && left.equals(right);
|
|
60
|
+
}
|