devflow-kit 3.5.0 → 3.7.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 +178 -1
- package/README.md +10 -6
- package/dist/agents/code.md +7 -11
- package/dist/agents/{diagnose.md → debug.md} +4 -4
- package/dist/agents/{code-fix.md → fix.md} +7 -8
- package/dist/agents/{scrutinize.md → harden.md} +6 -6
- package/dist/agents/{code-edit.md → refactor.md} +8 -9
- package/dist/agents/review.md +2 -0
- package/dist/cli/agents-view/render.js +1 -1
- package/dist/cli/commands/gates.js +276 -0
- package/dist/cli/commands/init-seed.js +9 -1
- package/dist/cli/commands/init.js +34 -11
- package/dist/cli/commands/learning.js +12 -12
- package/dist/cli/commands/legacy-hooks.js +1 -1
- package/dist/cli/commands/memory.js +18 -0
- package/dist/cli/commands/uninstall.js +111 -5
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +21 -17
- package/dist/commands/code-review.md +65 -66
- package/dist/commands/debug.md +17 -17
- package/dist/commands/dynamic-build.md +104 -116
- package/dist/commands/dynamic-plan.md +13 -11
- package/dist/commands/dynamic-profile.md +9 -7
- package/dist/commands/dynamic-tickets.md +13 -11
- package/dist/commands/explore.md +15 -15
- package/dist/commands/implement.md +80 -154
- package/dist/commands/plan.md +34 -66
- package/dist/commands/release.md +11 -7
- package/dist/commands/research.md +14 -10
- package/dist/commands/resolve.md +44 -88
- package/dist/commands/self-review.md +44 -44
- package/dist/core/agent-models.js +47 -11
- package/dist/core/{learning-tuning-config.js → curate-tuning-config.js} +14 -14
- package/dist/core/echo-value.js +42 -0
- package/dist/core/evidence-policy.js +17 -5
- package/dist/core/gate-level.js +213 -0
- package/dist/core/learning-queue-cleanup.js +1 -1
- package/dist/core/manifest.js +26 -10
- package/dist/core/migrations.js +63 -44
- package/dist/core/model-discovery.js +7 -1
- package/dist/core/plugins.js +14 -12
- package/dist/core/project-paths.js +3 -3
- package/dist/core/tracker.js +5 -33
- package/dist/hud/codex-usage.js +247 -0
- package/dist/hud/components/codex-quota.js +37 -0
- package/dist/hud/components/usage-quota.js +1 -1
- package/dist/hud/config.js +1 -0
- package/dist/hud/index.js +16 -3
- package/dist/hud/render.js +3 -1
- package/dist/learning-off/agents/code.md +7 -11
- package/dist/learning-off/agents/{diagnose.md → debug.md} +4 -4
- package/dist/learning-off/agents/{code-fix.md → fix.md} +7 -8
- package/dist/learning-off/agents/{scrutinize.md → harden.md} +6 -6
- package/dist/learning-off/agents/{code-edit.md → refactor.md} +8 -9
- package/dist/learning-off/agents/review.md +2 -0
- package/dist/learning-off/commands/bug-analysis.md +9 -9
- package/dist/learning-off/commands/code-review.md +54 -59
- package/dist/learning-off/commands/debug.md +6 -10
- package/dist/learning-off/commands/dynamic-build.md +98 -112
- package/dist/learning-off/commands/dynamic-plan.md +5 -5
- package/dist/learning-off/commands/dynamic-profile.md +1 -1
- package/dist/learning-off/commands/dynamic-tickets.md +5 -5
- package/dist/learning-off/commands/explore.md +4 -8
- package/dist/learning-off/commands/implement.md +68 -146
- package/dist/learning-off/commands/plan.md +23 -58
- package/dist/learning-off/commands/release.md +4 -2
- package/dist/learning-off/commands/research.md +2 -2
- package/dist/learning-off/commands/resolve.md +33 -81
- package/dist/learning-off/commands/self-review.md +32 -36
- package/dist/skills/git/references/pr/check-merge-readiness.md +2 -0
- package/dist/skills/git/references/pr/update-pr-evidence.md +4 -6
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +7 -9
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +7 -15
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +7 -15
- package/dist/targets/claude-code/installer.js +117 -84
- package/dist/targets/claude-code/learning-install.js +15 -3
- package/dist/targets/claude-code/tracker-servers.js +1 -1
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +7 -8
- package/src/assets/agents/code.mds +4 -7
- package/src/assets/agents/{learning.md → curate.md} +8 -10
- package/src/assets/agents/{diagnose.mds → debug.mds} +4 -4
- package/src/assets/agents/evaluate.md +4 -4
- package/src/assets/agents/{code-fix.mds → fix.mds} +5 -5
- package/src/assets/agents/{scrutinize.mds → harden.mds} +6 -6
- package/src/assets/agents/{simplify.md → polish.md} +4 -4
- package/src/assets/agents/{code-edit.mds → refactor.mds} +6 -6
- package/src/assets/agents/review.mds +2 -0
- package/src/assets/agents/synthesize.md +2 -2
- package/src/assets/agents/test.md +2 -2
- package/src/assets/commands/_partials/_compliance.mds +1 -1
- package/src/assets/commands/_partials/_decisions.mds +26 -6
- package/src/assets/commands/_partials/_engine.mds +15 -15
- package/src/assets/commands/_partials/_evidence_policy.mds +0 -17
- package/src/assets/commands/_partials/_knowledge.mds +14 -9
- package/src/assets/commands/_partials/_preamble.mds +4 -2
- package/src/assets/commands/_partials/_roster.mds +4 -4
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/_partials/_wave.mds +10 -17
- package/src/assets/commands/bug-analysis.mds +7 -7
- package/src/assets/commands/code-review.mds +51 -56
- package/src/assets/commands/debug.mds +2 -2
- package/src/assets/commands/dynamic-build.mds +67 -74
- package/src/assets/commands/implement.mds +65 -127
- package/src/assets/commands/plan.mds +21 -61
- package/src/assets/commands/release.mds +5 -1
- package/src/assets/commands/resolve.mds +28 -76
- package/src/assets/commands/self-review.mds +27 -27
- package/src/assets/mds/git/_ops_steps.mds +3 -3
- package/src/assets/mds/git/_pr.mds +6 -6
- package/src/assets/mds/tracker/_common.mds +23 -12
- package/src/assets/mds/tracker/_github.mds +2 -9
- package/src/assets/mds/tracker/_jira.mds +2 -15
- package/src/assets/mds/tracker/_linear.mds +2 -15
- package/src/assets/scripts/ci-wait.cjs +22 -4
- package/src/assets/scripts/claude-md-audit.cjs +66 -51
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -3
- package/src/assets/scripts/hooks/background-memory-update +404 -95
- package/src/assets/scripts/hooks/capture-prompt +1 -1
- package/src/assets/scripts/hooks/capture-question +1 -1
- package/src/assets/scripts/hooks/capture-turn +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +13 -2
- package/src/assets/scripts/hooks/json-parse +18 -3
- package/src/assets/scripts/hooks/lib/learning-store.cjs +1 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +4 -4
- package/src/assets/scripts/hooks/memory-permission +358 -0
- package/src/assets/scripts/hooks/preamble +1 -1
- package/src/assets/scripts/hooks/session-start-context +60 -47
- package/src/assets/scripts/hooks/session-start-memory +26 -5
- package/src/assets/scripts/lib/project-config.cjs +19 -4
- package/src/assets/scripts/resolve-settings.cjs +200 -32
- package/src/assets/scripts/verify-evidence.cjs +726 -9
- package/src/assets/skills/agent-references/SKILL.md +14 -6
- package/src/assets/skills/agent-references/references/commands/code-review-levels.md +19 -0
- package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +35 -0
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +15 -0
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +40 -0
- package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +1 -1
- package/src/assets/skills/agent-references/references/synthesize/planning.md +9 -22
- package/src/assets/skills/agent-references/references/synthesize/review.md +11 -11
- package/src/assets/skills/apply-decisions/SKILL.md +6 -4
- package/src/assets/skills/docs-framework/SKILL.md +3 -3
- package/src/assets/skills/qa/SKILL.md +1 -1
- package/src/assets/skills/quality-gates/SKILL.md +7 -7
- package/src/assets/skills/quality-gates/references/report-template.md +7 -7
- /package/src/assets/skills/agent-references/references/{learning → curate}/maintain.md +0 -0
package/dist/core/migrations.js
CHANGED
|
@@ -14,6 +14,55 @@ import { writeFileAtomicExclusive } from './fs-atomic.js';
|
|
|
14
14
|
import { getMemoryDir } from './project-paths.js';
|
|
15
15
|
import { LEGACY_AGENT_KEYS, canonicaliseAgentKeys, parseAgentMappingEnvelope } from './agent-models.js';
|
|
16
16
|
import { migrateLegacyTrackerConventions } from './tracker.js';
|
|
17
|
+
/**
|
|
18
|
+
* The body of every canonicalise-agent-keys-vN migration: read agent-models.json raw,
|
|
19
|
+
* apply LEGACY_AGENT_KEYS once, and write the result back atomically when a key changed.
|
|
20
|
+
* Each migration id passes its own id so that its warnings name it. Idempotent, and a
|
|
21
|
+
* file with no legacy key is left untouched.
|
|
22
|
+
*/
|
|
23
|
+
function canonicaliseAgentKeysRun(migrationId) {
|
|
24
|
+
return async (ctx) => {
|
|
25
|
+
const infos = [];
|
|
26
|
+
const warnings = [];
|
|
27
|
+
// Fast path: no legacy keys defined — skip without touching the file.
|
|
28
|
+
if (Object.keys(LEGACY_AGENT_KEYS).length === 0)
|
|
29
|
+
return { infos, warnings };
|
|
30
|
+
const filePath = path.join(ctx.devflowDir, 'agent-models.json');
|
|
31
|
+
// parseAgentMappingEnvelope handles IO, BOM-strip, JSON parse, and shape validation.
|
|
32
|
+
// Raw read intentionally avoids readAgentMapping (which would drop unknown fields).
|
|
33
|
+
const envelope = await parseAgentMappingEnvelope(filePath);
|
|
34
|
+
if (envelope.kind === 'skip')
|
|
35
|
+
return { infos, warnings };
|
|
36
|
+
if (envelope.kind === 'warn') {
|
|
37
|
+
warnings.push(`${migrationId}: ${envelope.message}`);
|
|
38
|
+
return { infos, warnings };
|
|
39
|
+
}
|
|
40
|
+
const onWarning = (msg) => warnings.push(msg);
|
|
41
|
+
const { agents: migrated, didMutate, renamed, dropped, guardDropped } = canonicaliseAgentKeys(envelope.rawAgents, onWarning);
|
|
42
|
+
if (!didMutate)
|
|
43
|
+
return { infos, warnings };
|
|
44
|
+
// Write back with the canonical keys, preserving the rest of the envelope.
|
|
45
|
+
// Idempotent under concurrent execution (no lock needed; see D31 comment above).
|
|
46
|
+
const updated = { ...envelope.envelope, agents: migrated };
|
|
47
|
+
try {
|
|
48
|
+
await writeFileAtomicExclusive(filePath, JSON.stringify(updated, null, 2) + '\n');
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
warnings.push(`${migrationId}: failed to write ${filePath}: ${err.message}`);
|
|
52
|
+
return { infos, warnings };
|
|
53
|
+
}
|
|
54
|
+
if (renamed.length > 0) {
|
|
55
|
+
infos.push(`Migrated agent-models.json: renamed ${renamed.map(k => `'${k}' → '${LEGACY_AGENT_KEYS[k]}'`).join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
if (dropped.length > 0) {
|
|
58
|
+
warnings.push(`${migrationId}: dropped legacy key(s) ${dropped.map(k => `'${k}'`).join(', ')} — the replacement key already holds a value, existing value kept`);
|
|
59
|
+
}
|
|
60
|
+
if (guardDropped.length > 0) {
|
|
61
|
+
warnings.push(`${migrationId}: dropped legacy key(s) ${guardDropped.map(k => `'${k}'`).join(', ')} — skipped due to prototype-pollution guard`);
|
|
62
|
+
}
|
|
63
|
+
return { infos, warnings };
|
|
64
|
+
};
|
|
65
|
+
}
|
|
17
66
|
/**
|
|
18
67
|
* D31: Registry pattern over scattered `if (!applied.includes(...))` conditionals.
|
|
19
68
|
*
|
|
@@ -33,9 +82,9 @@ import { migrateLegacyTrackerConventions } from './tracker.js';
|
|
|
33
82
|
* This was acceptable with an empty MIGRATIONS registry (D37: the vacuous-truth
|
|
34
83
|
* case); this registry's first real entry is added below. Fix deferred.
|
|
35
84
|
*
|
|
36
|
-
* Note how that interacts with `canonicalise-agent-keys-
|
|
37
|
-
* marks a migration applied for ANY non-throwing return, and
|
|
38
|
-
*
|
|
85
|
+
* Note how that interacts with the `canonicalise-agent-keys-vN` entries: runGlobalMigration
|
|
86
|
+
* marks a migration applied for ANY non-throwing return, and those entries never
|
|
87
|
+
* throw — it catches every I/O and parse failure and returns it as a *warning*.
|
|
39
88
|
* So it does NOT hit the unbounded-retry path. It hits the opposite one: a
|
|
40
89
|
* transient EACCES or a failed write records the migration as applied and it
|
|
41
90
|
* never runs again, leaving legacy keys on disk permanently.
|
|
@@ -52,47 +101,17 @@ export const MIGRATIONS = [
|
|
|
52
101
|
id: 'canonicalise-agent-keys-v1',
|
|
53
102
|
description: 'Rename legacy agent keys in ~/.devflow/agent-models.json to their canonical names',
|
|
54
103
|
scope: 'global',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return { infos, warnings };
|
|
67
|
-
if (envelope.kind === 'warn') {
|
|
68
|
-
warnings.push(`canonicalise-agent-keys-v1: ${envelope.message}`);
|
|
69
|
-
return { infos, warnings };
|
|
70
|
-
}
|
|
71
|
-
const onWarning = (msg) => warnings.push(msg);
|
|
72
|
-
const { agents: migrated, didMutate, renamed, dropped, guardDropped } = canonicaliseAgentKeys(envelope.rawAgents, onWarning);
|
|
73
|
-
if (!didMutate)
|
|
74
|
-
return { infos, warnings };
|
|
75
|
-
// Write back with the canonical keys, preserving the rest of the envelope.
|
|
76
|
-
// Idempotent under concurrent execution (no lock needed; see D31 comment above).
|
|
77
|
-
const updated = { ...envelope.envelope, agents: migrated };
|
|
78
|
-
try {
|
|
79
|
-
await writeFileAtomicExclusive(filePath, JSON.stringify(updated, null, 2) + '\n');
|
|
80
|
-
}
|
|
81
|
-
catch (err) {
|
|
82
|
-
warnings.push(`canonicalise-agent-keys-v1: failed to write ${filePath}: ${err.message}`);
|
|
83
|
-
return { infos, warnings };
|
|
84
|
-
}
|
|
85
|
-
if (renamed.length > 0) {
|
|
86
|
-
infos.push(`Migrated agent-models.json: renamed ${renamed.map(k => `'${k}' → '${LEGACY_AGENT_KEYS[k]}'`).join(', ')}`);
|
|
87
|
-
}
|
|
88
|
-
if (dropped.length > 0) {
|
|
89
|
-
warnings.push(`canonicalise-agent-keys-v1: dropped legacy key(s) ${dropped.map(k => `'${k}'`).join(', ')} — canonical key already present, existing value kept`);
|
|
90
|
-
}
|
|
91
|
-
if (guardDropped.length > 0) {
|
|
92
|
-
warnings.push(`canonicalise-agent-keys-v1: dropped legacy key(s) ${guardDropped.map(k => `'${k}'`).join(', ')} — skipped due to prototype-pollution guard`);
|
|
93
|
-
}
|
|
94
|
-
return { infos, warnings };
|
|
95
|
-
},
|
|
104
|
+
run: canonicaliseAgentKeysRun('canonicalise-agent-keys-v1'),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
// v1 is already recorded as applied on every machine that upgraded through 2.x, so the
|
|
108
|
+
// six-agent rename needs an id of its own. Both ids run the same body. On a 1.x machine
|
|
109
|
+
// v1 runs first and, with the single-hop LEGACY_AGENT_KEYS, already lands every key on its
|
|
110
|
+
// final slug, so v2 finds nothing to rewrite and makes no write.
|
|
111
|
+
id: 'canonicalise-agent-keys-v2',
|
|
112
|
+
description: 'Rename the keys of the six renamed agents in ~/.devflow/agent-models.json to their new names',
|
|
113
|
+
scope: 'global',
|
|
114
|
+
run: canonicaliseAgentKeysRun('canonicalise-agent-keys-v2'),
|
|
96
115
|
},
|
|
97
116
|
{
|
|
98
117
|
id: 'tracker-conventions-per-provider-v1',
|
|
@@ -61,9 +61,15 @@ const STDOUT_CAP = 262_144;
|
|
|
61
61
|
* within this window receives SIGTERM followed by a SIGKILL escalation after
|
|
62
62
|
* SIGKILL_GRACE_MS. Zero retries — the stale cache is the retry.
|
|
63
63
|
*
|
|
64
|
+
* The window must outlast a cold start on a loaded machine: the first exec of
|
|
65
|
+
* node with the routing runtime took 1.0-5.4 s in measurement while the OS vetted
|
|
66
|
+
* the new process. A shorter window reports an installed runtime as unknown. The
|
|
67
|
+
* test harness bounds (tests/model-discovery.test.ts) use a wider margin than this
|
|
68
|
+
* measured range.
|
|
69
|
+
*
|
|
64
70
|
* Exported so tests can derive timing bounds without mirroring the literal.
|
|
65
71
|
*/
|
|
66
|
-
export const SPAWN_TIMEOUT_MS =
|
|
72
|
+
export const SPAWN_TIMEOUT_MS = 6_000;
|
|
67
73
|
/**
|
|
68
74
|
* Grace period (ms) between SIGTERM and SIGKILL escalation. Unreffed so the
|
|
69
75
|
* timer never prevents the Node.js event loop from exiting on its own.
|
package/dist/core/plugins.js
CHANGED
|
@@ -34,7 +34,7 @@ export const DEVFLOW_PLUGINS = [
|
|
|
34
34
|
*
|
|
35
35
|
* Guard 5 reverse (registry-integrity.test.ts, "declared agents are spawned")
|
|
36
36
|
* skips a plugin whose commands spawn nothing — `if (spawned.size === 0)
|
|
37
|
-
* continue` — and this plugin ships no commands at all. So `
|
|
37
|
+
* continue` — and this plugin ships no commands at all. So `curate` and
|
|
38
38
|
* `tracker`, which are spawned by a SessionStart directive rather than by any
|
|
39
39
|
* command, satisfy the reverse check STRUCTURALLY.
|
|
40
40
|
*
|
|
@@ -48,11 +48,13 @@ export const DEVFLOW_PLUGINS = [
|
|
|
48
48
|
* set-equal, both directions, against the `agentType` values present in
|
|
49
49
|
* dist/commands/, and a hook-spawned agent appears in none of them.
|
|
50
50
|
*/
|
|
51
|
-
agents: ['
|
|
51
|
+
agents: ['curate', 'tracker'],
|
|
52
52
|
/**
|
|
53
53
|
* D-AGENT-REFS: `agent-references` is a reference-only skill. It holds the Synthesize mode
|
|
54
|
-
* templates and the
|
|
55
|
-
*
|
|
54
|
+
* templates and the Curate maintain step, which those two agents read by explicit path, and
|
|
55
|
+
* the command branches that D-CMD-REF-HAND moves out of a command (`references/commands/`),
|
|
56
|
+
* which a command reads by explicit path, so it sits here (core-skills is always installed)
|
|
57
|
+
* and no other plugin lists or requires it.
|
|
56
58
|
* It is never preloaded: tests/guards/agent-references.test.ts holds that, and the one
|
|
57
59
|
* exception to the repo-wide ban on `disable-model-invocation` names this skill alone.
|
|
58
60
|
*/
|
|
@@ -102,7 +104,7 @@ export const DEVFLOW_PLUGINS = [
|
|
|
102
104
|
name: 'devflow-implement',
|
|
103
105
|
description: 'Complete task implementation workflow - accepts plan documents, issues, or task descriptions',
|
|
104
106
|
commands: ['/implement'],
|
|
105
|
-
agents: ['git', 'code', '
|
|
107
|
+
agents: ['git', 'code', 'fix', 'polish', 'harden', 'evaluate', 'test', 'validate', 'knowledge'],
|
|
106
108
|
skills: ['patterns', 'qa', 'quality-gates', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge'],
|
|
107
109
|
requires: [
|
|
108
110
|
'apply-decisions',
|
|
@@ -141,7 +143,7 @@ export const DEVFLOW_PLUGINS = [
|
|
|
141
143
|
name: 'devflow-resolve',
|
|
142
144
|
description: 'Process and fix code review issues with blast-radius triage, Code agent fixes, and Validate agent verification',
|
|
143
145
|
commands: ['/resolve'],
|
|
144
|
-
agents: ['git', 'triage', 'code', '
|
|
146
|
+
agents: ['git', 'triage', 'code', 'fix', 'polish', 'validate', 'test', 'knowledge'],
|
|
145
147
|
skills: ['patterns', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge', 'apply-decisions'],
|
|
146
148
|
requires: [
|
|
147
149
|
'architecture',
|
|
@@ -167,7 +169,7 @@ export const DEVFLOW_PLUGINS = [
|
|
|
167
169
|
name: 'devflow-debug',
|
|
168
170
|
description: 'Debugging workflows with competing hypothesis investigation via parallel subagents',
|
|
169
171
|
commands: ['/debug'],
|
|
170
|
-
agents: ['git', 'synthesize', '
|
|
172
|
+
agents: ['git', 'synthesize', 'polish', 'knowledge', 'explore', 'code'],
|
|
171
173
|
skills: ['git', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge'],
|
|
172
174
|
// D-DEBUG-FIX-VIA-CODE: the fix is a Code spawn. Code loads no skill by language or mode
|
|
173
175
|
// (D-NO-RUNTIME-SKILLS), so `requires:` lists only the skills this command, its agents and the
|
|
@@ -232,9 +234,9 @@ export const DEVFLOW_PLUGINS = [
|
|
|
232
234
|
},
|
|
233
235
|
{
|
|
234
236
|
name: 'devflow-self-review',
|
|
235
|
-
description: 'Self-review workflow:
|
|
237
|
+
description: 'Self-review workflow: Polish agent + Harden agent for code quality',
|
|
236
238
|
commands: ['/self-review'],
|
|
237
|
-
agents: ['
|
|
239
|
+
agents: ['polish', 'harden', 'validate', 'knowledge'],
|
|
238
240
|
skills: ['quality-gates', 'software-design', 'worktree-support', 'feature-knowledge', 'apply-feature-knowledge'],
|
|
239
241
|
// D-PRELOAD-TRIM: no agent, command or skill in this plugin's corpus names
|
|
240
242
|
// `testing` (Validate preloads only `worktree-support`), so it is not required here.
|
|
@@ -245,7 +247,7 @@ export const DEVFLOW_PLUGINS = [
|
|
|
245
247
|
name: 'devflow-bug-analysis',
|
|
246
248
|
description: 'Proactive bug finding with static and semantic analysis',
|
|
247
249
|
commands: ['/bug-analysis'],
|
|
248
|
-
agents: ['git', '
|
|
250
|
+
agents: ['git', 'debug', 'synthesize'],
|
|
249
251
|
skills: [
|
|
250
252
|
'apply-decisions',
|
|
251
253
|
'apply-feature-knowledge',
|
|
@@ -272,7 +274,7 @@ export const DEVFLOW_PLUGINS = [
|
|
|
272
274
|
name: 'devflow-ambient',
|
|
273
275
|
description: 'Orchestrator ambient mode — session charter, per-prompt reminder, plan handoff',
|
|
274
276
|
commands: [],
|
|
275
|
-
agents: ['code', '
|
|
277
|
+
agents: ['code', 'refactor', 'validate', 'polish', 'harden', 'evaluate', 'test', 'skim', 'review', 'git', 'synthesize', 'triage', 'design', 'knowledge', 'research', 'curate', 'explore'],
|
|
276
278
|
skills: [
|
|
277
279
|
'review-methodology',
|
|
278
280
|
'security',
|
|
@@ -313,7 +315,7 @@ export const DEVFLOW_PLUGINS = [
|
|
|
313
315
|
description: 'Dynamic workflow recipes - dependency-aware tickets→plan→build delivery pipeline',
|
|
314
316
|
// Commands compiled from commands/*.mds at build time (build:mds).
|
|
315
317
|
commands: ['/dynamic-tickets', '/dynamic-plan', '/dynamic-build', '/dynamic-profile'],
|
|
316
|
-
agents: ['code', '
|
|
318
|
+
agents: ['code', 'fix', 'validate', 'polish', 'harden', 'evaluate', 'test', 'review', 'git', 'synthesize', 'knowledge', 'design'],
|
|
317
319
|
skills: ['apply-decisions', 'apply-feature-knowledge', 'worktree-support', 'docs-framework'],
|
|
318
320
|
requires: [
|
|
319
321
|
'architecture',
|
|
@@ -46,7 +46,7 @@ export function getFeatureConfigPath(projectRoot) {
|
|
|
46
46
|
export function getLearningPendingTurnsPath(projectRoot) {
|
|
47
47
|
return path.join(projectRoot, '.devflow', 'learning', '.pending-turns.jsonl');
|
|
48
48
|
}
|
|
49
|
-
/** .devflow/learning/.pending-turns.processing — the claimed batch a
|
|
49
|
+
/** .devflow/learning/.pending-turns.processing — the claimed batch a Curate run holds while it processes it */
|
|
50
50
|
export function getLearningPendingTurnsProcessingPath(projectRoot) {
|
|
51
51
|
return path.join(projectRoot, '.devflow', 'learning', '.pending-turns.processing');
|
|
52
52
|
}
|
|
@@ -65,8 +65,8 @@ export function getDecisionsFilePath(projectRoot) {
|
|
|
65
65
|
export function getPitfallsFilePath(projectRoot) {
|
|
66
66
|
return path.join(projectRoot, '.devflow', 'learning', 'pitfalls.md');
|
|
67
67
|
}
|
|
68
|
-
/** .devflow/learning/learning.json — project-level
|
|
69
|
-
export function
|
|
68
|
+
/** .devflow/learning/learning.json — project-level curate agent tuning config */
|
|
69
|
+
export function getCurateTuningConfigPath(projectRoot) {
|
|
70
70
|
return path.join(projectRoot, '.devflow', 'learning', 'learning.json');
|
|
71
71
|
}
|
|
72
72
|
/** .devflow/learning/decisions-ledger.jsonl — anchored ledger rows (single source of truth for rendering) */
|
package/dist/core/tracker.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { promises as fs } from 'fs';
|
|
30
30
|
import * as path from 'path';
|
|
31
|
+
import { describeEchoedValue as describeTrackerValue } from './echo-value.js';
|
|
31
32
|
/**
|
|
32
33
|
* Canonical issue-tracker provider registry.
|
|
33
34
|
*
|
|
@@ -193,8 +194,6 @@ export const TRACKER_ATTEMPTS_MAX = 5;
|
|
|
193
194
|
// ---------------------------------------------------------------------------
|
|
194
195
|
const REGISTRY_SET = new Set(TRACKER_PROVIDER_IDS);
|
|
195
196
|
const VALID_IDS_LIST = TRACKER_PROVIDER_IDS.join(', ');
|
|
196
|
-
/** Longest rejected value echoed back to the terminal, in CODE POINTS. */
|
|
197
|
-
const MAX_ECHOED_VALUE = 40;
|
|
198
197
|
function errorMessage(err) {
|
|
199
198
|
return err instanceof Error ? err.message : String(err);
|
|
200
199
|
}
|
|
@@ -217,38 +216,11 @@ export function isTrackerProvider(value) {
|
|
|
217
216
|
return typeof value === 'string' && REGISTRY_SET.has(value);
|
|
218
217
|
}
|
|
219
218
|
/**
|
|
220
|
-
* Render an untrusted provider token for a terminal message.
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* third-party input at a display sink: control characters (terminal escapes,
|
|
224
|
-
* BEL, newlines) are replaced and the value is truncated. Used by
|
|
225
|
-
* `parseTrackerId`'s error text and by `devflow tracker --status`.
|
|
226
|
-
*
|
|
227
|
-
* Takes `unknown`, and a non-string renders as its TYPE: the module's
|
|
228
|
-
* never-throws contract has to hold for what reaches this sink, not
|
|
229
|
-
* only for what the signature says does — `devflow tracker --status` reads
|
|
230
|
-
* a conventions file's hand-editable frontmatter, and a caller-side guard is one edit
|
|
231
|
-
* from being gone. Naming the type also keeps the render total, where `String()`
|
|
232
|
-
* would hand control to a caller-supplied `toString` — itself both a throw path
|
|
233
|
-
* and an echo path this function exists to close.
|
|
219
|
+
* Render an untrusted provider token for a terminal message. Used by
|
|
220
|
+
* `parseTrackerId`'s error text and by `devflow tracker --status`. The sanitizer
|
|
221
|
+
* itself lives in `echo-value.ts`; this is its tracker-facing name.
|
|
234
222
|
*/
|
|
235
|
-
export
|
|
236
|
-
if (typeof raw !== 'string')
|
|
237
|
-
return `<${raw === null ? 'null' : typeof raw}>`;
|
|
238
|
-
// The class is written with ESCAPES, never literal control bytes. A raw NUL
|
|
239
|
-
// makes grep classify this whole file as binary — it prints "Binary file
|
|
240
|
-
// matches" and skips the lines — so every grep-based sweep over src/core/
|
|
241
|
-
// silently stops covering tracker.ts while still exiting 0. Pinned by
|
|
242
|
-
// tests/guards/no-control-bytes.test.ts.
|
|
243
|
-
// eslint-disable-next-line no-control-regex
|
|
244
|
-
const stripped = raw.replace(/[\x00-\x1f\x7f]/g, '?');
|
|
245
|
-
// Cut by CODE POINT. `slice` cuts UTF-16 code units, so a cut landing inside an
|
|
246
|
-
// astral character (emoji, CJK ext-B) emits the lone surrogate half of it.
|
|
247
|
-
const points = [...stripped];
|
|
248
|
-
return points.length > MAX_ECHOED_VALUE
|
|
249
|
-
? `${points.slice(0, MAX_ECHOED_VALUE).join('')}…`
|
|
250
|
-
: stripped;
|
|
251
|
-
}
|
|
223
|
+
export { describeTrackerValue };
|
|
252
224
|
/**
|
|
253
225
|
* Strict boundary parser for a provider token (`--tracker <id>`,
|
|
254
226
|
* `devflow tracker --set <id>`).
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { readCache, writeCache, hudCacheDir } from '../core/cache.js';
|
|
4
|
+
const CODEX_CACHE_KEY = 'codex-quota';
|
|
5
|
+
const CODEX_CACHE_TTL_MS = 30_000;
|
|
6
|
+
/** Rollouts last modified longer ago than this are not read. */
|
|
7
|
+
export const CODEX_MAX_AGE_MS = 8 * 24 * 60 * 60 * 1000;
|
|
8
|
+
/** The first read of a rollout, and the one larger read when it holds no record. */
|
|
9
|
+
export const CODEX_TAIL_BYTES = 1024 * 1024;
|
|
10
|
+
export const CODEX_TAIL_RETRY_BYTES = 4 * 1024 * 1024;
|
|
11
|
+
const MAX_YEAR_DIRS = 3;
|
|
12
|
+
const MAX_DAY_DIRS = 14;
|
|
13
|
+
const MAX_ROLLOUTS_PER_DAY = 200;
|
|
14
|
+
const MAX_ROLLOUTS_READ = 3;
|
|
15
|
+
const YEAR_DIR = /^\d{4}$/;
|
|
16
|
+
const MONTH_DIR = /^(0[1-9]|1[0-2])$/;
|
|
17
|
+
const DAY_DIR = /^(0[1-9]|[12]\d|3[01])$/;
|
|
18
|
+
const ROLLOUT_FILE = /^rollout-.*\.jsonl$/;
|
|
19
|
+
/**
|
|
20
|
+
* D-CODEX-LOCAL-LOGS-ONLY: Codex usage comes from the rate-limit records the
|
|
21
|
+
* Codex CLI already writes to its own session logs under ~/.codex/sessions. The
|
|
22
|
+
* statusline reads those logs and nothing else: it opens no credential file
|
|
23
|
+
* (auth.json) and makes no network call, so it cannot leak a token or stall on a
|
|
24
|
+
* request. Reason: the HUD runs on every prompt under a 2 s budget, and Codex
|
|
25
|
+
* publishes the account's limits in every model response it logs.
|
|
26
|
+
*
|
|
27
|
+
* The directory is always <home>/.codex, as the proxy's Codex credential check
|
|
28
|
+
* resolves it; a CODEX_HOME override is not honoured.
|
|
29
|
+
*/
|
|
30
|
+
export function codexSessionsDir(home) {
|
|
31
|
+
return path.join(home, '.codex', 'sessions');
|
|
32
|
+
}
|
|
33
|
+
function asRecord(value) {
|
|
34
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
35
|
+
? value
|
|
36
|
+
: null;
|
|
37
|
+
}
|
|
38
|
+
function isFiniteNumber(value) {
|
|
39
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
40
|
+
}
|
|
41
|
+
function makeWindow(minutes, usedPercent, resetsAt) {
|
|
42
|
+
if (!isFiniteNumber(minutes) || minutes <= 0 || !isFiniteNumber(usedPercent) || !isFiniteNumber(resetsAt)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return { minutes, usedPercent: Math.max(0, Math.min(100, usedPercent)), resetsAt };
|
|
46
|
+
}
|
|
47
|
+
function makeUsage(windows, limitReached) {
|
|
48
|
+
if (windows.length === 0)
|
|
49
|
+
return null;
|
|
50
|
+
return {
|
|
51
|
+
windows: [...windows].sort((a, b) => a.minutes - b.minutes),
|
|
52
|
+
limitReached: typeof limitReached === 'string' && limitReached !== '' ? limitReached : null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* D-CODEX-WINDOW-BY-MINUTES: a window is identified by its window_minutes, never
|
|
57
|
+
* by the primary/secondary slot it arrives in. Reason: the slots carry different
|
|
58
|
+
* windows per plan (a prolite plan has the weekly window as primary and no
|
|
59
|
+
* secondary, a team plan a 5h primary and a weekly secondary), so a slot-keyed
|
|
60
|
+
* reader would label one plan's weekly window as the other's 5h one.
|
|
61
|
+
*
|
|
62
|
+
* Parses one rollout line into usage. Accepts only a token_count event whose
|
|
63
|
+
* rate_limits are the codex limit with at least one complete window; premium
|
|
64
|
+
* records, null windows, `rate_limits: null` and malformed lines are skipped.
|
|
65
|
+
*/
|
|
66
|
+
function parseUsageLine(line) {
|
|
67
|
+
let record;
|
|
68
|
+
try {
|
|
69
|
+
record = JSON.parse(line);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const payload = asRecord(asRecord(record)?.payload);
|
|
75
|
+
if (payload === null || payload.type !== 'token_count')
|
|
76
|
+
return null;
|
|
77
|
+
const limits = asRecord(payload.rate_limits);
|
|
78
|
+
if (limits === null || limits.limit_id !== 'codex')
|
|
79
|
+
return null;
|
|
80
|
+
const windows = [];
|
|
81
|
+
for (const slot of [limits.primary, limits.secondary]) {
|
|
82
|
+
const raw = asRecord(slot);
|
|
83
|
+
const parsed = raw && makeWindow(raw.window_minutes, raw.used_percent, raw.resets_at);
|
|
84
|
+
if (parsed)
|
|
85
|
+
windows.push(parsed);
|
|
86
|
+
}
|
|
87
|
+
return makeUsage(windows, limits.rate_limit_reached_type);
|
|
88
|
+
}
|
|
89
|
+
/** The last line in `text` that holds a valid record, scanning backwards. */
|
|
90
|
+
function lastUsageIn(text) {
|
|
91
|
+
const lines = text.split('\n');
|
|
92
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
93
|
+
// Cheap prefilter: most lines are not token_count events and are never parsed.
|
|
94
|
+
if (!lines[i].includes('"token_count"'))
|
|
95
|
+
continue;
|
|
96
|
+
const usage = parseUsageLine(lines[i]);
|
|
97
|
+
if (usage)
|
|
98
|
+
return usage;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/** Read the last `bytes` bytes of `file`. `whole` is true when that covered the entire file. */
|
|
103
|
+
function readTail(file, bytes) {
|
|
104
|
+
let fd;
|
|
105
|
+
try {
|
|
106
|
+
fd = fs.openSync(file, 'r');
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const size = fs.fstatSync(fd).size;
|
|
113
|
+
const length = Math.min(size, bytes);
|
|
114
|
+
const buf = Buffer.alloc(length);
|
|
115
|
+
let total = 0;
|
|
116
|
+
while (total < length) {
|
|
117
|
+
const read = fs.readSync(fd, buf, total, length - total, size - length + total);
|
|
118
|
+
if (read === 0)
|
|
119
|
+
break;
|
|
120
|
+
total += read;
|
|
121
|
+
}
|
|
122
|
+
return { text: buf.toString('utf-8', 0, total), whole: length === size };
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
fs.closeSync(fd);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The newest valid record in `file`, from its tail only. The first read takes
|
|
133
|
+
* CODEX_TAIL_BYTES; if that holds no record and the file is longer, one retry takes
|
|
134
|
+
* CODEX_TAIL_RETRY_BYTES. Rollouts reach tens of MB, and a tail holds the newest
|
|
135
|
+
* records.
|
|
136
|
+
*/
|
|
137
|
+
function latestUsageInFile(file) {
|
|
138
|
+
for (const bytes of [CODEX_TAIL_BYTES, CODEX_TAIL_RETRY_BYTES]) {
|
|
139
|
+
const tail = readTail(file, bytes);
|
|
140
|
+
if (tail === null)
|
|
141
|
+
return null;
|
|
142
|
+
const usage = lastUsageIn(tail.text);
|
|
143
|
+
if (usage !== null || tail.whole)
|
|
144
|
+
return usage;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/** Names of the entries of `dir` of one kind matching `pattern`, newest (greatest) first. */
|
|
149
|
+
function listNames(dir, kind, pattern) {
|
|
150
|
+
try {
|
|
151
|
+
return fs
|
|
152
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
153
|
+
.filter((e) => (kind === 'dir' ? e.isDirectory() : e.isFile()) && pattern.test(e.name))
|
|
154
|
+
.map((e) => e.name)
|
|
155
|
+
.sort()
|
|
156
|
+
.reverse();
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** The newest MAX_DAY_DIRS day directories, by name: sessions/YYYY/MM/DD. */
|
|
163
|
+
function recentDayDirs(sessionsDir) {
|
|
164
|
+
const days = [];
|
|
165
|
+
for (const year of listNames(sessionsDir, 'dir', YEAR_DIR).slice(0, MAX_YEAR_DIRS)) {
|
|
166
|
+
for (const month of listNames(path.join(sessionsDir, year), 'dir', MONTH_DIR)) {
|
|
167
|
+
for (const day of listNames(path.join(sessionsDir, year, month), 'dir', DAY_DIR)) {
|
|
168
|
+
if (days.length >= MAX_DAY_DIRS)
|
|
169
|
+
return days;
|
|
170
|
+
days.push(path.join(sessionsDir, year, month, day));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return days;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* D-CODEX-NEWEST-MTIME: the newest data is in the most recently modified rollout,
|
|
178
|
+
* not the newest-named one. Reason: a day directory is named for the day its
|
|
179
|
+
* session started, and Codex keeps appending to a session for days, so a rollout in
|
|
180
|
+
* an older directory can hold today's records. Only the newest MAX_DAY_DIRS day
|
|
181
|
+
* directories are searched and rollouts untouched for CODEX_MAX_AGE_MS are
|
|
182
|
+
* ignored, which bounds the work on a long-lived ~/.codex.
|
|
183
|
+
*/
|
|
184
|
+
function recentRollouts(sessionsDir, now) {
|
|
185
|
+
const rollouts = [];
|
|
186
|
+
for (const dayDir of recentDayDirs(sessionsDir)) {
|
|
187
|
+
for (const name of listNames(dayDir, 'file', ROLLOUT_FILE).slice(0, MAX_ROLLOUTS_PER_DAY)) {
|
|
188
|
+
const file = path.join(dayDir, name);
|
|
189
|
+
try {
|
|
190
|
+
const mtimeMs = fs.statSync(file).mtimeMs;
|
|
191
|
+
if (now - mtimeMs <= CODEX_MAX_AGE_MS)
|
|
192
|
+
rollouts.push({ file, mtimeMs });
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Rollout vanished between listing and stat.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return rollouts.sort((a, b) => b.mtimeMs - a.mtimeMs).map((r) => r.file);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Codex's latest rate-limit record, or null when there is none to show: no sessions
|
|
203
|
+
* directory, no rollout modified within CODEX_MAX_AGE_MS, or no valid record in the
|
|
204
|
+
* newest MAX_ROLLOUTS_READ of them (a session that has not yet had a model response
|
|
205
|
+
* holds no record, so the next-newest rollout is tried).
|
|
206
|
+
*
|
|
207
|
+
* @param sessionsDir Codex's sessions directory (see codexSessionsDir).
|
|
208
|
+
* @param now Epoch milliseconds, for the rollout age bound.
|
|
209
|
+
*/
|
|
210
|
+
export function readCodexUsage(sessionsDir, now) {
|
|
211
|
+
for (const file of recentRollouts(sessionsDir, now).slice(0, MAX_ROLLOUTS_READ)) {
|
|
212
|
+
const usage = latestUsageInFile(file);
|
|
213
|
+
if (usage !== null)
|
|
214
|
+
return usage;
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
function validateCodexUsage(data) {
|
|
219
|
+
const obj = asRecord(data);
|
|
220
|
+
if (obj === null || !Array.isArray(obj.windows))
|
|
221
|
+
return null;
|
|
222
|
+
const windows = [];
|
|
223
|
+
for (const entry of obj.windows) {
|
|
224
|
+
const raw = asRecord(entry);
|
|
225
|
+
const parsed = raw && makeWindow(raw.minutes, raw.usedPercent, raw.resetsAt);
|
|
226
|
+
if (!parsed)
|
|
227
|
+
return null;
|
|
228
|
+
windows.push(parsed);
|
|
229
|
+
}
|
|
230
|
+
return makeUsage(windows, obj.limitReached);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Codex usage for the statusline: the cached read while it is under 30 s old, else
|
|
234
|
+
* a fresh read of the logs, cached when it found a record. A run that finds none
|
|
235
|
+
* caches nothing and rescans, which is a handful of directory reads.
|
|
236
|
+
*/
|
|
237
|
+
export async function gatherCodexUsage(devflowDir, sessionsDir, now = Date.now()) {
|
|
238
|
+
const cacheDir = hudCacheDir(devflowDir);
|
|
239
|
+
const cached = readCache(cacheDir, CODEX_CACHE_KEY, validateCodexUsage);
|
|
240
|
+
if (cached !== null)
|
|
241
|
+
return cached;
|
|
242
|
+
const usage = readCodexUsage(sessionsDir, now);
|
|
243
|
+
if (usage !== null)
|
|
244
|
+
await writeCache(cacheDir, CODEX_CACHE_KEY, usage, CODEX_CACHE_TTL_MS);
|
|
245
|
+
return usage;
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=codex-usage.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { dim, red } from '../colors.js';
|
|
2
|
+
import { renderQuotaWindow } from './usage-quota.js';
|
|
3
|
+
/** "5h" for 300 minutes, "7d" for 10080; any other length reads as whole days, whole hours, else minutes. */
|
|
4
|
+
function windowLabel(minutes) {
|
|
5
|
+
if (minutes % 1440 === 0)
|
|
6
|
+
return `${minutes / 1440}d`;
|
|
7
|
+
if (minutes % 60 === 0)
|
|
8
|
+
return `${minutes / 60}h`;
|
|
9
|
+
return `${minutes}m`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Codex usage limits, in usage-quota's style: "Codex 5h ███░░░░░ 40% (1h 2m) · 7d ...".
|
|
13
|
+
*
|
|
14
|
+
* A window whose resets_at has passed has reset since Codex last logged it, so it
|
|
15
|
+
* renders as 0% with no countdown rather than as the stale figure. The red
|
|
16
|
+
* "limited" marker shows while Codex reports a reached limit and at least one
|
|
17
|
+
* window is still counting down; once every window has reset the limit is lifted
|
|
18
|
+
* and the stale marker is dropped.
|
|
19
|
+
*/
|
|
20
|
+
export default async function codexQuota(ctx) {
|
|
21
|
+
const usage = ctx.codexUsage;
|
|
22
|
+
if (!usage || usage.windows.length === 0)
|
|
23
|
+
return null;
|
|
24
|
+
const nowMs = Date.now();
|
|
25
|
+
const hasReset = (resetsAt) => resetsAt * 1000 <= nowMs;
|
|
26
|
+
const parts = usage.windows.map((w) => hasReset(w.resetsAt)
|
|
27
|
+
? renderQuotaWindow(windowLabel(w.minutes), 0, null)
|
|
28
|
+
: renderQuotaWindow(windowLabel(w.minutes), w.usedPercent, w.resetsAt));
|
|
29
|
+
const limited = usage.limitReached !== null && usage.windows.some((w) => !hasReset(w.resetsAt));
|
|
30
|
+
return {
|
|
31
|
+
text: dim('Codex ') +
|
|
32
|
+
parts.map((p) => p.text).join(dim(' · ')) +
|
|
33
|
+
(limited ? ' ' + red('limited') : ''),
|
|
34
|
+
raw: 'Codex ' + parts.map((p) => p.raw).join(' · ') + (limited ? ' limited' : ''),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=codex-quota.js.map
|
|
@@ -48,7 +48,7 @@ export function formatCountdown(resetsAtEpoch) {
|
|
|
48
48
|
return `${totalMinutes}m`;
|
|
49
49
|
}
|
|
50
50
|
/** Render a single quota window: "5h ████░░░░ 45% (2h 15m)" */
|
|
51
|
-
function renderQuotaWindow(label, percent, resetsAt) {
|
|
51
|
+
export function renderQuotaWindow(label, percent, resetsAt) {
|
|
52
52
|
const bar = renderBar(Math.round(percent));
|
|
53
53
|
const countdown = resetsAt != null ? formatCountdown(resetsAt) : '';
|
|
54
54
|
const countdownText = countdown ? dim(` (${countdown})`) : '';
|