@geraldmaron/construct 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/apps/chat/engine/ai-sdk-agent.mjs +183 -0
- package/apps/chat/engine/loop-driver.mjs +211 -0
- package/apps/chat/engine/models.mjs +122 -0
- package/apps/chat/engine/provider-adapters.mjs +171 -0
- package/apps/chat/engine/tools/permission.mjs +54 -0
- package/apps/chat/engine/tools/primitives.mjs +180 -0
- package/apps/chat/engine/tools/registry.mjs +122 -0
- package/apps/chat/engine/turn-controls.mjs +70 -0
- package/bin/construct +93 -79
- package/commands/plan/decide.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/examples/README.md +1 -1
- package/examples/distribution/README.md +1 -1
- package/examples/distribution/sources/adr.md +2 -2
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/examples/distribution/sources/prd-platform.md +1 -1
- package/examples/distribution/sources/research-brief.md +2 -2
- package/examples/distribution/sources/rfc-platform.md +1 -1
- package/examples/distribution/sources/runbook.md +2 -2
- package/examples/distribution/sources/strategy.md +1 -1
- package/lib/agent-instructions/inject.mjs +1 -1
- package/lib/artifact-type-from-path.mjs +9 -5
- package/lib/audit-specialists.mjs +1 -1
- package/lib/audit-trail.mjs +3 -3
- package/lib/auto-docs.mjs +10 -10
- package/lib/bootstrap/built-ins.mjs +3 -3
- package/lib/boundary.mjs +3 -2
- package/lib/brand-prose.mjs +1 -1
- package/lib/cache-governor.js +4 -4
- package/lib/certification/document-io-fixtures.mjs +1 -1
- package/lib/chat/config.mjs +2 -2
- package/lib/chat/desktop-binary.mjs +5 -3
- package/lib/cli-commands.mjs +4 -6
- package/lib/cli-service-inventory.mjs +1 -1
- package/lib/comment-lint.mjs +9 -5
- package/lib/config/alias.mjs +3 -2
- package/lib/config/intake-policy.mjs +8 -78
- package/lib/config/legacy-config-migration.mjs +59 -0
- package/lib/config/project-config.mjs +95 -5
- package/lib/config/schema.mjs +9 -19
- package/lib/config/xdg.mjs +63 -0
- package/lib/contracts/violation-log.mjs +2 -2
- package/lib/cost-ledger.mjs +4 -3
- package/lib/cost.mjs +3 -2
- package/lib/decisions/registry.mjs +3 -3
- package/lib/distill.mjs +2 -2
- package/lib/docs-verify.mjs +2 -0
- package/lib/doctor/audit.mjs +4 -3
- package/lib/doctor/cli.mjs +0 -1
- package/lib/doctor/index.mjs +4 -4
- package/lib/doctor/report.mjs +3 -3
- package/lib/doctor/watchers/bd-watch.mjs +5 -5
- package/lib/doctor/watchers/cost.mjs +4 -4
- package/lib/doctor/watchers/disk.mjs +4 -3
- package/lib/doctor/watchers/service-health.mjs +5 -5
- package/lib/document-export.mjs +1 -1
- package/lib/document-extract.mjs +3 -3
- package/lib/document-ingest.mjs +13 -3
- package/lib/efficiency.mjs +2 -1
- package/lib/embed/approval-queue.mjs +3 -2
- package/lib/embed/cli.mjs +7 -5
- package/lib/embed/config.mjs +6 -5
- package/lib/embed/daemon.mjs +6 -5
- package/lib/embed/inbox-live-watcher.mjs +1 -1
- package/lib/embed/inbox.mjs +52 -37
- package/lib/embed/intake-metrics.mjs +3 -2
- package/lib/embed/reconcile.mjs +1 -1
- package/lib/embed/semantic.mjs +3 -2
- package/lib/embed/supervision.mjs +4 -2
- package/lib/engine/registry.mjs +3 -3
- package/lib/env-config.mjs +34 -12
- package/lib/features.mjs +4 -3
- package/lib/health-check.mjs +7 -6
- package/lib/hook-health.mjs +5 -4
- package/lib/hooks/_lib/log.mjs +4 -3
- package/lib/hooks/_lib/output-mode.mjs +2 -1
- package/lib/hooks/agent-tracker.mjs +3 -4
- package/lib/hooks/audit-reads.mjs +2 -2
- package/lib/hooks/audit-trail.mjs +2 -2
- package/lib/hooks/bash-output-logger.mjs +3 -3
- package/lib/hooks/brand-prose-lint.mjs +1 -1
- package/lib/hooks/ci-status-check.mjs +3 -2
- package/lib/hooks/context-watch.mjs +2 -2
- package/lib/hooks/context-window-recovery.mjs +5 -5
- package/lib/hooks/dep-audit.mjs +2 -2
- package/lib/hooks/doc-coupling-check.mjs +3 -2
- package/lib/hooks/edit-accumulator.mjs +3 -2
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/guard-bash.mjs +2 -2
- package/lib/hooks/model-fallback.mjs +2 -1
- package/lib/hooks/policy-engine.mjs +4 -3
- package/lib/hooks/pre-compact.mjs +7 -7
- package/lib/hooks/readme-age-check.mjs +2 -2
- package/lib/hooks/session-optimize.mjs +4 -3
- package/lib/hooks/session-start.mjs +6 -5
- package/lib/hooks/stop-notify.mjs +9 -8
- package/lib/hooks/stop-typecheck.mjs +4 -3
- package/lib/hooks/test-watch.mjs +2 -2
- package/lib/host-disposition.mjs +1 -1
- package/lib/ingest/provider-extract.mjs +2 -1
- package/lib/init/detect-existing-structure.mjs +2 -2
- package/lib/init/doc-lanes.mjs +3 -10
- package/lib/init-docs.mjs +12 -54
- package/lib/init-unified.mjs +35 -81
- package/lib/init.mjs +7 -5
- package/lib/install/desktop-binary-download.mjs +5 -2
- package/lib/intake/daemon.mjs +31 -8
- package/lib/intake/intake-config.mjs +5 -32
- package/lib/integrations/intake-integrations.mjs +8 -7
- package/lib/knowledge/rag.mjs +2 -2
- package/lib/knowledge/search.mjs +15 -15
- package/lib/maintenance/cleanup.mjs +14 -10
- package/lib/mcp/server.mjs +1 -1
- package/lib/mcp/tools/project.mjs +2 -1
- package/lib/model-cheapest-provider.mjs +4 -3
- package/lib/model-pricing.mjs +3 -2
- package/lib/model-router.mjs +4 -3
- package/lib/models/catalog.mjs +2 -1
- package/lib/models/execution-capability-profile.mjs +2 -1
- package/lib/models/provider-poll.mjs +2 -1
- package/lib/ollama-manager.mjs +0 -1
- package/lib/op-log.mjs +2 -1
- package/lib/opencode-runtime-plugin.mjs +3 -2
- package/lib/oracle/artifact-gate.mjs +3 -0
- package/lib/oracle/cli.mjs +2 -1
- package/lib/oracle/execute.mjs +3 -2
- package/lib/oracle/index.mjs +2 -1
- package/lib/oracle/read-model.mjs +3 -2
- package/lib/performance/generate.mjs +4 -3
- package/lib/platforms/capabilities.mjs +1 -1
- package/lib/plugin-registry.mjs +2 -1
- package/lib/profiles/lifecycle.mjs +3 -3
- package/lib/project-profile.mjs +2 -1
- package/lib/project-root.mjs +9 -7
- package/lib/provider-capabilities.js +4 -3
- package/lib/providers/auth-manager.mjs +2 -1
- package/lib/providers/copilot-auth.mjs +3 -2
- package/lib/providers/creds.mjs +3 -2
- package/lib/providers/registry.mjs +3 -3
- package/lib/providers/secret-resolver.mjs +3 -2
- package/lib/read-tracker-store.mjs +2 -1
- package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
- package/lib/reflect.mjs +2 -1
- package/lib/registry/generate-docs.mjs +2 -2
- package/lib/roles/approval-surface.mjs +5 -4
- package/lib/roles/event-bus.mjs +0 -1
- package/lib/roles/gateway.mjs +6 -2
- package/lib/rules-delivery.mjs +1 -1
- package/lib/runtime-env.mjs +1 -1
- package/lib/runtime-pressure.mjs +5 -3
- package/lib/sandbox.mjs +3 -2
- package/lib/scheduler/solo.mjs +6 -4
- package/lib/server/auth.mjs +4 -3
- package/lib/server/index.mjs +24 -26
- package/lib/server/insights.mjs +5 -4
- package/lib/server/webhook.mjs +2 -1
- package/lib/service-manager.mjs +5 -3
- package/lib/setup.mjs +21 -11
- package/lib/status.mjs +6 -5
- package/lib/storage/embeddings-local.mjs +3 -2
- package/lib/storage/sync.mjs +2 -2
- package/lib/telemetry/client.mjs +0 -1
- package/lib/telemetry/intent-verifications.mjs +6 -6
- package/lib/telemetry/model-pricing-catalog.mjs +4 -3
- package/lib/telemetry/rule-calls.mjs +3 -3
- package/lib/telemetry/skill-calls.mjs +3 -3
- package/lib/template-registry.mjs +1 -0
- package/lib/templates/visual-requirements.mjs +1 -1
- package/lib/test-corpus-inventory.mjs +1 -1
- package/lib/uninstall/uninstall.mjs +12 -10
- package/package.json +4 -3
- package/platforms/claude/settings.template.json +43 -43
- package/rules/common/no-fabrication.md +1 -1
- package/rules/common/release-gates.md +1 -1
- package/rules/common/research.md +1 -1
- package/rules/common/review-before-change.md +1 -1
- package/scripts/sync-specialists.mjs +11 -8
- package/skills/docs/adr-workflow.md +3 -3
- package/skills/docs/init-docs.md +9 -9
- package/skills/docs/prd-workflow.md +5 -5
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +2 -2
- package/skills/docs/product-signal-workflow.md +1 -1
- package/skills/docs/runbook-workflow.md +4 -4
- package/skills/exploration/unknown-codebase-onboarding.md +1 -1
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/routing.md +3 -3
- package/specialists/prompts/cx-architect.md +1 -1
- package/specialists/prompts/cx-docs-keeper.md +1 -1
- package/specialists/prompts/cx-researcher.md +1 -1
- package/specialists/prompts/cx-sre.md +1 -1
- package/specialists/role-manifests.json +6 -6
- package/templates/docs/README.md +125 -0
- package/templates/docs/construct_guide.md +3 -4
- package/templates/docs/persona-artifact.md +1 -1
- package/templates/docs/prds/README.md +25 -0
- package/templates/docs/prds/templates/_template.md +206 -0
- package/templates/docs/prds/templates/meta-prd.template.md +177 -0
- package/templates/docs/prds/templates/prd-business.template.md +61 -0
- package/templates/docs/prds/templates/prd-platform.template.md +81 -0
- package/templates/docs/prds/templates/prfaq.template.md +38 -0
- package/templates/docs/rfcs/README.md +22 -0
- package/templates/docs/rfcs/templates/_template.md +58 -0
- package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
- package/templates/homebrew/construct.rb +1 -1
- package/templates/workflows/new-feature.yml +8 -8
- package/lib/intake/legacy-paths.mjs +0 -5
package/lib/health-check.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import os from 'node:os';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { existsSync, readFileSync } from 'node:fs';
|
|
13
13
|
import { extractOpRef, hasAnySecret, hasSecret } from './providers/secret-resolver.mjs';
|
|
14
|
+
import { configDir } from './config/xdg.mjs';
|
|
14
15
|
|
|
15
16
|
const isWindows = os.platform() === 'win32';
|
|
16
17
|
|
|
@@ -110,8 +111,8 @@ export async function checkPrerequisites(options = {}) {
|
|
|
110
111
|
});
|
|
111
112
|
|
|
112
113
|
// Check user config
|
|
113
|
-
const hasUserConfig = existsSync(path.join(homeDir || os.homedir(), '
|
|
114
|
-
existsSync(
|
|
114
|
+
const hasUserConfig = existsSync(path.join(configDir(homeDir || os.homedir()), 'config.env')) ||
|
|
115
|
+
existsSync(configDir(homeDir || os.homedir()));
|
|
115
116
|
checks.push({
|
|
116
117
|
name: 'User config',
|
|
117
118
|
required: false,
|
|
@@ -176,7 +177,7 @@ function readCredentialRaw(varName, homeDir) {
|
|
|
176
177
|
for (const envPath of [
|
|
177
178
|
path.join(process.cwd(), '.env'),
|
|
178
179
|
path.join(homeDir, '.env'),
|
|
179
|
-
path.join(homeDir, '
|
|
180
|
+
path.join(configDir(homeDir), 'config.env'),
|
|
180
181
|
]) {
|
|
181
182
|
if (!existsSync(envPath)) continue;
|
|
182
183
|
try {
|
|
@@ -222,7 +223,7 @@ export function describeCredentialPresence(varName, { homeDir = os.homedir(), cw
|
|
|
222
223
|
for (const envPath of [
|
|
223
224
|
path.join(cwd, '.env'),
|
|
224
225
|
path.join(homeDir, '.env'),
|
|
225
|
-
path.join(homeDir, '
|
|
226
|
+
path.join(configDir(homeDir), 'config.env'),
|
|
226
227
|
]) {
|
|
227
228
|
if (!existsSync(envPath)) continue;
|
|
228
229
|
try {
|
|
@@ -295,7 +296,7 @@ const CREDENTIAL_VARS = [
|
|
|
295
296
|
];
|
|
296
297
|
|
|
297
298
|
/**
|
|
298
|
-
* Link discovered credential references into
|
|
299
|
+
* Link discovered credential references into the XDG config dir config.env without
|
|
299
300
|
* invoking `op read`. Plain values and op:// refs are persisted as found.
|
|
300
301
|
* @param {Object} options
|
|
301
302
|
* @param {string} options.homeDir
|
|
@@ -342,7 +343,7 @@ export function detectCredentials(options = {}) {
|
|
|
342
343
|
// GitHub Copilot: a device-flow credential (Construct's own store or the shared
|
|
343
344
|
// github-copilot store) is the usable signal; the VS Code extension and the
|
|
344
345
|
// copilot CLI are softer fallbacks.
|
|
345
|
-
const copilotAuthStore = existsSync(path.join(homeDir, '
|
|
346
|
+
const copilotAuthStore = existsSync(path.join(configDir(homeDir), 'auth', 'github-copilot.json')) ||
|
|
346
347
|
existsSync(path.join(homeDir, '.config', 'github-copilot', 'apps.json')) ||
|
|
347
348
|
existsSync(path.join(homeDir, '.config', 'github-copilot', 'hosts.json'));
|
|
348
349
|
const copilotDir = path.join(homeDir, '.vscode', 'extensions');
|
package/lib/hook-health.mjs
CHANGED
|
@@ -15,8 +15,9 @@ import fs from 'node:fs';
|
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import { spawnSync } from 'node:child_process';
|
|
17
17
|
import { homedir } from 'node:os';
|
|
18
|
+
import { configDir, doctorRoot } from './config/xdg.mjs';
|
|
18
19
|
|
|
19
|
-
const CX_DIR =
|
|
20
|
+
const CX_DIR = doctorRoot();
|
|
20
21
|
const HOOK_HEALTH_DIR = path.join(CX_DIR, 'hook-health');
|
|
21
22
|
const HEALTH_CHECK_INTERVAL_MS = 60 * 1000; // 1 minute
|
|
22
23
|
|
|
@@ -266,12 +267,12 @@ async function checkSingleHook(hookId, config, hookPath) {
|
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
function findHookPath(hookName) {
|
|
269
|
-
//
|
|
270
|
+
// The user hook symlink lives in the XDG config dir; the project-local
|
|
271
|
+
// .construct/lib and the npm-global install are fallbacks.
|
|
270
272
|
const possiblePaths = [
|
|
271
|
-
path.join(
|
|
273
|
+
path.join(configDir(), 'lib', 'hooks', `${hookName}.mjs`),
|
|
272
274
|
path.join(process.cwd(), '.construct', 'lib', 'hooks', `${hookName}.mjs`),
|
|
273
275
|
path.join(homedir(), '.npm-global', 'lib', 'node_modules', '@geraldmaron/construct', 'lib', 'hooks', `${hookName}.mjs`),
|
|
274
|
-
// Try to find from PATH
|
|
275
276
|
];
|
|
276
277
|
|
|
277
278
|
for (const p of possiblePaths) {
|
package/lib/hooks/_lib/log.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* a regression in the hook harness or a malformed payload does not block the
|
|
6
6
|
* user. The trade-off is silent failure: until now, hook regressions were
|
|
7
7
|
* invisible. This helper writes a single JSONL entry per failure to
|
|
8
|
-
*
|
|
8
|
+
* <doctorRoot>/hook-failures.jsonl so `construct doctor` can surface the top
|
|
9
9
|
* failing hooks without changing the fail-open behavior of any individual
|
|
10
10
|
* hook.
|
|
11
11
|
*
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
|
|
22
22
|
import { appendFileSync, statSync, renameSync, mkdirSync } from 'node:fs';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
|
-
import { homedir } from 'node:os';
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
import { doctorRoot } from '../../config/xdg.mjs';
|
|
26
|
+
|
|
27
|
+
const LOG_DIR = doctorRoot();
|
|
27
28
|
const LOG_PATH = join(LOG_DIR, 'hook-failures.jsonl');
|
|
28
29
|
const LOG_ROTATED_PATH = join(LOG_DIR, 'hook-failures.1.jsonl');
|
|
29
30
|
const LOG_MAX_BYTES = 1_048_576; // 1 MiB
|
|
@@ -29,6 +29,7 @@ import { homedir } from 'node:os';
|
|
|
29
29
|
|
|
30
30
|
import { resolveSetting, loadProjectConfig } from '../../config/project-config.mjs';
|
|
31
31
|
import { HOOK_OUTPUT_MODES } from '../../config/schema.mjs';
|
|
32
|
+
import { doctorRoot } from '../../config/xdg.mjs';
|
|
32
33
|
|
|
33
34
|
export { HOOK_OUTPUT_MODES };
|
|
34
35
|
|
|
@@ -93,7 +94,7 @@ export function writeHookContext({ payload, mode, homeDir = homedir(), stdout =
|
|
|
93
94
|
if (mode === 'stdout') { stdout.write(payload); return 'stdout'; }
|
|
94
95
|
if (mode === 'stderr') { stderr.write(payload); return 'stderr'; }
|
|
95
96
|
try {
|
|
96
|
-
const dir =
|
|
97
|
+
const dir = doctorRoot(homeDir);
|
|
97
98
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
98
99
|
writeFileSync(join(dir, 'session-start-last.log'), payload);
|
|
99
100
|
} catch { /* best effort — never block the session on a debug-log write */ }
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
26
26
|
import { join } from 'node:path';
|
|
27
|
-
import { homedir } from 'node:os';
|
|
28
27
|
import { appendBounded } from '../logging/rotate.mjs';
|
|
29
28
|
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
30
29
|
import { emit as emitRoleEvent } from '../roles/event-bus.mjs';
|
|
30
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
31
31
|
import { logHookFailure } from './_lib/log.mjs';
|
|
32
32
|
|
|
33
33
|
let input = {};
|
|
@@ -74,8 +74,7 @@ if (errorIndicators.some(ind => lowerResult.includes(ind))) {
|
|
|
74
74
|
|
|
75
75
|
// Record agent invocation
|
|
76
76
|
try {
|
|
77
|
-
const
|
|
78
|
-
const cxDir = join(home, '.cx');
|
|
77
|
+
const cxDir = doctorRoot();
|
|
79
78
|
mkdirSync(cxDir, { recursive: true });
|
|
80
79
|
|
|
81
80
|
// Update last-agent files for coordination (shared + per-agent)
|
|
@@ -188,7 +187,7 @@ try {
|
|
|
188
187
|
// role-pending stays at user scope (a handoff might span projects),
|
|
189
188
|
// but each entry gets a projectId tag so a reader can attribute it.
|
|
190
189
|
|
|
191
|
-
const pendingPath = join(
|
|
190
|
+
const pendingPath = join(doctorRoot(), 'role-pending.jsonl');
|
|
192
191
|
const { resolveProjectScope } = await import('../project-root.mjs');
|
|
193
192
|
const scope = resolveProjectScope();
|
|
194
193
|
const bdMatch = /\b(construct-[a-z0-9]+)\b/i.exec(resultText) || [];
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
34
34
|
import { createHash } from 'node:crypto';
|
|
35
35
|
import { join, resolve, dirname } from 'node:path';
|
|
36
|
-
import { homedir } from 'node:os';
|
|
37
36
|
import { logHookFailure } from './_lib/log.mjs';
|
|
38
37
|
import { appendBounded, readLastLineAcrossSegments } from '../logging/rotate.mjs';
|
|
39
38
|
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
40
39
|
import { flushReadTrackerDeltas, recordReadDelta } from '../read-tracker-store.mjs';
|
|
40
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
41
41
|
|
|
42
|
-
const CX_DIR =
|
|
42
|
+
const CX_DIR = doctorRoot();
|
|
43
43
|
// audit-reads.jsonl is PROJECT-SCOPED — reads happen in a specific project,
|
|
44
44
|
// not across the user's whole machine. resolveProjectScopedPath returns
|
|
45
45
|
// <project>/.cx/audit-reads.jsonl when the hook fires from inside a
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
import { readFileSync, existsSync } from 'node:fs';
|
|
37
37
|
import { createHash } from 'node:crypto';
|
|
38
38
|
import { join, resolve } from 'node:path';
|
|
39
|
-
import { homedir } from 'node:os';
|
|
40
39
|
import { appendAuditRecord } from '../audit-trail.mjs';
|
|
41
40
|
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
42
41
|
import { logHookFailure } from './_lib/log.mjs';
|
|
42
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
43
43
|
|
|
44
|
-
const CX_DIR =
|
|
44
|
+
const CX_DIR = doctorRoot();
|
|
45
45
|
const AUDIT_FILE = resolveProjectScopedPath('audit-trail.jsonl', { ensureDir: false });
|
|
46
46
|
const LAST_AGENT = join(CX_DIR, 'last-agent.json');
|
|
47
47
|
const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash']);
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
|
-
import { homedir } from 'node:os';
|
|
25
24
|
import { appendBounded } from '../logging/rotate.mjs';
|
|
25
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
26
26
|
|
|
27
27
|
const SIZE_THRESHOLD_CHARS = 4000;
|
|
28
|
-
const LOG_DIR = join(
|
|
29
|
-
const WARN_FLAGS = join(
|
|
28
|
+
const LOG_DIR = join(doctorRoot(), 'bash-logs');
|
|
29
|
+
const WARN_FLAGS = join(doctorRoot(), 'warn-flags.txt');
|
|
30
30
|
|
|
31
31
|
let input = {};
|
|
32
32
|
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
@@ -33,6 +33,6 @@ if (!result.violations.length) process.exit(0);
|
|
|
33
33
|
const output = formatBrandViolations([result]);
|
|
34
34
|
process.stderr.write(`${output}\n`);
|
|
35
35
|
process.stderr.write(
|
|
36
|
-
'\nBrand policy blocked this edit (marketing voice, retired fonts, or Construct/cli naming). See docs/reference/branding.md and docs/STYLE.md.\n',
|
|
36
|
+
'\nBrand policy blocked this edit (marketing voice, retired fonts, or Construct/cli naming). See docs/guides/reference/branding.md and docs/STYLE.md.\n',
|
|
37
37
|
);
|
|
38
38
|
process.exit(2);
|
|
@@ -27,11 +27,12 @@
|
|
|
27
27
|
|
|
28
28
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
29
29
|
import { execSync, spawn } from 'node:child_process';
|
|
30
|
-
import { homedir } from 'node:os';
|
|
31
30
|
import { join, dirname, resolve } from 'node:path';
|
|
32
31
|
import { fileURLToPath } from 'node:url';
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
34
|
+
|
|
35
|
+
const CACHE_PATH = join(doctorRoot(), 'ci-status-cache.json');
|
|
35
36
|
const CACHE_TTL_MS = 60_000;
|
|
36
37
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
37
38
|
const REFRESHER = resolve(HERE, '..', '..', 'scripts', 'refresh-ci-status.mjs');
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
35
35
|
import { join } from 'node:path';
|
|
36
|
-
import { homedir } from 'node:os';
|
|
37
36
|
import { resolveContextWindow } from '../telemetry/model-pricing-catalog.mjs';
|
|
37
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
38
38
|
|
|
39
|
-
const CX_DIR =
|
|
39
|
+
const CX_DIR = doctorRoot();
|
|
40
40
|
const COST_LOG = join(CX_DIR, 'session-cost.jsonl');
|
|
41
41
|
const EFFICIENCY = join(CX_DIR, 'session-efficiency.json');
|
|
42
42
|
const STATE = join(CX_DIR, 'context-watch-state.json');
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* @exits 0 = pass
|
|
13
13
|
*/
|
|
14
14
|
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
15
|
-
import { homedir } from 'os';
|
|
16
15
|
import { join } from 'path';
|
|
17
16
|
import { readContextState, writeContextState } from '../context-state.mjs';
|
|
17
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
18
18
|
|
|
19
19
|
const CONTEXT_LIMIT_PATTERNS = [
|
|
20
20
|
'context window', 'maximum context', 'too long', 'prompt is too long',
|
|
@@ -23,7 +23,7 @@ const CONTEXT_LIMIT_PATTERNS = [
|
|
|
23
23
|
];
|
|
24
24
|
|
|
25
25
|
const COOLDOWN_MS = 10 * 60 * 1000;
|
|
26
|
-
const STATE_PATH = join(
|
|
26
|
+
const STATE_PATH = join(doctorRoot(), 'context-recovery.json');
|
|
27
27
|
|
|
28
28
|
let input = {};
|
|
29
29
|
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
@@ -42,7 +42,7 @@ try {
|
|
|
42
42
|
if (now - (state.lastTriggeredAt || 0) < COOLDOWN_MS) process.exit(0);
|
|
43
43
|
} catch { /* first run */ }
|
|
44
44
|
|
|
45
|
-
const cxDir =
|
|
45
|
+
const cxDir = doctorRoot();
|
|
46
46
|
try { mkdirSync(cxDir, { recursive: true }); } catch { /* exists */ }
|
|
47
47
|
try { writeFileSync(STATE_PATH, JSON.stringify({ lastTriggeredAt: now })); } catch { /* best effort */ }
|
|
48
48
|
|
|
@@ -50,7 +50,7 @@ const cwd = input?.cwd || process.cwd();
|
|
|
50
50
|
|
|
51
51
|
let existingContext = '';
|
|
52
52
|
try {
|
|
53
|
-
const state = readContextState(cwd) || readContextState(
|
|
53
|
+
const state = readContextState(cwd) || readContextState(doctorRoot());
|
|
54
54
|
if (state?.markdown) existingContext = String(state.markdown).slice(0, 800);
|
|
55
55
|
else if (state?.context) existingContext = String(state.context).slice(0, 800);
|
|
56
56
|
} catch { /* none */ }
|
|
@@ -66,7 +66,7 @@ const recoveryContext = [
|
|
|
66
66
|
const projectCxDir = join(cwd, '.cx');
|
|
67
67
|
try { mkdirSync(projectCxDir, { recursive: true }); } catch { /* exists */ }
|
|
68
68
|
try { writeContextState(cwd, { source: 'context-window-recovery', recoveryContext, format: 'json' }, { markdown: recoveryContext }); } catch { /* best effort */ }
|
|
69
|
-
try { writeContextState(
|
|
69
|
+
try { writeContextState(doctorRoot(), { source: 'context-window-recovery', recoveryContext, format: 'json' }, { markdown: recoveryContext }); } catch { /* best effort */ }
|
|
70
70
|
|
|
71
71
|
process.stdout.write([
|
|
72
72
|
``,
|
package/lib/hooks/dep-audit.mjs
CHANGED
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { readFileSync, existsSync } from 'node:fs';
|
|
17
17
|
import { execSync } from 'node:child_process';
|
|
18
|
-
import { homedir } from 'node:os';
|
|
19
18
|
import { join, dirname, basename } from 'node:path';
|
|
20
19
|
import { appendBounded } from '../logging/rotate.mjs';
|
|
21
20
|
import { logHookFailure } from './_lib/log.mjs';
|
|
21
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
22
22
|
|
|
23
23
|
const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
|
|
24
24
|
if (!filePath) process.exit(0);
|
|
@@ -31,7 +31,7 @@ if (!MANIFESTS.has(file)) process.exit(0);
|
|
|
31
31
|
if (/node_modules|\/dist\/|\/build\/|\/\.git\//.test(filePath)) process.exit(0);
|
|
32
32
|
|
|
33
33
|
const projectDir = dirname(filePath);
|
|
34
|
-
const warnFlagsPath = join(
|
|
34
|
+
const warnFlagsPath = join(doctorRoot(), 'warn-flags.txt');
|
|
35
35
|
|
|
36
36
|
function appendWarn(msg) {
|
|
37
37
|
try { appendBounded('bash-warn-flags', warnFlagsPath, msg + '\n'); }
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
22
|
-
import { homedir } from 'node:os';
|
|
23
22
|
import { join, dirname } from 'node:path';
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
25
|
+
|
|
26
|
+
const STATE_PATH = join(doctorRoot(), 'doc-coupling.json');
|
|
26
27
|
const CODE_RE = /^(lib|bin|src|app)\/.*\.(?:m?js|jsx?|tsx?|cjs)$/;
|
|
27
28
|
const DOC_RE = /^(?:CHANGELOG\.md|docs\/.+\.md|\.cx\/context\.(?:md|json))$/;
|
|
28
29
|
const THRESHOLDS = [3, 5, 10];
|
|
@@ -20,6 +20,7 @@ import { homedir } from 'node:os';
|
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { appendBounded } from '../logging/rotate.mjs';
|
|
22
22
|
import { logHookFailure } from './_lib/log.mjs';
|
|
23
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
23
24
|
|
|
24
25
|
const TS_EXTS = new Set(['.ts','.tsx','.js','.mjs','.jsx','.cjs']);
|
|
25
26
|
const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
|
|
@@ -28,7 +29,7 @@ if (!filePath) process.exit(0);
|
|
|
28
29
|
|
|
29
30
|
const ext = path.extname(filePath);
|
|
30
31
|
const home = homedir();
|
|
31
|
-
const countPath = path.join(home, '
|
|
32
|
+
const countPath = path.join(doctorRoot(home), 'files-changed-count.txt');
|
|
32
33
|
|
|
33
34
|
// Always increment the total files-changed counter for notify
|
|
34
35
|
try {
|
|
@@ -41,7 +42,7 @@ try {
|
|
|
41
42
|
// Only accumulate TS/JS files for typecheck
|
|
42
43
|
if (!TS_EXTS.has(ext)) process.exit(0);
|
|
43
44
|
|
|
44
|
-
const tcPath = path.join(home, '
|
|
45
|
+
const tcPath = path.join(doctorRoot(home), 'pending-typecheck.txt');
|
|
45
46
|
try {
|
|
46
47
|
const existing = existsSync(tcPath) ? readFileSync(tcPath, 'utf8').split('\n').filter(Boolean) : [];
|
|
47
48
|
if (!existing.includes(filePath)) {
|
package/lib/hooks/edit-guard.mjs
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
import { readFileSync, existsSync } from 'fs';
|
|
15
15
|
import { createHash } from 'crypto';
|
|
16
16
|
import { join, resolve } from 'path';
|
|
17
|
-
import { homedir } from 'os';
|
|
18
17
|
import { logHookFailure } from './_lib/log.mjs';
|
|
18
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
19
19
|
|
|
20
|
-
const HASH_STORE = join(
|
|
20
|
+
const HASH_STORE = join(doctorRoot(), 'file-hashes.json');
|
|
21
21
|
|
|
22
22
|
function storedHash(absPath) {
|
|
23
23
|
try {
|
|
@@ -48,7 +48,7 @@ function absOf(p) { return p.startsWith('/') ? p : resolve(cwd, p); }
|
|
|
48
48
|
|
|
49
49
|
if ((toolName === 'Edit' || toolName === 'Write' || toolName === 'MultiEdit') && process.env.CONSTRUCT_ROLES !== 'off') {
|
|
50
50
|
try {
|
|
51
|
-
const lastAgentPath = join(
|
|
51
|
+
const lastAgentPath = join(doctorRoot(), 'last-agent.json');
|
|
52
52
|
if (existsSync(lastAgentPath)) {
|
|
53
53
|
const last = JSON.parse(readFileSync(lastAgentPath, 'utf8'));
|
|
54
54
|
const lastTs = last?.ts ? Date.parse(last.ts) : 0;
|
package/lib/hooks/guard-bash.mjs
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createInterface } from 'readline';
|
|
15
15
|
import { readFileSync, existsSync } from 'fs';
|
|
16
|
-
import { homedir } from 'os';
|
|
17
16
|
import { join } from 'path';
|
|
18
17
|
import { logHookFailure } from './_lib/log.mjs';
|
|
18
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
19
19
|
|
|
20
20
|
const BLOCK_PATTERNS = [
|
|
21
21
|
{
|
|
@@ -99,7 +99,7 @@ for (const { pattern, reason } of WARN_PATTERNS) {
|
|
|
99
99
|
|
|
100
100
|
if (process.env.CONSTRUCT_ROLES !== 'off') {
|
|
101
101
|
try {
|
|
102
|
-
const cxDir =
|
|
102
|
+
const cxDir = doctorRoot();
|
|
103
103
|
const id = String(process.env.CONSTRUCT_AGENT_ID || '').replace(/^cx-/, '');
|
|
104
104
|
let agentData = null;
|
|
105
105
|
|
|
@@ -32,8 +32,9 @@ import {
|
|
|
32
32
|
writeProviderCooldown,
|
|
33
33
|
} from '../model-router.mjs';
|
|
34
34
|
import { readHookInput } from './_lib/input.mjs';
|
|
35
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
35
36
|
|
|
36
|
-
const cooldownPath = join(
|
|
37
|
+
const cooldownPath = join(doctorRoot(), 'provider-cooldowns.json');
|
|
37
38
|
const envPath = join(process.cwd(), '.env');
|
|
38
39
|
|
|
39
40
|
const input = readHookInput();
|
|
@@ -24,6 +24,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
|
24
24
|
import { execSync } from 'node:child_process';
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
import { homedir } from 'node:os';
|
|
27
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
27
28
|
|
|
28
29
|
let input = {};
|
|
29
30
|
try { input = JSON.parse(readFileSync(0, 'utf8')) || {}; } catch { process.exit(0); }
|
|
@@ -55,7 +56,7 @@ if (hookEvent === 'PreToolUse') {
|
|
|
55
56
|
const READ_TOOLS = new Set(['Read', 'Grep', 'Glob', 'LS', 'NotebookRead']);
|
|
56
57
|
const BLOCKED_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'TodoWrite']);
|
|
57
58
|
|
|
58
|
-
const stateDir =
|
|
59
|
+
const stateDir = doctorRoot(home);
|
|
59
60
|
const statePath = join(stateDir, 'bootstrap-state.json');
|
|
60
61
|
const sessionId = input.session_id || input.sessionId || 'default';
|
|
61
62
|
|
|
@@ -147,7 +148,7 @@ if (hookEvent === 'Stop') {
|
|
|
147
148
|
|
|
148
149
|
const branch = safeExec('git branch --show-current');
|
|
149
150
|
const isFeatureBranch = branch && branch !== 'main' && branch !== 'dev' && branch !== 'master';
|
|
150
|
-
const filesChangedPath = join(home, '
|
|
151
|
+
const filesChangedPath = join(doctorRoot(home), 'files-changed-count.txt');
|
|
151
152
|
let filesChanged = 0;
|
|
152
153
|
try { filesChanged = parseInt(readFileSync(filesChangedPath, 'utf8').trim() || '0', 10); } catch {}
|
|
153
154
|
|
|
@@ -188,7 +189,7 @@ if (hookEvent === 'Stop') {
|
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
const driveSessionPath = join(home, '
|
|
192
|
+
const driveSessionPath = join(doctorRoot(home), 'drive-session.json');
|
|
192
193
|
if (existsSync(driveSessionPath)) {
|
|
193
194
|
let ds = {};
|
|
194
195
|
try { ds = JSON.parse(readFileSync(driveSessionPath, 'utf8')); } catch {}
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
* @exits 0 = pass
|
|
18
18
|
*/
|
|
19
19
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
20
|
-
import { homedir } from 'os';
|
|
21
20
|
import { join } from 'path';
|
|
22
21
|
import { writeContextState, contextMarkdownPath, contextJsonPath } from '../context-state.mjs';
|
|
23
22
|
import { flushReadTrackerDeltas } from '../read-tracker-store.mjs';
|
|
23
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
24
24
|
|
|
25
25
|
// Section headings that are auto-generated by this hook. Any other heading in
|
|
26
26
|
// the existing context.md is treated as manually authored and preserved.
|
|
@@ -100,8 +100,8 @@ const transcriptPath = input?.transcript_path || '';
|
|
|
100
100
|
const filesChanged = [];
|
|
101
101
|
const decisions = [];
|
|
102
102
|
const pendingTodos = [];
|
|
103
|
-
const warnFlagsPath = join(
|
|
104
|
-
const efficiencyPath = join(
|
|
103
|
+
const warnFlagsPath = join(doctorRoot(), 'warn-flags.txt');
|
|
104
|
+
const efficiencyPath = join(doctorRoot(), 'session-efficiency.json');
|
|
105
105
|
let lastSummary = '';
|
|
106
106
|
|
|
107
107
|
if (transcriptPath && existsSync(transcriptPath)) {
|
|
@@ -275,12 +275,12 @@ if (decisions.length) {
|
|
|
275
275
|
try { writeFileSync(join(decisionsDir, `${slug}.md`), decisionDoc); } catch { /* best effort */ }
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
const globalCxDir = join(
|
|
278
|
+
const globalCxDir = join(doctorRoot(), '.cx');
|
|
279
279
|
try { mkdirSync(globalCxDir, { recursive: true }); } catch { /* exists */ }
|
|
280
|
-
const globalMdPath = contextMarkdownPath(
|
|
280
|
+
const globalMdPath = contextMarkdownPath(doctorRoot());
|
|
281
281
|
const globalManualSections = extractManualSections(globalMdPath);
|
|
282
282
|
const globalFinalContent = content + (globalManualSections ? '\n' + globalManualSections : '');
|
|
283
|
-
const globalMergedJson = mergeExistingJson(contextJsonPath(
|
|
284
|
-
try { writeContextState(
|
|
283
|
+
const globalMergedJson = mergeExistingJson(contextJsonPath(doctorRoot()), contextJson);
|
|
284
|
+
try { writeContextState(doctorRoot(), { ...globalMergedJson, contextSummary: lastSummary }, { markdown: globalFinalContent }); } catch { /* best effort */ }
|
|
285
285
|
|
|
286
286
|
process.exit(0);
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
|
|
18
18
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import { execSync } from 'node:child_process';
|
|
20
|
-
import { homedir } from 'node:os';
|
|
21
20
|
import { join } from 'node:path';
|
|
22
21
|
import { emitRoleEvent } from '../roles/hook-emit.mjs';
|
|
22
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
23
23
|
|
|
24
|
-
const STATE_PATH = join(
|
|
24
|
+
const STATE_PATH = join(doctorRoot(), 'readme-age-state.json');
|
|
25
25
|
const STALE_DAYS = 90;
|
|
26
26
|
const SUPPRESS_DAYS = 7;
|
|
27
27
|
|
|
@@ -14,18 +14,19 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
16
16
|
import { join, dirname } from 'path';
|
|
17
|
-
import { homedir } from 'os';
|
|
18
17
|
import { spawn } from 'child_process';
|
|
19
18
|
import { fileURLToPath } from 'url';
|
|
20
19
|
|
|
20
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
21
|
+
|
|
21
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
23
|
const CONSTRUCT_BIN = join(__dirname, '..', '..', 'bin', 'construct');
|
|
23
24
|
|
|
24
25
|
const OPTIMIZATION_THRESHOLD = 0.7;
|
|
25
26
|
const MIN_TRACES_PER_AGENT = 3;
|
|
26
27
|
const OPTIMIZATION_DAYS = 7;
|
|
27
|
-
const OPTIMIZATION_LOG_DIR = join(
|
|
28
|
-
const REVIEWS_DIR = join(
|
|
28
|
+
const OPTIMIZATION_LOG_DIR = join(doctorRoot(), 'optimization-logs');
|
|
29
|
+
const REVIEWS_DIR = join(doctorRoot(), 'performance-reviews');
|
|
29
30
|
|
|
30
31
|
mkdirSync(OPTIMIZATION_LOG_DIR, { recursive: true });
|
|
31
32
|
|
|
@@ -24,6 +24,7 @@ import { listObservations, searchObservations } from '../observation-store.mjs';
|
|
|
24
24
|
import { countEntities } from '../entity-store.mjs';
|
|
25
25
|
import { logHookFailure } from './_lib/log.mjs';
|
|
26
26
|
import { resolveHookOutputMode, writeHookContext } from './_lib/output-mode.mjs';
|
|
27
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
27
28
|
|
|
28
29
|
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
29
30
|
const CONSTRUCT_BIN = resolve(MODULE_DIR, '..', '..', 'bin', 'construct');
|
|
@@ -64,11 +65,11 @@ function fileIsFresh(path) {
|
|
|
64
65
|
// stale context files inject only the digest line as fallback (Tier 2 → Tier 1
|
|
65
66
|
// degradation), full body only when modified within the freshness window.
|
|
66
67
|
const localCtx = join(cwd, '.cx', 'context.md');
|
|
67
|
-
const globalCtx = join(
|
|
68
|
+
const globalCtx = join(doctorRoot(), '.cx', 'context.md');
|
|
68
69
|
let context = '';
|
|
69
70
|
let contextStale = false;
|
|
70
71
|
const localState = readContextState(cwd);
|
|
71
|
-
const globalState = localState ? null : readContextState(
|
|
72
|
+
const globalState = localState ? null : readContextState(doctorRoot());
|
|
72
73
|
const stateContext = localState || globalState;
|
|
73
74
|
const stateContextPath = localState ? localCtx : globalCtx;
|
|
74
75
|
if (stateContext?.markdown) {
|
|
@@ -151,7 +152,7 @@ try {
|
|
|
151
152
|
|
|
152
153
|
// Record memory injection stats for the Stop hook to persist into .cx/memory-stats.jsonl.
|
|
153
154
|
try {
|
|
154
|
-
const memStatsPath = join(
|
|
155
|
+
const memStatsPath = join(doctorRoot(), 'session-memory-stats.json');
|
|
155
156
|
const injected = observationsNote ? (observationsNote.match(/^- /gm) || []).length : 0;
|
|
156
157
|
writeFileSync(memStatsPath, JSON.stringify({
|
|
157
158
|
project: projectName,
|
|
@@ -162,7 +163,7 @@ try {
|
|
|
162
163
|
} catch { /* best effort */ }
|
|
163
164
|
|
|
164
165
|
// Pending typecheck warning
|
|
165
|
-
const tcPath = join(
|
|
166
|
+
const tcPath = join(doctorRoot(), 'pending-typecheck.txt');
|
|
166
167
|
let pendingNote = '';
|
|
167
168
|
try {
|
|
168
169
|
const pending = existsSync(tcPath) ? readFileSync(tcPath, 'utf8').split('\n').filter(Boolean) : [];
|
|
@@ -400,7 +401,7 @@ try {
|
|
|
400
401
|
try {
|
|
401
402
|
const sessionId = input?.session_id || input?.sessionId;
|
|
402
403
|
if (sessionId) {
|
|
403
|
-
const bootstrapPath = join(
|
|
404
|
+
const bootstrapPath = join(doctorRoot(), "bootstrap-state.json");
|
|
404
405
|
let state = {};
|
|
405
406
|
try { state = JSON.parse(readFileSync(bootstrapPath, "utf8")) || {}; } catch {}
|
|
406
407
|
const now = Date.now();
|
|
@@ -27,6 +27,7 @@ import { captureSessionArtifacts } from '../artifact-capture.mjs';
|
|
|
27
27
|
import { appendSessionStats } from '../memory-stats.mjs';
|
|
28
28
|
import { estimateUsageCost } from '../telemetry/model-pricing-catalog.mjs';
|
|
29
29
|
import { flushReadTrackerDeltas } from '../read-tracker-store.mjs';
|
|
30
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
30
31
|
|
|
31
32
|
function loadTranscriptCheckpoints(checkpointPath) {
|
|
32
33
|
try {
|
|
@@ -148,12 +149,12 @@ if (raw) process.stdout.write(raw);
|
|
|
148
149
|
try { flushReadTrackerDeltas({ env: process.env }); } catch { /* non-critical */ }
|
|
149
150
|
|
|
150
151
|
const home = homedir();
|
|
151
|
-
const tsResultPath = join(home, '
|
|
152
|
-
const warnFlagsPath = join(home, '
|
|
153
|
-
const countPath = join(home, '
|
|
154
|
-
const costLogPath = join(home, '
|
|
155
|
-
const lastAgentPath = join(home, '
|
|
156
|
-
const checkpointPath = join(home, '
|
|
152
|
+
const tsResultPath = join(doctorRoot(home), 'ts-result.txt');
|
|
153
|
+
const warnFlagsPath = join(doctorRoot(home), 'warn-flags.txt');
|
|
154
|
+
const countPath = join(doctorRoot(home), 'files-changed-count.txt');
|
|
155
|
+
const costLogPath = join(doctorRoot(home), 'session-cost.jsonl');
|
|
156
|
+
const lastAgentPath = join(doctorRoot(home), 'last-agent.json');
|
|
157
|
+
const checkpointPath = join(doctorRoot(home), 'transcript-checkpoints.json');
|
|
157
158
|
|
|
158
159
|
// Consume-clear like warn-flags/files-changed below: a TS result is reported
|
|
159
160
|
// once, not re-fired on every subsequent no-op Stop. stop-typecheck rewrites
|
|
@@ -304,7 +305,7 @@ try {
|
|
|
304
305
|
}
|
|
305
306
|
} catch { /* non-critical */ }
|
|
306
307
|
|
|
307
|
-
const memStatsPath = join(home, '
|
|
308
|
+
const memStatsPath = join(doctorRoot(home), 'session-memory-stats.json');
|
|
308
309
|
try {
|
|
309
310
|
if (existsSync(memStatsPath)) {
|
|
310
311
|
const memStats = JSON.parse(readFileSync(memStatsPath, 'utf8'));
|
|
@@ -323,7 +324,7 @@ try {
|
|
|
323
324
|
// repeat Stop events within the window to avoid review-spam.
|
|
324
325
|
try {
|
|
325
326
|
const homeDir = process.env.HOME || homedir();
|
|
326
|
-
const stampPath = join(homeDir, '
|
|
327
|
+
const stampPath = join(doctorRoot(homeDir), 'performance-reviews', '.last-run');
|
|
327
328
|
let lastRun = 0;
|
|
328
329
|
try { lastRun = Number(readFileSync(stampPath, 'utf8')) || 0; } catch { /* fresh */ }
|
|
329
330
|
if (Date.now() - lastRun > 24 * 60 * 60 * 1000) {
|
|
@@ -15,6 +15,7 @@ import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
|
15
15
|
import { execSync } from 'child_process';
|
|
16
16
|
import { homedir } from 'os';
|
|
17
17
|
import { join, dirname } from 'path';
|
|
18
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
18
19
|
|
|
19
20
|
// Stop hooks must echo stdin to stdout
|
|
20
21
|
let raw = '';
|
|
@@ -22,9 +23,9 @@ try { raw = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
|
|
|
22
23
|
if (raw) process.stdout.write(raw);
|
|
23
24
|
|
|
24
25
|
const home = homedir();
|
|
25
|
-
const tcPath = join(home, '
|
|
26
|
-
const tsResultPath = join(home, '
|
|
27
|
-
const warnFlagsPath = join(home, '
|
|
26
|
+
const tcPath = join(doctorRoot(home), 'pending-typecheck.txt');
|
|
27
|
+
const tsResultPath = join(doctorRoot(home), 'ts-result.txt');
|
|
28
|
+
const warnFlagsPath = join(doctorRoot(home), 'warn-flags.txt');
|
|
28
29
|
|
|
29
30
|
// Read pending files
|
|
30
31
|
const pending = existsSync(tcPath)
|