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
@@ -24,6 +24,35 @@ export const AGENT_PIPELINE = [
24
24
  { name: 'devops', phase: 'deploy', group: 'deploy', parallel: false },
25
25
  ];
26
26
 
27
+ const V2_PLANNING_PREDECESSORS = Object.freeze({
28
+ brief: Object.freeze(['work-understanding']),
29
+ detail: Object.freeze(['work-understanding', 'brief']),
30
+ architect: Object.freeze(['work-understanding', 'brief']),
31
+ ux: Object.freeze(['work-understanding', 'brief']),
32
+ 'qa-planning': Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux']),
33
+ phases: Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux', 'qa-planning']),
34
+ tasks: Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux', 'qa-planning', 'phases']),
35
+ });
36
+
37
+ // Shared by reservation, command consumption and canonical advance. Keep this
38
+ // check pure so the session writer can apply the same contract under its lock.
39
+ export function assertV2PlanningPredecessors(session, agent) {
40
+ const projectType = session?.project_type || session?.project?.type || 'greenfield';
41
+ const required = (V2_PLANNING_PREDECESSORS[agent] || []).map((name) =>
42
+ name === 'work-understanding' ? (projectType === 'brownfield' ? 'brownfield-wu' : 'greenfield-wu') : name
43
+ );
44
+ const incomplete = required.filter((name) =>
45
+ session?.agents?.[name]?.status !== 'completed' || !session?.completed_agents?.includes(name)
46
+ );
47
+ if (incomplete.length > 0) {
48
+ const error = new Error(`Agent ${agent} cannot execute or advance before its canonical predecessors complete: ${incomplete.join(', ')}`);
49
+ error.code = 'V2_PLANNING_PREDECESSORS_INCOMPLETE';
50
+ error.incomplete = incomplete;
51
+ throw error;
52
+ }
53
+ return Object.freeze({ valid: true, required: Object.freeze(required) });
54
+ }
55
+
27
56
  /**
28
57
  * Get agent definition by name.
29
58
  *
@@ -197,9 +226,10 @@ export function selectAgent(context) {
197
226
  *
198
227
  * @param {string} agentName - Current agent
199
228
  * @param {string[]} completedAgents - Already completed
229
+ * @param {object} [options] - V2-only recovery; legacy workflows retain forward selection
200
230
  * @returns {{ next: string|null, isParallel: boolean, group: string[] }}
201
231
  */
