apex-code 0.0.1-alpha.8 → 0.0.4
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 +231 -0
- package/README.md +9 -8
- package/dist/cli/args.d.ts +26 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +72 -10
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +2 -1
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/cli/mcp-command.d.ts +9 -0
- package/dist/cli/mcp-command.d.ts.map +1 -0
- package/dist/cli/mcp-command.js +53 -0
- package/dist/cli/mcp-command.js.map +1 -0
- package/dist/cli/project-trust.d.ts.map +1 -1
- package/dist/cli/project-trust.js +3 -1
- package/dist/cli/project-trust.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +2 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +94 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +45 -18
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +183 -15
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +728 -117
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +6 -7
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +7 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
- package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/git-checkpoints.js +251 -0
- package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.js +36 -0
- package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +7 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +9 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +59 -36
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/defaults.d.ts +1 -0
- package/dist/core/defaults.d.ts.map +1 -1
- package/dist/core/defaults.js +9 -0
- package/dist/core/defaults.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts +6 -5
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +4 -3
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/durable-state/sqlite.d.ts.map +1 -1
- package/dist/core/durable-state/sqlite.js +26 -2
- package/dist/core/durable-state/sqlite.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +5 -2
- package/dist/core/exec.js.map +1 -1
- package/dist/core/extensions/index.d.ts +2 -2
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +123 -50
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +45 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/source-runtime.d.ts +14 -0
- package/dist/core/extensions/source-runtime.d.ts.map +1 -0
- package/dist/core/extensions/source-runtime.js +56 -0
- package/dist/core/extensions/source-runtime.js.map +1 -0
- package/dist/core/extensions/types.d.ts +60 -11
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +3 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/formatter-lifecycle.d.ts +69 -0
- package/dist/core/formatter-lifecycle.d.ts.map +1 -0
- package/dist/core/formatter-lifecycle.js +360 -0
- package/dist/core/formatter-lifecycle.js.map +1 -0
- package/dist/core/hooks/command-handler.d.ts +19 -0
- package/dist/core/hooks/command-handler.d.ts.map +1 -0
- package/dist/core/hooks/command-handler.js +112 -0
- package/dist/core/hooks/command-handler.js.map +1 -0
- package/dist/core/hooks/http-handler.d.ts +10 -0
- package/dist/core/hooks/http-handler.d.ts.map +1 -0
- package/dist/core/hooks/http-handler.js +36 -0
- package/dist/core/hooks/http-handler.js.map +1 -0
- package/dist/core/hooks/loader.d.ts +29 -0
- package/dist/core/hooks/loader.d.ts.map +1 -0
- package/dist/core/hooks/loader.js +139 -0
- package/dist/core/hooks/loader.js.map +1 -0
- package/dist/core/hooks/runtime.d.ts +13 -0
- package/dist/core/hooks/runtime.d.ts.map +1 -0
- package/dist/core/hooks/runtime.js +63 -0
- package/dist/core/hooks/runtime.js.map +1 -0
- package/dist/core/hooks/types.d.ts +83 -0
- package/dist/core/hooks/types.d.ts.map +1 -0
- package/dist/core/hooks/types.js +24 -0
- package/dist/core/hooks/types.js.map +1 -0
- package/dist/core/keybindings.d.ts +20 -19
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +26 -5
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/mcp/config.d.ts +16 -0
- package/dist/core/mcp/config.d.ts.map +1 -0
- package/dist/core/mcp/config.js +166 -0
- package/dist/core/mcp/config.js.map +1 -0
- package/dist/core/mcp/connector.d.ts +35 -0
- package/dist/core/mcp/connector.d.ts.map +1 -0
- package/dist/core/mcp/connector.js +105 -0
- package/dist/core/mcp/connector.js.map +1 -0
- package/dist/core/mcp/contract.d.ts +19 -0
- package/dist/core/mcp/contract.d.ts.map +1 -0
- package/dist/core/mcp/contract.js +89 -0
- package/dist/core/mcp/contract.js.map +1 -0
- package/dist/core/mcp/mcp-tool.d.ts +32 -0
- package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
- package/dist/core/mcp/mcp-tool.js +171 -0
- package/dist/core/mcp/mcp-tool.js.map +1 -0
- package/dist/core/mcp/metadata-cache.d.ts +29 -0
- package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
- package/dist/core/mcp/metadata-cache.js +99 -0
- package/dist/core/mcp/metadata-cache.js.map +1 -0
- package/dist/core/mcp/oauth/authorize.d.ts +21 -0
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
- package/dist/core/mcp/oauth/authorize.js +32 -0
- package/dist/core/mcp/oauth/authorize.js.map +1 -0
- package/dist/core/mcp/oauth/discover.d.ts +27 -0
- package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
- package/dist/core/mcp/oauth/discover.js +116 -0
- package/dist/core/mcp/oauth/discover.js.map +1 -0
- package/dist/core/mcp/oauth/flow.d.ts +31 -0
- package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
- package/dist/core/mcp/oauth/flow.js +175 -0
- package/dist/core/mcp/oauth/flow.js.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.js +129 -0
- package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
- package/dist/core/mcp/oauth/pkce.d.ts +12 -0
- package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
- package/dist/core/mcp/oauth/pkce.js +11 -0
- package/dist/core/mcp/oauth/pkce.js.map +1 -0
- package/dist/core/mcp/oauth/register.d.ts +16 -0
- package/dist/core/mcp/oauth/register.d.ts.map +1 -0
- package/dist/core/mcp/oauth/register.js +42 -0
- package/dist/core/mcp/oauth/register.js.map +1 -0
- package/dist/core/mcp/oauth/token-client.d.ts +33 -0
- package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
- package/dist/core/mcp/oauth/token-client.js +65 -0
- package/dist/core/mcp/oauth/token-client.js.map +1 -0
- package/dist/core/mcp/runtime.d.ts +20 -0
- package/dist/core/mcp/runtime.d.ts.map +1 -0
- package/dist/core/mcp/runtime.js +37 -0
- package/dist/core/mcp/runtime.js.map +1 -0
- package/dist/core/mcp/schema.d.ts +22 -0
- package/dist/core/mcp/schema.d.ts.map +1 -0
- package/dist/core/mcp/schema.js +20 -0
- package/dist/core/mcp/schema.js.map +1 -0
- package/dist/core/mcp/server-manager.d.ts +43 -0
- package/dist/core/mcp/server-manager.d.ts.map +1 -0
- package/dist/core/mcp/server-manager.js +142 -0
- package/dist/core/mcp/server-manager.js.map +1 -0
- package/dist/core/mcp/types.d.ts +100 -0
- package/dist/core/mcp/types.d.ts.map +1 -0
- package/dist/core/mcp/types.js +8 -0
- package/dist/core/mcp/types.js.map +1 -0
- package/dist/core/model-config.d.ts +5 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +3 -1
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +14 -8
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/models-store.d.ts.map +1 -1
- package/dist/core/models-store.js +2 -1
- package/dist/core/models-store.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +22 -14
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/permissions/gate.d.ts.map +1 -1
- package/dist/core/permissions/gate.js +45 -4
- package/dist/core/permissions/gate.js.map +1 -1
- package/dist/core/permissions/operations.d.ts +20 -0
- package/dist/core/permissions/operations.d.ts.map +1 -0
- package/dist/core/permissions/operations.js +9 -0
- package/dist/core/permissions/operations.js.map +1 -0
- package/dist/core/permissions/policy-command.d.ts +42 -0
- package/dist/core/permissions/policy-command.d.ts.map +1 -0
- package/dist/core/permissions/policy-command.js +44 -0
- package/dist/core/permissions/policy-command.js.map +1 -0
- package/dist/core/permissions/responder.d.ts +49 -2
- package/dist/core/permissions/responder.d.ts.map +1 -1
- package/dist/core/permissions/responder.js +20 -8
- package/dist/core/permissions/responder.js.map +1 -1
- package/dist/core/permissions/rules.d.ts.map +1 -1
- package/dist/core/permissions/rules.js +11 -1
- package/dist/core/permissions/rules.js.map +1 -1
- package/dist/core/permissions/store.d.ts +18 -0
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +89 -8
- package/dist/core/permissions/store.js.map +1 -1
- package/dist/core/pi-manifest.d.ts.map +1 -1
- package/dist/core/pi-manifest.js +2 -1
- package/dist/core/pi-manifest.js.map +1 -1
- package/dist/core/policy-executor.d.ts +47 -0
- package/dist/core/policy-executor.d.ts.map +1 -0
- package/dist/core/policy-executor.js +228 -0
- package/dist/core/policy-executor.js.map +1 -0
- package/dist/core/policy-loader.d.ts +72 -0
- package/dist/core/policy-loader.d.ts.map +1 -0
- package/dist/core/policy-loader.js +195 -0
- package/dist/core/policy-loader.js.map +1 -0
- package/dist/core/project-trust.d.ts +1 -1
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +2 -1
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +3 -2
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sandbox/bwrap-arguments.d.ts +41 -0
- package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
- package/dist/core/sandbox/bwrap-arguments.js +136 -0
- package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
- package/dist/core/sandbox/child-entry.d.ts.map +1 -1
- package/dist/core/sandbox/child-entry.js +3 -1
- package/dist/core/sandbox/child-entry.js.map +1 -1
- package/dist/core/sandbox/cli-launch.d.ts +84 -4
- package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
- package/dist/core/sandbox/cli-launch.js +115 -16
- package/dist/core/sandbox/cli-launch.js.map +1 -1
- package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
- package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/cli-supervisor.js +50 -2
- package/dist/core/sandbox/cli-supervisor.js.map +1 -1
- package/dist/core/sandbox/full-access.d.ts +28 -0
- package/dist/core/sandbox/full-access.d.ts.map +1 -0
- package/dist/core/sandbox/full-access.js +63 -0
- package/dist/core/sandbox/full-access.js.map +1 -0
- package/dist/core/sandbox/git-identity.d.ts +57 -0
- package/dist/core/sandbox/git-identity.d.ts.map +1 -0
- package/dist/core/sandbox/git-identity.js +78 -0
- package/dist/core/sandbox/git-identity.js.map +1 -0
- package/dist/core/sandbox/host-approval.d.ts +42 -0
- package/dist/core/sandbox/host-approval.d.ts.map +1 -0
- package/dist/core/sandbox/host-approval.js +99 -0
- package/dist/core/sandbox/host-approval.js.map +1 -0
- package/dist/core/sandbox/linux-backend.d.ts +14 -0
- package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
- package/dist/core/sandbox/linux-backend.js +158 -80
- package/dist/core/sandbox/linux-backend.js.map +1 -1
- package/dist/core/sandbox/macos-backend.d.ts +5 -0
- package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
- package/dist/core/sandbox/macos-backend.js +164 -5
- package/dist/core/sandbox/macos-backend.js.map +1 -1
- package/dist/core/sandbox/network-proxy.d.ts +19 -0
- package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/network-proxy.js +93 -19
- package/dist/core/sandbox/network-proxy.js.map +1 -1
- package/dist/core/sandbox/policy.d.ts +8 -0
- package/dist/core/sandbox/policy.d.ts.map +1 -1
- package/dist/core/sandbox/policy.js +29 -12
- package/dist/core/sandbox/policy.js.map +1 -1
- package/dist/core/sandbox/profiles.d.ts +25 -0
- package/dist/core/sandbox/profiles.d.ts.map +1 -0
- package/dist/core/sandbox/profiles.js +23 -0
- package/dist/core/sandbox/profiles.js.map +1 -0
- package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
- package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-client.js +86 -0
- package/dist/core/sandbox/rpc/command-client.js.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.js +120 -0
- package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
- package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
- package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
- package/dist/core/sandbox/rpc/framing.d.ts +35 -0
- package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/framing.js +114 -0
- package/dist/core/sandbox/rpc/framing.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts +47 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
- package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
- package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
- package/dist/core/sandbox/supervisor-temp.js +21 -0
- package/dist/core/sandbox/supervisor-temp.js.map +1 -0
- package/dist/core/sandbox/supervisor.d.ts +12 -1
- package/dist/core/sandbox/supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/supervisor.js +15 -1
- package/dist/core/sandbox/supervisor.js.map +1 -1
- package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
- package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
- package/dist/core/sandbox/terminal-handoff.js +221 -0
- package/dist/core/sandbox/terminal-handoff.js.map +1 -0
- package/dist/core/sandbox/terminal-size.d.ts +13 -0
- package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
- package/dist/core/sandbox/terminal-size.js +23 -2
- package/dist/core/sandbox/terminal-size.js.map +1 -1
- package/dist/core/sdk.d.ts +24 -2
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +63 -4
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-export.d.ts +4 -0
- package/dist/core/session-export.d.ts.map +1 -0
- package/dist/core/session-export.js +32 -0
- package/dist/core/session-export.js.map +1 -0
- package/dist/core/session-manager.d.ts +13 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +34 -6
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-diagnostics.d.ts +9 -0
- package/dist/core/settings-diagnostics.d.ts.map +1 -0
- package/dist/core/settings-diagnostics.js +21 -0
- package/dist/core/settings-diagnostics.js.map +1 -0
- package/dist/core/settings-manager.d.ts +172 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +160 -11
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +51 -33
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +8 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/background-shell.d.ts +39 -0
- package/dist/core/tools/background-shell.d.ts.map +1 -0
- package/dist/core/tools/background-shell.js +81 -0
- package/dist/core/tools/background-shell.js.map +1 -0
- package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
- package/dist/core/tools/bash-command-segments.js +2 -0
- package/dist/core/tools/bash-command-segments.js.map +1 -1
- package/dist/core/tools/bash.d.ts +44 -16
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +264 -32
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/contract-snapshot.d.ts +58 -0
- package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
- package/dist/core/tools/contract-snapshot.js +51 -0
- package/dist/core/tools/contract-snapshot.js.map +1 -0
- package/dist/core/tools/contract.d.ts +53 -5
- package/dist/core/tools/contract.d.ts.map +1 -1
- package/dist/core/tools/contract.js +22 -2
- package/dist/core/tools/contract.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +6 -4
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +21 -11
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +48 -5
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +217 -8
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +8 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +72 -10
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +5 -2
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +5 -2
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +14 -2
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +12 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +5 -2
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/lsp.d.ts +19 -1
- package/dist/core/tools/lsp.d.ts.map +1 -1
- package/dist/core/tools/lsp.js +163 -10
- package/dist/core/tools/lsp.js.map +1 -1
- package/dist/core/tools/path-permission.d.ts +3 -0
- package/dist/core/tools/path-permission.d.ts.map +1 -1
- package/dist/core/tools/path-permission.js +16 -4
- package/dist/core/tools/path-permission.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +27 -2
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +221 -4
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/powershell.d.ts +16 -0
- package/dist/core/tools/powershell.d.ts.map +1 -0
- package/dist/core/tools/powershell.js +39 -0
- package/dist/core/tools/powershell.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +20 -11
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/test.d.ts +54 -9
- package/dist/core/tools/test.d.ts.map +1 -1
- package/dist/core/tools/test.js +239 -19
- package/dist/core/tools/test.js.map +1 -1
- package/dist/core/tools/write.d.ts +7 -0
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +64 -9
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/trust-manager.d.ts.map +1 -1
- package/dist/core/trust-manager.js +2 -1
- package/dist/core/trust-manager.js.map +1 -1
- package/dist/core/verification-lifecycle.d.ts +77 -0
- package/dist/core/verification-lifecycle.d.ts.map +1 -0
- package/dist/core/verification-lifecycle.js +146 -0
- package/dist/core/verification-lifecycle.js.map +1 -0
- package/dist/core/workspace/artifacts.d.ts +45 -0
- package/dist/core/workspace/artifacts.d.ts.map +1 -0
- package/dist/core/workspace/artifacts.js +165 -0
- package/dist/core/workspace/artifacts.js.map +1 -0
- package/dist/core/workspace/comparison.d.ts +27 -0
- package/dist/core/workspace/comparison.d.ts.map +1 -0
- package/dist/core/workspace/comparison.js +94 -0
- package/dist/core/workspace/comparison.js.map +1 -0
- package/dist/core/workspace/git-observer.d.ts +48 -0
- package/dist/core/workspace/git-observer.d.ts.map +1 -0
- package/dist/core/workspace/git-observer.js +346 -0
- package/dist/core/workspace/git-observer.js.map +1 -0
- package/dist/core/workspace/projection.d.ts +3 -0
- package/dist/core/workspace/projection.d.ts.map +1 -0
- package/dist/core/workspace/projection.js +56 -0
- package/dist/core/workspace/projection.js.map +1 -0
- package/dist/core/workspace/state.d.ts +133 -0
- package/dist/core/workspace/state.d.ts.map +1 -0
- package/dist/core/workspace/state.js +96 -0
- package/dist/core/workspace/state.js.map +1 -0
- package/dist/extensions/llama/client.d.ts +6 -0
- package/dist/extensions/llama/client.d.ts.map +1 -1
- package/dist/extensions/llama/client.js +7 -0
- package/dist/extensions/llama/client.js.map +1 -1
- package/dist/extensions/llama/index.d.ts.map +1 -1
- package/dist/extensions/llama/index.js +2 -0
- package/dist/extensions/llama/index.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts +3 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +29 -4
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +6 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +58 -23
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +4 -3
- package/dist/migrations.js.map +1 -1
- package/dist/modes/acp/server.d.ts +55 -0
- package/dist/modes/acp/server.d.ts.map +1 -0
- package/dist/modes/acp/server.js +244 -0
- package/dist/modes/acp/server.js.map +1 -0
- package/dist/modes/acp/translate.d.ts +37 -0
- package/dist/modes/acp/translate.d.ts.map +1 -0
- package/dist/modes/acp/translate.js +50 -0
- package/dist/modes/acp/translate.js.map +1 -0
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +182 -23
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +20 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +20 -9
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +7 -3
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +19 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +70 -10
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +10 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +56 -14
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
- package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
- package/dist/modes/interactive/components/permission-preview.js +33 -0
- package/dist/modes/interactive/components/permission-preview.js.map +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +9 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +128 -56
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.d.ts +72 -0
- package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -0
- package/dist/modes/interactive/components/settings-submenu.js +165 -0
- package/dist/modes/interactive/components/settings-submenu.js.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +74 -17
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +23 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +64 -10
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +2 -1
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +45 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +395 -43
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +12 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +20 -9
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/json-event.d.ts +16 -13
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +20 -6
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +27 -12
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +12 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts +1 -0
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +196 -10
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +2 -1
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/management-http.d.ts +5 -3
- package/dist/utils/management-http.d.ts.map +1 -1
- package/dist/utils/management-http.js +14 -10
- package/dist/utils/management-http.js.map +1 -1
- package/dist/utils/shell.d.ts +3 -0
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +23 -9
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/syntax-highlight.d.ts +1 -0
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +60 -1
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/dist/utils/text.d.ts +8 -0
- package/dist/utils/text.d.ts.map +1 -0
- package/dist/utils/text.js +9 -0
- package/dist/utils/text.js.map +1 -0
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +6 -4
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +19 -0
- package/docs/custom-provider.md +12 -9
- package/docs/environment-variables.md +1 -1
- package/docs/extensions.md +35 -7
- package/docs/json.md +7 -2
- package/docs/keybindings.md +9 -9
- package/docs/llama-cpp.md +2 -0
- package/docs/models.md +8 -2
- package/docs/packages.md +2 -2
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +32 -3
- package/docs/sdk.md +32 -2
- package/docs/settings.md +23 -7
- package/docs/skills.md +4 -3
- package/docs/terminal-setup.md +23 -7
- package/docs/usage.md +10 -5
- package/docs/windows.md +24 -2
- package/examples/extensions/border-status-editor.ts +1 -1
- package/examples/extensions/built-in-tool-renderer.ts +8 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/git-checkpoint.ts +24 -36
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/minimal-mode.ts +2 -2
- package/examples/extensions/notify.ts +3 -1
- package/examples/extensions/permission-gate.ts +1 -1
- package/examples/extensions/plan-mode/index.ts +1 -1
- package/examples/extensions/rpc-demo.ts +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/README.md +1 -1
- package/examples/extensions/subagent/index.ts +6 -1
- package/examples/extensions/titlebar-spinner.ts +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/rpc-extension-ui.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/npm-shrinkwrap.json +439 -405
- package/package.json +17 -17
- package/dist/server/create-harness.d.ts +0 -26
- package/dist/server/create-harness.d.ts.map +0 -1
- package/dist/server/create-harness.js +0 -107
- package/dist/server/create-harness.js.map +0 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { WORKSPACE_RECORD_VERSION } from "./state.js";
|
|
3
|
+
/** Upper bound on `changedPaths` carried in one comparison record. */
|
|
4
|
+
export const MAX_CHANGED_PATHS = 200;
|
|
5
|
+
function pathTupleKey(path) {
|
|
6
|
+
return [
|
|
7
|
+
path.path,
|
|
8
|
+
path.kind,
|
|
9
|
+
path.staged ? "s" : "",
|
|
10
|
+
path.unstaged ? "u" : "",
|
|
11
|
+
path.ignored ? "i" : "",
|
|
12
|
+
path.contentHash ?? "",
|
|
13
|
+
].join("\u0000");
|
|
14
|
+
}
|
|
15
|
+
function changedPathsBetween(stored, fresh, warnings) {
|
|
16
|
+
const storedKeys = new Map();
|
|
17
|
+
for (const p of stored.paths)
|
|
18
|
+
storedKeys.set(p.path, pathTupleKey(p));
|
|
19
|
+
const freshKeys = new Map();
|
|
20
|
+
for (const p of fresh.paths)
|
|
21
|
+
freshKeys.set(p.path, pathTupleKey(p));
|
|
22
|
+
const changed = new Set();
|
|
23
|
+
for (const [path, key] of storedKeys) {
|
|
24
|
+
if (freshKeys.get(path) !== key)
|
|
25
|
+
changed.add(path);
|
|
26
|
+
}
|
|
27
|
+
for (const [path, key] of freshKeys) {
|
|
28
|
+
if (storedKeys.get(path) !== key)
|
|
29
|
+
changed.add(path);
|
|
30
|
+
}
|
|
31
|
+
const sorted = [...changed].sort();
|
|
32
|
+
if (sorted.length > MAX_CHANGED_PATHS) {
|
|
33
|
+
warnings.push(`changed path list truncated at ${MAX_CHANGED_PATHS} of ${sorted.length} entries`);
|
|
34
|
+
return sorted.slice(0, MAX_CHANGED_PATHS);
|
|
35
|
+
}
|
|
36
|
+
return sorted;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* WS.5: compare fresh workspace state against a stored observation (spec
|
|
40
|
+
* 2026-09-01-harness-correctness-and-workspace-state.md § 3). Pure: same
|
|
41
|
+
* inputs, same outcome. `unavailable` — the workspace could not be observed
|
|
42
|
+
* now; `inconclusive` — no reliable comparison could be established, so no
|
|
43
|
+
* drift claim is made in either direction; drift is never silently hidden:
|
|
44
|
+
* an old patch is never presented as current state.
|
|
45
|
+
*/
|
|
46
|
+
export function compareWorkspaceObservations(stored, fresh, options = {}) {
|
|
47
|
+
const warnings = [];
|
|
48
|
+
if (!fresh || fresh.status === "failed" || fresh.status === "unsupported") {
|
|
49
|
+
return { result: "unavailable", warnings };
|
|
50
|
+
}
|
|
51
|
+
if (fresh.status === "incomplete") {
|
|
52
|
+
warnings.push("fresh observation is incomplete; comparison is not reliable");
|
|
53
|
+
return { result: "inconclusive", warnings };
|
|
54
|
+
}
|
|
55
|
+
if (stored.status === "incomplete") {
|
|
56
|
+
warnings.push("stored observation is incomplete; comparison is not reliable");
|
|
57
|
+
return { result: "inconclusive", warnings };
|
|
58
|
+
}
|
|
59
|
+
if (stored.status !== "observed") {
|
|
60
|
+
warnings.push("stored observation has no comparable baseline");
|
|
61
|
+
return { result: "inconclusive", warnings };
|
|
62
|
+
}
|
|
63
|
+
const storedBase = stored.base;
|
|
64
|
+
const freshBase = fresh.base;
|
|
65
|
+
if (!storedBase?.worktreeDigest || !freshBase?.worktreeDigest) {
|
|
66
|
+
warnings.push("worktree digest missing on one side; comparison is not reliable");
|
|
67
|
+
return { result: "inconclusive", warnings };
|
|
68
|
+
}
|
|
69
|
+
const sameIndex = (storedBase.indexDigest ?? "") === (freshBase.indexDigest ?? "");
|
|
70
|
+
const sameWorktree = storedBase.worktreeDigest === freshBase.worktreeDigest;
|
|
71
|
+
if (stored.patchArtifactRef) {
|
|
72
|
+
const probe = options.artifactProbe ?? ((p) => existsSync(p));
|
|
73
|
+
if (!probe(stored.patchArtifactRef.file)) {
|
|
74
|
+
warnings.push(`stored workspace patch artifact ${stored.patchArtifactRef.artifactId} is missing on disk`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (sameIndex && sameWorktree) {
|
|
78
|
+
return { result: "same", warnings };
|
|
79
|
+
}
|
|
80
|
+
const changedPaths = changedPathsBetween(stored, fresh, warnings);
|
|
81
|
+
return { result: "drifted", changedPaths, warnings };
|
|
82
|
+
}
|
|
83
|
+
/** Build the record handed to appendWorkspaceComparison. */
|
|
84
|
+
export function buildWorkspaceComparison(comparedToObservationId, outcome, now = () => new Date().toISOString()) {
|
|
85
|
+
return {
|
|
86
|
+
version: WORKSPACE_RECORD_VERSION,
|
|
87
|
+
comparedAt: now(),
|
|
88
|
+
comparedToObservationId,
|
|
89
|
+
result: outcome.result,
|
|
90
|
+
changedPaths: outcome.changedPaths,
|
|
91
|
+
warnings: outcome.warnings,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=comparison.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comparison.js","sourceRoot":"","sources":["../../../src/core/workspace/comparison.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport type { WorkspaceStateComparison, WorkspaceStateRecord } from \"./state.ts\";\nimport { WORKSPACE_RECORD_VERSION } from \"./state.ts\";\n\n/** Upper bound on `changedPaths` carried in one comparison record. */\nexport const MAX_CHANGED_PATHS = 200;\n\nexport type WorkspaceComparisonOutcome = Pick<WorkspaceStateComparison, \"result\" | \"changedPaths\" | \"warnings\">;\n\nexport interface WorkspaceComparisonOptions {\n\t/** Probe for stored patch artifacts; defaults to existence on disk. */\n\tartifactProbe?: (storePath: string) => boolean;\n}\n\nfunction pathTupleKey(path: {\n\tpath: string;\n\tkind: string;\n\tstaged?: boolean;\n\tunstaged?: boolean;\n\tignored?: boolean;\n\tcontentHash?: string;\n}): string {\n\treturn [\n\t\tpath.path,\n\t\tpath.kind,\n\t\tpath.staged ? \"s\" : \"\",\n\t\tpath.unstaged ? \"u\" : \"\",\n\t\tpath.ignored ? \"i\" : \"\",\n\t\tpath.contentHash ?? \"\",\n\t].join(\"\\u0000\");\n}\n\nfunction changedPathsBetween(stored: WorkspaceStateRecord, fresh: WorkspaceStateRecord, warnings: string[]): string[] {\n\tconst storedKeys = new Map<string, string>();\n\tfor (const p of stored.paths) storedKeys.set(p.path, pathTupleKey(p));\n\tconst freshKeys = new Map<string, string>();\n\tfor (const p of fresh.paths) freshKeys.set(p.path, pathTupleKey(p));\n\n\tconst changed = new Set<string>();\n\tfor (const [path, key] of storedKeys) {\n\t\tif (freshKeys.get(path) !== key) changed.add(path);\n\t}\n\tfor (const [path, key] of freshKeys) {\n\t\tif (storedKeys.get(path) !== key) changed.add(path);\n\t}\n\tconst sorted = [...changed].sort();\n\tif (sorted.length > MAX_CHANGED_PATHS) {\n\t\twarnings.push(`changed path list truncated at ${MAX_CHANGED_PATHS} of ${sorted.length} entries`);\n\t\treturn sorted.slice(0, MAX_CHANGED_PATHS);\n\t}\n\treturn sorted;\n}\n\n/**\n * WS.5: compare fresh workspace state against a stored observation (spec\n * 2026-09-01-harness-correctness-and-workspace-state.md § 3). Pure: same\n * inputs, same outcome. `unavailable` — the workspace could not be observed\n * now; `inconclusive` — no reliable comparison could be established, so no\n * drift claim is made in either direction; drift is never silently hidden:\n * an old patch is never presented as current state.\n */\nexport function compareWorkspaceObservations(\n\tstored: WorkspaceStateRecord,\n\tfresh: WorkspaceStateRecord | undefined,\n\toptions: WorkspaceComparisonOptions = {},\n): WorkspaceComparisonOutcome {\n\tconst warnings: string[] = [];\n\n\tif (!fresh || fresh.status === \"failed\" || fresh.status === \"unsupported\") {\n\t\treturn { result: \"unavailable\", warnings };\n\t}\n\tif (fresh.status === \"incomplete\") {\n\t\twarnings.push(\"fresh observation is incomplete; comparison is not reliable\");\n\t\treturn { result: \"inconclusive\", warnings };\n\t}\n\tif (stored.status === \"incomplete\") {\n\t\twarnings.push(\"stored observation is incomplete; comparison is not reliable\");\n\t\treturn { result: \"inconclusive\", warnings };\n\t}\n\tif (stored.status !== \"observed\") {\n\t\twarnings.push(\"stored observation has no comparable baseline\");\n\t\treturn { result: \"inconclusive\", warnings };\n\t}\n\n\tconst storedBase = stored.base;\n\tconst freshBase = fresh.base;\n\tif (!storedBase?.worktreeDigest || !freshBase?.worktreeDigest) {\n\t\twarnings.push(\"worktree digest missing on one side; comparison is not reliable\");\n\t\treturn { result: \"inconclusive\", warnings };\n\t}\n\n\tconst sameIndex = (storedBase.indexDigest ?? \"\") === (freshBase.indexDigest ?? \"\");\n\tconst sameWorktree = storedBase.worktreeDigest === freshBase.worktreeDigest;\n\n\tif (stored.patchArtifactRef) {\n\t\tconst probe = options.artifactProbe ?? ((p: string) => existsSync(p));\n\t\tif (!probe(stored.patchArtifactRef.file)) {\n\t\t\twarnings.push(`stored workspace patch artifact ${stored.patchArtifactRef.artifactId} is missing on disk`);\n\t\t}\n\t}\n\n\tif (sameIndex && sameWorktree) {\n\t\treturn { result: \"same\", warnings };\n\t}\n\tconst changedPaths = changedPathsBetween(stored, fresh, warnings);\n\treturn { result: \"drifted\", changedPaths, warnings };\n}\n\n/** Shape of a persisted comparison record, minus what the caller supplies. */\nexport type WorkspaceComparisonFields = WorkspaceComparisonOutcome & {\n\tcomparedAt: string;\n\tcomparedToObservationId: string;\n\tversion: typeof WORKSPACE_RECORD_VERSION;\n};\n\n/** Build the record handed to appendWorkspaceComparison. */\nexport function buildWorkspaceComparison(\n\tcomparedToObservationId: string,\n\toutcome: WorkspaceComparisonOutcome,\n\tnow: () => string = () => new Date().toISOString(),\n): WorkspaceStateComparison {\n\treturn {\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tcomparedAt: now(),\n\t\tcomparedToObservationId,\n\t\tresult: outcome.result,\n\t\tchangedPaths: outcome.changedPaths,\n\t\twarnings: outcome.warnings,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type WorkspaceStateRecord } from "./state.ts";
|
|
2
|
+
export interface GitObserveOptions {
|
|
3
|
+
/** Compute per-path content hashes of present files (default true). */
|
|
4
|
+
hashPaths?: boolean;
|
|
5
|
+
/** Maximum number of reported paths (default 200). */
|
|
6
|
+
maxPaths?: number;
|
|
7
|
+
/** Skip hashing files larger than this many bytes (default 5 MiB). */
|
|
8
|
+
maxHashBytes?: number;
|
|
9
|
+
/** Per-command timeout in milliseconds (default 10 000). */
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
/** Cancels the observation; the record reports `failed`. */
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
/**
|
|
14
|
+
* Repository-relative path prefixes to omit from `paths` (exact match or
|
|
15
|
+
* directory prefix). Used for harness bookkeeping that lives inside the
|
|
16
|
+
* workspace — e.g. the session directory — which is not workspace state.
|
|
17
|
+
*/
|
|
18
|
+
excludePaths?: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Observe a Git workspace without mutating it. Returns a version-1
|
|
22
|
+
* `WorkspaceStateRecord`: `unsupported` for non-Git workspaces, `failed`
|
|
23
|
+
* when git could not run or the observation was cancelled, and `incomplete`
|
|
24
|
+
* when a limit truncated the capture. `observed` never implies more than
|
|
25
|
+
* `coverage` states.
|
|
26
|
+
*/
|
|
27
|
+
export interface ToplevelComparison {
|
|
28
|
+
/** Whether the physically resolved workspace root sits under the toplevel. */
|
|
29
|
+
inside: boolean;
|
|
30
|
+
/** Root path relative to the toplevel; strip this prefix from porcelain paths. */
|
|
31
|
+
prefix: string;
|
|
32
|
+
/** Physically resolved toplevel; base for absolute-path work like hashing. */
|
|
33
|
+
toplevelReal: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Decide containment of the workspace root inside git's toplevel after both
|
|
37
|
+
* sides are physically resolved. Windows spellings diverge from what Node's
|
|
38
|
+
* realpath returns for the same directory (short 8.3 names, drive-letter
|
|
39
|
+
* case), so the win32 branch resolves through the native resolver, folds
|
|
40
|
+
* case for the comparison, and uses win32 path semantics. The platform and
|
|
41
|
+
* resolver are injectable so tests can exercise those branches anywhere.
|
|
42
|
+
*/
|
|
43
|
+
export declare function compareToplevel(workspaceRoot: string, toplevel: string, options?: {
|
|
44
|
+
platform?: NodeJS.Platform;
|
|
45
|
+
realpath?: (path: string) => string;
|
|
46
|
+
}): ToplevelComparison;
|
|
47
|
+
export declare function observeWorkspaceGit(workspaceRoot: string, options?: GitObserveOptions): Promise<WorkspaceStateRecord>;
|
|
48
|
+
//# sourceMappingURL=git-observer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-observer.d.ts","sourceRoot":"","sources":["../../../src/core/workspace/git-observer.ts"],"names":[],"mappings":"AAIA,OAAO,EAIN,KAAK,oBAAoB,EACzB,MAAM,YAAY,CAAC;AAoBpB,MAAM,WAAW,iBAAiB;IACjC,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAmLD;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IAClC,8EAA8E;IAC9E,MAAM,EAAE,OAAO,CAAC;IAChB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,YAAY,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC9B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACT,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACpC,GACC,kBAAkB,CA2BpB;AAED,wBAAsB,mBAAmB,CACxC,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAyI/B","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { lstatSync, readFileSync, readlinkSync, realpathSync } from \"node:fs\";\nimport { dirname, isAbsolute, win32 as pathWin32, relative, sep } from \"node:path\";\nimport {\n\tWORKSPACE_RECORD_VERSION,\n\ttype WorkspaceStateCoverage,\n\ttype WorkspaceStatePath,\n\ttype WorkspaceStateRecord,\n} from \"./state.ts\";\n\n/**\n * Read-only Git workspace observation (spec 2026-09-01-harness-correctness-\n * and-workspace-state.md § 1).\n *\n * The adapter never mutates repository state: `HEAD`, the current branch, the\n * index, the worktree, the stash, and local config are only read. Optional\n * index locks are disabled (`GIT_OPTIONAL_LOCKS=0`) so even `git status`\n * cannot refresh the on-disk index. It never walks the workspace; all path\n * knowledge comes from git. Symlinks are hashed as link text (what git\n * stores), never followed. Submodule (gitlink) paths carry no worktree hash.\n */\n\nconst DEFAULT_MAX_PATHS = 200;\nconst DEFAULT_MAX_HASH_BYTES = 5 * 1024 * 1024;\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst GIT_CONFIG = [\"-c\", \"core.autocrlf=false\", \"-c\", \"status.renames=true\"];\nconst UNMERGED_XY = new Set([\"DD\", \"AU\", \"UD\", \"UA\", \"DU\", \"AA\", \"UU\"]);\n\nexport interface GitObserveOptions {\n\t/** Compute per-path content hashes of present files (default true). */\n\thashPaths?: boolean;\n\t/** Maximum number of reported paths (default 200). */\n\tmaxPaths?: number;\n\t/** Skip hashing files larger than this many bytes (default 5 MiB). */\n\tmaxHashBytes?: number;\n\t/** Per-command timeout in milliseconds (default 10 000). */\n\ttimeoutMs?: number;\n\t/** Cancels the observation; the record reports `failed`. */\n\tsignal?: AbortSignal;\n\t/**\n\t * Repository-relative path prefixes to omit from `paths` (exact match or\n\t * directory prefix). Used for harness bookkeeping that lives inside the\n\t * workspace — e.g. the session directory — which is not workspace state.\n\t */\n\texcludePaths?: string[];\n}\n\ninterface GitRunResult {\n\tok: boolean;\n\tcode: number | null;\n\tstdout: Buffer;\n\tstderr: string;\n\ttimedOut: boolean;\n\tspawnError: boolean;\n}\n\nfunction runGit(cwd: string, args: string[], timeoutMs: number, signal?: AbortSignal): Promise<GitRunResult> {\n\treturn new Promise((resolve) => {\n\t\tlet stdout = Buffer.alloc(0);\n\t\tlet stderr = \"\";\n\t\tlet timedOut = false;\n\t\tlet settled = false;\n\t\tlet child: ChildProcess;\n\t\tconst finish = (code: number | null, spawnError: boolean) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve({\n\t\t\t\tok: !spawnError && !timedOut && code === 0,\n\t\t\t\tcode,\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\ttimedOut,\n\t\t\t\tspawnError,\n\t\t\t});\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\ttimedOut = true;\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t}, timeoutMs);\n\t\ttry {\n\t\t\tchild = spawn(\"git\", [...GIT_CONFIG, ...args], {\n\t\t\t\tcwd,\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t// Observation must leave the on-disk index untouched.\n\t\t\t\tenv: { ...process.env, GIT_OPTIONAL_LOCKS: \"0\" },\n\t\t\t});\n\t\t} catch {\n\t\t\tresolve({ ok: false, code: null, stdout, stderr: \"\", timedOut: false, spawnError: true });\n\t\t\treturn;\n\t\t}\n\t\tif (signal) {\n\t\t\tif (signal.aborted) onAbort();\n\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t}\n\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstdout = Buffer.concat([stdout, chunk]);\n\t\t});\n\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstderr += chunk.toString(\"utf-8\");\n\t\t});\n\t\tchild.on(\"error\", () => finish(null, true));\n\t\tchild.on(\"close\", (code) => finish(code, false));\n\t});\n}\n\nfunction emptyCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: false,\n\t\tstaged: false,\n\t\tunstaged: false,\n\t\tuntracked: false,\n\t\tignored: false,\n\t\thashes: false,\n\t\tpatch: false,\n\t};\n}\n\nfunction fullCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: true,\n\t\tstaged: true,\n\t\tunstaged: true,\n\t\tuntracked: true,\n\t\tignored: true,\n\t\thashes: true,\n\t\tpatch: false,\n\t};\n}\n\ninterface ParsedStatusPath {\n\tx: string;\n\ty: string;\n\tpath: string;\n\tpreviousPath?: string;\n}\n\nfunction parsePorcelainZ(stdout: Buffer): ParsedStatusPath[] {\n\tconst fields = stdout.toString(\"utf-8\").split(\"\\0\");\n\tconst parsed: ParsedStatusPath[] = [];\n\tlet i = 0;\n\twhile (i < fields.length) {\n\t\tconst record = fields[i++];\n\t\tif (!record || record.length < 4) continue;\n\t\tconst entry: ParsedStatusPath = { x: record[0], y: record[1], path: record.slice(3) };\n\t\tif (entry.x === \"R\" || entry.x === \"C\" || entry.y === \"R\" || entry.y === \"C\") {\n\t\t\tentry.previousPath = fields[i++];\n\t\t}\n\t\tparsed.push(entry);\n\t}\n\treturn parsed;\n}\n\nfunction classify(entry: ParsedStatusPath): {\n\tkind: WorkspaceStatePath[\"kind\"];\n\tstaged: boolean;\n\tunstaged: boolean;\n\tignored: boolean;\n} {\n\tconst { x, y } = entry;\n\tif (x === \"?\" && y === \"?\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: false };\n\t}\n\tif (x === \"!\" && y === \"!\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: true };\n\t}\n\tif (UNMERGED_XY.has(`${x}${y}`)) {\n\t\treturn {\n\t\t\tkind: x === \"D\" && y === \"D\" ? \"deleted\" : \"modified\",\n\t\t\tstaged: true,\n\t\t\tunstaged: true,\n\t\t\tignored: false,\n\t\t};\n\t}\n\tlet kind: WorkspaceStatePath[\"kind\"] = \"modified\";\n\tif (x === \"A\" || y === \"A\" || x === \"C\" || y === \"C\") kind = \"added\";\n\telse if (x === \"D\" || y === \"D\") kind = \"deleted\";\n\telse if (x === \"R\" || y === \"R\") kind = \"renamed\";\n\treturn {\n\t\tkind,\n\t\tstaged: x !== \" \" && x !== \"?\" && x !== \"!\",\n\t\tunstaged: y !== \" \" && y !== \"?\" && y !== \"!\",\n\t\tignored: false,\n\t};\n}\n\n/** Hash one workspace path: regular files by bytes, symlinks by link text. */\nfunction hashWorkspacePath(\n\ttoplevel: string,\n\tpath: string,\n\tmaxBytes: number,\n): { hash?: string; skipped: \"size\" | \"error\" | false } {\n\ttry {\n\t\tconst full = `${toplevel}/${path}`;\n\t\tconst stat = lstatSync(full);\n\t\tif (stat.isFile()) {\n\t\t\tif (stat.size > maxBytes) return { skipped: \"size\" };\n\t\t\treturn { hash: `sha256:${createHash(\"sha256\").update(readFileSync(full)).digest(\"hex\")}`, skipped: false };\n\t\t}\n\t\tif (stat.isSymbolicLink()) {\n\t\t\treturn {\n\t\t\t\thash: `sha256:${createHash(\"sha256\").update(readlinkSync(full)).digest(\"hex\")}`,\n\t\t\t\tskipped: false,\n\t\t\t};\n\t\t}\n\t\t// Directories (submodule gitlinks) have no worktree content to hash;\n\t\t// that is expected, not a coverage gap.\n\t\tif (stat.isDirectory()) return { skipped: false };\n\t\treturn { skipped: \"error\" };\n\t} catch {\n\t\treturn { skipped: \"error\" };\n\t}\n}\n\nfunction prefixRelative(prefix: string, path: string): string {\n\tif (prefix === \"\" || prefix === \".\") return path;\n\treturn `${prefix}/${path}`;\n}\n\n/**\n * Observe a Git workspace without mutating it. Returns a version-1\n * `WorkspaceStateRecord`: `unsupported` for non-Git workspaces, `failed`\n * when git could not run or the observation was cancelled, and `incomplete`\n * when a limit truncated the capture. `observed` never implies more than\n * `coverage` states.\n */\nexport interface ToplevelComparison {\n\t/** Whether the physically resolved workspace root sits under the toplevel. */\n\tinside: boolean;\n\t/** Root path relative to the toplevel; strip this prefix from porcelain paths. */\n\tprefix: string;\n\t/** Physically resolved toplevel; base for absolute-path work like hashing. */\n\ttoplevelReal: string;\n}\n\n/**\n * Decide containment of the workspace root inside git's toplevel after both\n * sides are physically resolved. Windows spellings diverge from what Node's\n * realpath returns for the same directory (short 8.3 names, drive-letter\n * case), so the win32 branch resolves through the native resolver, folds\n * case for the comparison, and uses win32 path semantics. The platform and\n * resolver are injectable so tests can exercise those branches anywhere.\n */\nexport function compareToplevel(\n\tworkspaceRoot: string,\n\ttoplevel: string,\n\toptions?: {\n\t\tplatform?: NodeJS.Platform;\n\t\trealpath?: (path: string) => string;\n\t},\n): ToplevelComparison {\n\tconst platform = options?.platform ?? process.platform;\n\tconst isWin32 = platform === \"win32\";\n\tconst realpath =\n\t\toptions?.realpath ??\n\t\t(isWin32\n\t\t\t? (p: string) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn realpathSync.native(p);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn realpathSync(p);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: realpathSync);\n\tconst rootReal = realpath(workspaceRoot);\n\tconst toplevelReal = realpath(toplevel);\n\tconst pathMod = isWin32 ? pathWin32 : { relative, sep, dirname };\n\tconst fold = (p: string) => (isWin32 ? p.toLowerCase() : p);\n\tconst prefix = pathMod.relative(toplevelReal, rootReal);\n\tconst inside = !(prefix === \"..\" || prefix.startsWith(`..${pathMod.sep}`) || isAbsolute(prefix));\n\tif (isWin32 && inside) {\n\t\t// Recompute the strip prefix under case folding so a divergent drive or\n\t\t// directory case still yields the clean \"\" or relative form.\n\t\tconst folded = pathMod.relative(fold(toplevelReal), fold(rootReal));\n\t\treturn { inside, prefix: folded, toplevelReal };\n\t}\n\treturn { inside, prefix, toplevelReal };\n}\n\nexport async function observeWorkspaceGit(\n\tworkspaceRoot: string,\n\toptions?: GitObserveOptions,\n): Promise<WorkspaceStateRecord> {\n\tconst maxPaths = options?.maxPaths ?? DEFAULT_MAX_PATHS;\n\tconst maxHashBytes = options?.maxHashBytes ?? DEFAULT_MAX_HASH_BYTES;\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst hashPaths = options?.hashPaths ?? true;\n\tconst signal = options?.signal;\n\tconst excludePaths = (options?.excludePaths ?? [])\n\t\t.map((p) => p.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/+$/, \"\"))\n\t\t.filter((p) => p.length > 0);\n\tconst isExcluded = (repoPath: string): boolean =>\n\t\texcludePaths.some((ex) => repoPath === ex || repoPath.startsWith(`${ex}/`));\n\n\tconst warnings: string[] = [];\n\tconst failed = (message: string): WorkspaceStateRecord => ({\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: \"failed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\tcoverage: emptyCoverage(),\n\t\tpaths: [],\n\t\twarnings: [message],\n\t});\n\n\tif (signal?.aborted) {\n\t\treturn failed(\"workspace observation cancelled\");\n\t}\n\n\tconst head = await runGit(workspaceRoot, [\"rev-parse\", \"--show-toplevel\"], timeoutMs, signal);\n\tif (head.spawnError) return failed(\"git could not be executed\");\n\tif (head.timedOut) return failed(\"workspace observation timed out\");\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (!head.ok) {\n\t\tif (/not a git repository/i.test(head.stderr) || head.code === 128) {\n\t\t\treturn {\n\t\t\t\t...failed(\"not a git repository\"),\n\t\t\t\tstatus: \"unsupported\",\n\t\t\t};\n\t\t}\n\t\treturn failed(\"git rev-parse failed\");\n\t}\n\t// git resolves the toplevel physically (macOS /tmp -> /private/tmp,\n\t// Windows drive-letter case and short names), so containment is decided\n\t// through compareToplevel; otherwise a divergent spelling of the same\n\t// directory would fail the observation or corrupt every reported path.\n\tconst toplevel = head.stdout.toString(\"utf-8\").trim();\n\tconst comparison = compareToplevel(workspaceRoot, toplevel);\n\tif (!comparison.inside) {\n\t\treturn failed(\"workspace root is outside the repository toplevel\");\n\t}\n\tconst insidePrefix = comparison.prefix;\n\n\tconst headCommit = await runGit(workspaceRoot, [\"rev-parse\", \"--verify\", \"HEAD\"], timeoutMs, signal);\n\tconst mergeHead = await runGit(workspaceRoot, [\"rev-parse\", \"-q\", \"--verify\", \"MERGE_HEAD\"], timeoutMs, signal);\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (mergeHead.ok) warnings.push(\"merge in progress\");\n\tconst branch = await runGit(workspaceRoot, [\"branch\", \"--show-current\"], timeoutMs, signal);\n\n\tconst headCommitValue = headCommit.ok ? headCommit.stdout.toString(\"utf-8\").trim() : undefined;\n\tif (!headCommit.ok) warnings.push(\"unborn HEAD (no commits)\");\n\tconst branchValue = branch.ok ? branch.stdout.toString(\"utf-8\").trim() : \"\";\n\tif (branchValue === \"\") warnings.push(\"detached HEAD\");\n\n\t// Index baseline.\n\tconst index = await runGit(workspaceRoot, [\"ls-files\", \"--stage\", \"-z\"], timeoutMs, signal);\n\tif (!index.ok) return failed(\"git ls-files failed\");\n\tconst indexDigest = `sha256:${createHash(\"sha256\").update(index.stdout).digest(\"hex\")}`;\n\n\t// Staged / unstaged / untracked / ignored change sets.\n\tconst status = await runGit(\n\t\tworkspaceRoot,\n\t\t[\"status\", \"--porcelain\", \"-z\", \"--untracked-files=all\", \"--ignored=matching\"],\n\t\ttimeoutMs,\n\t\tsignal,\n\t);\n\tif (!status.ok) return failed(\"git status failed\");\n\n\tconst paths: WorkspaceStatePath[] = [];\n\tlet truncated = false;\n\tlet skippedHashes = 0;\n\tfor (const entry of parsePorcelainZ(status.stdout)) {\n\t\tif (isExcluded(entry.path) || (entry.previousPath !== undefined && isExcluded(entry.previousPath))) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (paths.length >= maxPaths) {\n\t\t\ttruncated = true;\n\t\t\twarnings.push(`path list truncated at ${maxPaths} paths`);\n\t\t\tbreak;\n\t\t}\n\t\tconst { kind, staged, unstaged, ignored } = classify(entry);\n\t\tconst workspacePath: WorkspaceStatePath = {\n\t\t\tpath: prefixRelative(insidePrefix, entry.path),\n\t\t\tkind,\n\t\t\tstaged,\n\t\t\tunstaged,\n\t\t\t...(ignored ? { ignored: true } : {}),\n\t\t\t...(entry.previousPath ? { previousPath: prefixRelative(insidePrefix, entry.previousPath) } : {}),\n\t\t};\n\t\tif (hashPaths && kind !== \"deleted\") {\n\t\t\tconst { hash, skipped } = hashWorkspacePath(comparison.toplevelReal, entry.path, maxHashBytes);\n\t\t\tif (hash) workspacePath.contentHash = hash;\n\t\t\telse if (skipped) skippedHashes++;\n\t\t}\n\t\tpaths.push(workspacePath);\n\t}\n\tif (skippedHashes > 0) warnings.push(`content hashing skipped for ${skippedHashes} path(s)`);\n\n\t// Digest over exactly the paths this record includes, so a later\n\t// comparison can cheaply detect that the covered state moved.\n\tconst digestInput = paths\n\t\t.map(\n\t\t\t(p) =>\n\t\t\t\t`${p.path}\\0${p.kind}\\0${p.staged ? \"S\" : \"\"}${p.unstaged ? \"U\" : \"\"}${p.ignored ? \"I\" : \"\"}\\0${p.contentHash ?? \"\"}\\n`,\n\t\t)\n\t\t.sort()\n\t\t.join(\"\");\n\n\treturn {\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: truncated || skippedHashes > 0 ? \"incomplete\" : \"observed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\t// `hashes` reflects the configured policy: a disabled hash pass is a\n\t\t// coverage statement, not an incomplete capture.\n\t\tcoverage: { ...fullCoverage(), hashes: hashPaths },\n\t\tbase: {\n\t\t\t...(headCommitValue ? { headCommit: headCommitValue } : {}),\n\t\t\t...(branchValue ? { branch: branchValue } : {}),\n\t\t\tindexDigest,\n\t\t\t...(truncated ? {} : { worktreeDigest: `sha256:${createHash(\"sha256\").update(digestInput).digest(\"hex\")}` }),\n\t\t},\n\t\tpaths,\n\t\twarnings,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, win32 as pathWin32, relative, sep } from "node:path";
|
|
5
|
+
import { WORKSPACE_RECORD_VERSION, } from "./state.js";
|
|
6
|
+
/**
|
|
7
|
+
* Read-only Git workspace observation (spec 2026-09-01-harness-correctness-
|
|
8
|
+
* and-workspace-state.md § 1).
|
|
9
|
+
*
|
|
10
|
+
* The adapter never mutates repository state: `HEAD`, the current branch, the
|
|
11
|
+
* index, the worktree, the stash, and local config are only read. Optional
|
|
12
|
+
* index locks are disabled (`GIT_OPTIONAL_LOCKS=0`) so even `git status`
|
|
13
|
+
* cannot refresh the on-disk index. It never walks the workspace; all path
|
|
14
|
+
* knowledge comes from git. Symlinks are hashed as link text (what git
|
|
15
|
+
* stores), never followed. Submodule (gitlink) paths carry no worktree hash.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_MAX_PATHS = 200;
|
|
18
|
+
const DEFAULT_MAX_HASH_BYTES = 5 * 1024 * 1024;
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
20
|
+
const GIT_CONFIG = ["-c", "core.autocrlf=false", "-c", "status.renames=true"];
|
|
21
|
+
const UNMERGED_XY = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
22
|
+
function runGit(cwd, args, timeoutMs, signal) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
let stdout = Buffer.alloc(0);
|
|
25
|
+
let stderr = "";
|
|
26
|
+
let timedOut = false;
|
|
27
|
+
let settled = false;
|
|
28
|
+
let child;
|
|
29
|
+
const finish = (code, spawnError) => {
|
|
30
|
+
if (settled)
|
|
31
|
+
return;
|
|
32
|
+
settled = true;
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
signal?.removeEventListener("abort", onAbort);
|
|
35
|
+
resolve({
|
|
36
|
+
ok: !spawnError && !timedOut && code === 0,
|
|
37
|
+
code,
|
|
38
|
+
stdout,
|
|
39
|
+
stderr,
|
|
40
|
+
timedOut,
|
|
41
|
+
spawnError,
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const onAbort = () => {
|
|
45
|
+
child?.kill("SIGKILL");
|
|
46
|
+
};
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
timedOut = true;
|
|
49
|
+
child?.kill("SIGKILL");
|
|
50
|
+
}, timeoutMs);
|
|
51
|
+
try {
|
|
52
|
+
child = spawn("git", [...GIT_CONFIG, ...args], {
|
|
53
|
+
cwd,
|
|
54
|
+
shell: false,
|
|
55
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
56
|
+
// Observation must leave the on-disk index untouched.
|
|
57
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
resolve({ ok: false, code: null, stdout, stderr: "", timedOut: false, spawnError: true });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (signal) {
|
|
65
|
+
if (signal.aborted)
|
|
66
|
+
onAbort();
|
|
67
|
+
else
|
|
68
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
69
|
+
}
|
|
70
|
+
child.stdout?.on("data", (chunk) => {
|
|
71
|
+
stdout = Buffer.concat([stdout, chunk]);
|
|
72
|
+
});
|
|
73
|
+
child.stderr?.on("data", (chunk) => {
|
|
74
|
+
stderr += chunk.toString("utf-8");
|
|
75
|
+
});
|
|
76
|
+
child.on("error", () => finish(null, true));
|
|
77
|
+
child.on("close", (code) => finish(code, false));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function emptyCoverage() {
|
|
81
|
+
return {
|
|
82
|
+
tracked: false,
|
|
83
|
+
staged: false,
|
|
84
|
+
unstaged: false,
|
|
85
|
+
untracked: false,
|
|
86
|
+
ignored: false,
|
|
87
|
+
hashes: false,
|
|
88
|
+
patch: false,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function fullCoverage() {
|
|
92
|
+
return {
|
|
93
|
+
tracked: true,
|
|
94
|
+
staged: true,
|
|
95
|
+
unstaged: true,
|
|
96
|
+
untracked: true,
|
|
97
|
+
ignored: true,
|
|
98
|
+
hashes: true,
|
|
99
|
+
patch: false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function parsePorcelainZ(stdout) {
|
|
103
|
+
const fields = stdout.toString("utf-8").split("\0");
|
|
104
|
+
const parsed = [];
|
|
105
|
+
let i = 0;
|
|
106
|
+
while (i < fields.length) {
|
|
107
|
+
const record = fields[i++];
|
|
108
|
+
if (!record || record.length < 4)
|
|
109
|
+
continue;
|
|
110
|
+
const entry = { x: record[0], y: record[1], path: record.slice(3) };
|
|
111
|
+
if (entry.x === "R" || entry.x === "C" || entry.y === "R" || entry.y === "C") {
|
|
112
|
+
entry.previousPath = fields[i++];
|
|
113
|
+
}
|
|
114
|
+
parsed.push(entry);
|
|
115
|
+
}
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
function classify(entry) {
|
|
119
|
+
const { x, y } = entry;
|
|
120
|
+
if (x === "?" && y === "?") {
|
|
121
|
+
return { kind: "untracked", staged: false, unstaged: false, ignored: false };
|
|
122
|
+
}
|
|
123
|
+
if (x === "!" && y === "!") {
|
|
124
|
+
return { kind: "untracked", staged: false, unstaged: false, ignored: true };
|
|
125
|
+
}
|
|
126
|
+
if (UNMERGED_XY.has(`${x}${y}`)) {
|
|
127
|
+
return {
|
|
128
|
+
kind: x === "D" && y === "D" ? "deleted" : "modified",
|
|
129
|
+
staged: true,
|
|
130
|
+
unstaged: true,
|
|
131
|
+
ignored: false,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
let kind = "modified";
|
|
135
|
+
if (x === "A" || y === "A" || x === "C" || y === "C")
|
|
136
|
+
kind = "added";
|
|
137
|
+
else if (x === "D" || y === "D")
|
|
138
|
+
kind = "deleted";
|
|
139
|
+
else if (x === "R" || y === "R")
|
|
140
|
+
kind = "renamed";
|
|
141
|
+
return {
|
|
142
|
+
kind,
|
|
143
|
+
staged: x !== " " && x !== "?" && x !== "!",
|
|
144
|
+
unstaged: y !== " " && y !== "?" && y !== "!",
|
|
145
|
+
ignored: false,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** Hash one workspace path: regular files by bytes, symlinks by link text. */
|
|
149
|
+
function hashWorkspacePath(toplevel, path, maxBytes) {
|
|
150
|
+
try {
|
|
151
|
+
const full = `${toplevel}/${path}`;
|
|
152
|
+
const stat = lstatSync(full);
|
|
153
|
+
if (stat.isFile()) {
|
|
154
|
+
if (stat.size > maxBytes)
|
|
155
|
+
return { skipped: "size" };
|
|
156
|
+
return { hash: `sha256:${createHash("sha256").update(readFileSync(full)).digest("hex")}`, skipped: false };
|
|
157
|
+
}
|
|
158
|
+
if (stat.isSymbolicLink()) {
|
|
159
|
+
return {
|
|
160
|
+
hash: `sha256:${createHash("sha256").update(readlinkSync(full)).digest("hex")}`,
|
|
161
|
+
skipped: false,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// Directories (submodule gitlinks) have no worktree content to hash;
|
|
165
|
+
// that is expected, not a coverage gap.
|
|
166
|
+
if (stat.isDirectory())
|
|
167
|
+
return { skipped: false };
|
|
168
|
+
return { skipped: "error" };
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return { skipped: "error" };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function prefixRelative(prefix, path) {
|
|
175
|
+
if (prefix === "" || prefix === ".")
|
|
176
|
+
return path;
|
|
177
|
+
return `${prefix}/${path}`;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Decide containment of the workspace root inside git's toplevel after both
|
|
181
|
+
* sides are physically resolved. Windows spellings diverge from what Node's
|
|
182
|
+
* realpath returns for the same directory (short 8.3 names, drive-letter
|
|
183
|
+
* case), so the win32 branch resolves through the native resolver, folds
|
|
184
|
+
* case for the comparison, and uses win32 path semantics. The platform and
|
|
185
|
+
* resolver are injectable so tests can exercise those branches anywhere.
|
|
186
|
+
*/
|
|
187
|
+
export function compareToplevel(workspaceRoot, toplevel, options) {
|
|
188
|
+
const platform = options?.platform ?? process.platform;
|
|
189
|
+
const isWin32 = platform === "win32";
|
|
190
|
+
const realpath = options?.realpath ??
|
|
191
|
+
(isWin32
|
|
192
|
+
? (p) => {
|
|
193
|
+
try {
|
|
194
|
+
return realpathSync.native(p);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return realpathSync(p);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
: realpathSync);
|
|
201
|
+
const rootReal = realpath(workspaceRoot);
|
|
202
|
+
const toplevelReal = realpath(toplevel);
|
|
203
|
+
const pathMod = isWin32 ? pathWin32 : { relative, sep, dirname };
|
|
204
|
+
const fold = (p) => (isWin32 ? p.toLowerCase() : p);
|
|
205
|
+
const prefix = pathMod.relative(toplevelReal, rootReal);
|
|
206
|
+
const inside = !(prefix === ".." || prefix.startsWith(`..${pathMod.sep}`) || isAbsolute(prefix));
|
|
207
|
+
if (isWin32 && inside) {
|
|
208
|
+
// Recompute the strip prefix under case folding so a divergent drive or
|
|
209
|
+
// directory case still yields the clean "" or relative form.
|
|
210
|
+
const folded = pathMod.relative(fold(toplevelReal), fold(rootReal));
|
|
211
|
+
return { inside, prefix: folded, toplevelReal };
|
|
212
|
+
}
|
|
213
|
+
return { inside, prefix, toplevelReal };
|
|
214
|
+
}
|
|
215
|
+
export async function observeWorkspaceGit(workspaceRoot, options) {
|
|
216
|
+
const maxPaths = options?.maxPaths ?? DEFAULT_MAX_PATHS;
|
|
217
|
+
const maxHashBytes = options?.maxHashBytes ?? DEFAULT_MAX_HASH_BYTES;
|
|
218
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
219
|
+
const hashPaths = options?.hashPaths ?? true;
|
|
220
|
+
const signal = options?.signal;
|
|
221
|
+
const excludePaths = (options?.excludePaths ?? [])
|
|
222
|
+
.map((p) => p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, ""))
|
|
223
|
+
.filter((p) => p.length > 0);
|
|
224
|
+
const isExcluded = (repoPath) => excludePaths.some((ex) => repoPath === ex || repoPath.startsWith(`${ex}/`));
|
|
225
|
+
const warnings = [];
|
|
226
|
+
const failed = (message) => ({
|
|
227
|
+
version: WORKSPACE_RECORD_VERSION,
|
|
228
|
+
observationId: randomUUID(),
|
|
229
|
+
status: "failed",
|
|
230
|
+
backend: "git",
|
|
231
|
+
workspaceRoot,
|
|
232
|
+
capturedAt: new Date().toISOString(),
|
|
233
|
+
coverage: emptyCoverage(),
|
|
234
|
+
paths: [],
|
|
235
|
+
warnings: [message],
|
|
236
|
+
});
|
|
237
|
+
if (signal?.aborted) {
|
|
238
|
+
return failed("workspace observation cancelled");
|
|
239
|
+
}
|
|
240
|
+
const head = await runGit(workspaceRoot, ["rev-parse", "--show-toplevel"], timeoutMs, signal);
|
|
241
|
+
if (head.spawnError)
|
|
242
|
+
return failed("git could not be executed");
|
|
243
|
+
if (head.timedOut)
|
|
244
|
+
return failed("workspace observation timed out");
|
|
245
|
+
if (signal?.aborted)
|
|
246
|
+
return failed("workspace observation cancelled");
|
|
247
|
+
if (!head.ok) {
|
|
248
|
+
if (/not a git repository/i.test(head.stderr) || head.code === 128) {
|
|
249
|
+
return {
|
|
250
|
+
...failed("not a git repository"),
|
|
251
|
+
status: "unsupported",
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return failed("git rev-parse failed");
|
|
255
|
+
}
|
|
256
|
+
// git resolves the toplevel physically (macOS /tmp -> /private/tmp,
|
|
257
|
+
// Windows drive-letter case and short names), so containment is decided
|
|
258
|
+
// through compareToplevel; otherwise a divergent spelling of the same
|
|
259
|
+
// directory would fail the observation or corrupt every reported path.
|
|
260
|
+
const toplevel = head.stdout.toString("utf-8").trim();
|
|
261
|
+
const comparison = compareToplevel(workspaceRoot, toplevel);
|
|
262
|
+
if (!comparison.inside) {
|
|
263
|
+
return failed("workspace root is outside the repository toplevel");
|
|
264
|
+
}
|
|
265
|
+
const insidePrefix = comparison.prefix;
|
|
266
|
+
const headCommit = await runGit(workspaceRoot, ["rev-parse", "--verify", "HEAD"], timeoutMs, signal);
|
|
267
|
+
const mergeHead = await runGit(workspaceRoot, ["rev-parse", "-q", "--verify", "MERGE_HEAD"], timeoutMs, signal);
|
|
268
|
+
if (signal?.aborted)
|
|
269
|
+
return failed("workspace observation cancelled");
|
|
270
|
+
if (mergeHead.ok)
|
|
271
|
+
warnings.push("merge in progress");
|
|
272
|
+
const branch = await runGit(workspaceRoot, ["branch", "--show-current"], timeoutMs, signal);
|
|
273
|
+
const headCommitValue = headCommit.ok ? headCommit.stdout.toString("utf-8").trim() : undefined;
|
|
274
|
+
if (!headCommit.ok)
|
|
275
|
+
warnings.push("unborn HEAD (no commits)");
|
|
276
|
+
const branchValue = branch.ok ? branch.stdout.toString("utf-8").trim() : "";
|
|
277
|
+
if (branchValue === "")
|
|
278
|
+
warnings.push("detached HEAD");
|
|
279
|
+
// Index baseline.
|
|
280
|
+
const index = await runGit(workspaceRoot, ["ls-files", "--stage", "-z"], timeoutMs, signal);
|
|
281
|
+
if (!index.ok)
|
|
282
|
+
return failed("git ls-files failed");
|
|
283
|
+
const indexDigest = `sha256:${createHash("sha256").update(index.stdout).digest("hex")}`;
|
|
284
|
+
// Staged / unstaged / untracked / ignored change sets.
|
|
285
|
+
const status = await runGit(workspaceRoot, ["status", "--porcelain", "-z", "--untracked-files=all", "--ignored=matching"], timeoutMs, signal);
|
|
286
|
+
if (!status.ok)
|
|
287
|
+
return failed("git status failed");
|
|
288
|
+
const paths = [];
|
|
289
|
+
let truncated = false;
|
|
290
|
+
let skippedHashes = 0;
|
|
291
|
+
for (const entry of parsePorcelainZ(status.stdout)) {
|
|
292
|
+
if (isExcluded(entry.path) || (entry.previousPath !== undefined && isExcluded(entry.previousPath))) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (paths.length >= maxPaths) {
|
|
296
|
+
truncated = true;
|
|
297
|
+
warnings.push(`path list truncated at ${maxPaths} paths`);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
const { kind, staged, unstaged, ignored } = classify(entry);
|
|
301
|
+
const workspacePath = {
|
|
302
|
+
path: prefixRelative(insidePrefix, entry.path),
|
|
303
|
+
kind,
|
|
304
|
+
staged,
|
|
305
|
+
unstaged,
|
|
306
|
+
...(ignored ? { ignored: true } : {}),
|
|
307
|
+
...(entry.previousPath ? { previousPath: prefixRelative(insidePrefix, entry.previousPath) } : {}),
|
|
308
|
+
};
|
|
309
|
+
if (hashPaths && kind !== "deleted") {
|
|
310
|
+
const { hash, skipped } = hashWorkspacePath(comparison.toplevelReal, entry.path, maxHashBytes);
|
|
311
|
+
if (hash)
|
|
312
|
+
workspacePath.contentHash = hash;
|
|
313
|
+
else if (skipped)
|
|
314
|
+
skippedHashes++;
|
|
315
|
+
}
|
|
316
|
+
paths.push(workspacePath);
|
|
317
|
+
}
|
|
318
|
+
if (skippedHashes > 0)
|
|
319
|
+
warnings.push(`content hashing skipped for ${skippedHashes} path(s)`);
|
|
320
|
+
// Digest over exactly the paths this record includes, so a later
|
|
321
|
+
// comparison can cheaply detect that the covered state moved.
|
|
322
|
+
const digestInput = paths
|
|
323
|
+
.map((p) => `${p.path}\0${p.kind}\0${p.staged ? "S" : ""}${p.unstaged ? "U" : ""}${p.ignored ? "I" : ""}\0${p.contentHash ?? ""}\n`)
|
|
324
|
+
.sort()
|
|
325
|
+
.join("");
|
|
326
|
+
return {
|
|
327
|
+
version: WORKSPACE_RECORD_VERSION,
|
|
328
|
+
observationId: randomUUID(),
|
|
329
|
+
status: truncated || skippedHashes > 0 ? "incomplete" : "observed",
|
|
330
|
+
backend: "git",
|
|
331
|
+
workspaceRoot,
|
|
332
|
+
capturedAt: new Date().toISOString(),
|
|
333
|
+
// `hashes` reflects the configured policy: a disabled hash pass is a
|
|
334
|
+
// coverage statement, not an incomplete capture.
|
|
335
|
+
coverage: { ...fullCoverage(), hashes: hashPaths },
|
|
336
|
+
base: {
|
|
337
|
+
...(headCommitValue ? { headCommit: headCommitValue } : {}),
|
|
338
|
+
...(branchValue ? { branch: branchValue } : {}),
|
|
339
|
+
indexDigest,
|
|
340
|
+
...(truncated ? {} : { worktreeDigest: `sha256:${createHash("sha256").update(digestInput).digest("hex")}` }),
|
|
341
|
+
},
|
|
342
|
+
paths,
|
|
343
|
+
warnings,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
//# sourceMappingURL=git-observer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-observer.js","sourceRoot":"","sources":["../../../src/core/workspace/git-observer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { lstatSync, readFileSync, readlinkSync, realpathSync } from \"node:fs\";\nimport { dirname, isAbsolute, win32 as pathWin32, relative, sep } from \"node:path\";\nimport {\n\tWORKSPACE_RECORD_VERSION,\n\ttype WorkspaceStateCoverage,\n\ttype WorkspaceStatePath,\n\ttype WorkspaceStateRecord,\n} from \"./state.ts\";\n\n/**\n * Read-only Git workspace observation (spec 2026-09-01-harness-correctness-\n * and-workspace-state.md § 1).\n *\n * The adapter never mutates repository state: `HEAD`, the current branch, the\n * index, the worktree, the stash, and local config are only read. Optional\n * index locks are disabled (`GIT_OPTIONAL_LOCKS=0`) so even `git status`\n * cannot refresh the on-disk index. It never walks the workspace; all path\n * knowledge comes from git. Symlinks are hashed as link text (what git\n * stores), never followed. Submodule (gitlink) paths carry no worktree hash.\n */\n\nconst DEFAULT_MAX_PATHS = 200;\nconst DEFAULT_MAX_HASH_BYTES = 5 * 1024 * 1024;\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst GIT_CONFIG = [\"-c\", \"core.autocrlf=false\", \"-c\", \"status.renames=true\"];\nconst UNMERGED_XY = new Set([\"DD\", \"AU\", \"UD\", \"UA\", \"DU\", \"AA\", \"UU\"]);\n\nexport interface GitObserveOptions {\n\t/** Compute per-path content hashes of present files (default true). */\n\thashPaths?: boolean;\n\t/** Maximum number of reported paths (default 200). */\n\tmaxPaths?: number;\n\t/** Skip hashing files larger than this many bytes (default 5 MiB). */\n\tmaxHashBytes?: number;\n\t/** Per-command timeout in milliseconds (default 10 000). */\n\ttimeoutMs?: number;\n\t/** Cancels the observation; the record reports `failed`. */\n\tsignal?: AbortSignal;\n\t/**\n\t * Repository-relative path prefixes to omit from `paths` (exact match or\n\t * directory prefix). Used for harness bookkeeping that lives inside the\n\t * workspace — e.g. the session directory — which is not workspace state.\n\t */\n\texcludePaths?: string[];\n}\n\ninterface GitRunResult {\n\tok: boolean;\n\tcode: number | null;\n\tstdout: Buffer;\n\tstderr: string;\n\ttimedOut: boolean;\n\tspawnError: boolean;\n}\n\nfunction runGit(cwd: string, args: string[], timeoutMs: number, signal?: AbortSignal): Promise<GitRunResult> {\n\treturn new Promise((resolve) => {\n\t\tlet stdout = Buffer.alloc(0);\n\t\tlet stderr = \"\";\n\t\tlet timedOut = false;\n\t\tlet settled = false;\n\t\tlet child: ChildProcess;\n\t\tconst finish = (code: number | null, spawnError: boolean) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve({\n\t\t\t\tok: !spawnError && !timedOut && code === 0,\n\t\t\t\tcode,\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\ttimedOut,\n\t\t\t\tspawnError,\n\t\t\t});\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\ttimedOut = true;\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t}, timeoutMs);\n\t\ttry {\n\t\t\tchild = spawn(\"git\", [...GIT_CONFIG, ...args], {\n\t\t\t\tcwd,\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t// Observation must leave the on-disk index untouched.\n\t\t\t\tenv: { ...process.env, GIT_OPTIONAL_LOCKS: \"0\" },\n\t\t\t});\n\t\t} catch {\n\t\t\tresolve({ ok: false, code: null, stdout, stderr: \"\", timedOut: false, spawnError: true });\n\t\t\treturn;\n\t\t}\n\t\tif (signal) {\n\t\t\tif (signal.aborted) onAbort();\n\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t}\n\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstdout = Buffer.concat([stdout, chunk]);\n\t\t});\n\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstderr += chunk.toString(\"utf-8\");\n\t\t});\n\t\tchild.on(\"error\", () => finish(null, true));\n\t\tchild.on(\"close\", (code) => finish(code, false));\n\t});\n}\n\nfunction emptyCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: false,\n\t\tstaged: false,\n\t\tunstaged: false,\n\t\tuntracked: false,\n\t\tignored: false,\n\t\thashes: false,\n\t\tpatch: false,\n\t};\n}\n\nfunction fullCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: true,\n\t\tstaged: true,\n\t\tunstaged: true,\n\t\tuntracked: true,\n\t\tignored: true,\n\t\thashes: true,\n\t\tpatch: false,\n\t};\n}\n\ninterface ParsedStatusPath {\n\tx: string;\n\ty: string;\n\tpath: string;\n\tpreviousPath?: string;\n}\n\nfunction parsePorcelainZ(stdout: Buffer): ParsedStatusPath[] {\n\tconst fields = stdout.toString(\"utf-8\").split(\"\\0\");\n\tconst parsed: ParsedStatusPath[] = [];\n\tlet i = 0;\n\twhile (i < fields.length) {\n\t\tconst record = fields[i++];\n\t\tif (!record || record.length < 4) continue;\n\t\tconst entry: ParsedStatusPath = { x: record[0], y: record[1], path: record.slice(3) };\n\t\tif (entry.x === \"R\" || entry.x === \"C\" || entry.y === \"R\" || entry.y === \"C\") {\n\t\t\tentry.previousPath = fields[i++];\n\t\t}\n\t\tparsed.push(entry);\n\t}\n\treturn parsed;\n}\n\nfunction classify(entry: ParsedStatusPath): {\n\tkind: WorkspaceStatePath[\"kind\"];\n\tstaged: boolean;\n\tunstaged: boolean;\n\tignored: boolean;\n} {\n\tconst { x, y } = entry;\n\tif (x === \"?\" && y === \"?\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: false };\n\t}\n\tif (x === \"!\" && y === \"!\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: true };\n\t}\n\tif (UNMERGED_XY.has(`${x}${y}`)) {\n\t\treturn {\n\t\t\tkind: x === \"D\" && y === \"D\" ? \"deleted\" : \"modified\",\n\t\t\tstaged: true,\n\t\t\tunstaged: true,\n\t\t\tignored: false,\n\t\t};\n\t}\n\tlet kind: WorkspaceStatePath[\"kind\"] = \"modified\";\n\tif (x === \"A\" || y === \"A\" || x === \"C\" || y === \"C\") kind = \"added\";\n\telse if (x === \"D\" || y === \"D\") kind = \"deleted\";\n\telse if (x === \"R\" || y === \"R\") kind = \"renamed\";\n\treturn {\n\t\tkind,\n\t\tstaged: x !== \" \" && x !== \"?\" && x !== \"!\",\n\t\tunstaged: y !== \" \" && y !== \"?\" && y !== \"!\",\n\t\tignored: false,\n\t};\n}\n\n/** Hash one workspace path: regular files by bytes, symlinks by link text. */\nfunction hashWorkspacePath(\n\ttoplevel: string,\n\tpath: string,\n\tmaxBytes: number,\n): { hash?: string; skipped: \"size\" | \"error\" | false } {\n\ttry {\n\t\tconst full = `${toplevel}/${path}`;\n\t\tconst stat = lstatSync(full);\n\t\tif (stat.isFile()) {\n\t\t\tif (stat.size > maxBytes) return { skipped: \"size\" };\n\t\t\treturn { hash: `sha256:${createHash(\"sha256\").update(readFileSync(full)).digest(\"hex\")}`, skipped: false };\n\t\t}\n\t\tif (stat.isSymbolicLink()) {\n\t\t\treturn {\n\t\t\t\thash: `sha256:${createHash(\"sha256\").update(readlinkSync(full)).digest(\"hex\")}`,\n\t\t\t\tskipped: false,\n\t\t\t};\n\t\t}\n\t\t// Directories (submodule gitlinks) have no worktree content to hash;\n\t\t// that is expected, not a coverage gap.\n\t\tif (stat.isDirectory()) return { skipped: false };\n\t\treturn { skipped: \"error\" };\n\t} catch {\n\t\treturn { skipped: \"error\" };\n\t}\n}\n\nfunction prefixRelative(prefix: string, path: string): string {\n\tif (prefix === \"\" || prefix === \".\") return path;\n\treturn `${prefix}/${path}`;\n}\n\n/**\n * Observe a Git workspace without mutating it. Returns a version-1\n * `WorkspaceStateRecord`: `unsupported` for non-Git workspaces, `failed`\n * when git could not run or the observation was cancelled, and `incomplete`\n * when a limit truncated the capture. `observed` never implies more than\n * `coverage` states.\n */\nexport interface ToplevelComparison {\n\t/** Whether the physically resolved workspace root sits under the toplevel. */\n\tinside: boolean;\n\t/** Root path relative to the toplevel; strip this prefix from porcelain paths. */\n\tprefix: string;\n\t/** Physically resolved toplevel; base for absolute-path work like hashing. */\n\ttoplevelReal: string;\n}\n\n/**\n * Decide containment of the workspace root inside git's toplevel after both\n * sides are physically resolved. Windows spellings diverge from what Node's\n * realpath returns for the same directory (short 8.3 names, drive-letter\n * case), so the win32 branch resolves through the native resolver, folds\n * case for the comparison, and uses win32 path semantics. The platform and\n * resolver are injectable so tests can exercise those branches anywhere.\n */\nexport function compareToplevel(\n\tworkspaceRoot: string,\n\ttoplevel: string,\n\toptions?: {\n\t\tplatform?: NodeJS.Platform;\n\t\trealpath?: (path: string) => string;\n\t},\n): ToplevelComparison {\n\tconst platform = options?.platform ?? process.platform;\n\tconst isWin32 = platform === \"win32\";\n\tconst realpath =\n\t\toptions?.realpath ??\n\t\t(isWin32\n\t\t\t? (p: string) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn realpathSync.native(p);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn realpathSync(p);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: realpathSync);\n\tconst rootReal = realpath(workspaceRoot);\n\tconst toplevelReal = realpath(toplevel);\n\tconst pathMod = isWin32 ? pathWin32 : { relative, sep, dirname };\n\tconst fold = (p: string) => (isWin32 ? p.toLowerCase() : p);\n\tconst prefix = pathMod.relative(toplevelReal, rootReal);\n\tconst inside = !(prefix === \"..\" || prefix.startsWith(`..${pathMod.sep}`) || isAbsolute(prefix));\n\tif (isWin32 && inside) {\n\t\t// Recompute the strip prefix under case folding so a divergent drive or\n\t\t// directory case still yields the clean \"\" or relative form.\n\t\tconst folded = pathMod.relative(fold(toplevelReal), fold(rootReal));\n\t\treturn { inside, prefix: folded, toplevelReal };\n\t}\n\treturn { inside, prefix, toplevelReal };\n}\n\nexport async function observeWorkspaceGit(\n\tworkspaceRoot: string,\n\toptions?: GitObserveOptions,\n): Promise<WorkspaceStateRecord> {\n\tconst maxPaths = options?.maxPaths ?? DEFAULT_MAX_PATHS;\n\tconst maxHashBytes = options?.maxHashBytes ?? DEFAULT_MAX_HASH_BYTES;\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst hashPaths = options?.hashPaths ?? true;\n\tconst signal = options?.signal;\n\tconst excludePaths = (options?.excludePaths ?? [])\n\t\t.map((p) => p.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/+$/, \"\"))\n\t\t.filter((p) => p.length > 0);\n\tconst isExcluded = (repoPath: string): boolean =>\n\t\texcludePaths.some((ex) => repoPath === ex || repoPath.startsWith(`${ex}/`));\n\n\tconst warnings: string[] = [];\n\tconst failed = (message: string): WorkspaceStateRecord => ({\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: \"failed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\tcoverage: emptyCoverage(),\n\t\tpaths: [],\n\t\twarnings: [message],\n\t});\n\n\tif (signal?.aborted) {\n\t\treturn failed(\"workspace observation cancelled\");\n\t}\n\n\tconst head = await runGit(workspaceRoot, [\"rev-parse\", \"--show-toplevel\"], timeoutMs, signal);\n\tif (head.spawnError) return failed(\"git could not be executed\");\n\tif (head.timedOut) return failed(\"workspace observation timed out\");\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (!head.ok) {\n\t\tif (/not a git repository/i.test(head.stderr) || head.code === 128) {\n\t\t\treturn {\n\t\t\t\t...failed(\"not a git repository\"),\n\t\t\t\tstatus: \"unsupported\",\n\t\t\t};\n\t\t}\n\t\treturn failed(\"git rev-parse failed\");\n\t}\n\t// git resolves the toplevel physically (macOS /tmp -> /private/tmp,\n\t// Windows drive-letter case and short names), so containment is decided\n\t// through compareToplevel; otherwise a divergent spelling of the same\n\t// directory would fail the observation or corrupt every reported path.\n\tconst toplevel = head.stdout.toString(\"utf-8\").trim();\n\tconst comparison = compareToplevel(workspaceRoot, toplevel);\n\tif (!comparison.inside) {\n\t\treturn failed(\"workspace root is outside the repository toplevel\");\n\t}\n\tconst insidePrefix = comparison.prefix;\n\n\tconst headCommit = await runGit(workspaceRoot, [\"rev-parse\", \"--verify\", \"HEAD\"], timeoutMs, signal);\n\tconst mergeHead = await runGit(workspaceRoot, [\"rev-parse\", \"-q\", \"--verify\", \"MERGE_HEAD\"], timeoutMs, signal);\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (mergeHead.ok) warnings.push(\"merge in progress\");\n\tconst branch = await runGit(workspaceRoot, [\"branch\", \"--show-current\"], timeoutMs, signal);\n\n\tconst headCommitValue = headCommit.ok ? headCommit.stdout.toString(\"utf-8\").trim() : undefined;\n\tif (!headCommit.ok) warnings.push(\"unborn HEAD (no commits)\");\n\tconst branchValue = branch.ok ? branch.stdout.toString(\"utf-8\").trim() : \"\";\n\tif (branchValue === \"\") warnings.push(\"detached HEAD\");\n\n\t// Index baseline.\n\tconst index = await runGit(workspaceRoot, [\"ls-files\", \"--stage\", \"-z\"], timeoutMs, signal);\n\tif (!index.ok) return failed(\"git ls-files failed\");\n\tconst indexDigest = `sha256:${createHash(\"sha256\").update(index.stdout).digest(\"hex\")}`;\n\n\t// Staged / unstaged / untracked / ignored change sets.\n\tconst status = await runGit(\n\t\tworkspaceRoot,\n\t\t[\"status\", \"--porcelain\", \"-z\", \"--untracked-files=all\", \"--ignored=matching\"],\n\t\ttimeoutMs,\n\t\tsignal,\n\t);\n\tif (!status.ok) return failed(\"git status failed\");\n\n\tconst paths: WorkspaceStatePath[] = [];\n\tlet truncated = false;\n\tlet skippedHashes = 0;\n\tfor (const entry of parsePorcelainZ(status.stdout)) {\n\t\tif (isExcluded(entry.path) || (entry.previousPath !== undefined && isExcluded(entry.previousPath))) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (paths.length >= maxPaths) {\n\t\t\ttruncated = true;\n\t\t\twarnings.push(`path list truncated at ${maxPaths} paths`);\n\t\t\tbreak;\n\t\t}\n\t\tconst { kind, staged, unstaged, ignored } = classify(entry);\n\t\tconst workspacePath: WorkspaceStatePath = {\n\t\t\tpath: prefixRelative(insidePrefix, entry.path),\n\t\t\tkind,\n\t\t\tstaged,\n\t\t\tunstaged,\n\t\t\t...(ignored ? { ignored: true } : {}),\n\t\t\t...(entry.previousPath ? { previousPath: prefixRelative(insidePrefix, entry.previousPath) } : {}),\n\t\t};\n\t\tif (hashPaths && kind !== \"deleted\") {\n\t\t\tconst { hash, skipped } = hashWorkspacePath(comparison.toplevelReal, entry.path, maxHashBytes);\n\t\t\tif (hash) workspacePath.contentHash = hash;\n\t\t\telse if (skipped) skippedHashes++;\n\t\t}\n\t\tpaths.push(workspacePath);\n\t}\n\tif (skippedHashes > 0) warnings.push(`content hashing skipped for ${skippedHashes} path(s)`);\n\n\t// Digest over exactly the paths this record includes, so a later\n\t// comparison can cheaply detect that the covered state moved.\n\tconst digestInput = paths\n\t\t.map(\n\t\t\t(p) =>\n\t\t\t\t`${p.path}\\0${p.kind}\\0${p.staged ? \"S\" : \"\"}${p.unstaged ? \"U\" : \"\"}${p.ignored ? \"I\" : \"\"}\\0${p.contentHash ?? \"\"}\\n`,\n\t\t)\n\t\t.sort()\n\t\t.join(\"\");\n\n\treturn {\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: truncated || skippedHashes > 0 ? \"incomplete\" : \"observed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\t// `hashes` reflects the configured policy: a disabled hash pass is a\n\t\t// coverage statement, not an incomplete capture.\n\t\tcoverage: { ...fullCoverage(), hashes: hashPaths },\n\t\tbase: {\n\t\t\t...(headCommitValue ? { headCommit: headCommitValue } : {}),\n\t\t\t...(branchValue ? { branch: branchValue } : {}),\n\t\t\tindexDigest,\n\t\t\t...(truncated ? {} : { worktreeDigest: `sha256:${createHash(\"sha256\").update(digestInput).digest(\"hex\")}` }),\n\t\t},\n\t\tpaths,\n\t\twarnings,\n\t};\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../../../src/core/workspace/projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAyB3E,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAiC9E","sourcesContent":["import type { WorkspaceStatePath, WorkspaceStateRecord } from \"./state.ts\";\n\n/**\n * Bounded, model-facing projection of a workspace observation (spec\n * 2026-09-01-harness-correctness-and-workspace-state.md § 3): status, base\n * identity, grouped paths, coverage/incomplete notices. Never hashes, never\n * artifact bytes, never absolute workspace paths.\n */\n\nconst MAX_PATHS_PER_GROUP = 20;\nconst HEAD_ABBREV = 7;\n\nfunction groupLabel(path: WorkspaceStatePath): string {\n\tif (path.ignored) return \"ignored\";\n\tconst flags = [path.staged ? \"staged\" : \"\", path.unstaged ? \"unstaged\" : \"\"].filter(Boolean).join(\" + \");\n\treturn flags ? `${path.kind} (${flags})` : path.kind;\n}\n\nfunction pathLabel(path: WorkspaceStatePath): string {\n\tif (path.kind === \"renamed\" && path.previousPath) {\n\t\treturn `${path.previousPath} -> ${path.path}`;\n\t}\n\treturn path.path;\n}\n\nexport function formatWorkspaceProjection(record: WorkspaceStateRecord): string {\n\tconst headerParts = [`Workspace: ${record.status}`];\n\tif (record.base?.branch) headerParts.push(`branch ${record.base.branch}`);\n\tif (record.base?.headCommit) headerParts.push(`HEAD ${record.base.headCommit.slice(0, HEAD_ABBREV)}`);\n\tconst lines: string[] = [headerParts.join(\" · \")];\n\n\tif (record.status === \"unsupported\") {\n\t\tlines.push(\"No workspace adapter applies to this workspace; treat the workspace as unknown.\");\n\t} else if (record.status === \"failed\") {\n\t\tlines.push(\"Workspace capture failed; treat this snapshot as absent, not as current.\");\n\t} else if (record.status === \"incomplete\") {\n\t\tlines.push(\"Configured limits prevented a complete capture; treat this snapshot as partial.\");\n\t}\n\n\tconst groups = new Map<string, string[]>();\n\tfor (const path of record.paths) {\n\t\tconst label = groupLabel(path);\n\t\tconst group = groups.get(label) ?? [];\n\t\tgroup.push(pathLabel(path));\n\t\tgroups.set(label, group);\n\t}\n\tconst labels = [...groups.keys()].sort();\n\tfor (const label of labels) {\n\t\tconst paths = groups.get(label)!;\n\t\tconst shown = paths.slice(0, MAX_PATHS_PER_GROUP);\n\t\tconst rest = paths.length - shown.length;\n\t\tlines.push(`${label}: ${shown.join(\", \")}${rest > 0 ? `, and ${rest} more` : \"\"}`);\n\t}\n\n\tif (record.warnings.length > 0) {\n\t\tlines.push(`Workspace notices: ${record.warnings.join(\"; \")}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n"]}
|