chati-dev 4.5.28 → 4.5.29

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.
Files changed (172) hide show
  1. package/bin/chati.js +151 -123
  2. package/framework/agents/build/dev.md +4 -4
  3. package/framework/agents/deploy/devops.md +3 -3
  4. package/framework/agents/discover/brief.md +18 -6
  5. package/framework/agents/discover/brownfield-wu.md +3 -3
  6. package/framework/agents/discover/greenfield-wu.md +4 -4
  7. package/framework/agents/plan/architect.md +2 -2
  8. package/framework/agents/plan/detail.md +4 -4
  9. package/framework/agents/plan/phases.md +3 -3
  10. package/framework/agents/plan/tasks.md +47 -19
  11. package/framework/agents/plan/ux-brand-architect.md +1 -1
  12. package/framework/agents/plan/ux.md +26 -2
  13. package/framework/agents/quality/qa-implementation.md +8 -8
  14. package/framework/agents/quality/qa-planning.md +58 -4
  15. package/framework/agents/quality/qa-visual.md +4 -4
  16. package/framework/config.yaml +2 -2
  17. package/framework/constitution.md +12 -1
  18. package/framework/context/governance.md +2 -1
  19. package/framework/context/root.md +3 -1
  20. package/framework/domains/constitution.yaml +1 -1
  21. package/framework/domains/global.yaml +1 -1
  22. package/framework/domains/workflows/standard-flow.yaml +7 -8
  23. package/framework/hooks/advance-trigger.js +3 -2
  24. package/framework/hooks/brief-validator.js +1 -1
  25. package/framework/hooks/post-dev.js +1 -1
  26. package/framework/hooks/team-quality-gate.js +9 -2
  27. package/framework/i18n/en.yaml +1 -0
  28. package/framework/i18n/es.yaml +1 -0
  29. package/framework/i18n/fr.yaml +1 -0
  30. package/framework/i18n/pt.yaml +1 -0
  31. package/framework/manifest.json +83 -83
  32. package/framework/manifest.sig +1 -1
  33. package/framework/orchestrator/chati-router.js +14 -6
  34. package/framework/orchestrator/chati.md +61 -23
  35. package/framework/rules/human-writing-style.md +7 -0
  36. package/framework/schemas/session.schema.json +25 -2
  37. package/framework/schemas/task.schema.json +26 -0
  38. package/framework/tasks/orchestrator-handoff.md +10 -10
  39. package/framework/tasks/orchestrator-resume.md +3 -3
  40. package/framework/workflows/brownfield-fullstack.yaml +6 -3
  41. package/framework/workflows/brownfield-service.yaml +5 -3
  42. package/framework/workflows/brownfield-ui.yaml +5 -3
  43. package/framework/workflows/greenfield-fullstack.yaml +6 -3
  44. package/framework/workflows/standard-flow.yaml +8 -3
  45. package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
  46. package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
  47. package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
  48. package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
  49. package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
  50. package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
  51. package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
  52. package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
  53. package/node_modules/@chati/core/native/execution-admission.h +59 -0
  54. package/node_modules/@chati/core/native/execution-gate.c +41 -0
  55. package/node_modules/@chati/core/native/linux-process.c +41 -0
  56. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
  57. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
  58. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
  59. package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
  60. package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
  61. package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
  62. package/node_modules/@chati/core/package.json +2 -1
  63. package/node_modules/@chati/core/scripts/build-native.js +36 -0
  64. package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
  65. package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
  66. package/node_modules/@chati/core/src/darwin-process.js +79 -0
  67. package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
  68. package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
  69. package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
  70. package/node_modules/@chati/core/src/execution-gate.js +8 -0
  71. package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
  72. package/node_modules/@chati/core/src/index.js +18 -0
  73. package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
  74. package/node_modules/@chati/core/src/linux-process.js +133 -0
  75. package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
  76. package/node_modules/@chati/planning/src/index.js +33 -0
  77. package/node_modules/@chati/planning/src/planning-material.js +64 -0
  78. package/node_modules/@chati/planning/src/verification-plan.js +57 -0
  79. package/node_modules/@chati/rail/package.json +1 -0
  80. package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
  81. package/node_modules/@chati/rail/src/index.js +812 -167
  82. package/node_modules/@chati/rail/src/provider-processing.js +118 -0
  83. package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
  84. package/node_modules/@chati/release-lane/src/index.js +40 -43
  85. package/node_modules/@chati/review-council/src/index.js +223 -0
  86. package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
  87. package/package-artifact-manifest.json +1 -1
  88. package/package-artifact-manifest.sig +1 -1
  89. package/package.json +3 -2
  90. package/scripts/verify-real-harness-e2e.js +20 -8
  91. package/src/installer/core.js +147 -107
  92. package/src/installer/package-artifact.js +7 -0
  93. package/src/installer/preflight.js +13 -0
  94. package/src/installer/provider-overlay.js +1 -2
  95. package/src/installer/templates.js +58 -0
  96. package/src/installer-v2/catalog-client.js +31 -1
  97. package/src/installer-v2/clickup-connection-storage.js +92 -0
  98. package/src/installer-v2/clickup-mcp-client.js +155 -0
  99. package/src/installer-v2/clickup-oauth-loopback.js +96 -0
  100. package/src/installer-v2/clickup-oauth.js +233 -0
  101. package/src/installer-v2/index.js +141 -15
  102. package/src/installer-v2/installation-authority.js +98 -4
  103. package/src/installer-v2/provider-executable.js +117 -12
  104. package/src/installer-v2/wizard-installation.js +11 -0
  105. package/src/orchestrator/agent-selector.js +39 -1
  106. package/src/orchestrator/browser-artifact-store.js +131 -0
  107. package/src/orchestrator/cli.js +762 -205
  108. package/src/orchestrator/clickup-mcp-observer.js +226 -0
  109. package/src/orchestrator/clickup-runtime.js +45 -24
  110. package/src/orchestrator/council-gate-runtime.js +903 -0
  111. package/src/orchestrator/evidence-authority-enrollment.js +75 -0
  112. package/src/orchestrator/evidence-authority-request.js +46 -0
  113. package/src/orchestrator/handoff-engine.js +4 -11
  114. package/src/orchestrator/pipeline-manager.js +42 -4
  115. package/src/orchestrator/planning-contract-revision.js +711 -0
  116. package/src/orchestrator/planning-reconciliation.js +302 -0
  117. package/src/orchestrator/planning-runtime.js +476 -29
  118. package/src/orchestrator/rail-adjudication-evidence.js +15 -24
  119. package/src/orchestrator/rail-human-effort-authority.js +46 -0
  120. package/src/orchestrator/rail-human-effort.js +149 -0
  121. package/src/orchestrator/rail-runtime.js +291 -47
  122. package/src/orchestrator/rail-verification.js +278 -0
  123. package/src/orchestrator/review-runtime.js +37 -2
  124. package/src/orchestrator/runtime-installation-v2.js +143 -24
  125. package/src/orchestrator/session-manager.js +1457 -29
  126. package/src/tasks/handoff.js +57 -33
  127. package/src/terminal/adapters/claude-adapter.js +3 -0
  128. package/src/terminal/adapters/codex-adapter.js +13 -0
  129. package/src/terminal/adapters/grok-adapter.js +1 -0
  130. package/src/terminal/agent-attempt-budget.js +35 -0
  131. package/src/terminal/browser-endpoint-proxy.js +174 -0
  132. package/src/terminal/browser-observer-driver.js +94 -0
  133. package/src/terminal/browser-observer-supervisor.js +73 -0
  134. package/src/terminal/browser-observer.js +95 -0
  135. package/src/terminal/browser-process-facts.js +75 -0
  136. package/src/terminal/browser-runtime.js +131 -0
  137. package/src/terminal/darwin-authority-policy.js +213 -0
  138. package/src/terminal/darwin-browser-application.js +116 -0
  139. package/src/terminal/darwin-browser-observer.js +57 -0
  140. package/src/terminal/darwin-execution.js +185 -0
  141. package/src/terminal/executable-snapshot.js +202 -0
  142. package/src/terminal/handoff-parser.js +48 -2
  143. package/src/terminal/isolation.js +14 -13
  144. package/src/terminal/prompt-builder.js +176 -15
  145. package/src/terminal/provider-preflight.js +47 -27
  146. package/src/terminal/rail-execution-worktree.js +22 -6
  147. package/src/terminal/rail-process-observer.js +10 -0
  148. package/src/terminal/rail-prompts.js +21 -3
  149. package/src/terminal/rail-readonly-workspace.js +54 -5
  150. package/src/terminal/rail-worktree-checkpoint.js +315 -0
  151. package/src/terminal/rail-worktree-restoration.js +485 -0
  152. package/src/terminal/run-agent.js +164 -42
  153. package/src/terminal/run-council-gate.js +338 -0
  154. package/src/terminal/run-planning-reconciliation.js +49 -0
  155. package/src/terminal/run-rail-adjudication.js +29 -7
  156. package/src/terminal/run-rail-recovery.js +97 -0
  157. package/src/terminal/run-rail-review.js +36 -15
  158. package/src/terminal/run-rail-rework.js +26 -15
  159. package/src/terminal/run-rail-task.js +22 -9
  160. package/src/terminal/runtime-native-helper.js +85 -0
  161. package/src/terminal/spawner.js +899 -142
  162. package/src/terminal/verification-process-facts.js +49 -0
  163. package/src/upgrade/entrypoints.js +68 -0
  164. package/src/upgrade/generation.js +194 -0
  165. package/src/upgrade/maintenance.js +79 -0
  166. package/src/upgrade/paths.js +81 -0
  167. package/src/upgrade/upgrade.js +267 -0
  168. package/src/utils/execution-role.js +15 -0
  169. package/src/utils/handoff-paths.js +59 -0
  170. package/src/wizard/i18n.js +1 -0
  171. package/src/wizard/index.js +17 -5
  172. package/src/wizard/questions.js +24 -12
