@cyning/harness 2.8.0 → 2.10.0
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 +35 -0
- package/docs/ONBOARDING.md +16 -5
- package/docs/USER_GUIDE_v1.0_zh.md +16 -5
- package/docs/rethink/2026-07-mechanization-rate/03_coverage_matrix.md +4 -2
- package/docs/rethink/2026-07-mechanization-rate/04_next_steps.md +4 -3
- package/docs/spec/SPEC-discipline-coverage-yaml_v1.md +204 -0
- package/docs/spec/SPEC-lifecycle-engine-min_v1.md +246 -0
- package/docs/spec/SPEC-post-g4-debt-retro_v1.md +237 -0
- package/docs/spec/SPEC-verify-full-reviews-gate_v1.md +183 -0
- package/harness/discipline-coverage.yaml +289 -0
- package/harness/lifecycle.yaml +7 -5
- package/lib/cli.js +86 -21
- package/lib/discipline-coverage.js +96 -0
- package/lib/lifecycle.js +222 -0
- package/lib/task-meta.js +17 -9
- package/lib/verify.js +30 -4
- package/package.json +1 -1
- package/schema/discipline-coverage.v1.schema.json +55 -0
- package/schema/lifecycle.v1.schema.json +1 -1
- package/wizard/gate-check.sh +34 -20
package/lib/lifecycle.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import { resolveHarnessRoot } from './paths.js';
|
|
5
|
+
import { findReview, parseHumanGates } from './task-meta.js';
|
|
5
6
|
|
|
6
7
|
const SEVERITIES = new Set(['block', 'warn']);
|
|
7
8
|
|
|
@@ -119,3 +120,224 @@ export function formatLifecycleShow(data) {
|
|
|
119
120
|
}
|
|
120
121
|
return lines.join('\n').trimEnd();
|
|
121
122
|
}
|
|
123
|
+
|
|
124
|
+
/** 本波已接线的守卫(其余 unevaluated) */
|
|
125
|
+
const GUARD_ADAPTERS = {
|
|
126
|
+
'HG-AUDIT-R1': evaluateHgAuditR1,
|
|
127
|
+
reviews_retention: evaluateReviewsRetention,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function findRepoRootFromTask(absTask) {
|
|
131
|
+
let cur = path.dirname(absTask);
|
|
132
|
+
while (true) {
|
|
133
|
+
if (path.basename(cur) === 'docs') return path.dirname(cur);
|
|
134
|
+
const parent = path.dirname(cur);
|
|
135
|
+
if (parent === cur) return null;
|
|
136
|
+
cur = parent;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function flagsAllow(flags, allowFlag) {
|
|
141
|
+
if (allowFlag === '--allow-no-review') return Boolean(flags.allowNoReview);
|
|
142
|
+
if (allowFlag === '--allow-lint-fail') return Boolean(flags.allowLintFail);
|
|
143
|
+
if (allowFlag === '--allow-no-spec-review') return Boolean(flags.allowNoSpecReview);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function evaluateHgAuditR1({ taskPath }) {
|
|
148
|
+
const content = fs.readFileSync(taskPath, 'utf8');
|
|
149
|
+
const gates = parseHumanGates(content);
|
|
150
|
+
const audit = gates.find((g) => g.id === 'HG-AUDIT-R1');
|
|
151
|
+
if (!audit) {
|
|
152
|
+
return { status: 'fail', detail: '闸表无 HG-AUDIT-R1 行' };
|
|
153
|
+
}
|
|
154
|
+
if (audit.status === 'approved') {
|
|
155
|
+
return { status: 'pass', detail: 'HG-AUDIT-R1=approved' };
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
status: 'fail',
|
|
159
|
+
detail: `HG-AUDIT-R1=${audit.status}(须 approved)`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function evaluateReviewsRetention({ taskPath, target, flags }) {
|
|
164
|
+
const review = findReview(target, taskPath);
|
|
165
|
+
if (review.found) {
|
|
166
|
+
return { status: 'pass', detail: review.latest };
|
|
167
|
+
}
|
|
168
|
+
if (flags.allowNoReview) {
|
|
169
|
+
return {
|
|
170
|
+
status: 'warn',
|
|
171
|
+
detail: 'missing R<n> review(--allow-no-review)',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { status: 'fail', detail: 'missing R<n> review' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 转移 dry-run(引擎最小骨架 · v2.10+)。
|
|
179
|
+
* 默认不写盘;无 --apply。
|
|
180
|
+
*
|
|
181
|
+
* @param {{
|
|
182
|
+
* transitionId: string,
|
|
183
|
+
* fromState: string,
|
|
184
|
+
* taskPath?: string,
|
|
185
|
+
* harnessRoot?: string,
|
|
186
|
+
* flags?: { allowNoReview?: boolean, allowLintFail?: boolean, allowNoSpecReview?: boolean },
|
|
187
|
+
* cwd?: string,
|
|
188
|
+
* }} options
|
|
189
|
+
*/
|
|
190
|
+
export function dryRunTransition(options = {}) {
|
|
191
|
+
const {
|
|
192
|
+
transitionId,
|
|
193
|
+
fromState,
|
|
194
|
+
taskPath,
|
|
195
|
+
harnessRoot,
|
|
196
|
+
flags = {},
|
|
197
|
+
cwd = process.cwd(),
|
|
198
|
+
} = options;
|
|
199
|
+
|
|
200
|
+
const { data } = loadLifecycle({ harnessRoot });
|
|
201
|
+
const knownIds = data.transitions.map((t) => t.id);
|
|
202
|
+
const transition = data.transitions.find((t) => t.id === transitionId);
|
|
203
|
+
|
|
204
|
+
if (!transition) {
|
|
205
|
+
return {
|
|
206
|
+
engine: 'lifecycle-dry-run',
|
|
207
|
+
lifecycle_doc_version: data.version,
|
|
208
|
+
transition_id: transitionId ?? null,
|
|
209
|
+
from: fromState ?? null,
|
|
210
|
+
to: null,
|
|
211
|
+
hat: null,
|
|
212
|
+
structure_ok: false,
|
|
213
|
+
guards: [],
|
|
214
|
+
blocked: true,
|
|
215
|
+
unevaluated_count: 0,
|
|
216
|
+
detail: `未知 transition: ${transitionId}(已知: ${knownIds.join(', ')})`,
|
|
217
|
+
exitCode: 2,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!fromState || !transition.from.includes(fromState)) {
|
|
222
|
+
return {
|
|
223
|
+
engine: 'lifecycle-dry-run',
|
|
224
|
+
lifecycle_doc_version: data.version,
|
|
225
|
+
transition_id: transition.id,
|
|
226
|
+
from: fromState ?? null,
|
|
227
|
+
to: transition.to,
|
|
228
|
+
hat: transition.hat ?? null,
|
|
229
|
+
structure_ok: false,
|
|
230
|
+
guards: [],
|
|
231
|
+
blocked: true,
|
|
232
|
+
unevaluated_count: 0,
|
|
233
|
+
detail: `from "${fromState}" ∉ ${JSON.stringify(transition.from)}`,
|
|
234
|
+
exitCode: 2,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const absTask = taskPath
|
|
239
|
+
? path.isAbsolute(taskPath)
|
|
240
|
+
? taskPath
|
|
241
|
+
: path.resolve(cwd, taskPath)
|
|
242
|
+
: null;
|
|
243
|
+
|
|
244
|
+
if (taskPath && absTask && !fs.existsSync(absTask)) {
|
|
245
|
+
const e = new Error(`--task 不可读: ${taskPath}`);
|
|
246
|
+
e.exitCode = 1;
|
|
247
|
+
throw e;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const canEval = Boolean(absTask);
|
|
251
|
+
const target = absTask ? findRepoRootFromTask(absTask) ?? cwd : cwd;
|
|
252
|
+
const guards = [];
|
|
253
|
+
|
|
254
|
+
for (const g of transition.guards) {
|
|
255
|
+
const adapter = GUARD_ADAPTERS[g.id];
|
|
256
|
+
if (!canEval || !adapter) {
|
|
257
|
+
guards.push({
|
|
258
|
+
id: g.id,
|
|
259
|
+
severity: g.severity,
|
|
260
|
+
status: 'unevaluated',
|
|
261
|
+
detail: !canEval ? '无 --task · 未求值' : '本波未接线 adapter',
|
|
262
|
+
allow_flag: g.allow_flag ?? null,
|
|
263
|
+
});
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const evaluated = adapter({
|
|
268
|
+
taskPath: absTask,
|
|
269
|
+
target,
|
|
270
|
+
flags,
|
|
271
|
+
guard: g,
|
|
272
|
+
cwd,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
let status = evaluated.status;
|
|
276
|
+
let detail = evaluated.detail ?? null;
|
|
277
|
+
|
|
278
|
+
if (
|
|
279
|
+
status === 'fail' &&
|
|
280
|
+
g.allow_flag &&
|
|
281
|
+
flagsAllow(flags, g.allow_flag)
|
|
282
|
+
) {
|
|
283
|
+
status = 'warn';
|
|
284
|
+
detail = `${detail ?? 'fail'}(${g.allow_flag} 豁免)`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
guards.push({
|
|
288
|
+
id: g.id,
|
|
289
|
+
severity: g.severity,
|
|
290
|
+
status,
|
|
291
|
+
detail,
|
|
292
|
+
allow_flag: g.allow_flag ?? null,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const unevaluated_count = guards.filter((g) => g.status === 'unevaluated').length;
|
|
297
|
+
const blocked = guards.some(
|
|
298
|
+
(g) => g.severity === 'block' && g.status === 'fail',
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
engine: 'lifecycle-dry-run',
|
|
303
|
+
lifecycle_doc_version: data.version,
|
|
304
|
+
transition_id: transition.id,
|
|
305
|
+
from: fromState,
|
|
306
|
+
to: transition.to,
|
|
307
|
+
hat: transition.hat ?? null,
|
|
308
|
+
structure_ok: true,
|
|
309
|
+
guards,
|
|
310
|
+
blocked,
|
|
311
|
+
unevaluated_count,
|
|
312
|
+
exitCode: blocked ? 2 : 0,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 人读 dry-run 报告。
|
|
318
|
+
*/
|
|
319
|
+
export function formatLifecycleDryRun(report) {
|
|
320
|
+
const lines = [
|
|
321
|
+
'=== Harness lifecycle dry-run ===',
|
|
322
|
+
`engine: ${report.engine}`,
|
|
323
|
+
`lifecycle: v${report.lifecycle_doc_version}`,
|
|
324
|
+
`transition: ${report.transition_id} · ${report.from} → ${report.to}${report.hat ? ` · hat=${report.hat}` : ''}`,
|
|
325
|
+
`structure_ok: ${report.structure_ok}`,
|
|
326
|
+
`blocked: ${report.blocked}`,
|
|
327
|
+
`unevaluated_count: ${report.unevaluated_count}`,
|
|
328
|
+
];
|
|
329
|
+
if (report.detail) lines.push(`detail: ${report.detail}`);
|
|
330
|
+
if (report.unevaluated_count > 0) {
|
|
331
|
+
lines.push('WARN: 存在未求值守卫 · unevaluated ≠ pass(勿当作已通过)');
|
|
332
|
+
}
|
|
333
|
+
lines.push('', '## guards');
|
|
334
|
+
for (const g of report.guards ?? []) {
|
|
335
|
+
const d = g.detail ? ` · ${g.detail}` : '';
|
|
336
|
+
lines.push(`- [${g.severity}] ${g.id}: ${g.status}${d}`);
|
|
337
|
+
}
|
|
338
|
+
lines.push('');
|
|
339
|
+
lines.push(
|
|
340
|
+
'注: dry-run 为旁路资格报告 · 不替代 verify / gate-check 作为 30 硬闸 · 非 G7 runner',
|
|
341
|
+
);
|
|
342
|
+
return lines.join('\n');
|
|
343
|
+
}
|
package/lib/task-meta.js
CHANGED
|
@@ -310,17 +310,25 @@ export function buildTaskHandoff(target, taskFile, options = {}) {
|
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
/**
|
|
313
|
-
* 扫描 target 下 active task
|
|
313
|
+
* 扫描 target 下 active task 列表(N4 · v2.9+)。
|
|
314
|
+
* 双路径:docs/tasks/active ∪ docs/harness/tasks/active;
|
|
315
|
+
* 同 basename 优先 Starter;排序稳定。
|
|
314
316
|
*/
|
|
315
317
|
export function listActiveTasks(target) {
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
318
|
+
const dirs = ['docs/tasks/active', 'docs/harness/tasks/active'];
|
|
319
|
+
const byBasename = new Map();
|
|
320
|
+
|
|
321
|
+
for (const dir of dirs) {
|
|
322
|
+
const abs = path.join(target, dir);
|
|
323
|
+
if (!fs.existsSync(abs)) continue;
|
|
324
|
+
for (const f of fs.readdirSync(abs)) {
|
|
325
|
+
if (!f.startsWith('task_') || !f.endsWith('.md')) continue;
|
|
326
|
+
if (byBasename.has(f)) continue;
|
|
327
|
+
byBasename.set(f, path.join(dir, f).replace(/\\/g, '/'));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return [...byBasename.values()].sort();
|
|
324
332
|
}
|
|
325
333
|
|
|
326
334
|
function escapeRegExp(s) {
|
package/lib/verify.js
CHANGED
|
@@ -14,8 +14,9 @@ import {
|
|
|
14
14
|
import { lintTaskFile } from './task-lint.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* 30 前聚合验证:gate-check + audit D5(仅 --task)+ reviews
|
|
18
|
-
|
|
17
|
+
* 30 前聚合验证:gate-check + audit D5(仅 --task)+ reviews 留档闸
|
|
18
|
+
*(--task 与全量 · N4/v2.9+)+ task lint WARN(仅 --task · v2.7+)
|
|
19
|
+
* + S5 git-clean warn + 可选 --graph
|
|
19
20
|
*/
|
|
20
21
|
export function verifyTarget(target, options = {}) {
|
|
21
22
|
const { taskFile, graph, allowNoReview = false, allowLintFail = false } = options;
|
|
@@ -69,6 +70,26 @@ export function verifyTarget(target, options = {}) {
|
|
|
69
70
|
gateResult.stdout += `WARN: task lint FAIL · ${rules || 'errors'}(不挡 may_start_30 · 修结构或 --allow-lint-fail)\n`;
|
|
70
71
|
}
|
|
71
72
|
}
|
|
73
|
+
} else {
|
|
74
|
+
// 3c. 全量 reviews 闸(N4 · v2.9+):不跑 lint / D5
|
|
75
|
+
const taskFiles = listActiveTasks(target);
|
|
76
|
+
const missing = [];
|
|
77
|
+
for (const tf of taskFiles) {
|
|
78
|
+
if (!findReview(target, tf).found) missing.push(path.basename(tf));
|
|
79
|
+
}
|
|
80
|
+
if (missing.length > 0) {
|
|
81
|
+
const detail = `${missing.length}/${taskFiles.length} tasks · ${missing.join(', ')}`;
|
|
82
|
+
if (allowNoReview) {
|
|
83
|
+
gateResult.stdout += `WARN: missing R<n> review(--allow-no-review 豁免 · 留痕)· ${detail}\n`;
|
|
84
|
+
} else {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
exitCode: 2,
|
|
88
|
+
reason: `missing R<n> review(${detail})`,
|
|
89
|
+
stdout: gateResult.stdout,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
72
93
|
}
|
|
73
94
|
|
|
74
95
|
// 4. S5 git-clean:warn 不挡
|
|
@@ -335,14 +356,19 @@ export function buildVerifyHandoff(target, options = {}) {
|
|
|
335
356
|
const taskFiles = listActiveTasks(target);
|
|
336
357
|
const tasks = taskFiles.map((tf) => {
|
|
337
358
|
const handoff = buildTaskHandoff(target, tf, handoffOpts);
|
|
359
|
+
const reviewOk = handoff.review_found || allowNoReview;
|
|
360
|
+
const verifyOk = handoff.may_start_30 && reviewOk;
|
|
338
361
|
return {
|
|
339
362
|
schema_version: '1',
|
|
340
|
-
verify_ok:
|
|
363
|
+
verify_ok: verifyOk,
|
|
341
364
|
...handoff,
|
|
365
|
+
may_start_30: verifyOk,
|
|
366
|
+
blocked_reason: handoff.blocked_reason ?? (reviewOk ? null : 'missing R<n> review'),
|
|
367
|
+
next_hat: verifyOk ? '30' : null,
|
|
342
368
|
};
|
|
343
369
|
});
|
|
344
370
|
|
|
345
|
-
const verifyOk = tasks.length > 0 && tasks.every((t) => t.
|
|
371
|
+
const verifyOk = tasks.length > 0 && tasks.every((t) => t.verify_ok);
|
|
346
372
|
return {
|
|
347
373
|
schema_version: '1',
|
|
348
374
|
verify_ok: verifyOk,
|
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://cyning.dev/schemas/cyning-harness/discipline-coverage.v1.json",
|
|
4
|
+
"title": "cyning-harness Discipline Coverage (mechanization matrix)",
|
|
5
|
+
"description": "Starter 子集机械化率资产 · 随包版本维护 · 非 audit UI(v2.10+)",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["version", "as_of_package_version", "scope", "statements"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": { "type": "string", "minLength": 1 },
|
|
11
|
+
"as_of_package_version": { "type": "string", "minLength": 1 },
|
|
12
|
+
"scope": { "type": "string", "minLength": 1 },
|
|
13
|
+
"source_audit": { "type": "string" },
|
|
14
|
+
"statements": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"minItems": 1,
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"required": ["id", "source", "summary", "status"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"id": { "type": "string", "minLength": 1 },
|
|
23
|
+
"source": { "type": "string", "minLength": 1 },
|
|
24
|
+
"summary": { "type": "string", "minLength": 1 },
|
|
25
|
+
"status": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["mechanical", "partial", "prompt-only"]
|
|
28
|
+
},
|
|
29
|
+
"mechanism": { "type": ["string", "null"] },
|
|
30
|
+
"gap": { "type": ["string", "null"] },
|
|
31
|
+
"notes": { "type": ["string", "null"] },
|
|
32
|
+
"mechanism_quality": { "type": ["string", "null"] }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"gaps": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"required": ["id", "title", "status"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"id": { "type": "string", "minLength": 1 },
|
|
44
|
+
"title": { "type": "string", "minLength": 1 },
|
|
45
|
+
"status": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"enum": ["open", "closed", "deferred"]
|
|
48
|
+
},
|
|
49
|
+
"closed_in": { "type": ["string", "null"] },
|
|
50
|
+
"note": { "type": ["string", "null"] }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://cyning.dev/schemas/cyning-harness/lifecycle.v1.json",
|
|
4
4
|
"title": "cyning-harness Task Lifecycle (minimal)",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "状态/转移/守卫登记真值 · 由 lifecycle dry-run 引擎消费(yaml ≠ 引擎 · v2.7+ 登记 · v2.10+ dry-run)",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": ["version", "states", "transitions"],
|
package/wizard/gate-check.sh
CHANGED
|
@@ -174,6 +174,29 @@ if [[ "$JSON_MODE" != "1" ]]; then
|
|
|
174
174
|
echo ""
|
|
175
175
|
fi
|
|
176
176
|
|
|
177
|
+
# N4 · v2.9+:双路径发现(Starter ∪ Extended);同 basename 优先 Starter
|
|
178
|
+
# 输出绝对路径列表到 TASK_LIST(bash 3.2 兼容 · 无关联数组)
|
|
179
|
+
collect_active_tasks() {
|
|
180
|
+
TASK_LIST=()
|
|
181
|
+
local dir tf base existing already
|
|
182
|
+
for dir in "$TARGET/docs/tasks/active" "$TARGET/docs/harness/tasks/active"; do
|
|
183
|
+
[[ -d "$dir" ]] || continue
|
|
184
|
+
for tf in "$dir"/task_*.md; do
|
|
185
|
+
[[ -f "$tf" ]] || continue
|
|
186
|
+
base="$(basename "$tf")"
|
|
187
|
+
already=0
|
|
188
|
+
for existing in "${TASK_LIST[@]+"${TASK_LIST[@]}"}"; do
|
|
189
|
+
if [[ "$(basename "$existing")" == "$base" ]]; then
|
|
190
|
+
already=1
|
|
191
|
+
break
|
|
192
|
+
fi
|
|
193
|
+
done
|
|
194
|
+
[[ "$already" == "1" ]] && continue
|
|
195
|
+
TASK_LIST+=("$tf")
|
|
196
|
+
done
|
|
197
|
+
done
|
|
198
|
+
}
|
|
199
|
+
|
|
177
200
|
BLOCKED=0
|
|
178
201
|
|
|
179
202
|
if [[ "$GRAPH_MODE" == "1" ]]; then
|
|
@@ -191,26 +214,16 @@ if [[ "$GRAPH_MODE" == "1" ]]; then
|
|
|
191
214
|
fi
|
|
192
215
|
graph_for_task "$TASK_FILE"
|
|
193
216
|
else
|
|
194
|
-
|
|
195
|
-
if [[
|
|
217
|
+
collect_active_tasks
|
|
218
|
+
if [[ ${#TASK_LIST[@]} -eq 0 ]]; then
|
|
196
219
|
if [[ "$JSON_MODE" == "1" ]]; then
|
|
197
220
|
printf ']\n'
|
|
198
221
|
else
|
|
199
|
-
echo "
|
|
222
|
+
echo "无 active task_*.md(docs/tasks/active ∪ docs/harness/tasks/active)"
|
|
200
223
|
fi
|
|
201
224
|
exit 0
|
|
202
225
|
fi
|
|
203
|
-
|
|
204
|
-
if [[ ! -e "${TASK_FILES[0]}" ]]; then
|
|
205
|
-
if [[ "$JSON_MODE" == "1" ]]; then
|
|
206
|
-
printf ']\n'
|
|
207
|
-
else
|
|
208
|
-
echo "无 active task_*.md"
|
|
209
|
-
fi
|
|
210
|
-
exit 0
|
|
211
|
-
fi
|
|
212
|
-
for tf in "$ACTIVE_DIR"/task_*.md; do
|
|
213
|
-
[[ -f "$tf" ]] || continue
|
|
226
|
+
for tf in "${TASK_LIST[@]}"; do
|
|
214
227
|
if [[ "$JSON_MODE" == "1" ]]; then
|
|
215
228
|
[[ "$local_first" == "1" ]] || printf ','
|
|
216
229
|
local_first=0
|
|
@@ -234,15 +247,16 @@ if [[ -n "$TASK_FILE" ]]; then
|
|
|
234
247
|
fi
|
|
235
248
|
check_one_task "$TASK_FILE" || BLOCKED=1
|
|
236
249
|
else
|
|
237
|
-
|
|
238
|
-
[[ -
|
|
239
|
-
|
|
240
|
-
|
|
250
|
+
collect_active_tasks
|
|
251
|
+
if [[ ${#TASK_LIST[@]} -eq 0 ]]; then
|
|
252
|
+
if [[ ! -d "$TARGET/docs/tasks/active" && ! -d "$TARGET/docs/harness/tasks/active" ]]; then
|
|
253
|
+
echo "无 docs/tasks/active 且无 docs/harness/tasks/active" >&2
|
|
254
|
+
exit 1
|
|
255
|
+
fi
|
|
241
256
|
echo "无 active task_*.md"
|
|
242
257
|
exit 0
|
|
243
258
|
fi
|
|
244
|
-
for tf in "$
|
|
245
|
-
[[ -f "$tf" ]] || continue
|
|
259
|
+
for tf in "${TASK_LIST[@]}"; do
|
|
246
260
|
check_one_task "$tf" || BLOCKED=1
|
|
247
261
|
done
|
|
248
262
|
fi
|