scene-capability-engine 3.6.38 → 3.6.39

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
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.6.39] - 2026-03-13
11
+
12
+ ### Added
13
+ - Added state storage tiering policy artifacts and guidance in `docs/state-storage-tiering.md`, `docs/state-migration-reconciliation-runbook.md`, `.sce/config/state-storage-policy.json`, and `template/.sce/config/state-storage-policy.json`.
14
+ - Added append-only evidence projection and audit tooling via `scripts/interactive-approval-event-projection.js`, `scripts/state-storage-tiering-audit.js`, and the `audit:state-storage` / `report:state-storage` package scripts.
15
+ - Added scene command coverage and helper fixtures for the expanded `scene` subcommand surface, including publish/install/info/diff/validate/owner/tag/stats/lock/contribute/version and related property tests.
16
+ - Added release-facing closeout evidence for the scene command specs and the quality hardening master spec under `.sce/specs/*/custom/`.
17
+
18
+ ### Changed
19
+ - Expanded `lib/commands/scene.js` to cover the newer scene command flows and stabilized JSON summary output so CLI JSON mode preserves edge-case numeric payloads such as `-0`.
20
+ - Hardened state migration and storage behavior in `lib/state/sce-state-store.js`, `lib/state/state-migration-manager.js`, `lib/runtime/session-store.js`, and related governance/config plumbing while keeping file sources canonical.
21
+ - Completed the `115-00-sce-quality-hardening-program` integration closeout, including `watch logs --follow` completion, open-handle governance follow-through, and master/sub-spec status convergence.
22
+ - Updated `docs/command-reference.md`, `docs/document-governance.md`, and multiple Spec task ledgers to reflect the shipped command surface and governance posture.
23
+
24
+ ### Fixed
25
+ - Removed the remaining `forceExit` dependency from the validated Jest path and confirmed `npm run test:handles` passes without open-handle leakage.
26
+ - Stabilized watch/integration helper timing and watcher cleanup paths so `test:smoke`, `test:full`, and `test:handles` can run cleanly in sequence.
27
+
10
28
  ## [3.6.38] - 2026-03-12
11
29
 
12
30
  ### Added
@@ -876,6 +876,29 @@ Runtime read preference:
876
876
  - `sce state doctor --json` now includes:
877
877
  - `summary` aggregate (`pending_components`, `total_record_drift`, `blocking_count`, `alert_count`)
878
878
  - runtime read diagnostics (`runtime.timeline`, `runtime.scene_session`) with read-source/read-preference and consistency status
879
+ - hardened severity model:
880
+ - `blocking`: `sqlite-unavailable`, `source-parse-error`, `sqlite-ahead`, `sqlite-only`
881
+ - `alert`: `pending-migration`, `missing-source`, runtime `pending-sync`
882
+ - current release guidance:
883
+ - treat `sqlite-ahead` and `sqlite-only` as anomalies requiring repair before release
884
+ - treat `pending-migration` as repairable drift, not a steady-state
885
+ - only allow `missing-source` to persist when the component is intentionally out of scope for the project
886
+
887
+ State storage tiering policy:
888
+ - Policy file: `.sce/config/state-storage-policy.json`
889
+ - Audit command: `npm run audit:state-storage`
890
+ - Machine-readable report: `npm run report:state-storage`
891
+ - Tier meanings:
892
+ - `file-source`: canonical evidence, audit, recovery payloads, and low-cardinality personal state
893
+ - `sqlite-index`: query-oriented registry/index layer with files still canonical
894
+ - `derived-sqlite-projection`: rebuildable SQLite projection for append-only stream queries
895
+ - Explicit non-candidates for SQLite source replacement:
896
+ - `~/.sce/workspace-state.json`
897
+ - `.sce/reports/**/*.jsonl`
898
+ - `.sce/audit/**/*.jsonl`
899
+ - Supporting docs:
900
+ - `docs/state-storage-tiering.md`
901
+ - `docs/state-migration-reconciliation-runbook.md`
879
902
 
880
903
  Write lease model (optional, policy-driven, SQLite-backed):
881
904
  - Policy file: `.sce/config/authorization-policy.json`