@@ -1,10 +1,11 @@
1
1
  import {
2
- closeSync, constants, fstatSync, fsyncSync, linkSync, mkdirSync,
3
- openSync, readSync, unlinkSync, writeSync,
2
+ closeSync, constants, fstatSync, fsyncSync, openSync, readSync, writeSync,
4
3
  } from 'node:fs';
5
4
  import { randomUUID } from 'node:crypto';
6
5
  import { basename, dirname, join, parse, resolve, sep } from 'node:path';
7
- import { canonicalize, sha256 } from '@chati/core';
6
+ import {
7
+ canonicalize, directoryPathSync, linkAtSync, mkdirAtSync, openAtSync, sha256, unlinkAtSync,
8
+ } from '@chati/core';
8
9
 
9
10
  function fail(code, message) { const error = new Error(message); error.code = code; throw error; }
10
11
  const MAX_EVIDENCE_BYTES = 4 * 1024 * 1024;
@@ -41,8 +42,8 @@ function readDescriptorBounded(descriptor) {
41
42
  }
42
43
 
43
44
  function openPinnedEvidenceDirectory(path, { create = true } = {}) {
44
- if (process.platform !== 'linux') {
45
- fail('UNSUPPORTED_ADJUDICATION_EVIDENCE_PLATFORM', 'RAIL adjudication evidence requires Linux descriptor-relative filesystem operations');
45
+ if (!['linux', 'darwin'].includes(process.platform)) {
46
+ fail('UNSUPPORTED_ADJUDICATION_EVIDENCE_PLATFORM', `RAIL adjudication evidence requires descriptor-relative operations; unsupported platform: ${process.platform}`);
46
47
  }
47
48
  const absolute = resolve(path);
48
49
  const root = parse(absolute).root;
@@ -50,10 +51,9 @@ function openPinnedEvidenceDirectory(path, { create = true } = {}) {
50
51
  let descriptor = openSync(root, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
51
52
  try {
52
53
  for (const segment of segments) {
53
- const childPath = join(`/proc/self/fd/${descriptor}`, segment);
54
54
  let childDescriptor;
55
55
  try {
56
- childDescriptor = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
56
+ childDescriptor = openAtSync(descriptor, segment, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
57
57
  } catch (error) {
58
58
  if (error?.code !== 'ENOENT' || !create) {
59
59
  if (['ELOOP', 'ENOTDIR'].includes(error?.code)) {
@@ -61,9 +61,9 @@ function openPinnedEvidenceDirectory(path, { create = true } = {}) {
61
61
  }
62
62
  throw error;
63
63
  }
64
- try { mkdirSync(childPath, { mode: 0o700 }); }
64
+ try { mkdirAtSync(descriptor, segment, { mode: 0o700 }); }
65
65
  catch (mkdirError) { if (mkdirError?.code !== 'EEXIST') throw mkdirError; }
66
- childDescriptor = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
66
+ childDescriptor = openAtSync(descriptor, segment, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
67
67
  }
68
68
  const stat = fstatSync(childDescriptor);
69
69
  if (!stat.isDirectory()) {
@@ -77,7 +77,6 @@ function openPinnedEvidenceDirectory(path, { create = true } = {}) {
77
77
  absolute,
78
78
  descriptor,
79
79
  identity: fstatSync(descriptor),
80
- reference: `/proc/self/fd/${descriptor}`,
81
80
  });
82
81
  } catch (error) {
83
82
  closeSync(descriptor);
@@ -85,13 +84,6 @@ function openPinnedEvidenceDirectory(path, { create = true } = {}) {
85
84
  }
86
85
  }
87
86
 
88
- function evidenceEntryPath(directory, name) {
89
- if (basename(name) !== name || name === '.' || name === '..') {
90
- fail('UNSAFE_ADJUDICATION_EVIDENCE_FILE', 'adjudication evidence entry must be one path segment');
91
- }
92
- return join(directory.reference, name);
93
- }
94
-
95
87
  function assertPinnedEvidenceDirectory(directory) {
96
88
  const descriptorStat = fstatSync(directory.descriptor);
97
89
  if (!descriptorStat.isDirectory() || !sameIdentity(descriptorStat, directory.identity)) {
@@ -117,10 +109,9 @@ function readRegular(path, { allowMissing = false, directory = null } = {}) {
117
109
  try { activeDirectory = openPinnedEvidenceDirectory(dirname(path), { create: false }); }
118
110
  catch (error) { if (allowMissing && error?.code === 'ENOENT') return null; throw error; }
119
111
  }
120
- const entry = evidenceEntryPath(activeDirectory, basename(path));
121
112
  let descriptor;
122
113
  try {
123
- descriptor = openSync(entry, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
114
+ descriptor = openAtSync(activeDirectory.descriptor, basename(path), constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
124
115
  } catch (error) {
125
116
  if (ownedDirectory) closeSync(activeDirectory.descriptor);
126
117
  if (allowMissing && error?.code === 'ENOENT') return null;
@@ -162,8 +153,8 @@ function persistImmutable(path, serialized) {
162
153
  const directory = openPinnedEvidenceDirectory(dirname(path));
163
154
  const name = basename(path);
164
155
  const temporaryName = `${name}.${process.pid}.${randomUUID()}.tmp`;
165
- const temporary = evidenceEntryPath(directory, temporaryName);
166
- const destination = evidenceEntryPath(directory, name);
156
+ // Diagnostic/test-hook path only. Actual reads and mutations use *at ops.
157
+ const temporary = join(directoryPathSync(directory.descriptor), temporaryName);
167
158
  let temporaryDescriptor;
168
159
  try {
169
160
  const existing = readRegular(path, { allowMissing: true, directory });
@@ -172,7 +163,7 @@ function persistImmutable(path, serialized) {
172
163
  return false;
173
164
  }
174
165
  runSecurityTestHook('beforeEvidenceTempOpen', { path, temporary, directory: directory.absolute });
175
- temporaryDescriptor = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
166
+ temporaryDescriptor = openAtSync(directory.descriptor, temporaryName, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
176
167
  runSecurityTestHook('afterEvidenceTempOpen', { path, temporary, directory: directory.absolute });
177
168
  let offset = 0;
178
169
  while (offset < bytes.length) offset += writeSync(temporaryDescriptor, bytes, offset, bytes.length - offset);
@@ -181,7 +172,7 @@ function persistImmutable(path, serialized) {
181
172
  temporaryDescriptor = undefined;
182
173
  assertPinnedEvidenceDirectory(directory);
183
174
  runSecurityTestHook('beforeEvidenceCommit', { path, temporary, directory: directory.absolute });
184
- try { linkSync(temporary, destination); }
175
+ try { linkAtSync(directory.descriptor, temporaryName, directory.descriptor, name); }
185
176
  catch (error) {
186
177
  if (error?.code !== 'EEXIST') throw error;
187
178
  const raced = readRegular(path, { directory });
@@ -194,7 +185,7 @@ function persistImmutable(path, serialized) {
194
185
  return true;
195
186
  } finally {
196
187
  if (temporaryDescriptor !== undefined) closeSync(temporaryDescriptor);
197
- try { unlinkSync(temporary); } catch { /* preserve the primary result */ }
188
+ try { unlinkAtSync(directory.descriptor, temporaryName); } catch { /* preserve the primary result */ }
198
189
  fsyncSync(directory.descriptor);
199
190
  closeSync(directory.descriptor);
200
191
  }
@@ -0,0 +1,46 @@
1
+ import { canonicalSerialize } from '@chati/core';
2
+ import { installationAuthorityProjectBinding, verifyHostAuthorityPayload } from '../installer-v2/installation-authority.js';
3
+
4
+ export const HUMAN_EFFORT_AUTHORITY_DOMAIN = 'chati.rail-human-effort.v1';
5
+ const COMMON = ['schema_version', 'purpose', 'session_id', 'attempt_id', 'handoff_id', 'task_id',
6
+ 'project_binding', 'operator_id', 'claim_id', 'account', 'observed_at', 'phase'];
7
+ const FINAL = ['opened_receipt_digest', 'declaration', 'intervals', 'elapsed_ms', 'value', 'unit'];
8
+ const finite = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
9
+ const exact = (value, keys) => value && typeof value === 'object' && !Array.isArray(value)
10
+ && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
11
+
12
+ export function verifyHumanEffortReceipt(receipt, { projectDir, authorityDirectory, expected = {} } = {}) {
13
+ try {
14
+ if (!['opened', 'finished', 'interrupted'].includes(receipt?.phase)) return false;
15
+ const keys = [...COMMON, ...(receipt.phase === 'opened' ? [] : FINAL), 'authority_id', 'authority_signature'];
16
+ if (!exact(receipt, keys) || receipt.schema_version !== 1 || receipt.purpose !== 'rail_human_effort') return false;
17
+ for (const key of ['session_id', 'attempt_id', 'handoff_id', 'task_id', 'operator_id', 'claim_id', 'observed_at']) {
18
+ if (typeof receipt[key] !== 'string' || !receipt[key].trim()) return false;
19
+ }
20
+ if (!/^human-effort-[a-f0-9-]{36}$/.test(receipt.session_id) || !Number.isFinite(Date.parse(receipt.observed_at))) return false;
21
+ if (!exact(receipt.account, ['uid', 'username', 'hostname']) || !Number.isInteger(receipt.account.uid)
22
+ || receipt.account.uid < 0 || !['username', 'hostname'].every((key) => typeof receipt.account[key] === 'string' && receipt.account[key])) return false;
23
+ if (canonicalSerialize(receipt.project_binding) !== canonicalSerialize(installationAuthorityProjectBinding(projectDir, receipt.project_binding.project_id))) return false;
24
+ if (Object.entries(expected).some(([key, value]) => canonicalSerialize(receipt[key]) !== canonicalSerialize(value))) return false;
25
+ if (receipt.phase !== 'opened') {
26
+ if (!/^[a-f0-9]{64}$/.test(receipt.opened_receipt_digest) || !finite(receipt.elapsed_ms)
27
+ || receipt.unit !== 'milliseconds' || !Array.isArray(receipt.intervals) || receipt.intervals.length > 1024) return false;
28
+ let prior = 0;
29
+ let value = 0;
30
+ for (const interval of receipt.intervals) {
31
+ if (!exact(interval, ['start_ms', 'end_ms']) || !finite(interval.start_ms) || !finite(interval.end_ms)
32
+ || interval.start_ms < prior || interval.end_ms < interval.start_ms || interval.end_ms > receipt.elapsed_ms) return false;
33
+ prior = interval.end_ms;
34
+ value += interval.end_ms - interval.start_ms;
35
+ }
36
+ if (receipt.phase === 'interrupted') {
37
+ if (receipt.declaration !== 'incomplete' || receipt.value !== null) return false;
38
+ } else if (receipt.declaration === 'zero-effort') {
39
+ if (receipt.intervals.length !== 0 || receipt.value !== 0) return false;
40
+ } else if (receipt.declaration !== 'timed-effort' || receipt.intervals.length === 0 || !finite(receipt.value) || receipt.value !== value) return false;
41
+ }
42
+ const { authority_id, authority_signature, ...payload } = receipt;
43
+ return verifyHostAuthorityPayload(payload, { domain: HUMAN_EFFORT_AUTHORITY_DOMAIN,
44
+ authorityId: authority_id, signature: authority_signature, authorityDirectory });
45
+ } catch { return false; }
46
+ }
@@ -0,0 +1,149 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, constants, fstatSync, openSync, realpathSync } from 'node:fs';
3
+ import { hostname, userInfo } from 'node:os';
4
+ import { join, resolve, sep } from 'node:path';
5
+ import { createInterface } from 'node:readline';
6
+ import { sha256, tryExclusiveFileLockSync } from '@chati/core';
7
+ import { persistImmutableInstallationFile, readSafeInstallationStorageFile } from '../installer-v2/catalog-client.js';
8
+ import { installationAuthorityProjectBinding, installationAuthorityStateDirectory, signHostAuthorityPayload } from '../installer-v2/installation-authority.js';
9
+ import { createProjectRailEngine, resolveRailAttemptContext } from './rail-runtime.js';
10
+ import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
11
+ import { HUMAN_EFFORT_AUTHORITY_DOMAIN } from './rail-human-effort-authority.js';
12
+
13
+ function fail(code, message) { throw Object.assign(new Error(message), { code }); }
14
+
15
+ function acquireEffortLock(projectDir, authorityDirectory) {
16
+ const root = installationAuthorityStateDirectory(authorityDirectory);
17
+ if (root === projectDir || root.startsWith(`${projectDir}${sep}`) || projectDir.startsWith(`${root}${sep}`)) {
18
+ fail('HUMAN_EFFORT_STORAGE_OVERLAP', 'human effort lock must remain outside the project in private authority storage');
19
+ }
20
+ const path = join(root, 'human-effort.lock');
21
+ const content = 'chati-human-effort-lock-v1\n';
22
+ persistImmutableInstallationFile(path, content, { label: 'human effort host lock', maxBytes: 1024 });
23
+ const source = readSafeInstallationStorageFile(path, { maxBytes: 1024 });
24
+ const fd = openSync(path, constants.O_RDWR | constants.O_NOFOLLOW | constants.O_NONBLOCK);
25
+ const same = (stat) => stat.dev === source.stat.dev && stat.ino === source.stat.ino
26
+ && stat.isFile() && stat.nlink === 1n && (stat.mode & 0o777n) === 0o600n && stat.uid === BigInt(userInfo().uid);
27
+ const assertStable = () => {
28
+ const current = readSafeInstallationStorageFile(path, { maxBytes: 1024 });
29
+ if (!same(fstatSync(fd, { bigint: true })) || !same(current.stat) || current.content !== content) {
30
+ fail('HUMAN_EFFORT_LOCK_CHANGED', 'human effort lock identity or permissions changed during measurement');
31
+ }
32
+ };
33
+ try {
34
+ assertStable();
35
+ if (!tryExclusiveFileLockSync(fd)) fail('HUMAN_EFFORT_ALREADY_ACTIVE', 'this host account already has an active human effort session');
36
+ assertStable();
37
+ return { assertStable, close: () => closeSync(fd) };
38
+ } catch (error) { closeSync(fd); throw error; }
39
+ }
40
+
41
+ /** Live, monotonic supervisor. No API accepts a duration or a finished report. */
42
+ export function openHumanEffortSession(input = {}) {
43
+ const keys = ['projectDir', 'handoff_id', 'attempt_id', 'authorityDirectory'];
44
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some((key) => !keys.includes(key))
45
+ || ['projectDir', 'handoff_id', 'attempt_id'].some((key) => typeof input[key] !== 'string' || !input[key].trim())) {
46
+ fail('HUMAN_EFFORT_INPUT_INVALID', 'human effort accepts only canonical project, handoff and attempt identifiers');
47
+ }
48
+ const projectDir = realpathSync(resolve(input.projectDir));
49
+ const context = resolveRailAttemptContext({ ...input, projectDir });
50
+ const artifact = loadRuntimeInstallationV2(projectDir);
51
+ if (!artifact || artifact.project_id !== context.handoff.project_id) fail('V2_INSTALLATION_REQUIRED', 'human effort requires the verified project installation');
52
+ if (!['active', 'ready_to_merge'].includes(context.state)) fail('HUMAN_EFFORT_ATTEMPT_INACTIVE', 'human effort session requires an unfinished attempt');
53
+ const account = userInfo();
54
+ const base = { schema_version: 1, purpose: 'rail_human_effort', session_id: `human-effort-${randomUUID()}`,
55
+ project_binding: installationAuthorityProjectBinding(projectDir, artifact.project_id),
56
+ attempt_id: input.attempt_id, handoff_id: input.handoff_id, task_id: context.task.task_id,
57
+ operator_id: context.claim.operator_id, claim_id: context.claim.claim_id,
58
+ account: { uid: account.uid, username: account.username, hostname: hostname() } };
59
+ const lock = acquireEffortLock(projectDir, input.authorityDirectory);
60
+ const origin = process.hrtime.bigint();
61
+ const offset = () => Number(process.hrtime.bigint() - origin) / 1e6;
62
+ let state = 'ready';
63
+ let activeStart;
64
+ const intervals = [];
65
+ let opened;
66
+ const persist = (payload) => {
67
+ lock.assertStable();
68
+ const proof = signHostAuthorityPayload(payload, { domain: HUMAN_EFFORT_AUTHORITY_DOMAIN, authorityDirectory: input.authorityDirectory });
69
+ const receipt = { ...payload, authority_id: proof.authority_id, authority_signature: proof.signature };
70
+ const record = createProjectRailEngine({ projectDir, humanAuthorityDirectory: input.authorityDirectory })
71
+ .recordHumanEffortObservation({ attempt_id: input.attempt_id, receipt });
72
+ return { receipt, event_id: record.event_id };
73
+ };
74
+ try { opened = persist({ ...base, phase: 'opened', observed_at: new Date().toISOString() }); }
75
+ catch (error) { lock.close(); throw error; }
76
+ const result = () => Object.freeze({ ok: true, session_id: base.session_id, status: state, language: context.language });
77
+ return Object.freeze({
78
+ status: result,
79
+ transition(action) {
80
+ if (!['start', 'pause', 'resume', 'finish', 'zero', 'interrupt'].includes(action) || ['finished', 'interrupted'].includes(state)) {
81
+ fail('HUMAN_EFFORT_TRANSITION_INVALID', 'invalid human effort command or session already closed');
82
+ }
83
+ if ((action === 'start' || action === 'zero') && state !== 'ready'
84
+ || action === 'pause' && state !== 'active' || action === 'resume' && state !== 'paused'
85
+ || action === 'finish' && !['active', 'paused'].includes(state)) {
86
+ fail('HUMAN_EFFORT_TRANSITION_INVALID', `cannot ${action} while ${state}`);
87
+ }
88
+ try { lock.assertStable(); }
89
+ catch (error) { state = 'interrupted'; lock.close(); throw error; }
90
+ const now = offset();
91
+ if (action === 'start' || action === 'resume') {
92
+ if (intervals.length >= 1024) fail('HUMAN_EFFORT_INTERVAL_LIMIT', 'finish this bounded session before starting more intervals');
93
+ activeStart = now;
94
+ state = 'active';
95
+ return result();
96
+ }
97
+ if (state === 'active') { intervals.push({ start_ms: activeStart, end_ms: now }); activeStart = undefined; }
98
+ if (action === 'pause') { state = 'paused'; return result(); }
99
+ state = action === 'interrupt' ? 'interrupted' : 'finished';
100
+ try {
101
+ const final = persist({ ...base, phase: state, observed_at: new Date().toISOString(),
102
+ opened_receipt_digest: sha256(opened.receipt), declaration: action === 'interrupt' ? 'incomplete' : action === 'zero' ? 'zero-effort' : 'timed-effort',
103
+ intervals, elapsed_ms: now, unit: 'milliseconds',
104
+ value: action === 'interrupt' ? null : intervals.reduce((total, interval) => total + (interval.end_ms - interval.start_ms), 0) });
105
+ return Object.freeze({ ...result(), ok: state === 'finished', source_event_id: final.event_id,
106
+ receipt_ref: `rail-human-effort://sha256/${sha256(final.receipt)}` });
107
+ } finally { lock.close(); }
108
+ },
109
+ });
110
+ }
111
+
112
+ export function collectRailHumanEffort({ projectDir, attempt_id, authorityDirectory }) {
113
+ return createProjectRailEngine({ projectDir, humanAuthorityDirectory: authorityDirectory }).humanEffortForAttempt(attempt_id);
114
+ }
115
+
116
+ /** Explicit operator interaction, never piped duration JSON or inferred presence. */
117
+ export async function runHumanEffortTerminal(input) {
118
+ if (!process.stdin.isTTY) fail('HUMAN_EFFORT_TTY_REQUIRED', 'human effort requires an interactive terminal declaration');
119
+ const session = openHumanEffortSession(input);
120
+ const instructions = {
121
+ en: 'Declare your active effort: start, pause, resume, finish. Use zero only to explicitly declare no human effort. EOF/interrupt leaves effort incomplete.',
122
+ pt: 'Declare seu esforço ativo: start, pause, resume, finish. Use zero somente para declarar explicitamente ausência de esforço humano. EOF/interrupção deixa o registro incompleto.',
123
+ fr: 'Déclarez votre effort actif : start, pause, resume, finish. Utilisez zero uniquement pour déclarer explicitement aucun effort humain. EOF/interruption laisse le relevé incomplet.',
124
+ es: 'Declare su esfuerzo activo: start, pause, resume, finish. Use zero solo para declarar explícitamente ningún esfuerzo humano. EOF/interrupción deja el registro incompleto.',
125
+ };
126
+ process.stderr.write(`${instructions[session.status().language] ?? instructions.en}\n`);
127
+ const terminal = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
128
+ const stop = () => terminal.close();
129
+ terminal.on('SIGINT', stop);
130
+ process.on('SIGTERM', stop);
131
+ let result;
132
+ try {
133
+ for await (const line of terminal) {
134
+ try { result = session.transition(line.trim()); }
135
+ catch (error) {
136
+ if (error.code !== 'HUMAN_EFFORT_TRANSITION_INVALID') throw error;
137
+ process.stderr.write(`${error.code}: ${error.message}\n`);
138
+ continue;
139
+ }
140
+ if (['finished', 'interrupted'].includes(result.status)) break;
141
+ process.stderr.write(`${result.status}\n`);
142
+ }
143
+ } finally {
144
+ terminal.close();
145
+ process.removeListener('SIGTERM', stop);
146
+ if (!['finished', 'interrupted'].includes(session.status().status)) result = session.transition('interrupt');
147
+ }
148
+ return result;
149
+ }