fix(#2136): migrate ALL operator-facing date sites to localToday (anti-pattern elimination)
The UTC-slice anti-pattern (deriving a calendar day a human reads by slicing a
UTC instant) remained in several operator-facing sites beyond the original
seamed last_activity set. Eliminate it everywhere a human reads the value as a
calendar day — do not leave known bad code in place:
- commands.cts cmdTodoComplete + cmdScaffold: completion/scaffold dates.
- gsd2-import.cts: migrated STATE.md 'Last activity' / 'Last session'.
- template.cts: plan frontmatter 'completed:' date.
- verify.cts: health --repair session-log date + '(Backfilled: <date>)' header.
- workstream.cts: workstream-create 'Last Activity' / 'created' + archive dirname.
- init.cts: JSON-bundle 'date' (-> localToday) + 'timestamp' (-> nowIso) at all
three sites; drop the now-dead 'const now = new Date()' in cmdInitTodos /
cmdInitMapCodebase (cmdInitQuick keeps it for the local branch-id derivation).
- state.cts: prune-archive '## Pruned <date>' header.
- state-transition.cts: the 7 seamed last_activity writes (prior commit).
No realClock.today() / .clock.today() / raw new Date().toISOString().split('T')
operator-facing sites remain in src/. Rebuilds the tracked
bin/lib/state-transition.cjs artifact to match.
This commit is contained in:
@@ -226,7 +226,7 @@ function beginPhaseCore(content, intent, deps) {
|
||||
const reassemble = (b) => hasFrontmatter
|
||||
? `---\n${reconstructFrontmatter(existingFm)}\n---\n\n${b}`
|
||||
: b;
|
||||
const today = deps.clock.today();
|
||||
const today = deps.clock.localToday();
|
||||
// Consult the field-classification table for the frontmatter keys this
|
||||
// transition touches (codex Phase 1 review: "table not consulted by
|
||||
// transitionCore"). The table tracks FRONTMATTER keys (lowercase: `status`,
|
||||
@@ -502,7 +502,7 @@ function mutateCurrentPositionForAdvance(content, fields, statusDefaults, lastAc
|
||||
* adapter to construct CLI output.
|
||||
*/
|
||||
function advancePlanCore(content, deps) {
|
||||
const today = deps.clock.today();
|
||||
const today = deps.clock.localToday();
|
||||
// #1255: body-field replacements operate on body only (frontmatter stripped),
|
||||
// not on the full content. The YAML `status:` key matches `^Status:\s*`
|
||||
// before the body field if full content is passed (codex Phase 2 review:
|
||||
@@ -608,7 +608,7 @@ function advancePlanCore(content, deps) {
|
||||
*/
|
||||
function completePhaseCore(content, intent, deps) {
|
||||
const updated = [];
|
||||
const today = deps.clock.today();
|
||||
const today = deps.clock.localToday();
|
||||
// Consult the field-classification table for the frontmatter keys this
|
||||
// transition touches (same guard beginPhaseCore applies). A missing row is a
|
||||
// substrate defect — fail loudly rather than silently re-encoding policy.
|
||||
@@ -762,7 +762,7 @@ function completePhaseCore(content, intent, deps) {
|
||||
*/
|
||||
function plannedPhaseCore(content, intent, deps) {
|
||||
const updated = [];
|
||||
const today = deps.clock.today();
|
||||
const today = deps.clock.localToday();
|
||||
for (const fmKey of ['status', 'last_activity', 'last_activity_desc']) {
|
||||
const cls = getFieldClassification(fmKey);
|
||||
if (cls === null) {
|
||||
@@ -840,7 +840,7 @@ function plannedPhaseCore(content, intent, deps) {
|
||||
* directly and must not run the steady-state `syncStateFrontmatter` post-sync.
|
||||
*/
|
||||
function milestoneSwitchCore(content, intent, deps) {
|
||||
const today = deps.clock.today();
|
||||
const today = deps.clock.localToday();
|
||||
const updated = [
|
||||
'milestone',
|
||||
'milestone_name',
|
||||
@@ -927,7 +927,7 @@ function milestoneSwitchCore(content, intent, deps) {
|
||||
*/
|
||||
function milestoneCompleteCore(content, intent, deps) {
|
||||
const updated = [];
|
||||
const today = deps.clock.today();
|
||||
const today = deps.clock.localToday();
|
||||
const version = intent.version;
|
||||
for (const fmKey of ['status', 'last_activity', 'last_activity_desc']) {
|
||||
const cls = getFieldClassification(fmKey);
|
||||
@@ -1175,7 +1175,7 @@ function pruneCore(content, intent) {
|
||||
* (rather than silently writing fallback-derived wrong values).
|
||||
*/
|
||||
function syncCore(content, intent, deps) {
|
||||
const today = deps.clock.today();
|
||||
const today = deps.clock.localToday();
|
||||
const changes = [];
|
||||
let modified = content;
|
||||
const updated = [];
|
||||
|
||||
@@ -1431,6 +1431,9 @@ function cmdTodoComplete(cwd: string, filename: string | undefined, raw: boolean
|
||||
function cmdScaffold(cwd: string, type: string, options: ScaffoldOptions, raw: boolean): void {
|
||||
const { phase, name } = options;
|
||||
const padded = phase ? normalizePhaseName(phase) : '00';
|
||||
// #2136 sibling site (deliberately deferred per the issue's scope): scaffold's
|
||||
// date stays on the raw UTC slice for now; route through realClock.localToday()
|
||||
// alongside workstream.cts/gsd2-import.cts/template.cts/verify.cts in a follow-up.
|
||||
const today = realClock.localToday();
|
||||
|
||||
// Find phase directory
|
||||
|
||||
@@ -23,6 +23,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { platformWriteSync } from './shell-command-projection.cjs';
|
||||
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
|
||||
import { realClock } from './clock.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import ioMod = require('./io.cjs');
|
||||
const { output } = ioMod;
|
||||
@@ -351,7 +352,7 @@ function buildStateMd(phaseMap: PhaseMapEntry[]): string {
|
||||
|
||||
const filled = Math.round(pct / 10);
|
||||
const bar = `[${'█'.repeat(filled)}${'░'.repeat(10 - filled)}]`;
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = realClock.localToday();
|
||||
|
||||
return [
|
||||
'# Project State',
|
||||
|
||||
15
src/init.cts
15
src/init.cts
@@ -10,6 +10,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { execGit, platformWriteSync, platformReadSync } from './shell-command-projection.cjs';
|
||||
import { realClock } from './clock.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
|
||||
import io = require('./io.cjs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
|
||||
@@ -820,8 +821,8 @@ function cmdInitQuick(cwd: string, description: string | undefined, raw: boolean
|
||||
slug: slug,
|
||||
description: description || null,
|
||||
|
||||
date: now.toISOString().split('T')[0],
|
||||
timestamp: now.toISOString(),
|
||||
date: realClock.localToday(),
|
||||
timestamp: realClock.nowIso(),
|
||||
|
||||
quick_dir: '.planning/quick',
|
||||
task_dir: slug ? `.planning/quick/${quickId}-${slug}` : null,
|
||||
@@ -1133,7 +1134,6 @@ function cmdInitPhaseOp(cwd: string, phase: string, raw: boolean): void {
|
||||
|
||||
function cmdInitTodos(cwd: string, area: string | undefined, raw: boolean): void {
|
||||
const config = loadConfig(cwd);
|
||||
const now = new Date();
|
||||
|
||||
const pendingDir = path.join(planningDir(cwd), 'todos', 'pending');
|
||||
let count = 0;
|
||||
@@ -1176,8 +1176,8 @@ function cmdInitTodos(cwd: string, area: string | undefined, raw: boolean): void
|
||||
const result: Record<string, unknown> = {
|
||||
commit_docs: config.commit_docs,
|
||||
|
||||
date: now.toISOString().split('T')[0],
|
||||
timestamp: now.toISOString(),
|
||||
date: realClock.localToday(),
|
||||
timestamp: realClock.nowIso(),
|
||||
|
||||
todo_count: count,
|
||||
todos,
|
||||
@@ -1306,7 +1306,6 @@ function cmdInitMilestoneOp(cwd: string, raw: boolean): void {
|
||||
|
||||
function cmdInitMapCodebase(cwd: string, raw: boolean): void {
|
||||
const config = loadConfig(cwd);
|
||||
const now = new Date();
|
||||
|
||||
const codebaseDir = path.join(planningRoot(cwd), 'codebase');
|
||||
let existingMaps: string[] = [];
|
||||
@@ -1324,8 +1323,8 @@ function cmdInitMapCodebase(cwd: string, raw: boolean): void {
|
||||
parallelization: config.parallelization,
|
||||
subagent_timeout: config.subagent_timeout,
|
||||
|
||||
date: now.toISOString().split('T')[0],
|
||||
timestamp: now.toISOString(),
|
||||
date: realClock.localToday(),
|
||||
timestamp: realClock.nowIso(),
|
||||
|
||||
codebase_dir: '.planning/codebase',
|
||||
|
||||
|
||||
@@ -2582,7 +2582,7 @@ function cmdStatePrune(cwd: string, options: StatePruneOptions, raw: boolean): v
|
||||
|
||||
// Write archived entries to STATE-ARCHIVE.md
|
||||
if (archived.length > 0) {
|
||||
const timestamp = realClock.today();
|
||||
const timestamp = realClock.localToday();
|
||||
let archiveContent = platformReadSync(archivePath);
|
||||
if (archiveContent === null) {
|
||||
archiveContent = '# STATE Archive\n\nPruned entries from STATE.md. Recoverable but no longer loaded into agent context.\n\n';
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { realClock } from './clock.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import ioMod = require('./io.cjs');
|
||||
const { output, error } = ioMod;
|
||||
@@ -110,7 +111,7 @@ function cmdTemplateFill(cwd: string, templateType: string | null | undefined, o
|
||||
if (!phaseInfo || !phaseInfo.found) { output({ error: 'Phase not found', phase: options.phase }, raw, undefined); return; }
|
||||
|
||||
const padded = normalizePhaseName(options.phase);
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = realClock.localToday();
|
||||
const phaseName = options.name || phaseInfo.phase_name || 'Unnamed';
|
||||
const phaseSlug = phaseInfo.phase_slug || generateSlugInternal(phaseName);
|
||||
const phaseId = `${padded}-${phaseSlug}`;
|
||||
|
||||
@@ -10,6 +10,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { phaseVariants, buildRoadmapPhaseVariants, buildNotStartedPhaseVariants } from './validate.cjs';
|
||||
import { realClock } from './clock.cjs';
|
||||
import { phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem } from './validate.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
||||
import planningWorkspace = require('./planning-workspace.cjs');
|
||||
@@ -1893,7 +1894,7 @@ function cmdValidateHealth(
|
||||
stateContent += `**Current phase:** (determining...)\n`;
|
||||
stateContent += `**Status:** Resuming\n\n`;
|
||||
stateContent += `## Session Log\n\n`;
|
||||
stateContent += `- ${new Date().toISOString().split('T')[0]}: STATE.md regenerated by ${slash('health')} --repair\n`;
|
||||
stateContent += `- ${realClock.localToday()}: STATE.md regenerated by ${slash('health')} --repair\n`;
|
||||
writeStateMd(statePath, stateContent, cwd);
|
||||
repairActions.push({ action: repair, success: true, path: 'STATE.md' });
|
||||
break;
|
||||
@@ -1944,7 +1945,7 @@ function cmdValidateHealth(
|
||||
}
|
||||
case 'backfillMilestones': {
|
||||
if (!options['backfill'] && !options['repair']) break;
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = realClock.localToday();
|
||||
let backfilled = 0;
|
||||
for (const ver of missingFromRegistry) {
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { realClock } from './clock.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import io = require('./io.cjs');
|
||||
const { output, error } = io;
|
||||
@@ -177,7 +178,7 @@ function cmdWorkstreamCreate(cwd: string, name: string | null | undefined, optio
|
||||
platformEnsureDir(wsDir);
|
||||
platformEnsureDir(path.join(wsDir, 'phases'));
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = realClock.localToday();
|
||||
const stateContent = [
|
||||
'---',
|
||||
`workstream: ${slug}`,
|
||||
@@ -297,7 +298,7 @@ function cmdWorkstreamComplete(cwd: string, name: string | null | undefined, opt
|
||||
if (active === name) setActiveWorkstream(cwd, null as unknown as string);
|
||||
|
||||
const archiveDir = path.join(root, 'milestones');
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = realClock.localToday();
|
||||
let archivePath = path.join(archiveDir, `ws-${name}-${today}`);
|
||||
let suffix = 1;
|
||||
while (fs.existsSync(archivePath)) {
|
||||
|
||||
Reference in New Issue
Block a user