@@ -1836,6 +1859,10 @@ Interactive approval workflow helper (script-level stage-B approval state machin
1836
1859
  - `node scripts/interactive-approval-workflow.js --action <init|submit|approve|reject|execute|verify|archive|status> [--plan <path>] [--state-file <path>] [--audit-file <path>] [--actor <id>] [--actor-role <name>] [--role-policy <path>] [--comment <text>] [--password <text>] [--password-hash <sha256>] [--password-hash-env <name>] [--password-required] [--password-scope <csv>] [--json]`: maintain approval lifecycle state for interactive change plans and append approval events to JSONL audit logs.
1837
1860
  - Default state file: `.sce/reports/interactive-approval-state.json`
1838
1861
  - Default audit file: `.sce/reports/interactive-approval-events.jsonl`
1862
+ - `node scripts/interactive-approval-event-projection.js --action <rebuild|doctor|query> [--input <path>] [--read-source <auto|file|projection>] [--workflow-id <id>] [--actor <id>] [--approval-action <name>] [--event-type <type>] [--blocked|--not-blocked] [--limit <n>] [--fail-on-drift] [--fail-on-parse-error] [--json]`: build and inspect a rebuildable SQLite projection for `interactive-approval-events.jsonl` while keeping raw JSONL as canonical evidence.
1863
+ - `rebuild` clears and rebuilds the projection for the target audit file
1864
+ - `doctor` compares raw file count vs projection count and flags `projection-missing`, `pending-projection`, `projection-ahead`, or parse errors
1865
+ - `query` discloses `read_source=file|projection`
1839
1866
  - `init` requires `--plan`; high-risk plans are marked as `approval_required=true`.
1840
1867
  - Password authorization can be required per plan (`plan.authorization.password_required=true`) or overridden in `init`.
1841
1868
  - `execute` is blocked (exit code `2`) when approval is required but current status is not `approved`.
@@ -21,7 +21,7 @@ Document governance ensures your project follows these rules:
21
21
 
22
22
  1. **Root Directory** - Only 4 markdown files allowed: `README.md`, `README.zh.md`, `CHANGELOG.md`, `CONTRIBUTING.md`
23
23
  2. **Spec Structure** - Each Spec must have `requirements.md`, `design.md`, `tasks.md`
24
- 3. **Artifact Organization** - Spec artifacts must be in subdirectories: `reports/`, `scripts/`, `tests/`, `results/`, `docs/`
24
+ 3. **Artifact Organization** - Spec artifacts must be in subdirectories, except approved Spec-root metadata such as `requirements.md`, `design.md`, `tasks.md`, and `collaboration.json`
25
25
  4. **No Temporary Files** - Temporary documents (like `*-SUMMARY.md`, `SESSION-*.md`) must be deleted after use
26
26
 
27
27
  ### Why Use Document Governance?
@@ -128,7 +128,7 @@ sce docs diagnose
128
128
  - Root directory for non-allowed markdown files
129
129
  - Root directory for temporary documents
130
130
  - Spec directories for missing required files
131
- - Spec directories for misplaced artifacts
131
+ - Spec directories for misplaced artifacts outside approved Spec-root metadata
132
132
  - Spec directories for temporary documents
133
133
 
134
134
  **Output:**
@@ -240,6 +240,7 @@ sce docs validate [options]
240
240
  **What it validates:**
241
241
  - Root directory has only allowed markdown files
242
242
  - Spec directories have required files (requirements.md, design.md, tasks.md)
243
+ - Spec root contains only approved metadata files plus governed subdirectories
243
244
  - Spec subdirectories follow naming conventions
244
245
  - No misplaced artifacts
245
246
 
@@ -443,6 +444,7 @@ sce docs config [options]
443
444
 
444
445
  **Configuration keys:**
445
446
  - `root-allowed-files` - Allowed markdown files in root
447
+ - `spec-allowed-root-files` - Allowed files at Spec root before artifact warnings apply
446
448
  - `spec-subdirs` - Recognized Spec subdirectories
447
449
  - `temporary-patterns` - Patterns for temporary files
448
450
 
@@ -467,6 +469,12 @@ Spec Subdirectories:
467
469
  • results
468
470
  • docs
469
471
 
472
+ Spec Allowed Root Files:
473
+ • requirements.md
474
+ • design.md
475
+ • tasks.md
476
+ • collaboration.json
477
+
470
478
  Temporary Patterns:
471
479
  • *-SUMMARY.md
472
480
  • SESSION-*.md
@@ -724,6 +732,12 @@ The default configuration is:
724
732
  "CHANGELOG.md",
725
733
  "CONTRIBUTING.md"
726
734
  ],
