codehere 0.3.0-rc.9 → 0.3.2
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 +43 -35
- package/dist/domain/services/codebase-detector.js +1 -1
- package/dist/index.js +52 -5
- package/dist/infrastructure/hooks/session-analyzer.js +14 -5
- package/dist/infrastructure/orchestration/conductor-adapter.js +16 -5
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -3
- package/dist/infrastructure/protocols/mcp-server-impl.js +33 -3
- package/dist/infrastructure/security/file-collection.js +3 -0
- package/dist/presentation/cli/commands/init-command.js +245 -0
- package/dist/presentation/cli/commands/report-command.js +56 -11
- package/dist/presentation/cli/commands/status-command.js +32 -10
- package/dist/presentation/cli/repl.js +644 -319
- package/dist/ui.js +0 -3
- package/dist/utils/typo-guard.js +73 -0
- package/dist/utils/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init Command — scaffold a `.codehere/` directory at the repo root.
|
|
3
|
+
*
|
|
4
|
+
* The user-side analog to `git init`: gives the project a small, conventional
|
|
5
|
+
* shape codehere can read from when an agent runs here. Three files:
|
|
6
|
+
*
|
|
7
|
+
* .codehere/CONTEXT.md — what this repo is, conventions, constraints
|
|
8
|
+
* .codehere/HANDOVER.md — manual cross-session continuity (until F3 ships)
|
|
9
|
+
* .codehere/PROMPTS.md — your library of recurring task prompts
|
|
10
|
+
*
|
|
11
|
+
* Conventions:
|
|
12
|
+
* - CONTEXT.md and PROMPTS.md are committed (project config, shared)
|
|
13
|
+
* - HANDOVER.md is git-ignored by default (per-machine working state)
|
|
14
|
+
*
|
|
15
|
+
* Targets the repo root (via findCodebaseRoot), not cwd, so `cd src && codehere init`
|
|
16
|
+
* still scaffolds at the repo root, not inside src/.
|
|
17
|
+
*
|
|
18
|
+
* Idempotent: re-running detects existing files and skips them unless --force.
|
|
19
|
+
* Refuses to follow symlinks (lstat check) so a hostile/misplaced symlink at
|
|
20
|
+
* `.codehere/CONTEXT.md` or `.gitignore` cannot redirect writes outside the repo.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, lstatSync, statSync } from 'fs';
|
|
23
|
+
import { join } from 'path';
|
|
24
|
+
import { colors, success, info, warning, error, newline } from '../../../ui.js';
|
|
25
|
+
import { findCodebaseRoot } from '../../../domain/services/codebase-detector.js';
|
|
26
|
+
const TEMPLATES = [
|
|
27
|
+
{
|
|
28
|
+
filename: 'CONTEXT.md',
|
|
29
|
+
body: `# Project Context
|
|
30
|
+
|
|
31
|
+
> codehere reads this when an agent runs in this directory.
|
|
32
|
+
> Keep it short. Keep it true. The agent will follow what's written here.
|
|
33
|
+
|
|
34
|
+
## What this is
|
|
35
|
+
|
|
36
|
+
[One-paragraph description of what this repo does and who uses it.]
|
|
37
|
+
|
|
38
|
+
## Conventions
|
|
39
|
+
|
|
40
|
+
[How this codebase is organized — entry points, file naming, framework version.
|
|
41
|
+
Anything an agent would otherwise have to discover by grep.]
|
|
42
|
+
|
|
43
|
+
## Style
|
|
44
|
+
|
|
45
|
+
[Tone for generated code. Test framework. Lint rules. Imports style.
|
|
46
|
+
Prefer X. Avoid Y.]
|
|
47
|
+
|
|
48
|
+
## Constraints
|
|
49
|
+
|
|
50
|
+
[Don't do X. Always do Y. Files that are off-limits.
|
|
51
|
+
Things you've been burned by.]
|
|
52
|
+
|
|
53
|
+
## Useful commands
|
|
54
|
+
|
|
55
|
+
\`\`\`
|
|
56
|
+
[build, test, lint, dev server — the agent will run these]
|
|
57
|
+
\`\`\`
|
|
58
|
+
`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
filename: 'HANDOVER.md',
|
|
62
|
+
body: `# Handover
|
|
63
|
+
|
|
64
|
+
> Update at the end of each working session.
|
|
65
|
+
> codehere has no project memory across sessions yet (F3 ships in v0.5).
|
|
66
|
+
> This file is the manual handover so the next session — or the next agent —
|
|
67
|
+
> can pick up without you re-explaining.
|
|
68
|
+
|
|
69
|
+
## Last session
|
|
70
|
+
|
|
71
|
+
- **Date:**
|
|
72
|
+
- **What got done:**
|
|
73
|
+
- **What's in progress:**
|
|
74
|
+
- **What to pick up next:**
|
|
75
|
+
- **Open questions:**
|
|
76
|
+
|
|
77
|
+
## Decisions worth remembering
|
|
78
|
+
|
|
79
|
+
[Decisions made this session that future sessions should respect.]
|
|
80
|
+
|
|
81
|
+
## Files touched recently
|
|
82
|
+
|
|
83
|
+
[List the files you've been working on. The agent can prioritize them.]
|
|
84
|
+
`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
filename: 'PROMPTS.md',
|
|
88
|
+
body: `# Prompts
|
|
89
|
+
|
|
90
|
+
> Your library of recurring task prompts.
|
|
91
|
+
> Copy + adapt + delegate via \`codehere task\` or the web UI.
|
|
92
|
+
>
|
|
93
|
+
> The codehere prompt formula: **Context + End State + Constraints**.
|
|
94
|
+
> Stack-specific details (test framework, language, file conventions) belong
|
|
95
|
+
> in CONTEXT.md, not duplicated in every prompt here.
|
|
96
|
+
|
|
97
|
+
## Implement a change
|
|
98
|
+
|
|
99
|
+
> "Read .codehere/CONTEXT.md. [Describe the change in user-visible terms — what
|
|
100
|
+
> behavior is added, removed, or changed.] Match existing conventions. Don't
|
|
101
|
+
> touch unrelated files. Don't add new dependencies."
|
|
102
|
+
|
|
103
|
+
## Add tests
|
|
104
|
+
|
|
105
|
+
> "Read .codehere/CONTEXT.md. Add tests for [FILE / behavior]. Cover happy
|
|
106
|
+
> path, edge cases, and error paths. Match the existing test style and
|
|
107
|
+
> framework. Don't lower the strictness of any existing test."
|
|
108
|
+
|
|
109
|
+
## Refactor safely
|
|
110
|
+
|
|
111
|
+
> "Read .codehere/CONTEXT.md. Refactor [FILE / module] to [GOAL]. Keep the
|
|
112
|
+
> public API stable. All existing tests must still pass. Don't introduce
|
|
113
|
+
> new dependencies. Show the diff before applying."
|
|
114
|
+
|
|
115
|
+
## Review changes
|
|
116
|
+
|
|
117
|
+
> "Read .codehere/CONTEXT.md. Review the changes in [PR / branch / file] for:
|
|
118
|
+
> security issues, missing tests, breaking changes, style violations, and
|
|
119
|
+
> alignment with the constraints in CONTEXT.md. Output a short markdown
|
|
120
|
+
> report; don't change any code."
|
|
121
|
+
`,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
const GITIGNORE_MARKER = '# Added by codehere init';
|
|
125
|
+
const GITIGNORE_BLOCK = `\n${GITIGNORE_MARKER}\n.codehere/HANDOVER.md\n`;
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the directory to scaffold. Prefers git/package root over cwd so
|
|
128
|
+
* `cd repo/src && codehere init` still lands at repo/.
|
|
129
|
+
*/
|
|
130
|
+
function resolveTargetRoot() {
|
|
131
|
+
const info = findCodebaseRoot();
|
|
132
|
+
if (info)
|
|
133
|
+
return { root: info.rootPath, type: info.type };
|
|
134
|
+
return { root: process.cwd(), type: 'directory' };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Reject a path that exists AND is a symlink. Refusing on symlinks (rather
|
|
138
|
+
* than following) prevents an init run from clobbering files outside the
|
|
139
|
+
* repo if a hostile or misplaced symlink already exists at the target.
|
|
140
|
+
*/
|
|
141
|
+
function isSymlink(path) {
|
|
142
|
+
try {
|
|
143
|
+
return lstatSync(path).isSymbolicLink();
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function ensureGitignore(repoRoot) {
|
|
150
|
+
const gitDir = join(repoRoot, '.git');
|
|
151
|
+
if (!existsSync(gitDir))
|
|
152
|
+
return 'no-git';
|
|
153
|
+
const gitignorePath = join(repoRoot, '.gitignore');
|
|
154
|
+
if (isSymlink(gitignorePath))
|
|
155
|
+
return 'refused-symlink';
|
|
156
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
|
|
157
|
+
if (existing.includes('.codehere/HANDOVER.md'))
|
|
158
|
+
return 'already-present';
|
|
159
|
+
appendFileSync(gitignorePath, GITIGNORE_BLOCK);
|
|
160
|
+
return 'added';
|
|
161
|
+
}
|
|
162
|
+
export function executeInitCommand(options = {}) {
|
|
163
|
+
try {
|
|
164
|
+
const { root, type } = resolveTargetRoot();
|
|
165
|
+
const codehereDir = join(root, '.codehere');
|
|
166
|
+
newline();
|
|
167
|
+
console.log(colors.bold('codehere init'));
|
|
168
|
+
console.log(colors.dim(` scaffolding .codehere/ at ${type} root: ${root}`));
|
|
169
|
+
newline();
|
|
170
|
+
// Refuse if .codehere itself is a symlink — would redirect every subsequent write.
|
|
171
|
+
if (existsSync(codehereDir) && isSymlink(codehereDir)) {
|
|
172
|
+
console.log(` ${error('refused')} .codehere is a symlink — refusing to follow. Remove it first.`);
|
|
173
|
+
newline();
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!existsSync(codehereDir)) {
|
|
178
|
+
mkdirSync(codehereDir, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const codehereStat = statSync(codehereDir);
|
|
182
|
+
if (!codehereStat.isDirectory()) {
|
|
183
|
+
console.log(` ${error('refused')} .codehere exists but is not a directory.`);
|
|
184
|
+
newline();
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const created = [];
|
|
190
|
+
const skipped = [];
|
|
191
|
+
const refused = [];
|
|
192
|
+
for (const tmpl of TEMPLATES) {
|
|
193
|
+
const target = join(codehereDir, tmpl.filename);
|
|
194
|
+
if (isSymlink(target)) {
|
|
195
|
+
refused.push(tmpl.filename);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (existsSync(target) && !options.force) {
|
|
199
|
+
skipped.push(tmpl.filename);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
writeFileSync(target, tmpl.body);
|
|
203
|
+
created.push(tmpl.filename);
|
|
204
|
+
}
|
|
205
|
+
for (const f of created) {
|
|
206
|
+
console.log(` ${success(`created`)} .codehere/${f}`);
|
|
207
|
+
}
|
|
208
|
+
for (const f of skipped) {
|
|
209
|
+
console.log(` ${info(`exists `)} .codehere/${f} ${colors.dim('(use --force to overwrite)')}`);
|
|
210
|
+
}
|
|
211
|
+
for (const f of refused) {
|
|
212
|
+
console.log(` ${error(`refused`)} .codehere/${f} ${colors.dim('(symlink — remove it first)')}`);
|
|
213
|
+
}
|
|
214
|
+
if (!options.noGitignore) {
|
|
215
|
+
const gitignoreResult = ensureGitignore(root);
|
|
216
|
+
newline();
|
|
217
|
+
switch (gitignoreResult) {
|
|
218
|
+
case 'added':
|
|
219
|
+
console.log(` ${success('updated')} .gitignore ${colors.dim('(.codehere/HANDOVER.md ignored — per-machine working state)')}`);
|
|
220
|
+
break;
|
|
221
|
+
case 'already-present':
|
|
222
|
+
console.log(` ${info('exists ')} .gitignore ${colors.dim('(HANDOVER.md already ignored)')}`);
|
|
223
|
+
break;
|
|
224
|
+
case 'no-git':
|
|
225
|
+
console.log(` ${warning('skipped')} .gitignore ${colors.dim('(no .git directory at root)')}`);
|
|
226
|
+
break;
|
|
227
|
+
case 'refused-symlink':
|
|
228
|
+
console.log(` ${error('refused')} .gitignore is a symlink — left untouched.`);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
newline();
|
|
233
|
+
console.log(colors.bold('Next:'));
|
|
234
|
+
console.log(` 1. Open ${colors.cyan('.codehere/CONTEXT.md')} and fill in what this repo is`);
|
|
235
|
+
console.log(` 2. Run ${colors.cyan('codehere start')} and delegate your first task`);
|
|
236
|
+
console.log(` 3. Update ${colors.cyan('.codehere/HANDOVER.md')} at the end of the session`);
|
|
237
|
+
newline();
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
241
|
+
console.error(`\n ${error('failed')} codehere init — ${msg}\n`);
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=init-command.js.map
|
|
@@ -237,14 +237,61 @@ function generateSimpleReport(report, target) {
|
|
|
237
237
|
lines.push('Generated by codehere — know what your AI agent did before your tech lead asks.');
|
|
238
238
|
return lines.join('\n');
|
|
239
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Write report to the requested path. If the path is unwritable (EROFS,
|
|
242
|
+
* EACCES, EPERM, ENOENT on a parent dir we can't create), fall back to
|
|
243
|
+
* stdout so the user still gets the report instead of a stack trace.
|
|
244
|
+
*
|
|
245
|
+
* Returns true if the file was written, false if we fell back to stdout.
|
|
246
|
+
*
|
|
247
|
+
* Provoked by rc.25 UAT: `codehere report --simple` from `/` crashed with
|
|
248
|
+
* `EROFS: read-only file system, open 'codehere-audit.md'`. A user running
|
|
249
|
+
* the command from any unwritable cwd should still get their report.
|
|
250
|
+
*/
|
|
251
|
+
function writeReportWithFallback(targetPath, content, label) {
|
|
252
|
+
try {
|
|
253
|
+
writeFileSync(targetPath, content);
|
|
254
|
+
console.log(success(`${label} written to ${targetPath}`));
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
const code = e.code ?? '';
|
|
259
|
+
if (['EROFS', 'EACCES', 'EPERM', 'ENOENT'].includes(code)) {
|
|
260
|
+
console.error(error(`Could not write to ${targetPath} (${code}). Printing to stdout instead — pass --out <path> to a writable location.`));
|
|
261
|
+
process.stdout.write(content + '\n');
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
throw e;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
240
267
|
export async function executeReportCommand(target, options = {}) {
|
|
241
268
|
const cwd = process.cwd();
|
|
242
269
|
const resolved = target ? (isAbsolute(target) ? target : join(cwd, target)) : cwd;
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
270
|
+
// rc.18: explicit mutex on combinations Codex flagged as silently
|
|
271
|
+
// ambiguous. Two pairs:
|
|
272
|
+
// format {--json, --simple} — pick at most one output format
|
|
273
|
+
// dest {--stdout, --out} — pick at most one destination
|
|
274
|
+
// Previously the second flag silently lost; now we error so users know.
|
|
275
|
+
if (options.json && options.simple) {
|
|
276
|
+
console.error(error('--json and --simple cannot be used together. Pick one output format.'));
|
|
277
|
+
process.exit(2);
|
|
278
|
+
}
|
|
279
|
+
if (options.stdout && options.out) {
|
|
280
|
+
console.error(error('--stdout and --out cannot be used together. Pick one destination.'));
|
|
281
|
+
process.exit(2);
|
|
282
|
+
}
|
|
283
|
+
// Default routing rules (after mutex check):
|
|
284
|
+
// --json → stdout (pipeable to jq, every CLI convention).
|
|
285
|
+
// --simple / md → file (user expects a shareable artifact, not a wall of markdown).
|
|
286
|
+
// --out <path> → wins over the default (explicit beats default).
|
|
287
|
+
// --stdout → forces stdout for non-json formats.
|
|
288
|
+
// Rationale: rc.15 — `report --json` writing to disk silently broke pipelines
|
|
289
|
+
// (`codehere report --json | jq` produced empty stdout). Stdout for machine-
|
|
290
|
+
// readable formats matches `kubectl get -o json`, `docker inspect`, etc.
|
|
291
|
+
const defaultOut = options.json
|
|
292
|
+
? undefined // stdout
|
|
293
|
+
: 'codehere-audit.md';
|
|
294
|
+
const effectiveOut = options.stdout ? undefined : (options.out ?? defaultOut);
|
|
248
295
|
if (!existsSync(resolved)) {
|
|
249
296
|
console.error(error(`Not found: ${resolved}`));
|
|
250
297
|
process.exit(1);
|
|
@@ -297,8 +344,7 @@ export async function executeReportCommand(target, options = {}) {
|
|
|
297
344
|
if (options.json) {
|
|
298
345
|
const output = JSON.stringify(report, null, 2);
|
|
299
346
|
if (effectiveOut) {
|
|
300
|
-
|
|
301
|
-
console.log(success(`Report written to ${effectiveOut}`));
|
|
347
|
+
writeReportWithFallback(effectiveOut, output, 'Report');
|
|
302
348
|
}
|
|
303
349
|
else {
|
|
304
350
|
console.log(output);
|
|
@@ -309,8 +355,7 @@ export async function executeReportCommand(target, options = {}) {
|
|
|
309
355
|
if (options.simple) {
|
|
310
356
|
const simple = generateSimpleReport(report, resolved);
|
|
311
357
|
if (effectiveOut) {
|
|
312
|
-
|
|
313
|
-
console.log(success(`Plain-English report written to ${effectiveOut}`));
|
|
358
|
+
writeReportWithFallback(effectiveOut, simple, 'Plain-English report');
|
|
314
359
|
}
|
|
315
360
|
else {
|
|
316
361
|
process.stdout.write(simple + '\n');
|
|
@@ -320,9 +365,9 @@ export async function executeReportCommand(target, options = {}) {
|
|
|
320
365
|
// Markdown output
|
|
321
366
|
const markdown = generateMarkdown(report, resolved, cwd);
|
|
322
367
|
if (effectiveOut) {
|
|
323
|
-
|
|
368
|
+
if (!writeReportWithFallback(effectiveOut, markdown, 'Report'))
|
|
369
|
+
return;
|
|
324
370
|
newline();
|
|
325
|
-
console.log(success(`Report written to ${effectiveOut}`));
|
|
326
371
|
console.log(colors.dim(` ${paths.length} files · score ${overallScore}/100 · grade ${grade}`));
|
|
327
372
|
if (criticalCount > 0) {
|
|
328
373
|
console.log(colors.red(` ${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} — check the report`));
|
|
@@ -148,9 +148,14 @@ function renderSessionDrillDown(sessionId, entries) {
|
|
|
148
148
|
console.log(` ${icon} ${time} ${tool} ${colors.dim(reason)}`);
|
|
149
149
|
}
|
|
150
150
|
newline();
|
|
151
|
-
// Verdict
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
// Verdict — rc.17: tighten "injection" claim to actual high block ratio
|
|
152
|
+
// (UX_OBSERVATIONS.md P1; long attended sessions shouldn't get accused).
|
|
153
|
+
if (blocked.length > 2 && sorted.length > 20 && blocked.length / sorted.length > 0.05) {
|
|
154
|
+
const pct = ((blocked.length / sorted.length) * 100).toFixed(0);
|
|
155
|
+
console.log(colors.red(` ⚠ ${pct}% block rate (${blocked.length}/${sorted.length}) — review for injection patterns.`));
|
|
156
|
+
}
|
|
157
|
+
else if (blocked.length > 0 && sorted.length > 20) {
|
|
158
|
+
console.log(colors.yellow(` ${blocked.length} blocked / ${sorted.length} total — long session, review if unattended.`));
|
|
154
159
|
}
|
|
155
160
|
else if (blocked.length > 0) {
|
|
156
161
|
console.log(colors.yellow(` ${blocked.length} blocked call${blocked.length !== 1 ? 's' : ''} — review the blocked entries above.`));
|
|
@@ -413,6 +418,13 @@ export async function executeStatusCommand(options = {}) {
|
|
|
413
418
|
const cwd = process.cwd();
|
|
414
419
|
const version = getVersion();
|
|
415
420
|
const entries = readAuditLog();
|
|
421
|
+
// Validate --since up front — applies to ALL modes, not just --export.
|
|
422
|
+
// (rc.15 fix: previously the validation ran only inside the --export branch,
|
|
423
|
+
// so `codehere status --since bogus` silently succeeded with exit 0.)
|
|
424
|
+
if (options.since && parseDurationToHours(options.since) == null) {
|
|
425
|
+
console.error(`Invalid --since value: "${options.since}". Use e.g. 24h, 7d, 4w.`);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
416
428
|
// Session drill-down mode
|
|
417
429
|
if (options.session) {
|
|
418
430
|
renderSessionDrillDown(options.session, entries);
|
|
@@ -421,10 +433,6 @@ export async function executeStatusCommand(options = {}) {
|
|
|
421
433
|
// Audit export mode — dump raw events for archival/compliance/sharing
|
|
422
434
|
if (options.export) {
|
|
423
435
|
const sinceHours = options.since ? parseDurationToHours(options.since) : null;
|
|
424
|
-
if (options.since && sinceHours == null) {
|
|
425
|
-
console.error(`Invalid --since value: "${options.since}". Use e.g. 24h, 7d, 4w.`);
|
|
426
|
-
process.exit(1);
|
|
427
|
-
}
|
|
428
436
|
try {
|
|
429
437
|
exportAudit(entries, options.export, sinceHours);
|
|
430
438
|
}
|
|
@@ -480,9 +488,17 @@ export async function executeStatusCommand(options = {}) {
|
|
|
480
488
|
}));
|
|
481
489
|
result.taskCount = tasks.length;
|
|
482
490
|
}
|
|
483
|
-
catch {
|
|
491
|
+
catch (e) {
|
|
492
|
+
// rc.25: JSON consumers reading only stdout were still being fed a
|
|
493
|
+
// lie ([] tasks, 0 count) on real failure (Codex final review:
|
|
494
|
+
// "degraded behavior being presented as success"). Now we surface a
|
|
495
|
+
// structured taskLoadError + status:'degraded' so automation can
|
|
496
|
+
// distinguish "no tasks" from "load failed."
|
|
497
|
+
console.error('[status --tasks --json] task repository failed to load:', e);
|
|
484
498
|
result.tasks = [];
|
|
485
499
|
result.taskCount = 0;
|
|
500
|
+
result.taskLoadError = e instanceof Error ? e.message : String(e);
|
|
501
|
+
result.status = 'degraded';
|
|
486
502
|
}
|
|
487
503
|
}
|
|
488
504
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -630,8 +646,14 @@ export async function executeStatusCommand(options = {}) {
|
|
|
630
646
|
}
|
|
631
647
|
}
|
|
632
648
|
}
|
|
633
|
-
catch {
|
|
634
|
-
|
|
649
|
+
catch (e) {
|
|
650
|
+
// rc.25: don't print "No tasks found" — that's a lie when load failed
|
|
651
|
+
// (Codex final review: "degraded behavior presented as success"). Show
|
|
652
|
+
// a clear error line; stderr already has the cause for the operator.
|
|
653
|
+
console.error('[status] task repository failed to load:', e);
|
|
654
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
655
|
+
console.log(colors.red(` ✗ Could not load task repository: ${msg}`));
|
|
656
|
+
console.log(colors.dim(' See logs above for details. Try `codehere start` to (re)init the task intake.'));
|
|
635
657
|
}
|
|
636
658
|
}
|
|
637
659
|
// Quick trust scan (opt-in)
|