@phyzess/pappra 0.1.0-beta.13 → 0.1.0-beta.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -15
- package/assets/integrations/skills/pappra/SKILL.md +28 -7
- package/assets/integrations/skills/pappra/references/collaboration.md +633 -0
- package/assets/integrations/skills/pappra/references/environment.md +360 -0
- package/assets/integrations/skills/pappra/references/hosts.md +20 -0
- package/assets/integrations/skills/pappra/references/operations.md +36 -5
- package/dist/adapters/pi/recovery-extension.d.ts +65 -0
- package/dist/adapters/pi/recovery-extension.js +238 -0
- package/dist/adapters/pi/session-transition-extension.d.ts +30 -0
- package/dist/adapters/pi/session-transition-extension.js +265 -0
- package/dist/app/automatic-handoff-events.d.ts +26 -0
- package/dist/app/automatic-handoff-events.js +134 -0
- package/dist/app/bounded-file.d.ts +2 -0
- package/dist/app/bounded-file.js +2 -0
- package/dist/app/capability-evidence/claude.d.ts +37 -0
- package/dist/app/capability-evidence/claude.js +206 -0
- package/dist/app/capability-evidence/codex.d.ts +35 -0
- package/dist/app/capability-evidence/codex.js +193 -0
- package/dist/app/capability-evidence/index.d.ts +33 -0
- package/dist/app/capability-evidence/index.js +117 -0
- package/dist/app/capability-evidence/pi.d.ts +73 -0
- package/dist/app/capability-evidence/pi.js +669 -0
- package/dist/app/capability-evidence/shared.d.ts +69 -0
- package/dist/app/capability-evidence/shared.js +195 -0
- package/dist/app/collab-auto-handoff.d.ts +55 -0
- package/dist/app/collab-auto-handoff.js +384 -0
- package/dist/app/collab-bindings.d.ts +57 -0
- package/dist/app/collab-bindings.js +1006 -0
- package/dist/app/collab-bootstrap-transport.d.ts +163 -0
- package/dist/app/collab-bootstrap-transport.js +769 -0
- package/dist/app/collab-bootstrap.d.ts +137 -0
- package/dist/app/collab-bootstrap.js +3455 -0
- package/dist/app/collab-handoff.d.ts +12 -0
- package/dist/app/collab-handoff.js +511 -0
- package/dist/app/collab-messages.d.ts +2 -0
- package/dist/app/collab-messages.js +2 -0
- package/dist/app/collab-notify.d.ts +156 -0
- package/dist/app/collab-notify.js +1208 -0
- package/dist/app/collab-preflight.d.ts +34 -0
- package/dist/app/collab-preflight.js +96 -0
- package/dist/app/collab-recovery-executor.d.ts +161 -0
- package/dist/app/collab-recovery-executor.js +1153 -0
- package/dist/app/collab-recovery-projection.d.ts +103 -0
- package/dist/app/collab-recovery-projection.js +659 -0
- package/dist/app/collab-recovery.d.ts +25 -0
- package/dist/app/collab-recovery.js +429 -0
- package/dist/app/collab-resources.d.ts +16 -0
- package/dist/app/collab-resources.js +278 -0
- package/dist/app/collab-role-events.d.ts +22 -0
- package/dist/app/collab-role-events.js +329 -0
- package/dist/app/collab-role-orchestration.d.ts +53 -0
- package/dist/app/collab-role-orchestration.js +601 -0
- package/dist/app/collab-role-plan.d.ts +81 -0
- package/dist/app/collab-role-plan.js +504 -0
- package/dist/app/collab-role-repair.d.ts +109 -0
- package/dist/app/collab-role-repair.js +821 -0
- package/dist/app/collab-rollover.d.ts +263 -0
- package/dist/app/collab-rollover.js +1112 -0
- package/dist/app/collab-route.d.ts +64 -0
- package/dist/app/collab-route.js +240 -0
- package/dist/app/collab-session-transition.d.ts +221 -0
- package/dist/app/collab-session-transition.js +3334 -0
- package/dist/app/collab-state.d.ts +7 -0
- package/dist/app/collab-state.js +162 -0
- package/dist/app/collab-task-store.d.ts +22 -0
- package/dist/app/collab-task-store.js +204 -0
- package/dist/app/collab-work-execution.d.ts +127 -0
- package/dist/app/collab-work-execution.js +1027 -0
- package/dist/app/collab-work-plan.d.ts +87 -0
- package/dist/app/collab-work-plan.js +437 -0
- package/dist/app/collaboration.d.ts +8 -2
- package/dist/app/collaboration.js +101 -12
- package/dist/app/doctor.js +108 -5
- package/dist/app/host-mcp.js +34 -8
- package/dist/app/host-probe.js +4 -4
- package/dist/app/init.d.ts +58 -4
- package/dist/app/init.js +768 -155
- package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
- package/dist/app/managed-pi-bootstrap-admission.js +104 -0
- package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
- package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
- package/dist/app/managed-pi-delivery.d.ts +8 -0
- package/dist/app/managed-pi-delivery.js +110 -0
- package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
- package/dist/app/managed-pi-handoff-receiver.js +263 -0
- package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
- package/dist/app/managed-pi-handoff-reference.js +60 -0
- package/dist/app/managed-pi-herdr.d.ts +21 -0
- package/dist/app/managed-pi-herdr.js +185 -0
- package/dist/app/managed-pi-initial-admission.d.ts +28 -0
- package/dist/app/managed-pi-initial-admission.js +416 -0
- package/dist/app/managed-pi-initial-launch.d.ts +5 -0
- package/dist/app/managed-pi-initial-launch.js +61 -0
- package/dist/app/managed-pi-native-observation.d.ts +36 -0
- package/dist/app/managed-pi-native-observation.js +191 -0
- package/dist/app/managed-pi-readiness.d.ts +29 -0
- package/dist/app/managed-pi-readiness.js +288 -0
- package/dist/app/managed-pi-repair-stop.d.ts +21 -0
- package/dist/app/managed-pi-repair-stop.js +371 -0
- package/dist/app/managed-pi-resume-admission.d.ts +32 -0
- package/dist/app/managed-pi-resume-admission.js +618 -0
- package/dist/app/managed-pi-retirement.d.ts +28 -0
- package/dist/app/managed-pi-retirement.js +112 -0
- package/dist/app/managed-pi-sdk-control.d.ts +7 -0
- package/dist/app/managed-pi-sdk-control.js +169 -0
- package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
- package/dist/app/managed-pi-sdk-runtime.js +483 -0
- package/dist/app/managed-pi-source-admission.d.ts +14 -0
- package/dist/app/managed-pi-source-admission.js +323 -0
- package/dist/app/managed-pi-transition-host.d.ts +36 -0
- package/dist/app/managed-pi-transition-host.js +292 -0
- package/dist/app/managed-pi.d.ts +54 -0
- package/dist/app/managed-pi.js +248 -0
- package/dist/app/shell-readiness.d.ts +66 -0
- package/dist/app/shell-readiness.js +368 -0
- package/dist/app/task-pin.d.ts +2 -23
- package/dist/app/task-pin.js +2 -113
- package/dist/cli/commands/collaboration-binding.d.ts +13 -0
- package/dist/cli/commands/collaboration-binding.js +118 -0
- package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
- package/dist/cli/commands/collaboration-bootstrap.js +84 -0
- package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
- package/dist/cli/commands/collaboration-capabilities.js +127 -0
- package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
- package/dist/cli/commands/collaboration-handoff.js +102 -0
- package/dist/cli/commands/collaboration-message.d.ts +17 -0
- package/dist/cli/commands/collaboration-message.js +177 -0
- package/dist/cli/commands/collaboration-plan.d.ts +8 -0
- package/dist/cli/commands/collaboration-plan.js +108 -0
- package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
- package/dist/cli/commands/collaboration-preflight.js +167 -0
- package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
- package/dist/cli/commands/collaboration-recovery-run.js +224 -0
- package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
- package/dist/cli/commands/collaboration-recovery.js +104 -0
- package/dist/cli/commands/collaboration-resources.d.ts +11 -0
- package/dist/cli/commands/collaboration-resources.js +96 -0
- package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
- package/dist/cli/commands/collaboration-role-plan.js +69 -0
- package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
- package/dist/cli/commands/collaboration-role-run.js +128 -0
- package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
- package/dist/cli/commands/collaboration-rollover.js +103 -0
- package/dist/cli/commands/collaboration-state.d.ts +10 -0
- package/dist/cli/commands/collaboration-state.js +96 -0
- package/dist/cli/commands/collaboration-transition.d.ts +10 -0
- package/dist/cli/commands/collaboration-transition.js +75 -0
- package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
- package/dist/cli/commands/collaboration-work-run.js +178 -0
- package/dist/cli/commands/collaboration.d.ts +4 -0
- package/dist/cli/commands/collaboration.js +39 -4
- package/dist/cli/commands/continuity.js +2 -6
- package/dist/cli/commands/docs.d.ts +2 -0
- package/dist/cli/commands/docs.js +89 -15
- package/dist/cli/commands/host.d.ts +7 -1
- package/dist/cli/commands/host.js +122 -7
- package/dist/cli/commands/init.d.ts +20 -3
- package/dist/cli/commands/init.js +897 -232
- package/dist/cli/commands/mcp.d.ts +1 -0
- package/dist/cli/commands/mcp.js +11 -5
- package/dist/cli/commands/repair.d.ts +12 -0
- package/dist/cli/commands/repair.js +41 -0
- package/dist/cli/commands/runtime.d.ts +1 -1
- package/dist/cli/commands/runtime.js +139 -38
- package/dist/cli/commands/session.d.ts +7 -0
- package/dist/cli/commands/session.js +82 -0
- package/dist/cli/commands/task.js +19 -5
- package/dist/cli/run.js +866 -24
- package/dist/code/identity.d.ts +7 -0
- package/dist/code/identity.js +7 -3
- package/dist/code/registration.js +7 -0
- package/dist/code/service.d.ts +9 -0
- package/dist/code/service.js +16 -3
- package/dist/code/text.js +19 -7
- package/dist/collab/admission.d.ts +91 -66
- package/dist/collab/admission.js +21 -6
- package/dist/collab/canonical.d.ts +1 -1
- package/dist/collab/canonical.js +44 -3
- package/dist/collab/envelope.d.ts +2 -2
- package/dist/collab/envelope.js +12 -12
- package/dist/collab/first-work.d.ts +3 -0
- package/dist/collab/first-work.js +15 -0
- package/dist/collab/handoff.d.ts +16 -0
- package/dist/collab/handoff.js +39 -5
- package/dist/collab/index.d.ts +97 -1
- package/dist/collab/index.js +9 -1
- package/dist/collab/messages.js +126 -3
- package/dist/collab/persisted-messages.d.ts +123 -0
- package/dist/collab/persisted-messages.js +496 -0
- package/dist/collab/preflight.d.ts +1 -1
- package/dist/collab/resources.d.ts +13 -3
- package/dist/collab/resources.js +37 -6
- package/dist/collab/rollover.d.ts +311 -0
- package/dist/collab/rollover.js +1024 -0
- package/dist/collab/store.d.ts +1 -0
- package/dist/collab/store.js +1 -4
- package/dist/collab/types.d.ts +2 -354
- package/dist/collab/validation.d.ts +2 -10
- package/dist/collab/validation.js +2 -303
- package/dist/collab/work-plan.d.ts +105 -0
- package/dist/collab/work-plan.js +843 -0
- package/dist/continuity/context.d.ts +11 -2
- package/dist/continuity/context.js +100 -13
- package/dist/continuity/handoff-bundle.d.ts +296 -0
- package/dist/continuity/handoff-bundle.js +477 -0
- package/dist/continuity/index.d.ts +2 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.js +10 -2
- package/dist/continuity/service.js +91 -15
- package/dist/continuity/task-pin.d.ts +24 -0
- package/dist/continuity/task-pin.js +590 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +6 -0
- package/dist/experimental/index.d.ts +13 -0
- package/dist/experimental/index.js +25 -0
- package/dist/experimental/pi.d.ts +4 -0
- package/dist/experimental/pi.js +12 -0
- package/dist/hosts/claude/native.js +1 -1
- package/dist/hosts/codex/native.js +1 -1
- package/dist/hosts/codex/protocol.js +1 -1
- package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
- package/dist/hosts/integration/bootstrap-config.js +254 -0
- package/dist/hosts/integration/bundle-export.d.ts +44 -0
- package/dist/hosts/integration/bundle-export.js +366 -3
- package/dist/hosts/integration/bundle.js +3 -0
- package/dist/hosts/integration/hook.js +36 -9
- package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
- package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
- package/dist/hosts/integration/pappra-hook.mjs +61 -24749
- package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
- package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
- package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
- package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
- package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
- package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
- package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
- package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
- package/dist/hosts/integration/pi-native-events.d.ts +112 -0
- package/dist/hosts/integration/pi-native-events.js +348 -0
- package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
- package/dist/hosts/integration/pi-rollover-extension.js +431 -0
- package/dist/hosts/integration/recovery-config.d.ts +48 -0
- package/dist/hosts/integration/recovery-config.js +117 -0
- package/dist/hosts/integration/service.d.ts +6 -1
- package/dist/hosts/integration/service.js +485 -35
- package/dist/hosts/integration/support.d.ts +7 -0
- package/dist/hosts/integration/support.js +9 -1
- package/dist/hosts/integration/templates.d.ts +3 -0
- package/dist/hosts/integration/templates.js +26 -3
- package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
- package/dist/hosts/pi/managed-host-identity.js +40 -0
- package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
- package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
- package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
- package/dist/hosts/pi/managed-sdk-session.js +1639 -0
- package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
- package/dist/hosts/pi/managed-session-extension.js +84 -0
- package/dist/hosts/pi/managed-session.d.ts +177 -0
- package/dist/hosts/pi/managed-session.js +1557 -0
- package/dist/hosts/pi/protocol.js +1 -1
- package/dist/hosts/pi/query-result.js +1 -1
- package/dist/hosts/pi/resume-capability.d.ts +41 -0
- package/dist/hosts/pi/resume-capability.js +33 -0
- package/dist/hosts/pi/rpc.d.ts +182 -134
- package/dist/hosts/pi/rpc.js +230 -37
- package/dist/hosts/pilot.d.ts +97 -74
- package/dist/hosts/pilot.js +5 -1
- package/dist/hosts/version.d.ts +3 -0
- package/dist/hosts/version.js +18 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/infra/bounded-file.d.ts +19 -0
- package/dist/infra/bounded-file.js +66 -0
- package/dist/infra/fs.js +10 -2
- package/dist/infra/git.d.ts +29 -0
- package/dist/infra/git.js +45 -9
- package/dist/infra/json-property.d.ts +2 -0
- package/dist/infra/json-property.js +69 -0
- package/dist/infra/json.d.ts +4 -0
- package/dist/infra/json.js +24 -0
- package/dist/infra/owned-process-group.js +1 -1
- package/dist/infra/private-lock.d.ts +16 -0
- package/dist/infra/private-lock.js +148 -0
- package/dist/infra/private-state.d.ts +7 -0
- package/dist/infra/private-state.js +146 -0
- package/dist/infra/process-identity.d.ts +12 -0
- package/dist/infra/process-identity.js +142 -0
- package/dist/infra/validation.d.ts +5 -0
- package/dist/infra/validation.js +15 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +6 -0
- package/dist/install/apply.d.ts +9 -1
- package/dist/install/apply.js +176 -12
- package/dist/install/dependencies.d.ts +38 -0
- package/dist/install/dependencies.js +282 -0
- package/dist/install/ownership.js +30 -1
- package/dist/install/references.d.ts +31 -0
- package/dist/install/references.js +208 -0
- package/dist/install/repair.d.ts +37 -0
- package/dist/install/repair.js +798 -0
- package/dist/install/reset-local.d.ts +16 -0
- package/dist/install/reset-local.js +304 -0
- package/dist/install/rollback.d.ts +3 -0
- package/dist/install/rollback.js +9 -3
- package/dist/install/runtime.d.ts +10 -6
- package/dist/install/runtime.js +299 -59
- package/dist/install/source-fingerprint.d.ts +2 -0
- package/dist/install/source-fingerprint.js +103 -0
- package/dist/knowledge/catalog.d.ts +1 -0
- package/dist/knowledge/catalog.js +143 -10
- package/dist/knowledge/journal.js +17 -7
- package/dist/knowledge/paths.js +2 -1
- package/dist/knowledge/refs.js +4 -0
- package/dist/knowledge/register-batch.d.ts +4 -0
- package/dist/knowledge/register-batch.js +225 -0
- package/dist/knowledge/relocate.d.ts +5 -0
- package/dist/knowledge/relocate.js +92 -0
- package/dist/knowledge/scan.js +103 -47
- package/dist/knowledge/service.d.ts +3 -0
- package/dist/knowledge/service.js +33 -1
- package/dist/knowledge/write.d.ts +4 -1
- package/dist/knowledge/write.js +1 -1
- package/dist/mcp/server.d.ts +4 -2
- package/dist/mcp/server.js +87 -136
- package/dist/schemas/collaboration-state-validation.d.ts +6 -0
- package/dist/schemas/collaboration-state-validation.js +342 -0
- package/dist/schemas/collaboration-state.d.ts +436 -0
- package/dist/schemas/collaboration-state.js +1 -0
- package/dist/schemas/collaboration.d.ts +335 -0
- package/dist/schemas/collaboration.js +272 -0
- package/dist/schemas/continuity.d.ts +8 -0
- package/dist/schemas/continuity.js +13 -3
- package/dist/schemas/doctor.d.ts +1 -0
- package/dist/schemas/doctor.js +1 -0
- package/dist/schemas/document.d.ts +61 -0
- package/dist/schemas/document.js +9 -0
- package/dist/schemas/errors.d.ts +1 -0
- package/dist/schemas/errors.js +1 -0
- package/dist/schemas/host.d.ts +24 -0
- package/dist/schemas/host.js +19 -0
- package/dist/schemas/install.d.ts +8 -2
- package/dist/schemas/install.js +11 -1
- package/dist/schemas/managed-pi-execution.d.ts +16 -0
- package/dist/schemas/managed-pi-execution.js +2 -0
- package/dist/schemas/outcome.d.ts +9 -5
- package/dist/workspace/config-file.js +1 -3
- package/dist/workspace/documents.js +12 -2
- package/dist/workspace/repositories.d.ts +7 -0
- package/dist/workspace/repositories.js +14 -5
- package/package.json +35 -35
- package/skills/pappra/SKILL.md +28 -7
- package/skills/pappra/references/collaboration.md +633 -0
- package/skills/pappra/references/environment.md +360 -0
- package/skills/pappra/references/hosts.md +20 -0
- package/skills/pappra/references/operations.md +36 -5
- package/dist/app/collab-from.d.ts +0 -35
- package/dist/app/collab-from.js +0 -84
package/dist/collab/canonical.js
CHANGED
|
@@ -18,7 +18,7 @@ export const RESOURCE_KINDS = Object.freeze([...PATH_KINDS, ...OPAQUE_KINDS].sor
|
|
|
18
18
|
/**
|
|
19
19
|
* Resolve the longest existing ancestor with realpath and append the not-yet-existing
|
|
20
20
|
* suffix. Symlink aliases therefore collapse to one key. The suffix keeps the caller's
|
|
21
|
-
* spelling
|
|
21
|
+
* spelling for I/O; comparison keys account for the containing filesystem's case behavior.
|
|
22
22
|
*/
|
|
23
23
|
export function canonicalizeTarget(input) {
|
|
24
24
|
if (typeof input !== 'string' || input.trim().length === 0)
|
|
@@ -29,7 +29,7 @@ export function canonicalizeTarget(input) {
|
|
|
29
29
|
let probe = path.normalize(input);
|
|
30
30
|
for (;;) {
|
|
31
31
|
try {
|
|
32
|
-
const real = fs.realpathSync(probe);
|
|
32
|
+
const real = fs.realpathSync.native(probe);
|
|
33
33
|
return {
|
|
34
34
|
path: path.join(real, ...missing.slice().reverse()),
|
|
35
35
|
existing: missing.length === 0,
|
|
@@ -47,8 +47,49 @@ export function canonicalizeTarget(input) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
/** Probe existing directory aliases without creating files or assuming all macOS volumes match. */
|
|
51
|
+
function caseInsensitiveAt(input) {
|
|
52
|
+
let probe = input;
|
|
53
|
+
for (;;) {
|
|
54
|
+
try {
|
|
55
|
+
const observed = fs.statSync(probe);
|
|
56
|
+
if (!observed.isDirectory()) {
|
|
57
|
+
probe = path.dirname(probe);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const name = path.basename(probe);
|
|
61
|
+
const flipped = name.replace(/[a-zA-Z]/, (letter) => letter === letter.toLowerCase()
|
|
62
|
+
? letter.toUpperCase()
|
|
63
|
+
: letter.toLowerCase());
|
|
64
|
+
if (flipped !== name) {
|
|
65
|
+
try {
|
|
66
|
+
const alias = fs.statSync(path.join(path.dirname(probe), flipped));
|
|
67
|
+
return alias.dev === observed.dev && alias.ino === observed.ino;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Missing suffixes inherit the closest existing directory's behavior.
|
|
76
|
+
}
|
|
77
|
+
const parent = path.dirname(probe);
|
|
78
|
+
if (parent === probe)
|
|
79
|
+
return process.platform === 'win32';
|
|
80
|
+
probe = parent;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function comparisonPath(input) {
|
|
84
|
+
const resolved = canonicalizeTarget(input);
|
|
85
|
+
return caseInsensitiveAt(resolved.nearest_ancestor)
|
|
86
|
+
? resolved.path.normalize('NFC').toUpperCase().toLowerCase()
|
|
87
|
+
: resolved.path;
|
|
88
|
+
}
|
|
50
89
|
/** `same`, `ancestor`, `descendant` or `disjoint` on path-segment boundaries. */
|
|
51
90
|
export function pathRelation(left, right) {
|
|
91
|
+
left = comparisonPath(left);
|
|
92
|
+
right = comparisonPath(right);
|
|
52
93
|
if (left === right)
|
|
53
94
|
return 'same';
|
|
54
95
|
const a = left.split(path.sep).filter(Boolean);
|
|
@@ -110,7 +151,7 @@ export function describeResource(input) {
|
|
|
110
151
|
return {
|
|
111
152
|
kind,
|
|
112
153
|
scope,
|
|
113
|
-
key: `${kind}:${resolved.path}`,
|
|
154
|
+
key: `${kind}:${comparisonPath(resolved.path)}`,
|
|
114
155
|
path: resolved.path,
|
|
115
156
|
existing: resolved.existing,
|
|
116
157
|
label: input.label ?? `${kind} ${resolved.path}`,
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { Message, MessageInput } from './types.js';
|
|
2
|
+
export { canonicalJson } from '../infra/json.js';
|
|
2
3
|
export declare const MESSAGE_SCHEMA = "harness-collab/message/1";
|
|
3
4
|
export declare const MESSAGE_TYPES: readonly string[];
|
|
4
5
|
export declare const REPORT_STATUSES: readonly string[];
|
|
5
6
|
export declare const BLOCK_KINDS: readonly string[];
|
|
6
7
|
export declare function validateEnvelope(message: unknown): Message;
|
|
7
|
-
/** Stable JSON with sorted object keys, used for content-identity digests. */
|
|
8
|
-
export declare function canonicalJson(value: unknown): string;
|
|
9
8
|
/**
|
|
10
9
|
* Content identity of a message: everything that decides what the message means.
|
|
11
10
|
* `created_at` and `content_digest` are excluded so an honest resend is still the
|
|
@@ -15,6 +14,7 @@ export declare function contentDigest(message: Message): string;
|
|
|
15
14
|
export declare function buildMessage(input: MessageInput, { clock }?: {
|
|
16
15
|
clock?: number | undefined;
|
|
17
16
|
}): Message;
|
|
17
|
+
export declare function isSafeMessageId(value: unknown): value is string;
|
|
18
18
|
export declare function messageFile(taskRoot: string, messageId: string): string;
|
|
19
19
|
export declare function readRegisteredMessage(taskRoot: string, messageId: string): Message | null;
|
|
20
20
|
/**
|
package/dist/collab/envelope.js
CHANGED
|
@@ -2,6 +2,8 @@ import { errorCode, parseJson, object, text, isMessage } from './validation.js';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createNonce, digestText, messagesDir, nowIso } from './store.js';
|
|
5
|
+
import { canonicalJson } from '../infra/json.js';
|
|
6
|
+
export { canonicalJson } from '../infra/json.js';
|
|
5
7
|
// Message envelopes and their idempotency rules.
|
|
6
8
|
//
|
|
7
9
|
// Registration is an exclusive create of `messages/<message_id>.json`, which is atomic,
|
|
@@ -99,17 +101,6 @@ export function validateEnvelope(message) {
|
|
|
99
101
|
// Unknown extension fields are tolerated on purpose; unknown semantics are not inferred.
|
|
100
102
|
return message;
|
|
101
103
|
}
|
|
102
|
-
/** Stable JSON with sorted object keys, used for content-identity digests. */
|
|
103
|
-
export function canonicalJson(value) {
|
|
104
|
-
if (value === null || typeof value !== 'object')
|
|
105
|
-
return JSON.stringify(value ?? null);
|
|
106
|
-
if (Array.isArray(value))
|
|
107
|
-
return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
|
|
108
|
-
if (!object(value))
|
|
109
|
-
throw new Error('Canonical JSON requires an object');
|
|
110
|
-
const keys = Object.keys(value).sort();
|
|
111
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
112
|
-
}
|
|
113
104
|
/**
|
|
114
105
|
* Content identity of a message: everything that decides what the message means.
|
|
115
106
|
* `created_at` and `content_digest` are excluded so an honest resend is still the
|
|
@@ -149,8 +140,17 @@ export function buildMessage(input, { clock = Date.now() } = {}) {
|
|
|
149
140
|
message.content_digest = contentDigest(message);
|
|
150
141
|
return message;
|
|
151
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Message ids are single path segments: a letter or digit followed by letters,
|
|
145
|
+
* digits, dot, dash or underscore, bounded at 128 characters. Anything else
|
|
146
|
+
* (`..`, separators, control characters) is rejected before any path is built.
|
|
147
|
+
*/
|
|
148
|
+
const SAFE_MESSAGE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
149
|
+
export function isSafeMessageId(value) {
|
|
150
|
+
return typeof value === 'string' && SAFE_MESSAGE_ID.test(value);
|
|
151
|
+
}
|
|
152
152
|
export function messageFile(taskRoot, messageId) {
|
|
153
|
-
if (
|
|
153
|
+
if (!isSafeMessageId(messageId))
|
|
154
154
|
throw new Error('Invalid message field: message.message_id');
|
|
155
155
|
return path.join(messagesDir(taskRoot), `${messageId}.json`);
|
|
156
156
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A native turn may prove succession while the attempt remains active. */
|
|
2
|
+
export function isNativeFirstWorkResult(result, intent) {
|
|
3
|
+
if (result.status !== 'running' ||
|
|
4
|
+
typeof result.native_ref !== 'object' ||
|
|
5
|
+
result.native_ref === null)
|
|
6
|
+
return false;
|
|
7
|
+
const ref = result.native_ref;
|
|
8
|
+
return (ref.schema === 'pappra/native-first-work/1' &&
|
|
9
|
+
ref.handoff_id === intent.handoff_id &&
|
|
10
|
+
ref.generation === intent.generation &&
|
|
11
|
+
ref.session_id === intent.to.session_id &&
|
|
12
|
+
ref.observation === 'native_agent_settled' &&
|
|
13
|
+
typeof ref.evidence_ref === 'string' &&
|
|
14
|
+
ref.evidence_ref.length > 0);
|
|
15
|
+
}
|
package/dist/collab/handoff.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare function beginHandoff(record: TaskRecord, { handoffId, kind, subt
|
|
|
21
21
|
other_state?: never;
|
|
22
22
|
expected?: never;
|
|
23
23
|
actual?: never;
|
|
24
|
+
claim_id?: never;
|
|
24
25
|
handoff_id?: never;
|
|
25
26
|
state?: never;
|
|
26
27
|
} | {
|
|
@@ -31,6 +32,7 @@ export declare function beginHandoff(record: TaskRecord, { handoffId, kind, subt
|
|
|
31
32
|
shared_resources: string[];
|
|
32
33
|
expected?: never;
|
|
33
34
|
actual?: never;
|
|
35
|
+
claim_id?: never;
|
|
34
36
|
handoff_id?: never;
|
|
35
37
|
state?: never;
|
|
36
38
|
} | {
|
|
@@ -41,6 +43,7 @@ export declare function beginHandoff(record: TaskRecord, { handoffId, kind, subt
|
|
|
41
43
|
other_state: string;
|
|
42
44
|
expected?: never;
|
|
43
45
|
actual?: never;
|
|
46
|
+
claim_id?: never;
|
|
44
47
|
handoff_id?: never;
|
|
45
48
|
state?: never;
|
|
46
49
|
} | {
|
|
@@ -51,15 +54,28 @@ export declare function beginHandoff(record: TaskRecord, { handoffId, kind, subt
|
|
|
51
54
|
reason: string;
|
|
52
55
|
expected: number | null;
|
|
53
56
|
actual: number;
|
|
57
|
+
claim_id?: never;
|
|
54
58
|
handoff_id?: never;
|
|
55
59
|
state?: never;
|
|
56
60
|
} | {
|
|
57
61
|
shared_resources?: never;
|
|
58
62
|
other_handoff?: never;
|
|
59
63
|
other_state?: never;
|
|
64
|
+
expected?: never;
|
|
65
|
+
actual?: never;
|
|
66
|
+
ok: false;
|
|
67
|
+
reason: string;
|
|
68
|
+
claim_id: string;
|
|
69
|
+
handoff_id?: never;
|
|
70
|
+
state?: never;
|
|
71
|
+
} | {
|
|
60
72
|
reason?: never;
|
|
73
|
+
shared_resources?: never;
|
|
74
|
+
other_handoff?: never;
|
|
75
|
+
other_state?: never;
|
|
61
76
|
expected?: never;
|
|
62
77
|
actual?: never;
|
|
78
|
+
claim_id?: never;
|
|
63
79
|
ok: true;
|
|
64
80
|
handoff_id: string;
|
|
65
81
|
state: string;
|
package/dist/collab/handoff.js
CHANGED
|
@@ -71,12 +71,46 @@ export function beginHandoff(record, { handoffId, kind, subtaskId = null, attemp
|
|
|
71
71
|
actual: ownerEpoch ?? null,
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
const target = kind === 'writer' ? findAttempt(record, subtaskId, attempt) : null;
|
|
75
|
+
if (kind === 'writer' && !target)
|
|
76
|
+
return { ok: false, reason: 'unknown_attempt' };
|
|
77
|
+
const authority = kind === 'writer' ? target?.assignee : record.coordinator;
|
|
78
|
+
if (!authority ||
|
|
79
|
+
authority.agent_id !== from.agent_id ||
|
|
80
|
+
authority.session_id !== from.session_id)
|
|
81
|
+
return { ok: false, reason: 'unauthorized_sender' };
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
for (const claimId of resources) {
|
|
84
|
+
if (seen.has(claimId))
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: 'duplicate_handoff_resource',
|
|
88
|
+
claim_id: claimId,
|
|
89
|
+
};
|
|
90
|
+
seen.add(claimId);
|
|
91
|
+
const matches = (record.claims ?? []).filter((claim) => claim.claim_id === claimId);
|
|
92
|
+
if (matches.length !== 1)
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: matches.length ? 'ambiguous_claim_id' : 'unknown_claim',
|
|
96
|
+
claim_id: claimId,
|
|
97
|
+
};
|
|
98
|
+
const claim = matches[0];
|
|
99
|
+
if (claim.released_at ||
|
|
100
|
+
claim.holder?.agent_id !== from.agent_id ||
|
|
101
|
+
claim.holder?.session_id !== from.session_id ||
|
|
102
|
+
(kind === 'writer' &&
|
|
103
|
+
((claim.subtask_id !== null && claim.subtask_id !== subtaskId) ||
|
|
104
|
+
(claim.attempt !== null && claim.attempt !== attempt))))
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
reason: 'handoff_resource_not_owned',
|
|
108
|
+
claim_id: claimId,
|
|
109
|
+
};
|
|
79
110
|
}
|
|
111
|
+
// Validate the entire inventory before reserving an attempt or freezing dispatch.
|
|
112
|
+
if (target)
|
|
113
|
+
target.ownership = 'transferring';
|
|
80
114
|
const at = nowIso(clock);
|
|
81
115
|
const entry = {
|
|
82
116
|
schema: 'harness-collab/handoff/1',
|
package/dist/collab/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import * as storeInternals from './store.js';
|
|
2
2
|
export * as canonical from './canonical.js';
|
|
3
3
|
export * as resources from './resources.js';
|
|
4
4
|
export * as envelope from './envelope.js';
|
|
@@ -6,4 +6,100 @@ export * as messages from './messages.js';
|
|
|
6
6
|
export * as lifecycle from './lifecycle.js';
|
|
7
7
|
export * as handoff from './handoff.js';
|
|
8
8
|
export * as notify from './notify.js';
|
|
9
|
+
export * as workPlan from './work-plan.js';
|
|
10
|
+
export * as rollover from './rollover.js';
|
|
9
11
|
export type * from './types.js';
|
|
12
|
+
declare const storeSurface: {
|
|
13
|
+
STATE_SCHEMA: "harness-collab/state/1";
|
|
14
|
+
LEDGER_SCHEMA: "harness-collab/ledger/1";
|
|
15
|
+
RECORD_FILE: "task.json";
|
|
16
|
+
LEDGER_FILE: "ledger.jsonl";
|
|
17
|
+
DEFAULT_DOMAIN_ROOT: string;
|
|
18
|
+
LockHeldError: typeof storeInternals.LockHeldError;
|
|
19
|
+
RevisionConflictError: typeof storeInternals.RevisionConflictError;
|
|
20
|
+
digestBytes: (value: string | Uint8Array) => string;
|
|
21
|
+
digestText: (value: string | Uint8Array) => string;
|
|
22
|
+
createNonce(): string;
|
|
23
|
+
nowIso(clock?: number): string;
|
|
24
|
+
isAbsoluteExistingDirectory(value: unknown): boolean;
|
|
25
|
+
resolveDomainRoot(input?: string | null): string;
|
|
26
|
+
taskRootFor(domainRoot: string, taskId: string): string;
|
|
27
|
+
recordPath: (taskRoot: string) => string;
|
|
28
|
+
ledgerPath: (taskRoot: string) => string;
|
|
29
|
+
lockPath: (taskRoot: string) => string;
|
|
30
|
+
messagesDir: (taskRoot: string) => string;
|
|
31
|
+
resultsDir: (taskRoot: string) => string;
|
|
32
|
+
nativeDir: (taskRoot: string) => string;
|
|
33
|
+
abortedDir: (taskRoot: string) => string;
|
|
34
|
+
brokenLocksDir: (taskRoot: string) => string;
|
|
35
|
+
notificationsDir: (taskRoot: string) => string;
|
|
36
|
+
domainIdOf(domainRoot: string): string;
|
|
37
|
+
readRecord(taskRoot: string): import("./types.js").TaskRecord | null;
|
|
38
|
+
readLock(taskRoot: string): {
|
|
39
|
+
held: boolean;
|
|
40
|
+
holder?: never;
|
|
41
|
+
holder_process_alive?: never;
|
|
42
|
+
transaction_files?: never;
|
|
43
|
+
} | {
|
|
44
|
+
held: boolean;
|
|
45
|
+
holder: import("./types.js").LockHolder | null;
|
|
46
|
+
holder_process_alive: boolean;
|
|
47
|
+
transaction_files: string[];
|
|
48
|
+
};
|
|
49
|
+
listTransactions(taskRoot: string): import("./types.js").TransactionFile[];
|
|
50
|
+
acquireLock(taskRoot: string, { actor, purpose, waitMs, pollMs, }?: {
|
|
51
|
+
actor?: string;
|
|
52
|
+
purpose?: string;
|
|
53
|
+
waitMs?: number;
|
|
54
|
+
pollMs?: number;
|
|
55
|
+
}): {
|
|
56
|
+
schema: string;
|
|
57
|
+
nonce: string;
|
|
58
|
+
actor: string;
|
|
59
|
+
purpose: string;
|
|
60
|
+
pid: number;
|
|
61
|
+
hostname: string;
|
|
62
|
+
acquired_at: string;
|
|
63
|
+
};
|
|
64
|
+
releaseLock(taskRoot: string, holder: import("./types.js").LockHolder | null): boolean;
|
|
65
|
+
archiveAbortedTransaction(taskRoot: string, file: string, { actor, at, }?: {
|
|
66
|
+
actor?: string;
|
|
67
|
+
reason?: string;
|
|
68
|
+
confirmHolderStopped?: boolean;
|
|
69
|
+
at?: string;
|
|
70
|
+
}): string;
|
|
71
|
+
transact<T = unknown>(taskRoot: string, { actor, purpose, expectRevision, waitMs, apply, clock, }: {
|
|
72
|
+
actor?: string;
|
|
73
|
+
purpose?: string;
|
|
74
|
+
expectRevision?: number | null | undefined;
|
|
75
|
+
waitMs?: number;
|
|
76
|
+
apply: (record: import("./types.js").TaskRecord) => import("./types.js").Mutation<T> | import("./types.js").Failure | null | undefined;
|
|
77
|
+
clock?: number;
|
|
78
|
+
}): import("./types.js").TransactionResult<T>;
|
|
79
|
+
newRecord({ taskId, domainRoot, taskRoot, coordinator, clock, }: {
|
|
80
|
+
taskId: string;
|
|
81
|
+
domainRoot: string;
|
|
82
|
+
taskRoot: string;
|
|
83
|
+
coordinator?: import("./types.js").Coordinator | null;
|
|
84
|
+
clock?: number;
|
|
85
|
+
}): import("./types.js").TaskRecord;
|
|
86
|
+
initTask(taskRoot: string, { taskId, coordinator, domainRoot: explicitDomainRoot, clock, }: {
|
|
87
|
+
taskId: string;
|
|
88
|
+
coordinator?: import("./types.js").Coordinator | null;
|
|
89
|
+
domainRoot?: string | null;
|
|
90
|
+
clock?: number;
|
|
91
|
+
}): {
|
|
92
|
+
created: boolean;
|
|
93
|
+
record: import("./types.js").TaskRecord | null;
|
|
94
|
+
};
|
|
95
|
+
doctor(taskRoot: string): {
|
|
96
|
+
task_root: string;
|
|
97
|
+
state_revision: null;
|
|
98
|
+
findings: import("./types.js").Finding[];
|
|
99
|
+
} | {
|
|
100
|
+
task_root: string;
|
|
101
|
+
state_revision: number;
|
|
102
|
+
findings: import("./types.js").Finding[];
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
export { storeSurface as store };
|
package/dist/collab/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import * as storeInternals from './store.js';
|
|
2
2
|
export * as canonical from './canonical.js';
|
|
3
3
|
export * as resources from './resources.js';
|
|
4
4
|
export * as envelope from './envelope.js';
|
|
@@ -6,3 +6,11 @@ export * as messages from './messages.js';
|
|
|
6
6
|
export * as lifecycle from './lifecycle.js';
|
|
7
7
|
export * as handoff from './handoff.js';
|
|
8
8
|
export * as notify from './notify.js';
|
|
9
|
+
export * as workPlan from './work-plan.js';
|
|
10
|
+
export * as rollover from './rollover.js';
|
|
11
|
+
// The public barrel curates the store surface. `breakLock` stays in
|
|
12
|
+
// `./store.js` because the operator-only `collab recovery lock-break` command
|
|
13
|
+
// owns the confirmation flow; exposing the bare primitive invites a caller to
|
|
14
|
+
// break a live writer's lock without that evidence.
|
|
15
|
+
const { breakLock: _unsafeBreakLock, ...storeSurface } = storeInternals;
|
|
16
|
+
export { storeSurface as store };
|
package/dist/collab/messages.js
CHANGED
|
@@ -35,6 +35,104 @@ function findMessage(record, messageId) {
|
|
|
35
35
|
return ((record.messages ?? []).find((item) => item.message_id === messageId) ??
|
|
36
36
|
null);
|
|
37
37
|
}
|
|
38
|
+
function sameSender(sender, expected) {
|
|
39
|
+
return (expected?.agent_id === sender.agent_id &&
|
|
40
|
+
expected.session_id === sender.session_id);
|
|
41
|
+
}
|
|
42
|
+
/** An epoch identifies the generation of authority, not the agent holding it. */
|
|
43
|
+
function authorizeSender(record, message) {
|
|
44
|
+
let expected;
|
|
45
|
+
switch (message.type) {
|
|
46
|
+
case 'ASSIGN':
|
|
47
|
+
case 'CANCEL':
|
|
48
|
+
expected = record.coordinator;
|
|
49
|
+
break;
|
|
50
|
+
case 'HANDOFF': {
|
|
51
|
+
if (message.body.phase !== 'request') {
|
|
52
|
+
const handoff = record.handoffs.find((item) => item.handoff_id === message.body.handoff_id);
|
|
53
|
+
if (handoff &&
|
|
54
|
+
(sameSender(message.from, handoff.from) ||
|
|
55
|
+
sameSender(message.from, handoff.to)))
|
|
56
|
+
return null;
|
|
57
|
+
return { ok: false, reason: 'unauthorized_sender' };
|
|
58
|
+
}
|
|
59
|
+
if (message.body.kind === 'coordinator') {
|
|
60
|
+
expected = record.coordinator;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
expected = findAttempt(record, message.subtask_id, message.attempt)?.assignee;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'ACK':
|
|
67
|
+
case 'BLOCK':
|
|
68
|
+
case 'CANCELLED': {
|
|
69
|
+
const referenced = message.reply_to
|
|
70
|
+
? findMessage(record, message.reply_to)
|
|
71
|
+
: null;
|
|
72
|
+
const subtaskId = message.subtask_id ?? referenced?.subtask_id ?? null;
|
|
73
|
+
const attempt = message.attempt ?? referenced?.attempt ?? null;
|
|
74
|
+
const target = findAttempt(record, subtaskId, attempt);
|
|
75
|
+
if (message.type === 'ACK') {
|
|
76
|
+
const handoff = record.handoffs.find((item) => (item.handoff_id === message.body.handoff_id ||
|
|
77
|
+
item.handoff_id === message.reply_to) &&
|
|
78
|
+
(item.kind === 'coordinator' ||
|
|
79
|
+
(item.subtask_id === subtaskId && item.attempt === attempt)) &&
|
|
80
|
+
!['completed', 'aborted'].includes(item.state) &&
|
|
81
|
+
sameSender(item.from, item.kind === 'coordinator'
|
|
82
|
+
? record.coordinator
|
|
83
|
+
: target?.assignee) &&
|
|
84
|
+
sameSender(message.from, item.to));
|
|
85
|
+
if (handoff)
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (!target)
|
|
89
|
+
return { ok: false, reason: 'unknown_attempt' };
|
|
90
|
+
if (message.type === 'BLOCK' &&
|
|
91
|
+
sameSender(message.from, record.coordinator))
|
|
92
|
+
return null;
|
|
93
|
+
expected = target.assignee;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case 'ANSWER': {
|
|
97
|
+
const referenced = message.reply_to
|
|
98
|
+
? findMessage(record, message.reply_to)
|
|
99
|
+
: null;
|
|
100
|
+
if (!referenced)
|
|
101
|
+
return { ok: false, reason: 'unknown_question' };
|
|
102
|
+
// Coordinator decisions and instructions are evidence consumed by work-plan
|
|
103
|
+
// and recovery validation. They are not clarification state transitions.
|
|
104
|
+
if (!['ASK', 'BLOCK'].includes(referenced.type)) {
|
|
105
|
+
return sameSender(message.from, record.coordinator)
|
|
106
|
+
? null
|
|
107
|
+
: { ok: false, reason: 'unauthorized_sender' };
|
|
108
|
+
}
|
|
109
|
+
if (referenced.type === 'BLOCK' && referenced.body_kind === 'approval')
|
|
110
|
+
return { ok: false, reason: 'approval_not_auto_answerable' };
|
|
111
|
+
if (referenced.subtask_id && referenced.attempt) {
|
|
112
|
+
const target = findAttempt(record, referenced.subtask_id, referenced.attempt);
|
|
113
|
+
if ((referenced.type === 'BLOCK' && referenced.state !== 'applied') ||
|
|
114
|
+
!(sameSender(referenced.from, target?.assignee) ||
|
|
115
|
+
(sameSender(referenced.from, record.coordinator) &&
|
|
116
|
+
referenced.to !== null &&
|
|
117
|
+
sameSender(referenced.to, target?.assignee))))
|
|
118
|
+
return { ok: false, reason: 'stale_question' };
|
|
119
|
+
}
|
|
120
|
+
if ((message.subtask_id && message.subtask_id !== referenced.subtask_id) ||
|
|
121
|
+
(message.attempt && message.attempt !== referenced.attempt) ||
|
|
122
|
+
!sameSender(message.to, referenced.from))
|
|
123
|
+
return { ok: false, reason: 'reply_identity_mismatch' };
|
|
124
|
+
expected = referenced.to;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
// REPORT classifies a previous owner's result as evidence without changing the
|
|
128
|
+
// current attempt. Questions and status exchanges confer no execution authority.
|
|
129
|
+
default:
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return sameSender(message.from, expected)
|
|
133
|
+
? null
|
|
134
|
+
: { ok: false, reason: 'unauthorized_sender' };
|
|
135
|
+
}
|
|
38
136
|
/**
|
|
39
137
|
* Pure-ish apply function for `transact`: returns the outcome object or `{ok:false,...}`.
|
|
40
138
|
*/
|
|
@@ -62,6 +160,9 @@ export function applyMessage(record, message, { clock = Date.now(), nativeDir: _
|
|
|
62
160
|
reason: 'task_mismatch',
|
|
63
161
|
expected: record.task_id,
|
|
64
162
|
};
|
|
163
|
+
const authorization = authorizeSender(record, message);
|
|
164
|
+
if (authorization)
|
|
165
|
+
return authorization;
|
|
65
166
|
if (DISPATCH_TYPES.has(message.type)) {
|
|
66
167
|
if (record.dispatch_frozen && message.type === 'ASSIGN') {
|
|
67
168
|
return {
|
|
@@ -155,7 +256,7 @@ export function applyMessage(record, message, { clock = Date.now(), nativeDir: _
|
|
|
155
256
|
subtask_id: subtaskId,
|
|
156
257
|
attempt,
|
|
157
258
|
};
|
|
158
|
-
const transition = target.state === 'assigned'
|
|
259
|
+
const transition = target.state === 'assigned' && sameSender(message.from, target.assignee)
|
|
159
260
|
? transitionAttempt(record, {
|
|
160
261
|
subtaskId,
|
|
161
262
|
attempt,
|
|
@@ -178,6 +279,24 @@ export function applyMessage(record, message, { clock = Date.now(), nativeDir: _
|
|
|
178
279
|
result: transition,
|
|
179
280
|
};
|
|
180
281
|
}
|
|
282
|
+
case 'ASK': {
|
|
283
|
+
// A question is evidence, not a transition. Blocking a clarification stays
|
|
284
|
+
// BLOCK(kind=clarification); ANSWER resolves only that state. This branch completes
|
|
285
|
+
// the advertised envelope vocabulary without adding authority or execution.
|
|
286
|
+
const subtaskId = message.subtask_id ?? null;
|
|
287
|
+
const attempt = message.attempt ?? null;
|
|
288
|
+
recordMessage(record, message, 'recorded', 'question recorded; no task state change', clock);
|
|
289
|
+
return {
|
|
290
|
+
record,
|
|
291
|
+
op: 'ask',
|
|
292
|
+
summary: { subtask_id: subtaskId, attempt },
|
|
293
|
+
result: {
|
|
294
|
+
ok: true,
|
|
295
|
+
awaiting: 'response',
|
|
296
|
+
state_changed: false,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
181
300
|
case 'ANSWER': {
|
|
182
301
|
const referenced = message.reply_to
|
|
183
302
|
? findMessage(record, message.reply_to)
|
|
@@ -192,7 +311,9 @@ export function applyMessage(record, message, { clock = Date.now(), nativeDir: _
|
|
|
192
311
|
const subtaskId = message.subtask_id ?? referenced?.subtask_id ?? null;
|
|
193
312
|
const attempt = message.attempt ?? referenced?.attempt ?? null;
|
|
194
313
|
const target = subtaskId && attempt ? findAttempt(record, subtaskId, attempt) : null;
|
|
195
|
-
const transition =
|
|
314
|
+
const transition = referenced !== null &&
|
|
315
|
+
['ASK', 'BLOCK'].includes(referenced.type) &&
|
|
316
|
+
target &&
|
|
196
317
|
target.state === 'needs_input' &&
|
|
197
318
|
target.awaiting === 'clarification'
|
|
198
319
|
? transitionAttempt(record, {
|
|
@@ -209,7 +330,9 @@ export function applyMessage(record, message, { clock = Date.now(), nativeDir: _
|
|
|
209
330
|
reason: 'no_open_clarification',
|
|
210
331
|
state: target?.state ?? null,
|
|
211
332
|
};
|
|
212
|
-
recordMessage(record, message, 'applied',
|
|
333
|
+
recordMessage(record, message, 'applied', referenced && ['ASK', 'BLOCK'].includes(referenced.type)
|
|
334
|
+
? 'clarification answer'
|
|
335
|
+
: 'coordinator decision recorded; no task state change', clock);
|
|
213
336
|
return {
|
|
214
337
|
record,
|
|
215
338
|
op: 'answer',
|