@redue/cli 0.1.0-alpha.6 → 0.1.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-alpha.8 — 2026-10-08
4
+
5
+ - Consume compiler-certificate input asynchronously while preserving the input-size
6
+ limit, parent deadline, complete-input validation and conservative fallbacks.
7
+ - Support the exact reviewed TypeScript 5.5.2 distribution in the explicitly selected
8
+ single-package npm direct-compiler recipe. Enforce implementation membership and
9
+ reject ETW module overrides before compiler API loading.
10
+ - Real independent Codex/macOS sessions reused compiler evidence on the pinned
11
+ vscode-js-debug project. Cross-platform CLI/package acceptance is separate;
12
+ Claude behavior and cross-host handoff remain untested.
13
+ - The measured eight-case workflow avoided four compiler executions but cost
14
+ 20.01 seconds versus 16.97 seconds for eight ordinary compiler runs. No net
15
+ workflow or whole-agent speedup is claimed. This verifies compiler inputs only,
16
+ not npm lifecycle behavior or application correctness.
17
+ - Stop observers before upgrading, retain the same configuration/state, then
18
+ restart. Do not delete receipts or automatically convert existing checks.
19
+
20
+ ## 0.1.0-alpha.7 — 2026-10-07
21
+
22
+ - Guide explicitly configured direct-compiler reuse through one caller-aware
23
+ synchronized query; skip reuse queries when fresh execution is already required.
24
+ - Update intact project-local agent instructions safely; installation alone does
25
+ not update them. See the [existing-user procedure](docs/user/agents.md#update-an-existing-connection).
26
+ - In the finite Codex/macOS comparison, revised guidance reached eligible evidence
27
+ and reused it in 4/4 opportunities versus 1/4; verification tool calls fell
28
+ from 28 to 14. Evidence semantics are unchanged. No universal compliance,
29
+ tail-latency or causal agent-task speedup is claimed. Claude behavior and
30
+ cross-host handoff remain untested.
31
+
3
32
  ## 0.1.0-alpha.6 — 2026-10-07
4
33
 
5
34
  - Reuse guarded compiler discovery for warm direct-TypeScript decisions on macOS
package/bin/redue.mjs CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
+ import {mark,traceBase,traceContext,traceEnvironment,traceId} from '../src/decision-profile.mjs';
4
+ traceBase({...traceContext(),request:traceId(),work:'client'});
5
+ mark('client.loaded');
3
6
  import {directCompiler,directQualification,directEnvironment,directInterpretation} from '../src/direct-typescript.mjs';
4
7
  import os from 'node:os';
5
8
  import path from 'node:path';
@@ -154,6 +157,7 @@ function printLifecycle(output,action,machine){
154
157
  if(stateOverride&&['stop','remove-state'].includes(command)){
155
158
  const runtime=path.join(stateOverride,'project-runtime-v1.json');
156
159
  if(!fs.existsSync(runtime)){console.log('No REDUE state exists at this path.');process.exit(0);}
160
+ mark('client.engine_submit');
157
161
  const result=spawnSync(process.execPath,[engine,runtime,command==='stop'?'stop':'uninstall'],
158
162
  {cwd:os.tmpdir(),stdio:['inherit','pipe','inherit'],encoding:'utf8',env:process.env});
159
163
  if(result.error)error(result.error.message);
@@ -334,9 +338,11 @@ if(command==='run'){
334
338
  if(outcome.error)error(outcome.error.message);
335
339
  process.exit(outcome.status??(outcome.signal?128+os.constants.signals[outcome.signal]:1));
336
340
  }
341
+ mark('client.engine_submit');
337
342
  const result=spawnSync(process.execPath,[engine,runtimeFile,internal,...(check?[check]:[])],
338
343
  {stdio:['inherit','pipe','inherit'],
339
- encoding:'utf8',env:process.env});
344
+ encoding:'utf8',env:traceEnvironment()});
345
+ mark('client.engine_complete',{exit:result.status,signal:result.signal});
340
346
  if(result.error)error(result.error.message);
341
347
  if(result.stdout){
342
348
  if(['status','detail','explain'].includes(command)){
@@ -3,18 +3,27 @@
3
3
  Use the CLI with explicit project-local activation below, or add this policy to a
4
4
  generic agent's instructions with the project owner's permission.
5
5
 
6
- 1. Query ordinary `redue status --json` first.
7
- 2. Require schema 1 and healthy observation. If a check has `freshness: "CURRENT"`, `result: "PASS"`, and
8
- `reuse_eligible: true`, reuse it. Missing eligibility means false. Do not rerun merely for freshness.
9
- 3. If STALE, run `redue run CHECK` when that verification is needed.
10
- 4. If the historical `result` is FAIL, report it honestly. The human CLI calls
11
- this FAILED; applicability is a separate field, never an excuse to call it green.
12
- 5. If UNVERIFIED, inspect its reason. Use `status --sync --json` when establishing
13
- applicability is worthwhile, or rerun the underlying check when cheaper/simpler.
14
- Use `redue run CHECK` if a new receipt is wanted. Incomplete coverage can remain
15
- UNVERIFIED even after success.
16
- 6. Do not assume synchronization is cheaper than execution. There are no universal
17
- package-manager or OS thresholds. Inexpensive checks may be cheaper to rerun.
6
+ 1. If fresh execution is already required, execute it; do not synchronize merely
7
+ to confirm an irrelevant reuse opportunity. Use `redue run CHECK` when recording
8
+ the configured check. An explicit npm-script request still requires npm.
9
+ 2. For reuse of the explicitly configured direct compiler recipe
10
+ (`qualification: "npm-typescript-direct-v1"`, direct compiler command; receipt
11
+ `verification_recipe: "npm-direct-typescript@1"`), query once:
12
+ `redue --config CONFIG status --sync --json`. Do not infer a recipe from its name.
13
+ Cached status cannot establish its caller context. In mixed configurations,
14
+ assess each check's contract separately; this is not a global sync-first policy.
15
+ 3. For other/unknown recipes, query ordinary `status --json` first and decide whether
16
+ synchronization is worthwhile. Ordinary status and `--short` remain cheap inspection.
17
+ 4. Reuse only schema 1, healthy observation and the selected check's CURRENT/PASS
18
+ with `reuse_eligible: true`. Missing eligibility means false. A historical FAIL
19
+ is never green evidence. Do not rerun eligible evidence merely for freshness.
20
+ 5. STALE requires execution when verification is needed. UNVERIFIED requires an
21
+ honest choice of synchronization, permitted execution or reporting the limitation.
22
+ Recording-only success can remain UNVERIFIED. A deliberate cheap-check rerun is
23
+ permitted; there are no universal platform/package-manager timing thresholds.
24
+ 6. Use a successful synchronized response for the current decision without repeated
25
+ sync/explain calls unless edits, context changes or missing information require it.
26
+ Do not loop until green or use an earlier response after a later query fails.
18
27
 
19
28
  `redue explain CHECK --json` gives synchronized detail, including a known changed
20
29
  input when available. It has the same cost caveat as synchronized status.
@@ -37,9 +46,9 @@ CURRENT. Do not delete state or reset receipts to make the output green.
37
46
  REDUE tells you what evidence exists and whether it still applies. It does not
38
47
  promise that establishing applicability is cheaper than every possible check.
39
48
 
40
- ## Connect once (candidate)
49
+ ## Connect once (experimental)
41
50
 
42
- The alpha.5 candidate supports opt-in project-local activation and direct
51
+ Alpha.5 and later support opt-in project-local activation and direct
43
52
  TypeScript evidence, with fresh-session reuse demonstrated for Codex CLI 0.160.1
44
53
  on macOS under the documented owner-approved checkout/state/socket route. CLI
45
54
  mechanics are tested on macOS/Linux/Windows; model behavior is not claimed for
@@ -47,7 +56,7 @@ every host/platform or sandbox policy. Claude setup/removal mechanics are tested
47
56
  but Claude behavior and cross-host reuse remain untested. Reuse is not guaranteed,
48
57
  and the existing subsecond example establishes no meaningful net time saving.
49
58
 
50
- After installing this candidate and running `redue init` in a trusted project:
59
+ After installing REDUE and running `redue init` in a trusted project:
51
60
 
52
61
  ```sh
53
62
  redue agent setup codex --dry-run
@@ -62,6 +71,42 @@ setup requires `--apply`. `--dry-run` never writes. Installing REDUE or running
62
71
  `init` does not connect an agent automatically. Setup does not start an observer
63
72
  or execute verification. Use `redue start` when you want observation.
64
73
 
74
+ ## Update an existing connection
75
+
76
+ Installing a newer package does **not** rewrite project instructions. From the
77
+ same project and installation, stop every observer using that installation before
78
+ replacing it. Keep the original config and state selection; do not create a second
79
+ config or delete receipts. Example for a default npm installation/config:
80
+
81
+ ```sh
82
+ redue --config redue.config.json stop
83
+ npm install --global @redue/cli@alpha
84
+ redue --config redue.config.json agent setup codex --dry-run
85
+ redue --config redue.config.json agent setup codex --apply
86
+ redue --config redue.config.json start
87
+ redue --config redue.config.json status --sync --json
88
+ ```
89
+
90
+ Use the **same `--prefix`** as the existing npm installation when it has a custom
91
+ prefix, and invoke its corresponding `redue` binary. Keep your actual config path
92
+ on every command. For Claude, select `claude` in both setup commands. Preserve any
93
+ existing `--state-dir` on lifecycle/status commands; setup does not embed custom
94
+ state locations, so retain the documented generic instruction path if your agent
95
+ workflow needs an explicit custom state. PowerShell may use the existing
96
+ `npm.cmd`/`redue.cmd` shims without changing script policy.
97
+
98
+ Review the preview before applying. Intact owned content updates; unrelated text
99
+ is preserved. Edited managed content is refused, not overwritten. If refused,
100
+ review/reconcile those edits deliberately; do not remove ownership records to force
101
+ an update. Restart the observer with its original selection even if you postpone
102
+ updating instructions, then start a **fresh agent session** after a successful
103
+ update. Setup creates no verification receipt.
104
+
105
+ Managed project metadata can itself be an observed input. Prior receipts remain
106
+ historical evidence, but setup may make them STALE or UNVERIFIED. Reassess normally
107
+ and run required checks; an upgrade is not a promise that existing receipts remain
108
+ CURRENT. Do not suppress that invalidation or reset state.
109
+
65
110
  Codex gets a short managed section in project `AGENTS.md` and
66
111
  `.agents/skills/redue-verification/SKILL.md`. Claude Code gets a managed section
67
112
  in `CLAUDE.md` and `.claude/skills/redue-verification/SKILL.md`. Each uses the same
@@ -107,7 +152,7 @@ proven installation mismatch.
107
152
  Codex CLI 0.160.1 on macOS can use the approved exact observer-socket route while
108
153
  other sockets and direct network access remain denied. Its independent sessions
109
154
  receive different proxy values. Ordinary npm invocation is **not** equivalent
110
- across arbitrary notifier state: this candidate withholds automatic npm-launcher
155
+ across arbitrary notifier state: REDUE withholds automatic npm-launcher
111
156
  reuse. Do not change proxy settings or permissions to get green evidence.
112
157
 
113
158
  The separate opt-in [direct local compiler recipe](direct-typescript.md) does not
@@ -124,7 +169,7 @@ performs the same bounded, read-only decision check and exits 2 when reuse is no
124
169
  eligible. It neither executes verification nor grants access; synchronization
125
170
  may run configured probes. A successful outside-shell result is insufficient.
126
171
 
127
- Use the same setup command after a candidate upgrade; it updates intact managed
172
+ Use the same setup command after an upgrade; it updates intact managed
128
173
  content idempotently. Changed skill text or a changed managed block causes a
129
174
  conflict instead of an overwrite. Review your edits before retrying.
130
175
 
@@ -154,8 +199,8 @@ paths. Other worktrees and remote containers have separate local evidence.
154
199
 
155
200
  For another agent, put a short instruction in its documented project mechanism:
156
201
  “Before deciding whether configured verification needs repeating, read REDUE's
157
- agent policy, query ordinary `redue status --json` for the intended config, and
158
- apply the schema/health/reuse rules.” Supply this document and the
202
+ agent policy, identify the selected recipe and use its decision query for the intended config.
203
+ Apply the schema/health/reuse rules.” Supply this document and the
159
204
  [JSON contract](json.md) as the detailed policy. Review the agent's actual tool
160
205
  trace before calling activation verified. A local npm installation does not install
161
206
  REDUE in a remote container; that environment needs its own CLI, checkout,
@@ -5,7 +5,7 @@ check against declared, observed inputs and relevant execution context. It does
5
5
  not mean npm or npm lifecycle scripts completed.
6
6
 
7
7
  For a supported single-package npm project with an existing standalone
8
- `tsc --noEmit` script, installed dependencies, Node 22/24 and TypeScript 5.6.3/5.9.3:
8
+ `tsc --noEmit` script, installed dependencies, Node 22/24 and TypeScript 5.5.2/5.6.3/5.9.3:
9
9
 
10
10
  ```sh
11
11
  redue init --recipe typescript-direct --check typecheck --dry-run
@@ -33,7 +33,7 @@ A generated check contains:
33
33
 
34
34
  `script` identifies the reviewed source recipe; `command` is what actually runs:
35
35
  the resolved Node executable and local TypeScript implementation, with exact argv
36
- and no shell. TypeScript 5.6.3 uses `lib/tsc.js`; 5.9.3 uses `lib/_tsc.js` directly,
36
+ and no shell. TypeScript 5.5.2 and 5.6.3 use `lib/tsc.js`; 5.9.3 uses `lib/_tsc.js` directly,
37
37
  without its outer compile-cache-enabling shim. No compiler is fetched or bundled.
38
38
  The entire installed package must match reviewed official distribution bytes.
39
39
 
@@ -57,11 +57,13 @@ prefix. Uninstalling the package does not remove local verification evidence;
57
57
  `redue remove-state` is a separate, explicit action. Alpha.1 remains available as
58
58
  historical software and is superseded for package identity, not a runtime defect.
59
59
 
60
- ## Upgrading to alpha.4
60
+ ## Upgrading REDUE
61
61
 
62
62
  Stop every observer using the installation before upgrading, then restart each
63
63
  with the same config/state selection. See [the npm launcher notice](npm-launcher-notice.md)
64
- for exact commands. Do not remove state or delete receipts.
64
+ for exact commands. Do not remove state or delete receipts. Existing agent connections require a separate
65
+ [preview/apply policy update](agents.md#update-an-existing-connection); installing
66
+ the package alone does not update project instructions.
65
67
 
66
68
  ## First project
67
69
 
@@ -90,7 +92,7 @@ reusability. Qualification can change when configuration or installation changes
90
92
 
91
93
  `run` streams normal output and preserves execution environment and cancellation.
92
94
  For the narrow pnpm TypeScript contract, `init` explicitly shows the direct installed
93
- compiler invocation it selects. In this candidate, npm scripts remain recording-only;
95
+ compiler invocation it selects. Npm scripts remain recording-only;
94
96
  use `redue init --recipe typescript-direct --dry-run` to preview a distinct,
95
97
  explicit [local compiler recipe](direct-typescript.md). Other scripts run through
96
98
  the package manager. No existing configuration is silently migrated.
@@ -19,7 +19,7 @@ project script. Automatic npm-launcher applicability was withdrawn in alpha.4;
19
19
  ordinary npm scripts remain recording-only. See the [maintainer notice](npm-launcher-notice.md).
20
20
  The following direct recipe is opt-in and new in the alpha.5 candidate. The opt-in `init --recipe typescript-direct` supports
21
21
  a standalone `tsc --noEmit` script in a single-package npm installation using
22
- reviewed TypeScript 5.6.3/5.9.3 and Node 22/24. It executes the local compiler,
22
+ reviewed TypeScript 5.5.2/5.6.3/5.9.3 and Node 22/24. It executes the local compiler,
23
23
  not npm. Full installed compiler identity, config/membership, installed inputs
24
24
  and relevant Node environment are checked before reuse. See the
25
25
  [exact direct boundary](direct-typescript.md). Unsupported plugins, preloads,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@redue/cli",
3
3
  "private": false,
4
- "version": "0.1.0-alpha.6",
4
+ "version": "0.1.0-alpha.8",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "tag": "alpha"
@@ -13,7 +13,8 @@
13
13
  "scripts": {
14
14
  "test": "node --test --test-concurrency=1 test/*.test.mjs",
15
15
  "test:package": "node test/acceptance/package-smoke.mjs",
16
- "demo": "node test/acceptance/package-smoke.mjs --demo"
16
+ "demo": "node test/acceptance/package-smoke.mjs --demo",
17
+ "pretest": "npm --prefix test/fixtures/typescript-5-5-2 ci --no-audit --no-fund"
17
18
  },
18
19
  "dependencies": {
19
20
  "@parcel/watcher": "2.5.6",
@@ -45,15 +45,19 @@ function saved(root,host){const text=read(root,marker(host));if(text===null)retu
45
45
  const value=JSON.parse(text),h=hosts[host];
46
46
  if(value.schema!==1||value.host!==host||value.instruction!==h.instruction||value.skill!==h.skill||
47
47
  typeof value.config!=='string'||path.isAbsolute(value.config)||value.config.includes('/')||value.config.includes('\\')||
48
- value.block!==blockFor(host,value.config)||
48
+ (value.block!==blockFor(host,value.config)&&value.block!==blockFor(host,value.config,true))||
49
49
  typeof value.skillHash!=='string'||typeof value.instructionCreated!=='boolean')
50
50
  throw Error('unrecognized agent ownership record; inspect it without deleting user content');
51
51
  return value;
52
52
  }
53
- function blockFor(host,relative){return `\n\n<!-- REDUE:${host}:begin -->\n`+
53
+ // Accept only these exact owned template versions; never arbitrary stored text.
54
+ function blockFor(host,relative,legacy=false){return `\n\n<!-- REDUE:${host}:begin -->\n`+
54
55
  `Before deciding whether configured verification needs repeating, use REDUE.\n`+
55
- `Read ${hosts[host].skill} for the decision policy, then query ordinary status\n`+
56
- `before choosing reuse or execution. Only healthy schema-1 CURRENT/PASS with\n`+
56
+ (legacy?`Read ${hosts[host].skill} for the decision policy, then query ordinary status\n`+
57
+ `before choosing reuse or execution. Only healthy schema-1 CURRENT/PASS with\n`:
58
+ `Read ${hosts[host].skill}: verify the selected recipe, use caller-aware sync\n`+
59
+ `for direct-compiler reuse, and execute fresh obligations without a reuse query.\n`+
60
+ `Only healthy schema-1 CURRENT/PASS with\n`)+
57
61
  `explicit reuse_eligible=true permits reuse; honor fresh-run requests and other\n`+
58
62
  `project verification obligations. Reassess after edits or failed queries.\n`+
59
63
  `Selected config (JSON path relative to this file's directory): ${JSON.stringify(relative)}.\n`+
@@ -63,7 +67,7 @@ function blockFor(host,relative){return `\n\n<!-- REDUE:${host}:begin -->\n`+
63
67
  function skillFor(relative){return policy+`\nSelected configuration\n\n`+
64
68
  `The configuration path, relative to the project instruction file's directory, is\n`+
65
69
  `${JSON.stringify(relative)} (a JSON string, not shell code). From that directory use\n`+
66
- `\`redue --config CONFIG status --json\`, replacing CONFIG with that single quoted\n`+
70
+ `the policy-selected \`redue --config CONFIG ...\` command, replacing CONFIG with one quoted\n`+
67
71
  `path argument. Apply the same --config to run, explain, start and stop. From a\n`+
68
72
  `nested directory use the corresponding relative config path. Do not guess among\n`+
69
73
  `multiple configs. Receipts are local to the selected checkout/configuration.\n`;}
@@ -3,11 +3,30 @@ name: redue-verification
3
3
  description: Decide whether configured verification needs running, using REDUE evidence before verification and completion decisions.
4
4
  ---
5
5
 
6
- Query ordinary `redue status --json` before deciding whether configured checks need
7
- repeating. Apply the selected configuration described below to every command.
8
- Recheck at the decision point after edits; an earlier CURRENT is not continuing
9
- authorization after changes, context changes, observation loss, or a failed query.
10
- There is no atomic guarantee against concurrent edits.
6
+ Apply the selected configuration below to every command. First distinguish an
7
+ execution obligation from a reuse decision:
8
+
9
+ - If fresh execution is explicitly requested or independently required, execute
10
+ it without a reuse query that cannot change that decision. Use `redue run CHECK`
11
+ to record the configured check. An explicit npm-script request requires npm,
12
+ not a direct compiler substitute.
13
+ - To evaluate reuse for a check whose selected config has
14
+ `qualification: "npm-typescript-direct-v1"` and the direct compiler command
15
+ (or whose receipt metadata has `verification_recipe: "npm-direct-typescript@1"`
16
+ consistent with that config), use one `redue --config CONFIG status --sync --json`
17
+ directly. Cached status cannot establish this recipe's caller eligibility.
18
+ Do not infer the recipe from a check's name or precede sync with cached status
19
+ solely as a ritual. Evaluate each check separately in mixed configurations.
20
+ - For other or unidentified recipes, start with ordinary `status --json` and
21
+ choose sync or execution from its evidence. Cached status and `--short` remain
22
+ cheap inspection, not caller-aware proof. A deliberate inexpensive rerun is
23
+ still permitted; there are no universal OS/package-manager cost thresholds.
24
+
25
+ Use one successful, still-applicable synchronized response for the current
26
+ decision; do not repeat sync/explain without edits, context changes or missing
27
+ information. Do not loop until green. Reassess after changes, observation loss or
28
+ a later failed query; earlier CURRENT is not continuing authority. There is no
29
+ time-based authorization or atomic guarantee against concurrent edits.
11
30
 
12
31
  Read schema 1 JSON. Reuse only when the read succeeds, `schema` is 1,
13
32
  `observation.healthy` is true, and the selected check has all three:
@@ -29,8 +48,7 @@ checks merely for freshness.
29
48
  lifecycle scripts. Inspect the configuration/verification_recipe before claiming
30
49
  what ran. An explicit request for `npm run typecheck` requires that npm invocation;
31
50
  direct compiler evidence does not satisfy it.
32
- - Explicit user requests for fresh execution and independent task-specific
33
- verification obligations still apply. REDUE covers only configured checks.
51
+ - REDUE covers only configured checks, not every task-specific verification obligation.
34
52
 
35
53
  Use `redue explain CHECK --json` for synchronized detail (also potentially costly).
36
54
  `changed_inputs` can be incomplete; an empty list does not establish no changes.
package/src/cli.mjs CHANGED
@@ -17,10 +17,11 @@ import {assertOwnedStatePlacement} from './owned-state.mjs';
17
17
  import {permittedRoots} from './installed-inputs.mjs';
18
18
  import {acquireRunLock,inspectRunLock,recoverRunLock} from './run-lock.mjs';
19
19
  import {atomicJson,commitReceipt,receiptRevision} from './state-store.mjs';
20
- import {timing,profiling} from './decision-profile.mjs';
20
+ import {timing,profiling,mark,traceContext} from './decision-profile.mjs';
21
21
  import {readCachedStatus,unavailableCached} from './cached-state.mjs';
22
22
  import {renderRun} from './presentation.mjs';
23
23
 
24
+ mark('cli.loaded');
24
25
  const here=path.dirname(fileURLToPath(import.meta.url));
25
26
  const [configArg,action,name]=process.argv.slice(2);
26
27
  if(!configArg||!action)throw Error('internal usage: cli.mjs CONFIG ACTION [check]');
@@ -93,13 +94,13 @@ function owner(){
93
94
  }
94
95
  function call(message,timeout=5000) {
95
96
  return new Promise((resolve,reject)=>{
96
- let data='',done=false;const client=net.createConnection(socket);
97
- const timer=setTimeout(()=>{client.destroy();reject(Error('observer synchronization timeout'));},timeout);
98
- client.on('connect',()=>client.write(JSON.stringify(message)+'\n'));
97
+ let data='',done=false;mark('client.control_connect',{deadlineMs:timeout});const client=net.createConnection(socket);
98
+ const timer=setTimeout(()=>{mark('client.control_timeout');client.destroy();reject(Error('observer synchronization timeout'));},timeout);
99
+ client.on('connect',()=>{mark('client.control_submit');client.write(JSON.stringify({...message,...(profiling?{diagnostic:traceContext()}: {})})+'\n');});
99
100
  client.on('data',chunk=>data+=chunk);
100
- client.on('end',()=>{clearTimeout(timer);if(done)return;done=true;
101
+ client.on('end',()=>{mark('client.control_complete',{bytes:data.length});clearTimeout(timer);if(done)return;done=true;
101
102
  try{resolve(JSON.parse(data));}catch(e){reject(e);}});
102
- client.on('error',e=>{clearTimeout(timer);reject(e);});
103
+ client.on('error',e=>{mark('client.control_error',{code:e.code});clearTimeout(timer);reject(e);});
103
104
  });
104
105
  }
105
106
  async function decisionCall(message,timeout) {
package/src/daemon.mjs CHANGED
@@ -17,7 +17,7 @@ import {assertOwnedStatePlacement} from './owned-state.mjs';
17
17
  import {atomicJson,readReceipts,readReceiptSnapshot,receiptRevision} from './state-store.mjs';
18
18
  import {history} from './observation.mjs';
19
19
  import {inputKey,planGuard} from './decision-validation.mjs';
20
- import {timing} from './decision-profile.mjs';
20
+ import {timing,mark,traceBase,traceContext,traceScope,traceEnvironment,traceId} from './decision-profile.mjs';
21
21
 
22
22
  const configFile=path.resolve(process.argv[2]);
23
23
  const config=JSON.parse(fs.readFileSync(configFile));
@@ -31,6 +31,7 @@ const endpoint=controlEndpoint(state),socket=endpoint.address,
31
31
  fs.mkdirSync(lock); // Existing lock requires operator inspection; never steal it.
32
32
  fs.writeFileSync(path.join(lock,'pid'),String(process.pid),{mode:0o600});
33
33
  const generation=randomUUID(), startedAt=Date.now();
34
+ traceBase({generation,work:'startup'});
34
35
  let index, receipts={}, watcher, healthy=false, reason='initial reconciliation', stopping=false;
35
36
  let receiptStateError=null;
36
37
  let receiptStateRevision=null;
@@ -136,10 +137,12 @@ function probe(row,allowReuse=false) {
136
137
  const forceFull=process.env.VSTATE_TEST_FAULTS==='1'&&process.env.VSTATE_TEST_FULL_PROBES==='1';
137
138
  if(cacheable&&allowReuse&&macGuarded&&!forceFull&&cache?.input===key){
138
139
  const argv=row.plan.probes[0];
140
+ mark('daemon.probe_launch',{mode:'certificate',deadlineMs:10000});
139
141
  const out=spawnSync(argv[0],[...argv.slice(1),macDirect?'--redue-validate-queries':'--redue-context'],{
140
- cwd:row.plan.cwd,timeout:10000,maxBuffer:1024*1024,
142
+ cwd:row.plan.cwd,timeout:10000,maxBuffer:1024*1024,env:traceEnvironment(),
141
143
  ...(macDirect?{input:JSON.stringify({schema:cache.schema,context:cache.context,queries:cache.queries})}:
142
144
  {stdio:['ignore','pipe','pipe']})});
145
+ mark('daemon.probe_complete',{mode:'certificate',childPid:out.pid,exit:out.status,signal:out.signal,code:out.error?.code});
143
146
  metrics.probeContextProcesses=(metrics.probeContextProcesses||0)+1;
144
147
  let stillValid=!macDirect;
145
148
  if(macDirect)try{stillValid=macProbeBarrier()&&inputKey(row)===key&&planGuard(index.bundle)===guard;}catch{}
@@ -170,8 +173,10 @@ function probe(row,allowReuse=false) {
170
173
  const timeout=row.plan.qualification==='pnpm-tsc-v1'||
171
174
  (process.platform==='win32'&&['typescript-noemit-v1','npm-typescript-direct-v1','yarn-workspace-tsc-v1']
172
175
  .includes(row.plan.qualification))?10000:5000;
176
+ mark('daemon.probe_launch',{mode:'full',reason:fullReason,deadlineMs:timeout});
173
177
  const out=spawnSync(argv[0],[...argv.slice(1),...(cacheable?['--redue-checkpoint']:[])],{cwd:row.plan.cwd,
174
- timeout,maxBuffer:1024*1024,stdio:['ignore','pipe','pipe']});
178
+ timeout,maxBuffer:1024*1024,stdio:['ignore','pipe','pipe'],env:traceEnvironment()});
179
+ mark('daemon.probe_complete',{mode:'full',childPid:out.pid,exit:out.status,signal:out.signal,code:out.error?.code});
175
180
  if(out.error||out.signal||out.status!==0) {
176
181
  if(!row.probeError){row.revision++;row.lastReason='declared state probe became unavailable';}
177
182
  row.probeHash=null;row.probeAt=0;
@@ -180,7 +185,7 @@ function probe(row,allowReuse=false) {
180
185
  /VSTATE_REASON:([a-z-]+)/.exec(out.stderr?.toString()||'')?.[1]:null;
181
186
  const explanations={
182
187
  'direct-typescript-implementation-unreviewed':'installed TypeScript bytes or membership differ from the reviewed compiler',
183
- 'direct-typescript-version-unsupported':'direct compiler recipe supports reviewed TypeScript 5.6.3 and 5.9.3 only',
188
+ 'direct-typescript-version-unsupported':'direct compiler recipe supports reviewed TypeScript 5.5.2, 5.6.3 and 5.9.3 only',
184
189
  'direct-execution-environment-unsupported':'direct compiler rejects development helpers, Node cache/preloads, and unreviewed runtime overrides',
185
190
  'direct-node-version-unsupported':'direct compiler recipe supports Node 22 and 24',
186
191
  'npm-launcher-unobserved-inputs':'npm launcher uses unobserved notifier inputs; execution is recording-only',
@@ -420,7 +425,7 @@ function buildInWorker(forceCold=false,reuse=null) {
420
425
  return new Promise((resolve,reject)=>{
421
426
  const workerStarted=performance.now();
422
427
  const file=path.join(path.dirname(fileURLToPath(import.meta.url)),'plan-worker.mjs');
423
- const worker=new Worker(file,{workerData:{config:configFile,state,forceCold,reuse,
428
+ const worker=new Worker(file,{workerData:{config:configFile,state,forceCold,reuse,diagnostic:traceContext(),
424
429
  reconcileFaultFile:process.env.VSTATE_TEST_RECONCILE_FAULT_FILE}});
425
430
  planningWorker=worker;let settled=false;
426
431
  const deadline=setTimeout(()=>{
@@ -794,7 +799,7 @@ async function catchUp() {
794
799
  lastObservation=Date.now();return;
795
800
  }
796
801
  if(historyDisabled)return;
797
- if(synchronizing)return synchronizing;
802
+ if(synchronizing){mark('daemon.catch_up_join');return synchronizing;}
798
803
  synchronizing=(async()=>{
799
804
  if(!verifyRoot())throw Error('checkout identity changed');
800
805
  if(!watcher)throw Error('subscription unavailable');
@@ -825,7 +830,8 @@ async function catchUp() {
825
830
  function respond(client,value) {
826
831
  if(client.destroyed){event('control_reply_unavailable',{classification:'client_destroyed'});return;}
827
832
  try{const payload=JSON.stringify(value)+'\n';
828
- client.end(payload);client.vstateReplied=true;
833
+ mark('server.response_constructed',{bytes:payload.length});
834
+ client.end(payload,()=>mark('server.response_delivered'));client.vstateReplied=true;
829
835
  if(process.env.VSTATE_DEBUG_CONTROL==='1')event('control_reply_sent',{bytes:payload.length});}
830
836
  catch(e){event('control_reply_unavailable',{classification:e.name||'serialization_error'});
831
837
  if(!client.destroyed)client.end('{"error":"control response unavailable"}\n');}
@@ -843,6 +849,7 @@ async function request(message) {
843
849
  if(message.action==='sync'||message.action==='snapshot'||message.action==='prelaunch') {
844
850
  const totalStarted=performance.now();
845
851
  const decision=++decisionSequence;
852
+ mark('server.decision_start',{decision,pending:pending.size,planDirty,planning,recovering,healthy,historyDisabled,decisionRequests,historyDeadlineMs,reconciliationDeadlineMs});
846
853
  timing('daemon.decision_reason',totalStarted,{decision,action:message.action,
847
854
  plan:process.platform==='win32'?'validate_dependencies_before_reuse':'existing_plan',
848
855
  probe:'reuse_only_with_identical_content_and_context',installed:'current_content_required',
@@ -970,9 +977,11 @@ const server=net.createServer(client=>{
970
977
  try{const message=JSON.parse(text);
971
978
  const decision=message.action==='sync'||message.action==='snapshot';
972
979
  if(decision)decisionRequests++;
973
- Promise.resolve(request(message)).then(v=>respond(client,v))
974
- .catch(e=>respond(client,{error:e.message}))
975
- .finally(()=>{if(decision)decisionRequests--;});}
980
+ traceScope({request:message.diagnostic?.request||traceId(),work:'request'},()=>{
981
+ mark('server.request_received',{activeDecisions:decisionRequests});
982
+ Promise.resolve(request(message)).then(v=>respond(client,v))
983
+ .catch(e=>respond(client,{error:e.message}))
984
+ .finally(()=>{mark('server.request_complete');if(decision)decisionRequests--;});});}
976
985
  catch(e){respond(client,{error:e.message});}
977
986
  });
978
987
  });
@@ -1032,7 +1041,8 @@ const heartbeat=setInterval(()=>{
1032
1041
  }
1033
1042
  publish();
1034
1043
  },1000);
1035
- const probeTimer=setInterval(async()=>{
1044
+ const probeTimer=setInterval(()=>traceScope({request:traceId(),work:'periodic'},async()=>{
1045
+ mark('server.periodic_start',{pending:pending.size,planDirty,planning,decisionRequests});
1036
1046
  // Windows cached reads are conservatively UNVERIFIED until a full decision
1037
1047
  // reconciliation. Periodic synchronous probes cannot strengthen that cache,
1038
1048
  // but can block control requests behind several seconds of child work.
@@ -1045,7 +1055,9 @@ const probeTimer=setInterval(async()=>{
1045
1055
  }
1046
1056
  publish();}
1047
1057
  catch(e){observationGap('periodic_query',e);}
1048
- }},5000);
1058
+ }
1059
+ mark('server.periodic_complete');
1060
+ }),5000);
1049
1061
  process.on('uncaughtExceptionMonitor',error=>{
1050
1062
  try{event('observer_uncaught_exception',{pid:process.pid,code:error.code||null,
1051
1063
  message:error.message,stack:error.stack});}catch{}
@@ -1,15 +1,29 @@
1
1
  // Opt-in, bounded local diagnostics. Never include paths, command arguments,
2
2
  // environment values, source, or probe output in a timing record.
3
3
  import fs from 'node:fs';
4
+ import {AsyncLocalStorage} from 'node:async_hooks';
5
+ import {randomUUID} from 'node:crypto';
4
6
 
5
7
  const file=process.env.REDUE_DECISION_PROFILE_FILE;
6
8
  export const profiling=process.env.REDUE_DECISION_PROFILE==='1'&&!!file;
7
9
 
10
+ const traces=new AsyncLocalStorage();
11
+ const safe=value=>typeof value==='string'&&/^[a-zA-Z0-9_-]{1,64}$/.test(value)?value:undefined;
12
+ const clean=value=>Object.fromEntries(['request','generation','work'].map(key=>[key,safe(value?.[key])]).filter(([,v])=>v));
13
+ let base={};
14
+ try{base=clean(JSON.parse(process.env.REDUE_PROFILE_CONTEXT||'{}'));}catch{}
15
+ export const traceContext=()=>profiling?{...base,...traces.getStore()}:{};
16
+ export function traceBase(value){if(profiling)base=clean(value);}
17
+ export const traceId=()=>randomUUID();
18
+ export const traceScope=(value,fn)=>profiling?traces.run({...traceContext(),...clean(value)},fn):fn();
19
+ export const traceEnvironment=()=>profiling?{...process.env,REDUE_PROFILE_CONTEXT:JSON.stringify(traceContext())}:process.env;
20
+ export function mark(phase,fields={}){timing(phase,performance.now(),fields);}
21
+
8
22
  export function timing(phase,started,counts={}) {
9
23
  if(!profiling)return;
10
24
  const memory=process.memoryUsage();
11
25
  try{fs.appendFileSync(file,'REDUE_TIMING '+JSON.stringify({
12
- at:Date.now(),pid:process.pid,phase,
26
+ at:Date.now(),monoNs:process.hrtime.bigint().toString(),pid:process.pid,phase,...traceContext(),
13
27
  ms:Math.round((performance.now()-started)*10)/10,
14
28
  rssMiB:Math.round(memory.rss/1048576*10)/10,
15
29
  ...counts
@@ -4,10 +4,9 @@ import {directProject,directInterpretation} from './direct-typescript.mjs';
4
4
  import {typeScriptInputFacts} from './typescript-input-facts.mjs';
5
5
  import {contextOnly,probeResult} from './probe-result.mjs';
6
6
  import {realObservedPath} from './path-identity.mjs';
7
- import fs from 'node:fs';
8
7
  import path from 'node:path';
9
8
  import {queriesMatch} from './typescript-list.mjs';
10
- import {timing} from './decision-profile.mjs';
9
+ import {timing,mark} from './decision-profile.mjs';
11
10
  const hash=value=>createHash('sha256').update(JSON.stringify(value)).digest('hex');
12
11
  try{
13
12
  const [rootArg,script]=process.argv.slice(2),root=realObservedPath(rootArg);
@@ -22,8 +21,16 @@ try{
22
21
  if(process.argv.includes('--redue-validate-queries')){
23
22
  // Internal, process-local discovery certificate; never a verification receipt.
24
23
  // Verify the current implementation above before queryAnswer can load its API.
25
- const started=performance.now(),raw=fs.readFileSync(0,'utf8');
26
- if(Buffer.byteLength(raw)>8*1024*1024)throw Error('certificate too large');
24
+ const started=performance.now();mark('certificate.input_start');
25
+ // A synchronous fd-0 read can stall with spawnSync's piped input on macOS
26
+ // Node 22.13.0. Drain the stream without blocking its input delivery. The
27
+ // parent's existing deadline and the same size limit remain authoritative.
28
+ const chunks=[];let bytes=0;
29
+ for await(const chunk of process.stdin){
30
+ bytes+=chunk.length;if(bytes>8*1024*1024)throw Error('certificate too large');
31
+ chunks.push(chunk);
32
+ }
33
+ const raw=Buffer.concat(chunks).toString('utf8');mark('certificate.input_complete',{bytes});
27
34
  const certificate=JSON.parse(raw),kinds=new Set(['readDirectory','readFile',
28
35
  'fileExists','directoryExists','realpath','directories','entries']);
29
36
  if(certificate.schema!==1||certificate.context!==contextHash||
@@ -34,7 +41,9 @@ try{
34
41
  q[3]?.argsVersion!==1||!Array.isArray(q[3]?.undefinedArguments)||
35
42
  q[3].undefinedArguments.some(i=>!Number.isInteger(i)||i<0||i>=q[3].args.length))))
36
43
  throw Error('certificate unavailable');
44
+ mark('certificate.queries_start',{queries:certificate.queries.length});
37
45
  const matched=queriesMatch(certificate.queries);
46
+ mark('certificate.queries_complete',{matched});
38
47
  timing('direct_probe.query_validation',started,{queries:certificate.queries.length,matched:matched?1:0});
39
48
  if(!matched)throw Error('compiler queries changed');
40
49
  process.stdout.write(contextHash);
@@ -7,17 +7,21 @@ import {realObservedPath,withinPath} from './path-identity.mjs';
7
7
  export const directInterpretation='npm-direct-typescript@1';
8
8
  export const directQualification='npm-typescript-direct-v1';
9
9
  const reviewed={
10
+ '5.5.2':{entry:'lib/tsc.js',digest:'953b4844b5f6edc745be844b6c367c227b59029b73c4182f508f90cc47e21816'},
10
11
  '5.6.3':{entry:'lib/tsc.js',digest:'e674b5dc4dac50f6e91be30094aeea983a1f340231717e62b0b6199ae3049eb2'},
11
12
  '5.9.3':{entry:'lib/_tsc.js',digest:'d1b32e75475710fdc85102b1365254d84ceb192d3639d962c53fe1355ce274ca'},
12
13
  };
13
14
  const fail=code=>{throw Object.assign(Error(code),{code});};
14
15
  const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
15
16
  export const directEnvironment={variables:['NODE_ENV','NODE_OPTIONS','NODE_PATH','NODE_COMPILE_CACHE','VSCODE_INSPECTOR_OPTIONS'],
16
- prefixes:['NODE_','DYLD_','LD_','TSGO_','TSC_'],executableIdentity:true};
17
+ prefixes:['NODE_','DYLD_','LD_','TSGO_','TSC_','TS_ETW_'],executableIdentity:true};
17
18
  export function directEnvironmentIssue(env=process.env){
18
19
  if(![22,24].includes(Number(process.versions.node.split('.')[0])))return 'direct-node-version-unsupported';
19
20
  if(env.VSCODE_INSPECTOR_OPTIONS)return 'direct-execution-environment-unsupported';
20
21
  for(const [key,value] of Object.entries(env)){
22
+ // The 5.5.2 API can require this unreviewed module during probe loading.
23
+ // Reject even empty/case aliases; do not mutate the caller's environment.
24
+ if(/^TS_ETW_MODULE_PATH$/i.test(key))return 'direct-execution-environment-unsupported';
21
25
  if(/^NODE_/i.test(key)){
22
26
  // Node 22.13.0 predates env-proxy initialization. This injected flag is
23
27
  // inert there, but remains strictly fingerprinted as NODE_ context.
@@ -1,6 +1,8 @@
1
1
  // Native history is an optional acceleration. Keep its operations off the
2
2
  // control thread so a wedged native call cannot block status or shutdown.
3
3
  import fs from 'node:fs';
4
+ import {mark} from './decision-profile.mjs';
5
+ mark('history.worker_loaded');
4
6
  import watcher from '@parcel/watcher';
5
7
 
6
8
  try {
@@ -10,8 +12,11 @@ try {
10
12
  catch(e){if(e.code!=='ENOENT')throw e;}
11
13
  if(fault==='hang')Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
12
14
  if(fault==='fail')throw Error('injected history failure');
15
+ mark('history.snapshot_start');
13
16
  await watcher.writeSnapshot(root,next,{backend});
17
+ mark('history.snapshot_complete');
14
18
  const changes=previous?await watcher.getEventsSince(root,previous,{backend}):[];
19
+ mark('history.events_complete',{events:changes.length});
15
20
  process.send({ok:true,changes});
16
21
  } catch(error) {
17
22
  process.send({ok:false,error:error.message,code:error.code||error.name});
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import {mark,traceEnvironment} from './decision-profile.mjs';
2
3
  import path from 'node:path';
3
4
  import {randomUUID} from 'node:crypto';
4
5
  import {fork} from 'node:child_process';
@@ -11,11 +12,12 @@ const workerFile=path.join(path.dirname(fileURLToPath(import.meta.url)),'history
11
12
  export function history(root,cursor,backend,deadlineMs,faultFile) {
12
13
  const next=cursor+'.'+randomUUID()+'.next';
13
14
  return new Promise((resolve,reject)=>{
15
+ mark('history.submit',{deadlineMs});
14
16
  const worker=fork(workerFile,[root,fs.existsSync(cursor)?cursor:'',next,backend,
15
- faultFile||''],{stdio:['ignore','ignore','ignore','ipc']});
17
+ faultFile||''],{stdio:['ignore','ignore','ignore','ipc'],env:traceEnvironment()});
16
18
  let settled=false,abandoned=false;
17
19
  const finish=(error,value)=>{
18
- if(settled)return;settled=true;clearTimeout(timer);
20
+ if(settled)return;settled=true;mark('history.complete',{childPid:worker.pid,outcome:error?.code||error?.name||'ok',events:value?.length});clearTimeout(timer);
19
21
  worker.kill('SIGKILL');
20
22
  if(error){abandoned=true;try{fs.unlinkSync(next);}catch{}
21
23
  reject(error);return;}
@@ -7,8 +7,9 @@ import {InputIndex} from './index.mjs';
7
7
  import {directProject,directQualification} from './direct-typescript.mjs';
8
8
  import {queriesMatch} from './typescript-list.mjs';
9
9
  import {planGuard,inputKey} from './decision-validation.mjs';
10
- import {timing} from './decision-profile.mjs';
10
+ import {timing,traceBase} from './decision-profile.mjs';
11
11
 
12
+ traceBase(workerData.diagnostic);
12
13
  try {
13
14
  const totalStarted=performance.now();
14
15
  const configStarted=performance.now();