kenaz 0.3.10 → 0.3.12
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/core/.claude/skills/code_reviewer/SKILL.md +0 -1
- package/core/.claude/skills/code_reviewer/patterns.yaml +0 -8
- package/core/.claude/skills/dev_agent/SKILL.md +3 -6
- package/core/.claude/skills/heimdall/SKILL.md +5 -6
- package/core/.claude/skills/huginn/SKILL.md +11 -38
- package/core/.collaboration/core/task-2288-followups.test.js +2 -37
- package/core/.collaboration/core/task-2421-replica.test.js +229 -0
- package/core/.collaboration/core/task-api.js +223 -230
- package/core/.collaboration/core/task-api.test.js +1 -138
- package/core/.collaboration/core/task-replica.js +189 -0
- package/core/.formation/.merge-lock +1 -0
- package/core/.formation/bifrost/codex-doctor.js +12 -2
- package/core/.formation/bifrost/codex-doctor.test.js +15 -0
- package/core/.formation/bifrost/launch.js +2 -2
- package/core/.formation/bifrost/launch.test.js +2 -2
- package/core/.formation/bifrost/setup-codex.js +84 -16
- package/core/.formation/bifrost/shared/ansuz-boot-policy.js +3 -3
- package/core/.formation/bifrost/shared/codex-injection.js +3 -17
- package/core/.formation/bifrost/shared/consolidate.js +1 -51
- package/core/.formation/bifrost/sync.js +4 -5
- package/core/.formation/tools/__tests__/subagent-bifrost-guard.test.js +1 -1
- package/core/.formation/tools/dispatch.js +2 -2
- package/core/.formation/tools/kenaz-stats.js +1 -1
- package/core/.formation/tools/mcp-allowlist.test.js +5 -3
- package/core/.formation/tools/post-complete/bounded-http.js +1 -40
- package/core/.formation/tools/post-complete/bounded-http.test.js +2 -89
- package/core/.formation/tools/post-complete/cognitive.js +2 -119
- package/core/.formation/tools/post-complete/task2420.test.js +129 -0
- package/core/.formation/tools/post-complete/testing.js +3 -21
- package/core/.formation/tools/post-complete.js +131 -436
- package/core/.formation/tools/post-complete.test.js +0 -135
- package/core/.formation/tools/route-audit.test.js +8 -20
- package/core/.kenaz/specialist/memory_guard_fired.json +1 -1
- package/core/.kenaz/specialist/memory_guard_trigger_cache.json +1 -1
- package/core/.rule/INDEX.yaml +3 -3
- package/core/.rule/general/ddx-synapse-stack.yaml +11 -11
- package/core/.rule/general/specialist-identity-contract.yaml +5 -5
- package/core/.rule/general/synapse-code-intelligence.yaml +25 -111
- package/core/.rule/general/worktree-isolation.yaml +2 -2
- package/core/.rule/pipeline/task-execution.yaml +6 -9
- package/core/.specialist/Amelia/README.md +5 -5
- package/core/.specialist/Amelia/capability.yaml +10 -18
- package/core/.specialist/Amelia/knowledge.yaml +15 -66
- package/core/.specialist/Amelia/workflow.yaml +12 -21
- package/core/.specialist/Ansuz/capability.yaml +12 -87
- package/core/.specialist/Ansuz/concepts/autonomous_agenda.yaml +1 -1
- package/core/.specialist/Ansuz/concepts/autonomy_contract.yaml +2 -3
- package/core/.specialist/Ansuz/concepts/capability_pointers.yaml +1 -3
- package/core/.specialist/Ansuz/concepts/cognitive_fluency_contract.yaml +8 -21
- package/core/.specialist/Ansuz/concepts/cognitive_systems_overview.yaml +0 -4
- package/core/.specialist/Ansuz/concepts/cognitive_systems_turn_context.yaml +1 -1
- package/core/.specialist/Ansuz/concepts/completion_tracking.yaml +0 -1
- package/core/.specialist/Ansuz/concepts/deep_knowledge_pointers.yaml +0 -3
- package/core/.specialist/Ansuz/concepts/evolvable_guardrails.yaml +3 -3
- package/core/.specialist/Ansuz/concepts/fatigue_handoff.yaml +1 -7
- package/core/.specialist/Ansuz/concepts/guardrails_immutable.yaml +3 -4
- package/core/.specialist/Ansuz/concepts/identity.yaml +1 -2
- package/core/.specialist/Ansuz/concepts/phase9_verification.yaml +0 -1
- package/core/.specialist/Ansuz/concepts/phase_protocol.yaml +3 -4
- package/core/.specialist/Ansuz/core.yaml +3 -6
- package/core/.specialist/Ansuz/elixir.yaml +6 -6
- package/core/.specialist/Ansuz/exo_cortex_tools.yaml +5 -9
- package/core/.specialist/Ansuz/guards/blind_spot_guards.yaml +2 -2
- package/core/.specialist/Ansuz/guards/lesson_guards.yaml +6 -19
- package/core/.specialist/Ansuz/identity_contract.yaml +4 -4
- package/core/.specialist/Ansuz/knowledge.yaml +13 -40
- package/core/.specialist/Ansuz/modules/ddx.yaml +2 -2
- package/core/.specialist/Ansuz/modules/prophecy.yaml +19 -19
- package/core/.specialist/Ansuz/modules/pulse_empty.yaml +6 -17
- package/core/.specialist/Ansuz/tools/ansuz-handoff-snapshot.js +3 -102
- package/core/.specialist/Ansuz/tools/ansuz-sdk-runner.js +2 -44
- package/core/.specialist/Ansuz/workflow.yaml +32 -76
- package/core/.specialist/Forseti/core.yaml +2 -7
- package/core/.specialist/Forseti/rule.yaml +1 -1
- package/core/.specialist/Heimdall/SKILL.md +2 -3
- package/core/.specialist/Heimdall/capability.yaml +22 -45
- package/core/.specialist/Heimdall/core.yaml +24 -126
- package/core/.specialist/Heimdall/principles.yaml +8 -11
- package/core/.specialist/Heimdall/rule.yaml +2 -2
- package/core/.specialist/Huginn/capability.yaml +31 -52
- package/core/.specialist/Huginn/core.yaml +43 -123
- package/core/.specialist/Huginn/principles.yaml +5 -5
- package/core/.specialist/Huginn/rule.yaml +3 -9
- package/core/.specialist/REFLECTION_TO_CONTROL.md +3 -4
- package/core/.specialist/shared/guard_schema.json +1 -1
- package/core/.specialist/shared/principles.yaml +6 -6
- package/core/.specialist/tools/__tests__/rea-detect.test.js +10 -0
- package/core/.specialist/tools/phase-gate.js +10 -230
- package/core/.specialist/tools/phase-gate.test.js +2 -2
- package/core/.specialist/tools/prompt-files.test.js +1 -1
- package/core/.specialist/tools/rea-detect.js +6 -2
- package/core/.specialist/tools/session.js +2 -89
- package/core/.synapse/memory-shared.json.sync +1 -0
- package/core/.system/memory/README.md +3 -0
- package/core/CLAUDE.md +6 -7
- package/core/KENAZ_CORE_VERSION +1 -1
- package/core/dev/plans/CLI_FIRST_INSTALL_FLOW.md +75 -83
- package/core/dev/specs/peer/CODEX_MANAGED_DAEMON.md +54 -0
- package/core/dev/specs/peer/setup-codex-hooks.test.js +38 -0
- package/core/dev/spikes/task2145-evidence-20261004/backend-log-excerpt.txt +56 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph.shadow +0 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph.wal +0 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph_corrupt_suspect.marker +1 -0
- package/core/plugins/kenaz/INVENTORY.md +1 -1
- package/core/plugins/kenaz/scripts/kenaz.test.js +70 -1
- package/core/plugins/kenaz/scripts/ygg.js +45 -4
- package/core/plugins/kenaz/skills/code_reviewer/SKILL.md +0 -1
- package/core/plugins/kenaz/skills/code_reviewer/patterns.yaml +0 -8
- package/core/plugins/kenaz/skills/dev_agent/SKILL.md +3 -6
- package/lib/cli.js +17 -9
- package/lib/cli.test.js +10 -0
- package/lib/dashboard.test.js +3 -3
- package/package.json +6 -6
- package/vendor/install-kenaz-cli.js +167 -16
- package/core/.claude/hooks/handoff-snapshot-on-decision.js +0 -188
- package/core/.claude/skills/synapse_maintain/SKILL.md +0 -84
- package/core/.collaboration/core/task-2264-single-after-capture.test.js +0 -61
- package/core/.formation/tools/alaya-gate.js +0 -198
- package/core/.formation/tools/post-complete/health.js +0 -464
- package/core/.formation/tools/post-complete/task2038.test.js +0 -246
- package/core/.formation/tools/post-complete/task2341.test.js +0 -74
- package/core/.rule/general/cortex-tools.yaml +0 -181
- package/core/.rule/general/memory-language.yaml +0 -67
- package/core/.rule/general/memory-source-priority.yaml +0 -207
- package/core/.rule/general/synapse-cognitive-fusion.yaml +0 -301
- package/core/.specialist/Ansuz/concepts/cognitive_systems_hippocampus.yaml +0 -14
- package/core/.specialist/Ansuz/concepts/write_verification.yaml +0 -26
- package/core/.specialist/Ansuz/modules/recall.yaml +0 -45
- package/core/.specialist/tools/MEMORY_RECALL_TRIGGERS.md +0 -156
- package/core/.specialist/tools/context-injection.js +0 -395
- package/core/dev/scripts/probe/hippocampus-concurrency.js +0 -247
|
@@ -170,101 +170,6 @@ function releaseClaimMarker(taskId, projectRoot) {
|
|
|
170
170
|
try { require('fs').unlinkSync(markerPath); } catch { /* already gone */ }
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
// ─────────────────────────────────────────
|
|
174
|
-
// TASK_1861: Dispatch-path-agnostic health snapshot capture
|
|
175
|
-
// _before at activation (move/claim/update→active), _after + observe-only verdict
|
|
176
|
-
// at completion (complete AND bare `move X done`). Fires a detached child so the
|
|
177
|
-
// short-lived CLI never blocks, fails, or slows on Dashboard state. Every capture
|
|
178
|
-
// attempt is audited in .kenaz/health_snapshots/snapshot_events.jsonl (never silent).
|
|
179
|
-
// Reuses .formation/tools/post-complete/health.js (measureHealthScoreSync etc.) —
|
|
180
|
-
// no third implementation. Does NOT touch guardian_counter.json (TASK_1720 periodic
|
|
181
|
-
// counter semantics unchanged; verdict stays observe-only, no git action).
|
|
182
|
-
// ─────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
function _appendSnapshotEvent(projectRoot, taskId, phase, action, reason) {
|
|
185
|
-
try {
|
|
186
|
-
const dir = path.join(projectRoot, '.kenaz', 'health_snapshots');
|
|
187
|
-
fsSync.mkdirSync(dir, { recursive: true });
|
|
188
|
-
const line = JSON.stringify({
|
|
189
|
-
task_id: taskId,
|
|
190
|
-
ts: new Date().toISOString(),
|
|
191
|
-
snapshot_type: phase,
|
|
192
|
-
action,
|
|
193
|
-
...(reason ? { reason } : {}),
|
|
194
|
-
});
|
|
195
|
-
fsSync.appendFileSync(path.join(dir, 'snapshot_events.jsonl'), line + '\n', 'utf8');
|
|
196
|
-
} catch { /* audit is best-effort — must never block task ops */ }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function scheduleHealthSnapshotCapture(taskId, phase, projectRoot) {
|
|
200
|
-
try {
|
|
201
|
-
if (!taskId || !projectRoot) return;
|
|
202
|
-
const healthJs = path.join(__dirname, '..', '..', '.formation', 'tools', 'post-complete', 'health.js');
|
|
203
|
-
if (!fsSync.existsSync(healthJs)) {
|
|
204
|
-
_appendSnapshotEvent(projectRoot, taskId, phase, 'skipped', 'health_module_missing');
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const cfg = { taskId, phase, projectRoot, healthJs };
|
|
208
|
-
// ASCII-only inline script (Windows argv codepage safety). Child owns the full
|
|
209
|
-
// outcome audit: written | skipped(reason). measureHealthScoreSync already
|
|
210
|
-
// handles Dashboard-down gracefully (returns null within its own timeout).
|
|
211
|
-
const script = [
|
|
212
|
-
`const CFG = ${JSON.stringify(cfg)};`,
|
|
213
|
-
"const fs = require('fs');",
|
|
214
|
-
"const path = require('path');",
|
|
215
|
-
"function logEvent(action, reason) {",
|
|
216
|
-
" try {",
|
|
217
|
-
" const dir = path.join(CFG.projectRoot, '.kenaz', 'health_snapshots');",
|
|
218
|
-
" fs.mkdirSync(dir, { recursive: true });",
|
|
219
|
-
" const line = JSON.stringify({ task_id: CFG.taskId, ts: new Date().toISOString(), snapshot_type: CFG.phase, action, ...(reason ? { reason } : {}) });",
|
|
220
|
-
" fs.appendFileSync(path.join(dir, 'snapshot_events.jsonl'), line + '\\n', 'utf8');",
|
|
221
|
-
" } catch (_e) { /* best-effort */ }",
|
|
222
|
-
"}",
|
|
223
|
-
"try {",
|
|
224
|
-
" const snapFile = path.join(CFG.projectRoot, '.kenaz', 'health_snapshots', CFG.taskId + '_' + CFG.phase + '.json');",
|
|
225
|
-
" if (fs.existsSync(snapFile)) { logEvent('skipped', 'snapshot_already_exists'); process.exit(0); }",
|
|
226
|
-
" const h = require(CFG.healthJs);",
|
|
227
|
-
" const health = h.measureHealthScoreSync(CFG.projectRoot);",
|
|
228
|
-
" if (!health || typeof health.score !== 'number') { logEvent('skipped', 'dashboard_unreachable'); process.exit(0); }",
|
|
229
|
-
// TASK_1891: capture the written payload (delta_from_previous) for
|
|
230
|
-
// observability threading into computeHealthVerdict below -- see
|
|
231
|
-
// health.js's computeHealthVerdict doc comment for why that field
|
|
232
|
-
// never gates the verdict itself.
|
|
233
|
-
" const afterPayload = h.saveTaskHealthSnapshot(CFG.taskId, CFG.phase, health, CFG.projectRoot);",
|
|
234
|
-
" logEvent('written');",
|
|
235
|
-
" if (CFG.phase === 'after') {",
|
|
236
|
-
" const pre = h.readPreTaskHealthSnapshot(CFG.taskId, CFG.projectRoot);",
|
|
237
|
-
" if (pre && typeof pre.score === 'number') {",
|
|
238
|
-
" const loopCfg = h.readLoopConfig(CFG.projectRoot);",
|
|
239
|
-
" const threshold = typeof loopCfg.revert_threshold === 'number' ? loopCfg.revert_threshold : h.DEFAULT_REVERT_THRESHOLD;",
|
|
240
|
-
// TASK_1890/1891: same computeHealthVerdict as post-complete.js's
|
|
241
|
-
// auto/manual arms -- no third re-implementation of the delta/trust-gate
|
|
242
|
-
// logic. No git access here (no isScopeUnchanged callback), so guard 3
|
|
243
|
-
// (stale snapshot + zero scope diff) is a safe no-op for this call
|
|
244
|
-
// site, same as it always was before TASK_1890. TASK_1891: pass the
|
|
245
|
-
// full `pre` object (not pre.score) so ordering + symmetric trust can
|
|
246
|
-
// be checked -- this is one of the two call sites that actually runs
|
|
247
|
-
// in production (readLoopConfig's TASK_1885 note).
|
|
248
|
-
" const vr = h.computeHealthVerdict(pre, health, threshold, { projectPath: CFG.projectRoot, trendDeltaFromPrevious: afterPayload ? afterPayload.delta_from_previous : null });",
|
|
249
|
-
" const reason = vr.verdict === 'REVERT'",
|
|
250
|
-
" ? vr.reason + ' - observe-only capture at task completion, no git action taken'",
|
|
251
|
-
" : vr.reason;",
|
|
252
|
-
" h.writeVerdictToTask(CFG.taskId, vr.verdict, vr.delta, reason, CFG.projectRoot);",
|
|
253
|
-
" }",
|
|
254
|
-
" }",
|
|
255
|
-
"} catch (e) { logEvent('skipped', 'error: ' + String(e && e.message || e)); }",
|
|
256
|
-
].join('\n');
|
|
257
|
-
const { spawn } = require('child_process');
|
|
258
|
-
const child = spawn(process.execPath, ['-e', script], {
|
|
259
|
-
detached: true,
|
|
260
|
-
stdio: 'ignore',
|
|
261
|
-
windowsHide: true,
|
|
262
|
-
});
|
|
263
|
-
child.unref();
|
|
264
|
-
} catch (e) {
|
|
265
|
-
_appendSnapshotEvent(projectRoot, taskId, phase, 'skipped', 'spawn_failed: ' + String(e && e.message || e));
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
173
|
const yaml = (() => {
|
|
269
174
|
try { return require('js-yaml'); } catch {
|
|
270
175
|
// Fallback: resolve .vendor/js-yaml.cjs relative to this script's real location.
|
|
@@ -311,6 +216,9 @@ function getTaskSync() {
|
|
|
311
216
|
return _taskSync;
|
|
312
217
|
}
|
|
313
218
|
|
|
219
|
+
// TASK_2421: shared-room card replicas (relay) -- pure helper module, safe to require eagerly.
|
|
220
|
+
const taskReplica = require('./task-replica');
|
|
221
|
+
|
|
314
222
|
// TASK_2142 R2: Task Pool status push (lazy loaded, graceful degradation --
|
|
315
223
|
// a sub-project checkout without .formation/ must not break moveTask).
|
|
316
224
|
// Overridable via setPoolStatusPush() so tests can inject a fake/spy
|
|
@@ -582,6 +490,11 @@ function emitEmptyListDiagnostics(tasksDir) {
|
|
|
582
490
|
} catch { /* never break list */ }
|
|
583
491
|
}
|
|
584
492
|
|
|
493
|
+
/** TASK_2421: is this api's project in an active shared room (relay-leased ids)? */
|
|
494
|
+
function taskReplicaIsShared(api) {
|
|
495
|
+
return taskReplica.isSharedRoom(api._loadYggdrasilConfigSync());
|
|
496
|
+
}
|
|
497
|
+
|
|
585
498
|
class TaskAPI {
|
|
586
499
|
/**
|
|
587
500
|
* @param {Object} config
|
|
@@ -1663,6 +1576,14 @@ class TaskAPI {
|
|
|
1663
1576
|
// { level_1: string[], level_2: [{endpoint, method, expect}], level_3: {scenario, generates} }
|
|
1664
1577
|
if (taskData.verification) task.verification = taskData.verification;
|
|
1665
1578
|
if (idAllocStamp) task.id_alloc = idAllocStamp;
|
|
1579
|
+
// TASK_2421: plan-document id (e.g. 791-17) that maps onto this TASK id
|
|
1580
|
+
if (taskData.alias && String(taskData.alias).trim()) {
|
|
1581
|
+
task.alias = String(taskData.alias).trim();
|
|
1582
|
+
const dup = await this._findAliasOwner(task.alias);
|
|
1583
|
+
if (dup) console.warn(`⚠️ alias "${task.alias}" is already used by ${dup} -- alias lookups resolve to the first match`);
|
|
1584
|
+
}
|
|
1585
|
+
const replicaCtx = this._replicaCtx();
|
|
1586
|
+
if (replicaCtx) task.shared_replica = true; // marker: later status changes keep the relay replica current
|
|
1666
1587
|
|
|
1667
1588
|
// TASK_1110: parallel scope overlap warning (non-blocking).
|
|
1668
1589
|
// When two tasks declare overlapping scope_paths / file references in
|
|
@@ -1699,16 +1620,146 @@ class TaskAPI {
|
|
|
1699
1620
|
task._file_path = filePath;
|
|
1700
1621
|
task._status_dir = 'backlog';
|
|
1701
1622
|
|
|
1623
|
+
// TASK_2421: id came from the local counter inside a shared room -> loud warning.
|
|
1624
|
+
if (idAllocStamp && taskReplicaIsShared(this)) {
|
|
1625
|
+
const w = `${taskId} was allocated locally because the relay is unreachable -- this id may collide with one allocated by another node in the shared room. Re-check before pushing, and rename the card if another node reports the same id.`;
|
|
1626
|
+
task.warnings = [...(task.warnings || []), w];
|
|
1627
|
+
console.warn(`⚠️ ID COLLISION RISK: ${w}`);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// TASK_2421: publish a read-only replica to the shared room (best-effort).
|
|
1631
|
+
if (replicaCtx) {
|
|
1632
|
+
const r = await taskReplica.publishReplica(replicaCtx, task, 'backlog');
|
|
1633
|
+
task.replica_published = r.ok;
|
|
1634
|
+
if (!r.ok) {
|
|
1635
|
+
const w = `card replica not published to the shared room (${r.error}); other nodes cannot read ${taskId} until a later update re-publishes it`;
|
|
1636
|
+
task.warnings = [...(task.warnings || []), w];
|
|
1637
|
+
console.warn(`⚠️ ${w}`);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1702
1641
|
return task;
|
|
1703
1642
|
}
|
|
1704
1643
|
|
|
1644
|
+
// ---- TASK_2421: shared-room replicas, alias, import ----
|
|
1645
|
+
|
|
1646
|
+
/** Context for relay replica publishing, or null when not in a shared room / opted out. */
|
|
1647
|
+
_replicaCtx() {
|
|
1648
|
+
const yggConfig = this._loadYggdrasilConfigSync();
|
|
1649
|
+
if (!taskReplica.shouldShareCards(yggConfig)) return null;
|
|
1650
|
+
return this._buildReplicaCtx(yggConfig);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/** Read-side context: any shared room (share_cards only gates publishing). */
|
|
1654
|
+
_replicaReadCtx() {
|
|
1655
|
+
const yggConfig = this._loadYggdrasilConfigSync();
|
|
1656
|
+
if (!taskReplica.isSharedRoom(yggConfig)) return null;
|
|
1657
|
+
return this._buildReplicaCtx(yggConfig);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
_buildReplicaCtx(yggConfig) {
|
|
1661
|
+
return {
|
|
1662
|
+
yggConfig,
|
|
1663
|
+
key: this._readPublishableKey(),
|
|
1664
|
+
projectRoot: this._resolveProjectRoot(),
|
|
1665
|
+
node: taskReplica.nodeOf(yggConfig, this._getGitUser().name),
|
|
1666
|
+
toYAML,
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/** Keep the relay replica current (status + content). Never throws. */
|
|
1671
|
+
async _syncReplica(task, statusDir) {
|
|
1672
|
+
try {
|
|
1673
|
+
if (!task || !task.shared_replica) return;
|
|
1674
|
+
const ctx = this._replicaCtx();
|
|
1675
|
+
if (!ctx) return;
|
|
1676
|
+
const r = await taskReplica.syncReplica(ctx, task, statusDir);
|
|
1677
|
+
// Row missing (create-time publish failed, or retention removed it): re-publish.
|
|
1678
|
+
if (!r.ok) await taskReplica.publishReplica(ctx, task, statusDir || task._status_dir || 'backlog');
|
|
1679
|
+
} catch { /* best-effort */ }
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
async _findAliasOwner(alias) {
|
|
1683
|
+
try {
|
|
1684
|
+
const want = String(alias).toLowerCase();
|
|
1685
|
+
const all = await this.getAllTasks({ includeArchived: true });
|
|
1686
|
+
const hit = all.tasks.find(t => t.alias && String(t.alias).toLowerCase() === want);
|
|
1687
|
+
return hit ? hit.id : null;
|
|
1688
|
+
} catch { return null; }
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/** Map an alias (e.g. "791-17") to a TASK id; a TASK id or unknown input is returned unchanged. */
|
|
1692
|
+
async resolveTaskRef(ref) {
|
|
1693
|
+
if (!ref || this.validateTaskID(ref)) return ref;
|
|
1694
|
+
return (await this._findAliasOwner(ref)) || ref;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/** Relay lookup by id or alias. Returns {found, row, replica}; never throws. */
|
|
1698
|
+
async lookupReplica(ref) {
|
|
1699
|
+
const ctx = this._replicaReadCtx();
|
|
1700
|
+
if (!ctx) return { found: false };
|
|
1701
|
+
return taskReplica.fetchReplica(ctx, ref, parseYAML);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/** Error for a command on an id missing locally; names the replica owner when the relay has the card. */
|
|
1705
|
+
async _missingTaskError(taskId) {
|
|
1706
|
+
const r = await this.lookupReplica(taskId);
|
|
1707
|
+
if (r.found) {
|
|
1708
|
+
const e = new Error(`任務不存在於本機: ${taskId} -- ${taskReplica.replicaNotice(taskId, r.row, r.replica)}. ` +
|
|
1709
|
+
`This node cannot modify the owner's card.`);
|
|
1710
|
+
e.replica = r;
|
|
1711
|
+
return e;
|
|
1712
|
+
}
|
|
1713
|
+
return new Error(`任務不存在: ${taskId}`);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/** Import a relay replica as a local card marked with its origin node. */
|
|
1717
|
+
async pullTask(taskId) {
|
|
1718
|
+
await this._ensureStatusDirs();
|
|
1719
|
+
const local = await this.getTask(taskId).catch(() => null);
|
|
1720
|
+
if (local) throw new Error(`${taskId} already exists locally (${local._file_path}); nothing to import`);
|
|
1721
|
+
const r = await this.lookupReplica(taskId);
|
|
1722
|
+
if (!r.found) {
|
|
1723
|
+
throw new Error(r.error ? `relay lookup failed: ${r.error}` : `任務不存在: ${taskId} (not among this shared room's replicas either)`);
|
|
1724
|
+
}
|
|
1725
|
+
const rep = r.replica || {};
|
|
1726
|
+
const gitUser = this._getGitUser();
|
|
1727
|
+
const origin = rep.origin_node || (r.row && r.row.created_by) || 'unknown';
|
|
1728
|
+
const card = {
|
|
1729
|
+
id: r.row.task_id,
|
|
1730
|
+
hash: randomUUID(),
|
|
1731
|
+
title: rep.title || r.row.title || '未命名任務',
|
|
1732
|
+
description: rep.description || r.row.description || '',
|
|
1733
|
+
priority: rep.priority || r.row.priority || 'P2',
|
|
1734
|
+
status: 'pending',
|
|
1735
|
+
role: rep.role || 'developer',
|
|
1736
|
+
tags: [],
|
|
1737
|
+
dependencies: [],
|
|
1738
|
+
scope_paths: [],
|
|
1739
|
+
requirements: toTextArray(rep.requirements),
|
|
1740
|
+
acceptance_criteria: toTextArray(rep.acceptance_criteria),
|
|
1741
|
+
technical_notes: [],
|
|
1742
|
+
created_by: rep.created_by || { name: origin, git_user: origin, timestamp: new Date().toISOString() },
|
|
1743
|
+
executed_by: null,
|
|
1744
|
+
origin_node: origin,
|
|
1745
|
+
imported_from_replica: { relay_status: r.row.status, imported_by: gitUser.name, imported_at: new Date().toISOString() },
|
|
1746
|
+
};
|
|
1747
|
+
if (rep.alias) card.alias = rep.alias;
|
|
1748
|
+
const filePath = path.join(this.tasksDir, 'backlog', `${card.id}_${gitUser.git_user}.yaml`);
|
|
1749
|
+
await fs.writeFile(filePath, toYAML(card));
|
|
1750
|
+
await this._verifyPersisted(filePath, card);
|
|
1751
|
+
card._file_path = filePath;
|
|
1752
|
+
card._status_dir = 'backlog';
|
|
1753
|
+
return card;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1705
1756
|
/**
|
|
1706
1757
|
* 更新任務
|
|
1707
1758
|
*/
|
|
1708
1759
|
async updateTask(taskId, updates) {
|
|
1709
1760
|
const task = await this.getTask(taskId);
|
|
1710
1761
|
if (!task) {
|
|
1711
|
-
throw
|
|
1762
|
+
throw await this._missingTaskError(taskId);
|
|
1712
1763
|
}
|
|
1713
1764
|
|
|
1714
1765
|
// TASK_1771: coerce array-typed fields so a raw-string update can never
|
|
@@ -1762,6 +1813,8 @@ class TaskAPI {
|
|
|
1762
1813
|
// 垃圾值只污染回傳值,不落 YAML(toYAML 過濾底線鍵 + 寫檔前已 delete)。
|
|
1763
1814
|
updatedTask._status_dir = this._resolveStatusDirName(filePath);
|
|
1764
1815
|
|
|
1816
|
+
await this._syncReplica(updatedTask, undefined); // TASK_2421: content refresh (title/alias/...)
|
|
1817
|
+
|
|
1765
1818
|
return updatedTask;
|
|
1766
1819
|
}
|
|
1767
1820
|
|
|
@@ -1770,16 +1823,14 @@ class TaskAPI {
|
|
|
1770
1823
|
*/
|
|
1771
1824
|
async moveTask(taskId, newStatus, taskData = null, options = {}) {
|
|
1772
1825
|
await this._ensureStatusDirs();
|
|
1773
|
-
|
|
1774
|
-
// save the 'after' snapshot itself, so scheduling a second detached measurement is pure waste.
|
|
1775
|
-
const { skipValidation = false, forceClaim = false, skipHealthAfter = false } = options;
|
|
1826
|
+
const { skipValidation = false, forceClaim = false } = options;
|
|
1776
1827
|
|
|
1777
1828
|
// 取得任務資料
|
|
1778
1829
|
let task = taskData;
|
|
1779
1830
|
if (!task) {
|
|
1780
1831
|
task = await this.getTask(taskId);
|
|
1781
1832
|
if (!task) {
|
|
1782
|
-
throw
|
|
1833
|
+
throw await this._missingTaskError(taskId);
|
|
1783
1834
|
}
|
|
1784
1835
|
}
|
|
1785
1836
|
|
|
@@ -1919,20 +1970,6 @@ class TaskAPI {
|
|
|
1919
1970
|
releaseClaimMarker(taskId, _projectRootForClaim);
|
|
1920
1971
|
}
|
|
1921
1972
|
|
|
1922
|
-
// TASK_1861: dispatch-path-agnostic health capture. moveTask is the single choke
|
|
1923
|
-
// point every activation/completion path funnels through (claim → moveTask,
|
|
1924
|
-
// update --status → moveTask, complete → moveTask, bare move). Fire-and-forget
|
|
1925
|
-
// detached child; idempotent only against snapshots that ALREADY exist when the child
|
|
1926
|
-
// starts (it can race a concurrent writer). TASK_2264: completeTask therefore passes
|
|
1927
|
-
// skipHealthAfter when its own in-process postComplete will measure 'after', so that
|
|
1928
|
-
// path is measured exactly once; paths with no post-complete (bare move, update --status,
|
|
1929
|
-
// complete --skip-post-complete) keep this capture as their only one.
|
|
1930
|
-
if (targetDir === 'active') {
|
|
1931
|
-
scheduleHealthSnapshotCapture(taskId, 'before', _projectRootForClaim);
|
|
1932
|
-
} else if (targetDir === 'done' && !skipHealthAfter) {
|
|
1933
|
-
scheduleHealthSnapshotCapture(taskId, 'after', _projectRootForClaim);
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
1973
|
// Yggdrasil: sync status change to kenaz_tasks
|
|
1937
1974
|
const sync = getTaskSync();
|
|
1938
1975
|
if (sync) {
|
|
@@ -1955,6 +1992,9 @@ class TaskAPI {
|
|
|
1955
1992
|
projectPath: this.projectPath,
|
|
1956
1993
|
}).catch(() => {});
|
|
1957
1994
|
|
|
1995
|
+
// TASK_2421: keep the shared-room replica's status current (best-effort, bounded by the relay timeout).
|
|
1996
|
+
await this._syncReplica(updatedTask, targetDir);
|
|
1997
|
+
|
|
1958
1998
|
return updatedTask;
|
|
1959
1999
|
}
|
|
1960
2000
|
|
|
@@ -1985,7 +2025,7 @@ class TaskAPI {
|
|
|
1985
2025
|
|
|
1986
2026
|
const task = await this.getTask(taskId);
|
|
1987
2027
|
if (!task) {
|
|
1988
|
-
throw
|
|
2028
|
+
throw await this._missingTaskError(taskId);
|
|
1989
2029
|
}
|
|
1990
2030
|
|
|
1991
2031
|
// M1: acquire claim marker (wx atomic — no TOCTOU)
|
|
@@ -2086,7 +2126,7 @@ class TaskAPI {
|
|
|
2086
2126
|
|
|
2087
2127
|
const task = await this.getTask(taskId);
|
|
2088
2128
|
if (!task) {
|
|
2089
|
-
throw
|
|
2129
|
+
throw await this._missingTaskError(taskId);
|
|
2090
2130
|
}
|
|
2091
2131
|
|
|
2092
2132
|
// TASK_2233: backlog → done is not an allowed transition; say what to do instead
|
|
@@ -2126,13 +2166,9 @@ class TaskAPI {
|
|
|
2126
2166
|
updatedTask.actual_hours = options.actual_hours;
|
|
2127
2167
|
}
|
|
2128
2168
|
|
|
2129
|
-
// TASK_2264: decide up front whether post-complete (below) will measure 'after' in-process.
|
|
2130
|
-
// TASK_2288 single-winner rule: whenever a post-complete run is expected for this completion,
|
|
2131
|
-
// post-complete's own measurement is the ONE 'after' capture (it runs after merge and feeds the
|
|
2132
|
-
// verdict), so moveTask must not also spawn one. Expected = in-process (below).
|
|
2133
|
-
// moveTask's detached capture stays the sole one only when nobody measures: --skip-post-complete.
|
|
2134
2169
|
const postCompleteRuns = !options.skipPostComplete;
|
|
2135
|
-
|
|
2170
|
+
let postCompleteSummary = null;
|
|
2171
|
+
const result = await this.moveTask(taskId, 'done', updatedTask);
|
|
2136
2172
|
|
|
2137
2173
|
try {
|
|
2138
2174
|
this._autoPopThoughtsForCompletedTask(taskId, now);
|
|
@@ -2156,7 +2192,7 @@ class TaskAPI {
|
|
|
2156
2192
|
try {
|
|
2157
2193
|
const { postComplete } = require('../../.formation/tools/post-complete.js');
|
|
2158
2194
|
console.log(`\n🔗 自動執行 post-complete for ${taskId}...\n`);
|
|
2159
|
-
await postComplete({
|
|
2195
|
+
const pc = await postComplete({
|
|
2160
2196
|
taskId,
|
|
2161
2197
|
keep: options.keep || 10,
|
|
2162
2198
|
skipArchive: options.skipArchive || false,
|
|
@@ -2164,13 +2200,19 @@ class TaskAPI {
|
|
|
2164
2200
|
dryRun: options.dryRun || false,
|
|
2165
2201
|
message: options.message,
|
|
2166
2202
|
projectPath: this.projectPath,
|
|
2203
|
+
noSource: options.noSource || false,
|
|
2167
2204
|
});
|
|
2205
|
+
postCompleteSummary = (pc && pc.summary) || null;
|
|
2168
2206
|
} catch (error) {
|
|
2169
2207
|
console.error(`⚠️ post-complete 失敗: ${error.message}`);
|
|
2170
|
-
|
|
2208
|
+
// TASK_2420: surface the failure to the caller instead of a bare "completed".
|
|
2209
|
+
postCompleteSummary = { outcome: 'not_completed', cardState: 'done_unreconciled', warnings: [],
|
|
2210
|
+
steps: [{ step: 'post_complete', status: 'failed', reason: error.message }] };
|
|
2171
2211
|
}
|
|
2172
2212
|
}
|
|
2173
2213
|
|
|
2214
|
+
// TASK_2420: non-enumerable so it never leaks into YAML / JSON serialisation of the task.
|
|
2215
|
+
Object.defineProperty(result, 'postComplete', { value: postCompleteSummary, enumerable: false });
|
|
2174
2216
|
return result;
|
|
2175
2217
|
}
|
|
2176
2218
|
|
|
@@ -2227,7 +2269,8 @@ class TaskAPI {
|
|
|
2227
2269
|
const desc = (t.description || '').toLowerCase();
|
|
2228
2270
|
const id = (t.id || '').toLowerCase();
|
|
2229
2271
|
const tags = (t.tags || []).join(' ').toLowerCase();
|
|
2230
|
-
|
|
2272
|
+
const alias = String(t.alias || '').toLowerCase(); // TASK_2421
|
|
2273
|
+
return title.includes(searchLower) || desc.includes(searchLower) || id.includes(searchLower) || tags.includes(searchLower) || (alias !== '' && alias.includes(searchLower));
|
|
2231
2274
|
});
|
|
2232
2275
|
}
|
|
2233
2276
|
|
|
@@ -2520,100 +2563,6 @@ class TaskAPI {
|
|
|
2520
2563
|
}
|
|
2521
2564
|
}
|
|
2522
2565
|
|
|
2523
|
-
// ============================================================================
|
|
2524
|
-
// TASK_1992/TASK_2104 — Alaya process-lessons echo (best-effort, create-only)
|
|
2525
|
-
// ============================================================================
|
|
2526
|
-
// Mirrors prompt.js's appendAlayaLessons: same query, same alaya_recall
|
|
2527
|
-
// shape, and (TASK_2104) the same TASK_2101 cosine-floor + stack-match
|
|
2528
|
-
// gate — loaded from the dependency-free .formation/tools/alaya-gate.js
|
|
2529
|
-
// module (require() resolved relative to __dirname; GEN-008) instead of
|
|
2530
|
-
// the hand-copied query builder that used to live here with no gate at all.
|
|
2531
|
-
// Never affects create's exit code: any failure is swallowed silently.
|
|
2532
|
-
// Opt-out: KENAZ_NO_ALAYA_ECHO=1.
|
|
2533
|
-
|
|
2534
|
-
// Best-effort: null (not a throw) when unavailable — e.g. a sub-project
|
|
2535
|
-
// with no .formation/ checkout — so the caller prints nothing rather than
|
|
2536
|
-
// falling back to ungated results.
|
|
2537
|
-
function loadAlayaGate() {
|
|
2538
|
-
try {
|
|
2539
|
-
return require('../../.formation/tools/alaya-gate');
|
|
2540
|
-
} catch {
|
|
2541
|
-
return null;
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
// fetchImpl/gateImpl: injectable test seams; default to the real fetch and loadAlayaGate().
|
|
2546
|
-
async function echoAlayaLessons(taskData, fetchImpl, gateImpl) {
|
|
2547
|
-
if (process.env.KENAZ_NO_ALAYA_ECHO === '1') return;
|
|
2548
|
-
|
|
2549
|
-
const doFetch = fetchImpl || (typeof fetch !== 'undefined' ? fetch : null);
|
|
2550
|
-
if (!doFetch) return;
|
|
2551
|
-
|
|
2552
|
-
const gate = gateImpl !== undefined ? gateImpl : loadAlayaGate();
|
|
2553
|
-
if (!gate) return; // shared gate unavailable — never print ungated results
|
|
2554
|
-
|
|
2555
|
-
const query = gate.buildTaskContextQuery(taskData);
|
|
2556
|
-
if (!query) return;
|
|
2557
|
-
|
|
2558
|
-
const taskStack = gate.deriveTaskStack((taskData && taskData.scope_paths) || []);
|
|
2559
|
-
|
|
2560
|
-
const controller = new AbortController();
|
|
2561
|
-
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
2562
|
-
try {
|
|
2563
|
-
const payload = JSON.stringify({
|
|
2564
|
-
jsonrpc: '2.0',
|
|
2565
|
-
id: 1,
|
|
2566
|
-
method: 'tools/call',
|
|
2567
|
-
params: {
|
|
2568
|
-
name: 'alaya_recall',
|
|
2569
|
-
arguments: {
|
|
2570
|
-
query, mode: 'hybrid', seed_types: ['failure_seed', 'lesson_seed'],
|
|
2571
|
-
limit: gate.ALAYA_CANDIDATE_POOL, min_cosine: gate.ALAYA_MIN_COSINE_DEFAULT,
|
|
2572
|
-
},
|
|
2573
|
-
},
|
|
2574
|
-
});
|
|
2575
|
-
const res = await doFetch('http://127.0.0.1:18080/api/mcp', {
|
|
2576
|
-
method: 'POST',
|
|
2577
|
-
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' },
|
|
2578
|
-
body: payload,
|
|
2579
|
-
signal: controller.signal,
|
|
2580
|
-
});
|
|
2581
|
-
if (!res || !res.ok) return;
|
|
2582
|
-
const result = await res.json();
|
|
2583
|
-
if (!result || result.error) return;
|
|
2584
|
-
|
|
2585
|
-
const content = result.result && result.result.content;
|
|
2586
|
-
if (!content || !Array.isArray(content)) return;
|
|
2587
|
-
const textPart = content.find(c => c.type === 'text');
|
|
2588
|
-
if (!textPart || !textPart.text) return;
|
|
2589
|
-
|
|
2590
|
-
let recallData;
|
|
2591
|
-
try { recallData = JSON.parse(textPart.text); } catch { return; }
|
|
2592
|
-
if (!recallData) return;
|
|
2593
|
-
|
|
2594
|
-
let bijas = recallData.bijas || [];
|
|
2595
|
-
if (bijas.length === 0) return;
|
|
2596
|
-
|
|
2597
|
-
bijas = gate.dedupBijas(bijas);
|
|
2598
|
-
// TASK_2104: same relevance gate as prompt.js's appendAlayaLessons —
|
|
2599
|
-
// the server already applied min_cosine, but re-checking (+ the
|
|
2600
|
-
// stack-match half, which the server does not apply) keeps this
|
|
2601
|
-
// correct against a stub/older server and drops stack mismatches.
|
|
2602
|
-
bijas = bijas.filter(b => gate.passesAlayaRelevanceGate(b, taskStack, gate.ALAYA_MIN_COSINE_DEFAULT)).slice(0, 3);
|
|
2603
|
-
if (bijas.length === 0) return;
|
|
2604
|
-
|
|
2605
|
-
console.log(' Alaya lessons for this card:');
|
|
2606
|
-
for (const b of bijas) {
|
|
2607
|
-
const preview = (b.content || '').replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
2608
|
-
console.log(` - [${b.seed_type || 'seed'}/${b.origin || 'unknown'}] ${preview}`);
|
|
2609
|
-
}
|
|
2610
|
-
} catch {
|
|
2611
|
-
// Best-effort: MCP down, timeout, network error, malformed response — silent.
|
|
2612
|
-
} finally {
|
|
2613
|
-
clearTimeout(timeoutId);
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
2566
|
// TASK_2257: estimated_hours is a finite number >= 0 or null. '' (flag present, empty value)
|
|
2618
2567
|
// CLEARS the field (null) -- same "clearable" contract as --description/--notes. Anything
|
|
2619
2568
|
// non-numeric or negative exits 1 instead of persisting NaN / a nonsense value.
|
|
@@ -2698,6 +2647,7 @@ Commands:
|
|
|
2698
2647
|
node task-api.js update TASK_ID [--title T] [--description D] [--priority P] [--role R] [--technical-notes N]...
|
|
2699
2648
|
陣列欄位(tags/dependencies/scope-paths/requirements/acceptance-criteria/technical-notes 等)
|
|
2700
2649
|
預設為整組取代(會回顯被移除的既有項目);加 --append 改為追加到既有項目之後(與既有重複的項目會略過並回顯)
|
|
2650
|
+
node task-api.js pull TASK_ID 匯入共享 room 的唯讀 replica 為本機卡片(標記 origin_node;TASK_2421)
|
|
2701
2651
|
node task-api.js claim TASK_ID
|
|
2702
2652
|
node task-api.js complete TASK_ID [--actual-hours HOURS]
|
|
2703
2653
|
node task-api.js reconcile [--remote origin] [--base main] [--apply] [--worktrees] 關閉分支已併入 base 的 active 任務(預設 dry-run)
|
|
@@ -2751,6 +2701,7 @@ Create 選項:
|
|
|
2751
2701
|
--args-file PATH UTF-8 JSON 物件;支援 camelCase / snake_case,CLI 旗標優先
|
|
2752
2702
|
requirements / acceptance_criteria / technical_notes 使用 JSON 陣列
|
|
2753
2703
|
一般品質門檻:requirements >= 3,acceptance_criteria >= 3,technical_notes >= 2
|
|
2704
|
+
--alias PLAN_ID 計畫文件編號(如 791-17),get/update/move/complete/search 皆可用(TASK_2421)
|
|
2754
2705
|
--estimated-hours N 預估工時(小時)
|
|
2755
2706
|
--branch NAME Git 分支
|
|
2756
2707
|
--notes TEXT 備註
|
|
@@ -2787,6 +2738,11 @@ Create 選項:
|
|
|
2787
2738
|
|
|
2788
2739
|
const command = args[0];
|
|
2789
2740
|
|
|
2741
|
+
// TASK_2421: a plan-document alias (e.g. 791-17) works anywhere a TASK id does.
|
|
2742
|
+
if (['get', 'update', 'move', 'complete', 'claim', 'pull'].includes(command) && args[1] && !args[1].startsWith('--')) {
|
|
2743
|
+
args[1] = await api.resolveTaskRef(args[1]);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2790
2746
|
if (command === 'reconcile') {
|
|
2791
2747
|
// TASK_2404: close active cards whose branches already landed on <remote>/<base>
|
|
2792
2748
|
const argOf = (n, d) => { const i = args.indexOf(n); return i >= 0 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : d; };
|
|
@@ -3056,14 +3012,32 @@ Create 選項:
|
|
|
3056
3012
|
console.error('錯誤: 缺少任務 ID');
|
|
3057
3013
|
process.exit(1);
|
|
3058
3014
|
}
|
|
3059
|
-
|
|
3015
|
+
// TASK_2421: a non-TASK ref (relay-only alias) skips the local lookup (getTask throws on it)
|
|
3016
|
+
const task = api.validateTaskID(taskId) ? await api.getTask(taskId) : null;
|
|
3060
3017
|
if (task) {
|
|
3061
3018
|
console.log(JSON.stringify(task, null, 2));
|
|
3062
3019
|
} else {
|
|
3063
|
-
|
|
3064
|
-
|
|
3020
|
+
// TASK_2421: id unknown locally -> look for a read-only replica in the shared room
|
|
3021
|
+
const rep = await api.lookupReplica(taskId);
|
|
3022
|
+
if (rep.found) {
|
|
3023
|
+
const out = { ...rep.replica, id: rep.row.task_id, relay_status: rep.row.status, read_only_replica: true,
|
|
3024
|
+
replica_notice: require('./task-replica').replicaNotice(taskId, rep.row, rep.replica) };
|
|
3025
|
+
console.log(JSON.stringify(out, null, 2));
|
|
3026
|
+
console.error(`ℹ️ ${taskId}: ${out.replica_notice}`);
|
|
3027
|
+
} else {
|
|
3028
|
+
console.error(`任務不存在: ${taskId}`);
|
|
3029
|
+
if (rep.error) console.error(` (shared-room replica lookup failed: ${rep.error})`);
|
|
3030
|
+
process.exit(1);
|
|
3031
|
+
}
|
|
3065
3032
|
}
|
|
3066
3033
|
|
|
3034
|
+
} else if (command === 'pull') {
|
|
3035
|
+
// TASK_2421: import a shared-room replica as a local card marked with its origin node
|
|
3036
|
+
const taskId = args[1];
|
|
3037
|
+
if (!taskId) { console.error('錯誤: 缺少任務 ID'); process.exit(1); }
|
|
3038
|
+
const card = await api.pullTask(taskId);
|
|
3039
|
+
console.log(`✅ 已匯入: ${card.id} (origin node: ${card.origin_node}) -> ${card._file_path}`);
|
|
3040
|
+
|
|
3067
3041
|
} else if (command === 'create') {
|
|
3068
3042
|
// TASK_1599: Windows CreateProcess caps command line at 32767 UTF-16 chars.
|
|
3069
3043
|
// Detailed task cards (long description + many requirements) easily exceed this
|
|
@@ -3161,6 +3135,7 @@ Create 選項:
|
|
|
3161
3135
|
// is the documented flag name; it maps onto the pre-existing `source`
|
|
3162
3136
|
// field that createTask() already promotes onto the task when truthy.
|
|
3163
3137
|
source: getArg('--source') || getArg('--origin') || fromFile('source') || fromFile('origin') || null,
|
|
3138
|
+
alias: getArg('--alias') || fromFile('alias') || undefined, // TASK_2421
|
|
3164
3139
|
};
|
|
3165
3140
|
|
|
3166
3141
|
const reqs = getMulti('--requirements');
|
|
@@ -3197,8 +3172,9 @@ Create 選項:
|
|
|
3197
3172
|
if (task.scope_paths && task.scope_paths.length > 0) console.log(`範圍: ${task.scope_paths.join(', ')}`);
|
|
3198
3173
|
if (task.estimated_hours != null) console.log(`預估工時: ${task.estimated_hours}h`);
|
|
3199
3174
|
if (task.branch) console.log(`分支: ${task.branch}`);
|
|
3200
|
-
|
|
3201
|
-
|
|
3175
|
+
if (task.alias) console.log(`別名: ${task.alias}`);
|
|
3176
|
+
if (task.replica_published) console.log(`共享: replica published to the shared room (read-only for other nodes)`);
|
|
3177
|
+
for (const w of task.warnings || []) console.log(`⚠️ ${w}`);
|
|
3202
3178
|
|
|
3203
3179
|
} else if (command === 'claim') {
|
|
3204
3180
|
const taskId = args[1];
|
|
@@ -3233,9 +3209,29 @@ Create 選項:
|
|
|
3233
3209
|
if (args.includes('--skip-post-complete')) {
|
|
3234
3210
|
options.skipPostComplete = true;
|
|
3235
3211
|
}
|
|
3212
|
+
// TASK_2420: runtime/config tasks with no branch — git merge/verify reported as skipped
|
|
3213
|
+
if (args.includes('--no-source')) {
|
|
3214
|
+
options.noSource = true;
|
|
3215
|
+
}
|
|
3236
3216
|
const task = await api.completeTask(taskId, options);
|
|
3237
|
-
|
|
3238
|
-
|
|
3217
|
+
const pcs = task.postComplete;
|
|
3218
|
+
const stepLines = pcs ? (pcs.steps || []).map(st => ` ${st.status === 'failed' ? '❌' : st.status === 'skipped' ? '⏭' : '✅'} ${st.step}: ${st.status}${st.reason ? ' — ' + st.reason : ''}`) : [];
|
|
3219
|
+
if (pcs && pcs.outcome === 'not_completed') {
|
|
3220
|
+
// The card was NOT delivered: say so, with the real cause, and fail the command.
|
|
3221
|
+
console.error(`❌ 任務未完成: ${task.id} — ${pcs.cardState === 'blocked' ? '已移至 blocked/' : pcs.cardState === 'active' ? '留在 active/' : '卡片仍在 done/ 但交付失敗(需人工處理)'}`);
|
|
3222
|
+
for (const l of stepLines) console.error(l);
|
|
3223
|
+
console.error(` 若此任務沒有 source 變更(runtime/config 修復),請改用: complete ${task.id} --no-source`);
|
|
3224
|
+
process.exitCode = 1;
|
|
3225
|
+
} else {
|
|
3226
|
+
console.log(`✅ 任務已完成${pcs && pcs.outcome === 'completed_with_warnings' ? '(含警告)' : ''}: ${task.id}`);
|
|
3227
|
+
if (pcs && pcs.outcome === 'completed_with_warnings') {
|
|
3228
|
+
for (const l of stepLines) console.log(l);
|
|
3229
|
+
for (const w of pcs.warnings) console.log(` ⚠️ ${w}`);
|
|
3230
|
+
} else {
|
|
3231
|
+
for (const l of stepLines.filter(l => l.includes('⏭'))) console.log(l);
|
|
3232
|
+
}
|
|
3233
|
+
if (task.actual_hours != null) console.log(`實際工時: ${task.actual_hours} 小時`);
|
|
3234
|
+
}
|
|
3239
3235
|
|
|
3240
3236
|
} else if (command === 'move') {
|
|
3241
3237
|
const taskId = args[1];
|
|
@@ -3586,6 +3582,7 @@ Create 選項:
|
|
|
3586
3582
|
const reqs = getMulti('--requirements'); if (reqs) updates.requirements = reqs;
|
|
3587
3583
|
const ac = getMulti('--acceptance-criteria'); if (ac) updates.acceptance_criteria = ac;
|
|
3588
3584
|
const tn = getMulti('--technical-notes'); if (tn) updates.technical_notes = tn;
|
|
3585
|
+
const aliasArg = getArg('--alias'); if (aliasArg !== null) updates.alias = aliasArg; // TASK_2421
|
|
3589
3586
|
const mergeLandedArg = getArg('--merge-landed');
|
|
3590
3587
|
if (mergeLandedArg !== null) updates.merge_landed = (mergeLandedArg === 'true');
|
|
3591
3588
|
|
|
@@ -3737,10 +3734,6 @@ module.exports = {
|
|
|
3737
3734
|
parseYAML,
|
|
3738
3735
|
toYAML,
|
|
3739
3736
|
resolveTasksDir,
|
|
3740
|
-
// TASK_1992/TASK_2104: exported for direct unit-testing with a mocked
|
|
3741
|
-
// fetch (and an injectable alaya-gate module) — no live MCP dependency.
|
|
3742
|
-
loadAlayaGate,
|
|
3743
|
-
echoAlayaLessons,
|
|
3744
3737
|
// TASK_2142 R2: test seam for injecting a fake pool-status-push module.
|
|
3745
3738
|
setPoolStatusPush,
|
|
3746
3739
|
};
|