scene-capability-engine 3.6.10 → 3.6.13

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.
@@ -12,6 +12,16 @@ const TaskClaimer = require('../task/task-claimer');
12
12
  const WorkspaceManager = require('../workspace/workspace-manager');
13
13
  const { TaskRefRegistry } = require('../task/task-ref-registry');
14
14
  const { ensureWriteAuthorization } = require('../security/write-authorization');
15
+ const {
16
+ buildDraft,
17
+ scoreDraft,
18
+ appendDraft,
19
+ updateDraft,
20
+ consolidateDrafts,
21
+ loadDraftStore,
22
+ promoteDraftToTasks
23
+ } = require('../task/task-quality');
24
+ const { loadTaskQualityPolicy } = require('../task/task-quality-policy');
15
25
 
16
26
  function normalizeString(value) {
17
27
  if (typeof value !== 'string') {
@@ -32,6 +42,195 @@ function resolveStudioStageFromTaskKey(taskKey) {
32
42
  return normalizeString(normalized.slice('studio:'.length));
33
43
  }
34
44
 
45
+ async function runTaskDraftCommand(options = {}) {
46
+ const projectPath = process.cwd();
47
+ const sceneId = normalizeString(options.scene);
48
+ const specId = normalizeString(options.spec);
49
+ const inputText = normalizeString(options.input);
50
+ const inputFile = normalizeString(options.inputFile);
51
+ const policyConfig = await loadTaskQualityPolicy(projectPath, options.policy, fs);
52
+ const policy = policyConfig.policy;
53
+
54
+ if (!sceneId) {
55
+ throw new Error('scene is required');
56
+ }
57
+
58
+ let rawRequest = inputText;
59
+ if (!rawRequest && inputFile) {
60
+ rawRequest = await fs.readFile(inputFile, 'utf8');
61
+ }
62
+ if (!rawRequest) {
63
+ throw new Error('input text is required (use --input or --input-file)');
64
+ }
65
+
66
+ const draft = buildDraft(rawRequest, {
67
+ scene_id: sceneId,
68
+ spec_id: specId,
69
+ acceptance_criteria: options.acceptance ? String(options.acceptance).split('|') : [],
70
+ confidence: options.confidence,
71
+ policy
72
+ });
73
+ const quality = scoreDraft(draft, policy);
74
+ draft.quality_score = quality.score;
75
+ draft.quality_breakdown = quality.breakdown;
76
+ draft.quality_issues = quality.issues;
77
+ draft.quality_passed = quality.passed;
78
+
79
+ const result = await appendDraft(projectPath, draft, fs);
80
+ const payload = {
81
+ mode: 'task-draft',
82
+ draft: draft,
83
+ store_path: result.store_path
84
+ };
85
+
86
+ if (options.json) {
87
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
88
+ return;
89
+ }
90
+
91
+ console.log(chalk.green('✅ Draft created'));
92
+ console.log(chalk.gray(` id: ${draft.draft_id}`));
93
+ console.log(chalk.gray(` score: ${draft.quality_score}`));
94
+ }
95
+
96
+ async function runTaskConsolidateCommand(options = {}) {
97
+ const projectPath = process.cwd();
98
+ const sceneId = normalizeString(options.scene);
99
+ const specId = normalizeString(options.spec);
100
+ if (!sceneId) {
101
+ throw new Error('scene is required');
102
+ }
103
+ const result = await consolidateDrafts(projectPath, {
104
+ scene_id: sceneId,
105
+ spec_id: specId
106
+ }, fs);
107
+
108
+ const payload = {
109
+ mode: 'task-consolidate',
110
+ scene_id: sceneId,
111
+ spec_id: specId || null,
112
+ merged: result.merged,
113
+ drafts: result.drafts,
114
+ store_path: result.store_path
115
+ };
116
+
117
+ if (options.json) {
118
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
119
+ return;
120
+ }
121
+
122
+ console.log(chalk.green('✅ Drafts consolidated'));
123
+ console.log(chalk.gray(` merged: ${result.merged.length}`));
124
+ }
125
+
126
+ async function runTaskScoreCommand(options = {}) {
127
+ const projectPath = process.cwd();
128
+ const draftId = normalizeString(options.draft);
129
+ const all = Boolean(options.all);
130
+ const policyConfig = await loadTaskQualityPolicy(projectPath, options.policy, fs);
131
+ const policy = policyConfig.policy;
132
+ const store = await loadDraftStore(projectPath, fs);
133
+ const drafts = Array.isArray(store.payload.drafts) ? store.payload.drafts : [];
134
+
135
+ const targets = all
136
+ ? drafts
137
+ : drafts.filter((draft) => draft.draft_id === draftId);
138
+
139
+ if (!all && targets.length === 0) {
140
+ throw new Error(`draft not found: ${draftId}`);
141
+ }
142
+
143
+ const scored = [];
144
+ for (const draft of targets) {
145
+ const quality = scoreDraft(draft, policy);
146
+ const updated = await updateDraft(projectPath, draft.draft_id, (current) => ({
147
+ ...current,
148
+ quality_score: quality.score,
149
+ quality_breakdown: quality.breakdown,
150
+ quality_issues: quality.issues,
151
+ quality_passed: quality.passed
152
+ }), fs);
153
+ if (updated) {
154
+ scored.push(updated);
155
+ }
156
+ }
157
+
158
+ const payload = {
159
+ mode: 'task-score',
160
+ draft_id: draftId || null,
161
+ total: scored.length,
162
+ drafts: scored
163
+ };
164
+
165
+ if (options.json) {
166
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
167
+ return;
168
+ }
169
+
170
+ console.log(chalk.green('✅ Draft scoring complete'));
171
+ console.log(chalk.gray(` scored: ${scored.length}`));
172
+ }
173
+
174
+ async function runTaskPromoteCommand(options = {}) {
175
+ const projectPath = process.cwd();
176
+ const draftId = normalizeString(options.draft);
177
+ const specId = normalizeString(options.spec);
178
+ const policyConfig = await loadTaskQualityPolicy(projectPath, options.policy, fs);
179
+ const policy = policyConfig.policy;
180
+ if (!draftId) {
181
+ throw new Error('draft id is required');
182
+ }
183
+ if (!specId) {
184
+ throw new Error('spec is required to promote draft');
185
+ }
186
+
187
+ const store = await loadDraftStore(projectPath, fs);
188
+ const drafts = Array.isArray(store.payload.drafts) ? store.payload.drafts : [];
189
+ const draft = drafts.find((item) => item.draft_id === draftId);
190
+ if (!draft) {
191
+ throw new Error(`draft not found: ${draftId}`);
192
+ }
193
+ const quality = scoreDraft(draft, policy);
194
+ if (!quality.passed && !options.force) {
195
+ throw new Error('draft quality gate failed; use --force to override');
196
+ }
197
+
198
+ const promoted = await promoteDraftToTasks(projectPath, {
199
+ ...draft,
200
+ spec_id: specId
201
+ }, fs);
202
+
203
+ const updated = await updateDraft(projectPath, draftId, (current) => ({
204
+ ...current,
205
+ spec_id: specId,
206
+ status: 'promoted',
207
+ quality_score: quality.score,
208
+ quality_breakdown: quality.breakdown,
209
+ quality_issues: quality.issues,
210
+ quality_passed: quality.passed,
211
+ promoted_at: new Date().toISOString(),
212
+ promoted_task_id: promoted.task_id,
213
+ promoted_tasks_path: promoted.tasks_path
214
+ }), fs);
215
+
216
+ const payload = {
217
+ mode: 'task-promote',
218
+ draft_id: draftId,
219
+ spec_id: specId,
220
+ task_id: promoted.task_id,
221
+ tasks_path: toRelativePosix(projectPath, promoted.tasks_path),
222
+ draft: updated
223
+ };
224
+
225
+ if (options.json) {
226
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
227
+ return;
228
+ }
229
+
230
+ console.log(chalk.green('✅ Draft promoted to tasks.md'));
231
+ console.log(chalk.gray(` task: ${promoted.task_id}`));
232
+ }
233
+
35
234
  function isStudioTaskRef(lookup = {}) {
36
235
  const source = normalizeString(lookup.source);
37
236
  if (source === 'studio-stage') {
@@ -771,6 +970,74 @@ function registerTaskCommands(program) {
771
970
  process.exitCode = 1;
772
971
  }
773
972
  });
973
+
974
+ task
975
+ .command('draft')
976
+ .description('Create a task draft from dialogue input')
977
+ .requiredOption('--scene <scene-id>', 'Scene identifier')
978
+ .option('--spec <spec-id>', 'Spec identifier')
979
+ .option('--input <text>', 'Raw task input text')
980
+ .option('--input-file <path>', 'File containing raw task input')
981
+ .option('--acceptance <items>', 'Pipe-delimited acceptance criteria')
982
+ .option('--confidence <score>', 'Manual confidence (0-1)')
983
+ .option('--policy <path>', 'Task quality policy path override')
984
+ .option('--json', 'Print machine-readable JSON output')
985
+ .action(async (options) => {
986
+ try {
987
+ await runTaskDraftCommand(options);
988
+ } catch (error) {
989
+ console.error(chalk.red(`Task draft failed: ${error.message}`));
990
+ process.exitCode = 1;
991
+ }
992
+ });
993
+
994
+ task
995
+ .command('consolidate')
996
+ .description('Consolidate task drafts by scene/spec')
997
+ .requiredOption('--scene <scene-id>', 'Scene identifier')
998
+ .option('--spec <spec-id>', 'Spec identifier')
999
+ .option('--json', 'Print machine-readable JSON output')
1000
+ .action(async (options) => {
1001
+ try {
1002
+ await runTaskConsolidateCommand(options);
1003
+ } catch (error) {
1004
+ console.error(chalk.red(`Task consolidate failed: ${error.message}`));
1005
+ process.exitCode = 1;
1006
+ }
1007
+ });
1008
+
1009
+ task
1010
+ .command('score')
1011
+ .description('Score task draft quality')
1012
+ .option('--draft <draft-id>', 'Draft identifier')
1013
+ .option('--all', 'Score all drafts')
1014
+ .option('--policy <path>', 'Task quality policy path override')
1015
+ .option('--json', 'Print machine-readable JSON output')
1016
+ .action(async (options) => {
1017
+ try {
1018
+ await runTaskScoreCommand(options);
1019
+ } catch (error) {
1020
+ console.error(chalk.red(`Task score failed: ${error.message}`));
1021
+ process.exitCode = 1;
1022
+ }
1023
+ });
1024
+
1025
+ task
1026
+ .command('promote')
1027
+ .description('Promote a task draft into spec tasks.md')
1028
+ .requiredOption('--draft <draft-id>', 'Draft identifier')
1029
+ .requiredOption('--spec <spec-id>', 'Spec identifier')
1030
+ .option('--force', 'Override quality gate')
1031
+ .option('--policy <path>', 'Task quality policy path override')
1032
+ .option('--json', 'Print machine-readable JSON output')
1033
+ .action(async (options) => {
1034
+ try {
1035
+ await runTaskPromoteCommand(options);
1036
+ } catch (error) {
1037
+ console.error(chalk.red(`Task promote failed: ${error.message}`));
1038
+ process.exitCode = 1;
1039
+ }
1040
+ });
774
1041
  }
775
1042
 
776
1043
  module.exports = {
@@ -780,5 +1047,9 @@ module.exports = {
780
1047
  runTaskRefCommand,
781
1048
  runTaskShowCommand,
782
1049
  runTaskRerunCommand,
1050
+ runTaskDraftCommand,
1051
+ runTaskConsolidateCommand,
1052
+ runTaskScoreCommand,
1053
+ runTaskPromoteCommand,
783
1054
  registerTaskCommands
784
1055
  };
@@ -0,0 +1,109 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ const DEFAULT_POLICY_PATH = path.join('.sce', 'config', 'task-quality-policy.json');
5
+
6
+ const DEFAULT_TASK_QUALITY_POLICY = Object.freeze({
7
+ schema_version: '1.0',
8
+ min_quality_score: 70,
9
+ require_acceptance_criteria: true,
10
+ allow_needs_split: false,
11
+ auto_suggest_acceptance: true,
12
+ max_sub_goals: 3
13
+ });
14
+
15
+ function normalizeText(value) {
16
+ if (typeof value !== 'string') {
17
+ return '';
18
+ }
19
+ return value.trim();
20
+ }
21
+
22
+ function normalizeBoolean(value, fallback) {
23
+ if (typeof value === 'boolean') {
24
+ return value;
25
+ }
26
+ const normalized = normalizeText(`${value || ''}`).toLowerCase();
27
+ if (!normalized) {
28
+ return fallback;
29
+ }
30
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
31
+ return true;
32
+ }
33
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
34
+ return false;
35
+ }
36
+ return fallback;
37
+ }
38
+
39
+ function toPositiveInteger(value, fallback) {
40
+ const parsed = Number.parseInt(`${value}`, 10);
41
+ if (!Number.isFinite(parsed) || parsed <= 0) {
42
+ return fallback;
43
+ }
44
+ return parsed;
45
+ }
46
+
47
+ function normalizePolicy(payload = {}) {
48
+ const policy = payload && typeof payload === 'object' ? payload : {};
49
+ return {
50
+ schema_version: normalizeText(policy.schema_version) || DEFAULT_TASK_QUALITY_POLICY.schema_version,
51
+ min_quality_score: toPositiveInteger(
52
+ policy.min_quality_score,
53
+ DEFAULT_TASK_QUALITY_POLICY.min_quality_score
54
+ ),
55
+ require_acceptance_criteria: normalizeBoolean(
56
+ policy.require_acceptance_criteria,
57
+ DEFAULT_TASK_QUALITY_POLICY.require_acceptance_criteria
58
+ ),
59
+ allow_needs_split: normalizeBoolean(
60
+ policy.allow_needs_split,
61
+ DEFAULT_TASK_QUALITY_POLICY.allow_needs_split
62
+ ),
63
+ auto_suggest_acceptance: normalizeBoolean(
64
+ policy.auto_suggest_acceptance,
65
+ DEFAULT_TASK_QUALITY_POLICY.auto_suggest_acceptance
66
+ ),
67
+ max_sub_goals: toPositiveInteger(
68
+ policy.max_sub_goals,
69
+ DEFAULT_TASK_QUALITY_POLICY.max_sub_goals
70
+ )
71
+ };
72
+ }
73
+
74
+ async function loadTaskQualityPolicy(projectPath, policyPath, fileSystem = fs) {
75
+ const resolvedPath = normalizeText(policyPath) || DEFAULT_POLICY_PATH;
76
+ const absolutePath = path.isAbsolute(resolvedPath)
77
+ ? resolvedPath
78
+ : path.join(projectPath, resolvedPath);
79
+
80
+ if (!await fileSystem.pathExists(absolutePath)) {
81
+ return {
82
+ policy: normalizePolicy(DEFAULT_TASK_QUALITY_POLICY),
83
+ path: resolvedPath,
84
+ loaded_from: 'default'
85
+ };
86
+ }
87
+
88
+ try {
89
+ const payload = await fileSystem.readJson(absolutePath);
90
+ return {
91
+ policy: normalizePolicy(payload),
92
+ path: resolvedPath,
93
+ loaded_from: 'file'
94
+ };
95
+ } catch (_error) {
96
+ return {
97
+ policy: normalizePolicy(DEFAULT_TASK_QUALITY_POLICY),
98
+ path: resolvedPath,
99
+ loaded_from: 'default'
100
+ };
101
+ }
102
+ }
103
+
104
+ module.exports = {
105
+ DEFAULT_POLICY_PATH,
106
+ DEFAULT_TASK_QUALITY_POLICY,
107
+ normalizePolicy,
108
+ loadTaskQualityPolicy
109
+ };