735
+ "specAllowedRootFiles": [
736
+ "requirements.md",
737
+ "design.md",
738
+ "tasks.md",
739
+ "collaboration.json"
740
+ ],
727
741
  "specSubdirs": [
728
742
  "reports",
729
743
  "scripts",
@@ -749,6 +763,11 @@ The default configuration is:
749
763
  sce docs config --set root-allowed-files "README.md,README.zh.md,CHANGELOG.md,CONTRIBUTING.md,LICENSE.md,SECURITY.md"
750
764
  ```
751
765
 
766
+ **Allow additional Spec-root metadata:**
767
+ ```bash
768
+ sce docs config --set spec-allowed-root-files "requirements.md,design.md,tasks.md,collaboration.json"
769
+ ```
770
+
752
771
  **Add custom subdirectories:**
753
772
  ```bash
754
773
  sce docs config --set spec-subdirs "reports,scripts,tests,results,docs,diagrams,examples"
@@ -768,6 +787,7 @@ You can also edit this file directly:
768
787
  ```json
769
788
  {
770
789
  "rootAllowedFiles": ["README.md", "CUSTOM.md"],
790
+ "specAllowedRootFiles": ["requirements.md", "design.md", "tasks.md", "collaboration.json"],
771
791
  "specSubdirs": ["reports", "scripts", "custom"],
772
792
  "temporaryPatterns": ["*-TEMP.md"]
773
793
  }
@@ -9,6 +9,7 @@ This directory stores release-facing documents:
9
9
  ## Archived Versions
10
10
 
11
11
  - [Release checklist](../release-checklist.md)
12
+ - [v3.6.39 release notes](./v3.6.39.md)
12
13
  - [v3.6.38 release notes](./v3.6.38.md)
13
14
  - [v3.6.37 release notes](./v3.6.37.md)
14
15
  - [v1.46.2 release notes](./v1.46.2.md) (historical)
@@ -0,0 +1,24 @@
1
+ # v3.6.39 Release Notes
2
+
3
+ Release date: 2026-03-13
4
+
5
+ ## Highlights
6
+
7
+ - Added a canonical state-storage tiering policy, reconciliation runbook, and audit/report tooling so SQLite remains a scoped index layer rather than a silent source-of-truth replacement.
8
+ - Expanded and hardened the `sce scene` command surface with broader command coverage, stronger property tests, and JSON summary output that preserves edge-case numeric payloads.
9
+ - Closed the `115-00-sce-quality-hardening-program` integration loop, including `watch logs --follow` completion and Jest open-handle governance without relying on `forceExit`.
10
+
11
+ ## Verification
12
+
13
+ - `node bin/sce.js collab status 115-00-sce-quality-hardening-program --graph`
14
+ - `npm run test:smoke`
15
+ - `npm run test:skip-audit`
16
+ - `npm run test:brand-consistency`
17
+ - `npm run test:full`
18
+ - `npm run test:handles`
19
+
20
+ ## Release Notes
21
+
22
+ - State persistence is now explicitly tiered: evidence and operator-facing files remain canonical, while SQLite stays limited to rebuildable index/projection use cases.
23
+ - The scene command family is substantially more release-ready, with dedicated tests and closeout artifacts across publish/install/info/diff/validate/owner/tag/stats/lock/contribute/version flows.
24
+ - Quality hardening gates should be executed in sequence for release validation; `test:full` and `test:handles` are both green when run serially.
@@ -0,0 +1,76 @@
1
+ # State Migration Reconciliation Runbook
2
+
3
+ Use this runbook when working with the current file-to-SQLite migration surface.
4
+
5
+ ## Normal Flow
6
+
7
+ 1. Inspect scope
8
+ - `sce state plan --json`
9
+ 2. Diagnose drift
10
+ - `sce state doctor --json`
11
+ 3. Apply repair
12
+ - `sce state reconcile --all --apply --json`
13
+ 4. Re-check gate posture
14
+ - `node scripts/state-migration-reconciliation-gate.js --fail-on-blocking --fail-on-pending --json`
15
+
16
+ ## Severity Model
17
+
18
+ ### Blocking
19
+
20
+ Treat these as release-blocking anomalies:
21
+
22
+ - `sqlite-unavailable`
23
+ - `source-parse-error`
24
+ - `sqlite-ahead`
25
+ - `sqlite-only`
26
+ - runtime `sqlite-ahead`
27
+ - runtime `sqlite-only`
28
+
29
+ Typical meaning:
30
+
31
+ - `sqlite-ahead`: SQLite contains index rows that are no longer represented by canonical files
32
+ - `sqlite-only`: canonical file source disappeared but SQLite rows still exist
33
+
34
+ ### Alert
35
+
36
+ Treat these as repairable but non-steady-state conditions:
37
+
38
+ - `pending-migration`
39
+ - `missing-source`
40
+ - runtime `pending-sync`
41
+
42
+ Typical meaning:
43
+
44
+ - `pending-migration`: file source has more records than SQLite index
45
+ - `missing-source`: the project does not currently have that source artifact
46
+
47
+ ## Operator Decisions
48
+
49
+ ### When `pending-migration`
50
+
51
+ - Run `sce state reconcile --all --apply --json`
52
+ - Re-run `sce state doctor --json`
53
+ - If it persists, inspect source parser assumptions and source file health
54
+
55
+ ### When `sqlite-ahead`
56
+
57
+ - Treat as anomaly first, not optimization
58
+ - Inspect whether source artifacts were deleted, rotated, or never written
59
+ - Rebuild or re-sync from canonical file source before release
60
+
61
+ ### When `sqlite-only`
62
+
63
+ - Confirm whether the file source was removed accidentally
64
+ - Restore canonical files when possible
65
+ - Do not normalize this into a permanent state for current migration-scope components
66
+
67
+ ### When `missing-source`
68
+
69
+ - If the component is intentionally unused in the current project, keep it advisory
70
+ - If the component should exist for this workflow, generate or restore the source artifact and reconcile
71
+
72
+ ## Related Commands
73
+
74
+ - `npm run gate:state-migration-reconciliation`
75
+ - `npm run audit:state-storage`
76
+ - `npm run report:interactive-approval-projection`
@@ -0,0 +1,104 @@
1
+ # State Storage Tiering
2
+
3
+ SCE uses a selective SQLite strategy.
4
+
5
+ The operating rule is:
6
+
7
+ - keep canonical evidence, audit, recovery payloads, and low-cardinality personal state in files
8
+ - use SQLite for registry/index workloads with real query pressure
9
+ - allow SQLite projections for append-only streams only when they are rebuildable from files
10
+
11
+ ## Tiers
12
+
13
+ ### `file-source`
14
+
15
+ Use this tier when:
16
+
17
+ - the file is the canonical evidence or audit artifact
18
+ - the resource is a small personal or local configuration object
19
+ - manual inspection, Git diff, and recovery are more important than indexed querying
20
+
21
+ Examples:
22
+
23
+ - `~/.sce/workspace-state.json`
24
+ - `.sce/reports/**/*.jsonl`
25
+ - `.sce/audit/**/*.jsonl`
26
+ - `.sce/timeline/snapshots/**`
27
+ - `.sce/session-governance/sessions/**`
28
+
29
+ ### `sqlite-index`
30
+
31
+ Use this tier when:
32
+
33
+ - the canonical content still lives in files
34
+ - the resource behaves like a registry or index
35
+ - repeated filtering, sorting, and cross-run queries justify an index
36
+
37
+ Current active scope:
38
+
39
+ - `collab.agent-registry` -> `.sce/config/agent-registry.json`
40
+ - `runtime.timeline-index` -> `.sce/timeline/index.json`
41
+ - `runtime.scene-session-index` -> `.sce/session-governance/scene-index.json`
42
+ - `errorbook.entry-index` -> `.sce/errorbook/index.json`
43
+ - `errorbook.incident-index` -> `.sce/errorbook/staging/index.json`
44
+ - `governance.spec-scene-overrides` -> `.sce/spec-governance/spec-scene-overrides.json`
45
+ - `governance.scene-index` -> `.sce/spec-governance/scene-index.json`
46
+ - `release.evidence-runs-index` -> `.sce/reports/release-evidence/handoff-runs.json`
47
+ - `release.gate-history-index` -> `.sce/reports/release-evidence/release-gate-history.json`
48
+
49
+ ### `derived-sqlite-projection`
50
+
51
+ Use this tier only when:
52
+
53
+ - the source remains append-only files
54
+ - the projection can be deleted and rebuilt
55
+ - reads clearly disclose when SQLite projection is used
56
+
57
+ This tier is for query acceleration, not source-of-truth replacement.
58
+
59
+ ## Admission Rubric
60
+
61
+ A resource should only enter SQLite scope when all of the following are true:
62
+
63
+ 1. Cross-run or cross-session query pressure is real.
64
+ 2. File scans are materially weaker than indexed filtering/sorting.
65
+ 3. SQLite rows are rebuildable from a canonical file or stream.
66
+ 4. Diagnostics, reconcile behavior, and operator guidance are defined up front.
67
+
68
+ Reject SQLite source migration when any of the following are true:
69
+
70
+ 1. The resource is raw audit or evidence.
71
+ 2. The resource is low-cardinality workspace or preference state.
72
+ 3. Human-readable recovery and Git diff matter more than query speed.
73
+ 4. The change would silently cut over the source of truth.
74
+
75
+ ## Current Classification
76
+
77
+ | Resource | Tier | Why |
78
+ | --- | --- | --- |
79
+ | `~/.sce/workspace-state.json` | `file-source` | Atomic personal state; no meaningful query pressure |
80
+ | `.sce/reports/**/*.jsonl` | `file-source` | Canonical append-only governance/evidence streams |
81
+ | `.sce/audit/**/*.jsonl` | `file-source` | Canonical audit evidence |
82
+ | `.sce/timeline/index.json` | `sqlite-index` | Index-like query workload |
83
+ | `.sce/session-governance/scene-index.json` | `sqlite-index` | Registry-like query workload |
84
+ | `.sce/errorbook/index.json` | `sqlite-index` | Filtered status/quality lookup |
85
+ | `.sce/errorbook/staging/index.json` | `sqlite-index` | Incident triage lookup |
86
+ | `.sce/spec-governance/*.json` indexes | `sqlite-index` | Governance registry lookup |
87
+ | `.sce/reports/release-evidence/handoff-runs.json` | `sqlite-index` | Historical release summary lookup |
88
+ | `.sce/reports/release-evidence/release-gate-history.json` | `sqlite-index` | Gate history lookup |
89
+ | `.sce/timeline/snapshots/**` | `file-source` | Recovery payloads |
90
+ | `.sce/session-governance/sessions/**` | `file-source` | Session payload archives |
91
+
92
+ ## Operator Rules
93
+
94
+ - Do not propose blanket sqlite-ization.
95
+ - Before adding a new SQLite candidate, update `.sce/config/state-storage-policy.json`.
96
+ - Run `npm run audit:state-storage` after changing state storage behavior.
97
+ - If the resource is append-only evidence, prefer a projection pilot over source migration.
98
+
99
+ ## Related Assets
100
+
101
+ - Policy file: `.sce/config/state-storage-policy.json`
102
+ - Audit command: `npm run audit:state-storage`
103
+ - Machine-readable report: `npm run report:state-storage`
104
+ - State migration commands: `sce state plan|doctor|migrate|reconcile`
@@ -9,6 +9,7 @@
9
9
  ## 历史版本归档
10
10
 
11
11
  - [发布检查清单](../release-checklist.md)
12
+ - [v3.6.39 发布说明](./v3.6.39.md)
12
13
  - [v3.6.38 发布说明](./v3.6.38.md)
13
14
  - [v3.6.37 发布说明](./v3.6.37.md)
14
15
  - [v1.46.2 发布说明](./v1.46.2.md)(历史归档)
@@ -0,0 +1,24 @@
1
+ # v3.6.39 发布说明
2
+
3
+ 发布日期:2026-03-13
4
+
5
+ ## 重点变化
6
+
7
+ - 新增规范化的状态存储分层策略、迁移对账 runbook,以及配套 audit/report 工具,明确 SQLite 只承担受控索引层,不替代文件真源。
8
+ - 扩展并加固了 `sce scene` 命令族,补齐多类命令覆盖、性质测试和 JSON summary 输出边界处理。
9
+ - 完成 `115-00-sce-quality-hardening-program` 主 Spec 的集成收口,包含 `watch logs --follow` 补完,以及不依赖 `forceExit` 的 Jest open-handle 治理。
10
+
11
+ ## 验证
12
+
13
+ - `node bin/sce.js collab status 115-00-sce-quality-hardening-program --graph`
14
+ - `npm run test:smoke`
15
+ - `npm run test:skip-audit`
16
+ - `npm run test:brand-consistency`
17
+ - `npm run test:full`
18
+ - `npm run test:handles`
19
+
20
+ ## 发布说明
21
+
22
+ - 状态持久化现在有了明确分层:证据、审计和操作员可读文件继续保持 canonical,SQLite 仅用于可重建索引或投影。
23
+ - `scene` 命令面已经更接近稳定发布状态,publish/install/info/diff/validate/owner/tag/stats/lock/contribute/version 等流都有专门测试和 closeout 证据。
24
+ - 质量门禁建议串行执行;`test:full` 与 `test:handles` 在串行场景下均已验证通过。
@@ -301,6 +301,12 @@ async function handleConfigDisplay(configManager, reporter) {
301
301
  console.log(` • ${dir}`);
302
302
  });
303
303
  console.log();
304
+
305
+ console.log(chalk.bold('Spec Allowed Root Files:'));
306
+ (config.specAllowedRootFiles || []).forEach(file => {
307
+ console.log(` • ${file}`);
308
+ });
309
+ console.log();
304
310
 
305
311
  console.log(chalk.bold('Temporary Patterns:'));
306
312
  config.temporaryPatterns.forEach(pattern => {
@@ -350,10 +356,10 @@ async function handleConfigSet(configManager, reporter, options) {
350
356
  const camelKey = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
351
357
 
352
358
  // Validate key
353
- const validKeys = ['rootAllowedFiles', 'specSubdirs', 'temporaryPatterns'];
359
+ const validKeys = ['rootAllowedFiles', 'specAllowedRootFiles', 'specSubdirs', 'temporaryPatterns'];
354
360
  if (!validKeys.includes(camelKey)) {
355
361
  reporter.displayError(`Invalid configuration key: ${key}`);
356
- console.log(chalk.gray('Valid keys: root-allowed-files, spec-subdirs, temporary-patterns\n'));
362
+ console.log(chalk.gray('Valid keys: root-allowed-files, spec-allowed-root-files, spec-subdirs, temporary-patterns\n'));
357
363
  return 2;
358
364
  }
359
365
 
@@ -158,6 +158,45 @@ function createDoctorTraceId() {
158
158
  return `doctor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
159
159
  }
160
160
 
161
+ function stringifyJsonForCli(value, indentSize = 2, currentIndent = 0) {
162
+ if (value === null) {
163
+ return 'null';
164
+ }
165
+
166
+ if (typeof value === 'number') {
167
+ return Object.is(value, -0) ? '-0' : JSON.stringify(value);
168
+ }
169
+
170
+ if (typeof value === 'string' || typeof value === 'boolean') {
171
+ return JSON.stringify(value);
172
+ }
173
+
174
+ if (Array.isArray(value)) {
175
+ if (value.length === 0) {
176
+ return '[]';
177
+ }
178
+
179
+ const nextIndent = currentIndent + indentSize;
180
+ const indent = ' '.repeat(currentIndent);
181
+ const childIndent = ' '.repeat(nextIndent);
182
+ const items = value.map((entry) => `${childIndent}${stringifyJsonForCli(entry, indentSize, nextIndent)}`);
183
+ return `[\n${items.join(',\n')}\n${indent}]`;
184
+ }
185
+
186
+ const keys = Object.keys(value);
187
+ if (keys.length === 0) {
188
+ return '{}';
189
+ }
190
+
191
+ const nextIndent = currentIndent + indentSize;
192
+ const indent = ' '.repeat(currentIndent);
193
+ const childIndent = ' '.repeat(nextIndent);
194
+ const entries = keys.map((key) => (
195
+ `${childIndent}${JSON.stringify(key)}: ${stringifyJsonForCli(value[key], indentSize, nextIndent)}`
196
+ ));
197
+ return `{\n${entries.join(',\n')}\n${indent}}`;
198
+ }
199
+
161
200
  function registerSceneCommands(program) {
162
201
  const sceneCmd = program
163
202
  .command('scene')
@@ -4795,7 +4834,7 @@ function createSceneEvalConfigTemplateByProfile(profile = 'default') {
4795
4834
  }
4796
4835
 
4797
4836
  function normalizeRelativePath(targetPath = '') {
4798
- return String(targetPath || '').replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\.\//, '');
4837
+ return String(targetPath || '').trim().replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\.\//, '');
4799
4838
  }
4800
4839
 
4801
4840
  function collectManifestDiscoveryCandidates(preferredPath = 'custom/scene.yaml') {
@@ -13639,6 +13678,35 @@ async function runSceneVersionCommand(rawOptions = {}, dependencies = {}) {
13639
13678
  }
13640
13679
  }
13641
13680
 
13681
+ function isBinaryContent(buffer) {
13682
+ if (!Buffer.isBuffer(buffer)) {
13683
+ return false;
13684
+ }
13685
+
13686
+ for (let index = 0; index < buffer.length; index++) {
13687
+ if (buffer[index] === 0) {
13688
+ return true;
13689
+ }
13690
+ }
13691
+
13692
+ return false;
13693
+ }
13694
+
13695
+ function countChangedLines(fromContent, toContent) {
13696
+ const oldLines = fromContent.toString('utf8').split('\n');
13697
+ const newLines = toContent.toString('utf8').split('\n');
13698
+ const maxLen = Math.max(oldLines.length, newLines.length);
13699
+ let changedLines = 0;
13700
+
13701
+ for (let index = 0; index < maxLen; index++) {
13702
+ if ((oldLines[index] || '') !== (newLines[index] || '')) {
13703
+ changedLines++;
13704
+ }
13705
+ }
13706
+
13707
+ return changedLines;
13708
+ }
13709
+
13642
13710
  function buildPackageDiff(fromFiles, toFiles) {
13643
13711
  const fromMap = new Map();
13644
13712
  for (const f of (fromFiles || [])) {
@@ -13662,19 +13730,9 @@ function buildPackageDiff(fromFiles, toFiles) {
13662
13730
  if (Buffer.compare(content, toContent) === 0) {
13663
13731
  unchanged.push(filePath);
13664
13732
  } else {
13665
- let changedLines = 0;
13666
- try {
13667
- const oldLines = content.toString('utf8').split('\n');
13668
- const newLines = toContent.toString('utf8').split('\n');
13669
- const maxLen = Math.max(oldLines.length, newLines.length);
13670
- for (let i = 0; i < maxLen; i++) {
13671
- if ((oldLines[i] || '') !== (newLines[i] || '')) {
13672
- changedLines++;
13673
- }
13674
- }
13675
- } catch (_e) {
13676
- changedLines = -1;
13677
- }
13733
+ const changedLines = isBinaryContent(content) || isBinaryContent(toContent)
13734
+ ? -1
13735
+ : countChangedLines(content, toContent);
13678
13736
  modified.push({ path: filePath, changedLines });
13679
13737
  }
13680
13738
  }
@@ -13734,7 +13792,9 @@ function printSceneDiffSummary(options, payload, projectRoot = process.cwd()) {
13734
13792
  console.log(chalk.red(` - ${f}`));
13735
13793
  }
13736
13794
  for (const f of payload.files.modified) {
13737
- const detail = f.changedLines >= 0 ? ` (${f.changedLines} lines changed)` : ' (binary content differs)';
13795
+ const detail = options.stat
13796
+ ? ''
13797
+ : (f.changedLines >= 0 ? ` (${f.changedLines} lines changed)` : ' (binary content differs)');
13738
13798
  console.log(chalk.yellow(` ~ ${f.path}${detail}`));
13739
13799
  }
13740
13800
  }
@@ -15248,7 +15308,7 @@ async function runSceneLintCommand(rawOptions = {}, dependencies = {}) {
15248
15308
 
15249
15309
  function printSceneLintSummary(options, payload, projectRoot = process.cwd()) {
15250
15310
  if (options.json) {
15251
- console.log(JSON.stringify(payload, null, 2));
15311
+ console.log(stringifyJsonForCli(payload));
15252
15312
  return;
15253
15313
  }
15254
15314
 
@@ -15344,7 +15404,7 @@ async function runSceneScoreCommand(rawOptions = {}, dependencies = {}) {
15344
15404
 
15345
15405
  function printSceneScoreSummary(options, payload, projectRoot = process.cwd()) {
15346
15406
  if (options.json) {
15347
- console.log(JSON.stringify(payload, null, 2));
15407
+ console.log(stringifyJsonForCli(payload));
15348
15408
  return;
15349
15409
  }
15350
15410
 
@@ -15524,7 +15584,7 @@ async function runSceneContributeCommand(rawOptions = {}, dependencies = {}) {
15524
15584
 
15525
15585
  function printSceneContributeSummary(options, payload, projectRoot = process.cwd()) {
15526
15586
  if (options.json) {
15527
- console.log(JSON.stringify(payload, null, 2));
15587
+ console.log(stringifyJsonForCli(payload));
15528
15588
  return;
15529
15589
  }
15530
15590
 
@@ -11,6 +11,15 @@ const inquirer = require('inquirer');
11
11
  const WatchManager = require('../watch/watch-manager');
12
12
  const { listPresets, getPreset, mergePreset, validatePreset } = require('../watch/presets');
13
13
 
14
+ function sleep(ms) {
15
+ return new Promise(resolve => {
16
+ const timer = setTimeout(resolve, ms);
17
+ if (typeof timer.unref === 'function') {
18
+ timer.unref();
19
+ }
20
+ });
21
+ }
22
+
14
23
  /**
15
24
  * Start watch mode
16
25
  *
@@ -329,7 +338,7 @@ async function followLogStream(logPath, options = {}) {
329
338
  break;
330
339
  }
331
340
 
332
- await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
341
+ await sleep(pollIntervalMs);
333
342
  }
334
343
  } finally {
335
344
  process.removeListener('SIGINT', onSigInt);
@@ -52,6 +52,12 @@ class ConfigManager {
52
52
  'CHANGELOG.md',
53
53
  'CONTRIBUTING.md'
54
54
  ],
55
+ specAllowedRootFiles: [
56
+ 'requirements.md',
57
+ 'design.md',
58
+ 'tasks.md',
59
+ 'collaboration.json'
60
+ ],
55
61
  specSubdirs: [
56
62
  'reports',
57
63
  'scripts',
@@ -152,6 +158,10 @@ class ConfigManager {
152
158
  if (!config.specSubdirs || !Array.isArray(config.specSubdirs)) {
153
159
  errors.push('specSubdirs must be an array');
154
160
  }
161
+
162
+ if (!config.specAllowedRootFiles || !Array.isArray(config.specAllowedRootFiles)) {
163
+ errors.push('specAllowedRootFiles must be an array');
164
+ }
155
165
 
156
166
  if (!config.temporaryPatterns || !Array.isArray(config.temporaryPatterns)) {
157
167
  errors.push('temporaryPatterns must be an array');
@@ -169,6 +179,12 @@ class ConfigManager {
169
179
  errors.push('specSubdirs must contain only strings');
170
180
  }
171
181
  }
182
+
183
+ if (config.specAllowedRootFiles && Array.isArray(config.specAllowedRootFiles)) {
184
+ if (config.specAllowedRootFiles.some(f => typeof f !== 'string')) {
185
+ errors.push('specAllowedRootFiles must contain only strings');
186
+ }
187
+ }
172
188
 
173
189
  if (config.temporaryPatterns && Array.isArray(config.temporaryPatterns)) {
174
190
  if (config.temporaryPatterns.some(p => typeof p !== 'string')) {
@@ -81,6 +81,7 @@ class DiagnosticEngine {
81
81
  async scanSpecDirectory(specPath) {
82
82
  const specName = path.basename(specPath);
83
83
  const requiredFiles = ['requirements.md', 'design.md', 'tasks.md'];
84
+ const allowedRootFiles = this.config.specAllowedRootFiles || requiredFiles;
84
85
 
85
86
  // Check for missing required files
86
87
  for (const requiredFile of requiredFiles) {
@@ -124,7 +125,7 @@ class DiagnosticEngine {
124
125
  const basename = path.basename(filePath);
125
126
 
126
127
  // Skip required files
127
- if (requiredFiles.includes(basename)) {
128
+ if (allowedRootFiles.includes(basename)) {
128
129
  continue;
129
130
  }
130
131