202
- export function getNextAgent(agentName, completedAgents = []) {
232
+ export function getNextAgent(agentName, completedAgents = [], { recoverPlanningPeers = false } = {}) {
203
233
  const currentDef = getAgentDef(agentName);
204
234
  if (!currentDef) {
205
235
  return { next: null, isParallel: false, group: [] };
@@ -207,6 +237,14 @@ export function getNextAgent(agentName, completedAgents = []) {
207
237
 
208
238
  const currentIndex = AGENT_PIPELINE.indexOf(currentDef);
209
239
 
240
+ // Planning peers may finish in any order. The last completed member is not
241
+ // a watermark for the whole group, including after a legacy downstream cursor.
242
+ if (recoverPlanningPeers && (currentDef.phase === 'plan' || agentName === 'brief') && completedAgents.includes('brief')) {
243
+ const peers = getAgentsByGroup('plan-parallel');
244
+ const unfinished = peers.find(({ name }) => !completedAgents.includes(name));
245
+ if (unfinished) return { next: unfinished.name, isParallel: true, group: peers.map(({ name }) => name) };
246
+ }
247
+
210
248
  // Look ahead for next incomplete agent
211
249
  for (let i = currentIndex + 1; i < AGENT_PIPELINE.length; i++) {
212
250
  const nextDef = AGENT_PIPELINE[i];
@@ -0,0 +1,131 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { join, resolve } from 'node:path';
3
+ import { canonicalSerialize, sha256 } from '@chati/core';
4
+ import { installationAuthorityProjectBinding, signHostAuthorityPayload, verifyHostAuthorityPayload } from '../installer-v2/installation-authority.js';
5
+ import { listSafeInstallationStorageFiles, persistImmutableInstallationFile, readSafeInstallationStorageFile, removeSafeInstallationStorageFile } from '../installer-v2/catalog-client.js';
6
+
7
+ export const LOCAL_BROWSER_ARTIFACT_PREFIX = 'rail-browser-artifact://sha256/';
8
+ const DOMAIN = 'chati.browser-artifact-retention.v1';
9
+ const SEMANTICS = 'logical-ttl-opportunistic-purge-v1';
10
+ const RETENTION = Object.freeze({ 'internal-30d': 30 * 24 * 60 * 60 * 1000 });
11
+ const META_FIELDS = ['schema_version', 'artifact_id', 'run_id', 'project_id', 'project_binding', 'attempt_id',
12
+ 'commit_ref', 'policy_ref', 'browser_session_id', 'kind', 'content_sha256', 'retention_class',
13
+ 'retention_semantics', 'captured_at', 'expires_at'];
14
+ function fail(code, message) { throw Object.assign(new Error(message), { code }); }
15
+ function exact(value, fields) {
16
+ return value && typeof value === 'object' && !Array.isArray(value)
17
+ && Object.keys(value).length === fields.length && Object.keys(value).every((key) => fields.includes(key));
18
+ }
19
+ function now(clock) {
20
+ const value = new Date(clock()).getTime();
21
+ if (!Number.isFinite(value)) fail('BROWSER_RETENTION_TIME_INVALID', 'browser retention clock must be valid');
22
+ return value;
23
+ }
24
+ function directory(projectDir) { return join(resolve(projectDir), '.chati/v2/rail/browser-artifacts'); }
25
+ function pathFor(projectDir, ref) {
26
+ if (typeof ref !== 'string' || !/^rail-browser-artifact:\/\/sha256\/[a-f0-9]{64}$/.test(ref)) fail('BROWSER_ARTIFACT_REF_INVALID', 'expected a content-addressed local browser artifact reference');
27
+ return join(directory(projectDir), `${ref.slice(LOCAL_BROWSER_ARTIFACT_PREFIX.length)}.json`);
28
+ }
29
+ export function browserRetentionPolicy(retentionClass) {
30
+ if (!Object.hasOwn(RETENTION, retentionClass)) fail('BROWSER_RETENTION_UNSUPPORTED', 'this local store supports internal-30d logical TTL only; physical-deletion guarantees require a different backend');
31
+ return Object.freeze({ retention_class: retentionClass, semantics: SEMANTICS, duration_ms: RETENTION[retentionClass] });
32
+ }
33
+
34
+ function assertRedactedContent(content) {
35
+ if (!exact(content, ['schema_version', 'projection', 'nodes']) || content.schema_version !== 1
36
+ || content.projection !== 'dom-structure-all-values-redacted' || !Array.isArray(content.nodes)
37
+ || content.nodes.length === 0 || content.nodes.length > 5000) fail('BROWSER_ARTIFACT_CONTENT_INVALID', 'only the supported all-values-redacted DOM projection can enter this store');
38
+ const tags = new Set('HTML HEAD BODY TITLE META LINK STYLE SCRIPT DIV SPAN P H1 H2 H3 H4 H5 H6 A BUTTON INPUT FORM LABEL SELECT OPTION TEXTAREA UL OL LI TABLE THEAD TBODY TR TH TD MAIN NAV HEADER FOOTER SECTION ARTICLE ASIDE IMG SVG PATH CANVAS VIDEO AUDIO IFRAME BR HR STRONG EM B I PRE CODE #text #document OTHER'.split(' '));
39
+ for (const node of content.nodes) {
40
+ if (!exact(node, ['document', 'index', 'parent', 'type', 'tag', ...(node?.type === 3 ? ['text'] : [])])
41
+ || !['document', 'index', 'parent', 'type'].every((key) => Number.isInteger(node[key]))
42
+ || node.document < 0 || node.index < 0 || node.parent < -1 || node.type < 1 || node.type > 12
43
+ || !tags.has(node.tag) || (node.type === 3 && node.text !== '[REDACTED]')) fail('BROWSER_ARTIFACT_CONTENT_INVALID', 'DOM structure contains unsupported or unredacted fields');
44
+ }
45
+ }
46
+
47
+ function authenticatedRecord({ projectDir, projectId, ref, authorityDirectory }) {
48
+ const path = pathFor(projectDir, ref);
49
+ const source = readSafeInstallationStorageFile(path, { allowMissing: true, label: 'retained browser artifact', maxBytes: 2 * 1024 * 1024 });
50
+ if (!source) fail('BROWSER_ARTIFACT_MISSING', 'retained browser artifact is missing');
51
+ if (source.stat.uid !== BigInt(process.geteuid()) || (source.stat.mode & 0o777n) !== 0o600n) fail('BROWSER_ARTIFACT_STORAGE_UNSAFE', 'retained browser artifact must be private and owned by the current account');
52
+ let record;
53
+ try { record = JSON.parse(source.content); } catch { fail('BROWSER_ARTIFACT_INVALID', 'retained browser artifact is not valid JSON'); }
54
+ const metadata = record?.metadata;
55
+ if (!exact(record, ['metadata', 'content', 'authority_receipt']) || !exact(metadata, META_FIELDS)
56
+ || metadata.schema_version !== 1 || metadata.kind !== 'dom' || metadata.project_id !== projectId
57
+ || metadata.project_binding !== sha256(installationAuthorityProjectBinding(projectDir, projectId))
58
+ || ref !== `${LOCAL_BROWSER_ARTIFACT_PREFIX}${sha256(metadata)}`
59
+ || metadata.content_sha256 !== sha256(record.content)
60
+ || metadata.retention_semantics !== SEMANTICS
61
+ || !exact(record.authority_receipt, ['authority_id', 'signature'])
62
+ || !verifyHostAuthorityPayload(metadata, { domain: DOMAIN, authorityDirectory,
63
+ authorityId: record.authority_receipt.authority_id, signature: record.authority_receipt.signature })) {
64
+ fail('BROWSER_ARTIFACT_INVALID', 'retained browser artifact metadata, bytes or host authentication changed');
65
+ }
66
+ const policy = browserRetentionPolicy(metadata.retention_class);
67
+ const captured = Date.parse(metadata.captured_at); const expires = Date.parse(metadata.expires_at);
68
+ if (!Number.isFinite(captured) || !Number.isFinite(expires) || expires - captured !== policy.duration_ms
69
+ || new Date(captured).toISOString() !== metadata.captured_at || new Date(expires).toISOString() !== metadata.expires_at) fail('BROWSER_ARTIFACT_INVALID', 'retained browser artifact has an invalid fixed lifetime');
70
+ assertRedactedContent(record.content);
71
+ return { path, source, record, expires };
72
+ }
73
+
74
+ /** Fixed lifetime from capture. Reading/retrying never modifies this record. */
75
+ export function readBrowserArtifact({ projectDir, projectId, ref, clock = () => new Date(), authorityDirectory } = {}) {
76
+ const loaded = authenticatedRecord({ projectDir, projectId, ref, authorityDirectory });
77
+ if (now(clock) >= loaded.expires) {
78
+ let purgeError;
79
+ try { removeSafeInstallationStorageFile(loaded.path, loaded.source); } catch (error) { purgeError = error.code || error.name; }
80
+ const error = Object.assign(new Error(`browser artifact expired${purgeError ? `; physical purge failed: ${purgeError}` : ''}`), { code: 'BROWSER_ARTIFACT_EXPIRED' });
81
+ if (purgeError) error.purge_error = purgeError;
82
+ throw error;
83
+ }
84
+ return Object.freeze({ ref, ...loaded.record });
85
+ }
86
+
87
+ export function persistBrowserArtifact({ projectDir, projectId, run_id, attempt_id, commit_ref, policy_ref,
88
+ browser_session_id, artifact, retention_class, captured_at, clock = () => new Date(), authorityDirectory } = {}) {
89
+ const policy = browserRetentionPolicy(retention_class);
90
+ if (!artifact || artifact.kind !== 'dom' || artifact.redaction_status !== 'redacted') fail('BROWSER_ARTIFACT_CONTENT_INVALID', 'artifact must be an actually redacted DOM observation');
91
+ assertRedactedContent(artifact.content);
92
+ const captured = Date.parse(captured_at);
93
+ if (!Number.isFinite(captured) || new Date(captured).toISOString() !== captured_at || captured > now(clock)
94
+ || now(clock) >= captured + policy.duration_ms) fail('BROWSER_ARTIFACT_CAPTURE_INVALID', 'artifact capture must be current, never future or already expired');
95
+ if (![run_id, attempt_id, commit_ref, policy_ref, browser_session_id, projectId].every((value) => typeof value === 'string' && value.trim())) fail('BROWSER_ARTIFACT_BINDING_REQUIRED', 'retained artifact requires canonical project, attempt, result and browser provenance');
96
+ const metadata = { schema_version: 1, artifact_id: `browser-artifact-${randomUUID()}`, run_id, project_id: projectId,
97
+ project_binding: sha256(installationAuthorityProjectBinding(projectDir, projectId)), attempt_id, commit_ref,
98
+ policy_ref, browser_session_id, kind: 'dom', content_sha256: sha256(artifact.content), retention_class,
99
+ retention_semantics: SEMANTICS, captured_at, expires_at: new Date(captured + policy.duration_ms).toISOString() };
100
+ const ref = `${LOCAL_BROWSER_ARTIFACT_PREFIX}${sha256(metadata)}`;
101
+ const record = { metadata, content: artifact.content,
102
+ authority_receipt: signHostAuthorityPayload(metadata, { domain: DOMAIN, authorityDirectory }) };
103
+ persistImmutableInstallationFile(pathFor(projectDir, ref), `${canonicalSerialize(record)}\n`, { label: 'retained browser artifact', maxBytes: 2 * 1024 * 1024 });
104
+ readBrowserArtifact({ projectDir, projectId, ref, clock, authorityDirectory });
105
+ return Object.freeze({ artifact_id: metadata.artifact_id, kind: 'dom', sanitized_ref: ref,
106
+ redaction_status: 'redacted', retention_class, content_sha256: metadata.content_sha256 });
107
+ }
108
+
109
+ export function pruneBrowserArtifacts({ projectDir, projectId, clock = () => new Date(), authorityDirectory } = {}) {
110
+ const timestamp = now(clock); const removed = []; const failures = [];
111
+ for (const name of listSafeInstallationStorageFiles(directory(projectDir))) {
112
+ if (!/^[a-f0-9]{64}\.json$/.test(name)) continue;
113
+ const ref = `${LOCAL_BROWSER_ARTIFACT_PREFIX}${name.slice(0, -5)}`;
114
+ try {
115
+ const loaded = authenticatedRecord({ projectDir, projectId, ref, authorityDirectory });
116
+ if (timestamp >= loaded.expires && removeSafeInstallationStorageFile(loaded.path, loaded.source)) removed.push(ref);
117
+ } catch (error) { failures.push({ ref, code: error.code || error.name }); }
118
+ }
119
+ return Object.freeze({ ok: failures.length === 0, removed, failures });
120
+ }
121
+
122
+ export function assertLocalBrowserArtifacts({ projectDir, projectId, artifacts, expected, clock = () => new Date() }) {
123
+ for (const artifact of artifacts) {
124
+ if (!artifact.sanitized_ref.startsWith(LOCAL_BROWSER_ARTIFACT_PREFIX)) continue;
125
+ const { metadata } = readBrowserArtifact({ projectDir, projectId, ref: artifact.sanitized_ref, clock });
126
+ if (metadata.content_sha256 !== artifact.content_sha256 || metadata.retention_class !== artifact.retention_class
127
+ || metadata.artifact_id !== artifact.artifact_id || metadata.kind !== artifact.kind
128
+ || !Object.entries(expected).every(([key, value]) => metadata[key] === value)) fail('BROWSER_ARTIFACT_BINDING_MISMATCH', 'local browser artifact does not match certified execution and policy provenance');
129
+ }
130
+ return true;
131
+ }