feat(linkedin-studio): N15 — do-next-kontrakt (recordDoNext + skrivere/lesere + forrige-utgave-kalibrering) [skip-docs]
Måling som ikke endrer neste utgave er teater: report/analyze/ab-test/48h-monitor endte alle i chat (grep do.next = 0 i hele repoet). N15 gjør kanalen til en kontrakt. - recordDoNext i hooks/scripts/state-updater.mjs (TDD, 17 nye tester): seksjonen Do-Next Directives, rader "[dato] (kilde) direktiv — evidence: peker". Levetid = erstatt-per-kilde + 60-dagers aldersgulv, maks 3 per skriv, additiv scalar last_donext_date, $-sikre replacement-funksjoner. CLI: --record-do-next. - Skrivere: report Step 7b, analyze Step 6b, ab-test 2c.6 (kun Adopt), post-feedback-monitor (kun 48h-sjekken); analytics-interpreter emitterer nå anbefalinger på do-next-form (imperativ + evidens). - Lesere: post/quick/batch/create Step 0 + newsletter Step 1, som i tillegg kalibrerer på forrige utgaves MÅLTE tall via queue-id-match (queue.json → analytics/posts) med eksplisitt honest-miss — aldri estimert. - Guards: test-runner Section 16v (15 ubetingede sjekker, floor 198 → 213), $-safety-batteriet dekker recordDoNext (coverage-gate fanget den nye eksporten). Suiter: test-runner 232/0, hooks 191/0, trends 300/0, brain 134/0, editions 72/0, specifics-bank 45/0, contract-gate 33/0, tests 35/0, render 60/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
This commit is contained in:
parent
f0532dce3f
commit
b45fdad911
16 changed files with 713 additions and 6 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { updatePostTracking, pruneContentHistory, updateFollowerCount, recordFirstHourPlan, recordOutreachContact } from '../state-updater.mjs';
|
||||
import { updatePostTracking, pruneContentHistory, updateFollowerCount, recordFirstHourPlan, recordOutreachContact, recordDoNext } from '../state-updater.mjs';
|
||||
|
||||
const SAMPLE_STATE = `---
|
||||
last_post_date: "2026-04-05"
|
||||
|
|
@ -693,3 +693,218 @@ describe('recordOutreachContact', () => {
|
|||
assert.ok(result.content.includes('[2026-05-30 14:00]'));
|
||||
});
|
||||
});
|
||||
|
||||
// Mirrors the ## Do-Next Directives block config/state-file.template.md ships
|
||||
// (own fixture rather than an edit to TEMPLATE_STATE, so the recordOutreachContact
|
||||
// "entry lands inside the last section" assertion keeps testing exactly what it
|
||||
// tested before). Every template-initialized state file therefore hits the
|
||||
// section-APPEND branch; SAMPLE_STATE (no section) exercises section-CREATE.
|
||||
const DONEXT_TEMPLATE_STATE = `---
|
||||
last_post_date: "2026-04-05"
|
||||
last_post_topic: "AI strategy"
|
||||
follower_count: 850
|
||||
---
|
||||
|
||||
# LinkedIn Session State
|
||||
|
||||
## Recent Posts
|
||||
|
||||
- [2026-04-05] "AI governance is not about..." (1450) - AI strategy
|
||||
|
||||
## Do-Next Directives
|
||||
|
||||
<!-- Measurement -> next action. Written by /linkedin:report, /linkedin:analyze, /linkedin:ab-test (Adopt) and the 48h post-feedback-monitor; read by the create surfaces at Step 0. -->
|
||||
<!-- Format: - [YYYY-MM-DD] (source) directive - evidence: pointer -->
|
||||
`;
|
||||
|
||||
describe('recordDoNext', () => {
|
||||
// "Now" for the write tests: days — not months — after the 2026-05-3x record dates,
|
||||
// so the age floor is NOT what those assertions end up measuring. (Reusing
|
||||
// FIXED_TODAY here made the supersede test pass for the wrong reason: every row
|
||||
// was already past the 60-day floor, so "replaced" and "expired" were
|
||||
// indistinguishable.) The prune tests below deliberately use FIXED_TODAY with
|
||||
// dates computed relative to it.
|
||||
const DONEXT_TODAY = new Date('2026-06-10T00:00:00Z');
|
||||
const REPORT_OPTS = {
|
||||
recordDate: '2026-05-30',
|
||||
source: 'report',
|
||||
directives: [
|
||||
{ directive: 'Double down on AI-agent posts', evidence: 'weekly 2026-W22: 3 posts avg 2400 impressions vs 1200 baseline' },
|
||||
{ directive: 'Move Tuesday posts to 08:00', evidence: 'weekly 2026-W22: Tuesday -40% impressions' }
|
||||
],
|
||||
today: DONEXT_TODAY
|
||||
};
|
||||
|
||||
test('creates a non-R-initial Do-Next Directives section when absent', () => {
|
||||
assert.ok(!SAMPLE_STATE.includes('## Do-Next Directives'));
|
||||
const result = recordDoNext(SAMPLE_STATE, REPORT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(result.content.includes('## Do-Next Directives'));
|
||||
});
|
||||
|
||||
test('records date, source tag, directive text and evidence pointer in each row', () => {
|
||||
const result = recordDoNext(SAMPLE_STATE, REPORT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.match(result.content, /^- \[2026-05-30\] \(report\) Double down on AI-agent posts — evidence: weekly 2026-W22: 3 posts avg 2400 impressions vs 1200 baseline$/m);
|
||||
assert.match(result.content, /^- \[2026-05-30\] \(report\) Move Tuesday posts to 08:00 — evidence: weekly 2026-W22: Tuesday -40% impressions$/m);
|
||||
});
|
||||
|
||||
test('accepts a plain-string directive and marks the missing evidence pointer as none', () => {
|
||||
const result = recordDoNext(SAMPLE_STATE, {
|
||||
recordDate: '2026-05-30',
|
||||
source: 'report',
|
||||
directives: ['Ship one carousel this week'],
|
||||
today: DONEXT_TODAY
|
||||
});
|
||||
assert.notEqual(result, null);
|
||||
assert.match(result.content, /^- \[2026-05-30\] \(report\) Ship one carousel this week — evidence: _\(none\)_$/m);
|
||||
});
|
||||
|
||||
test('caps a single write at 3 directives (2-3 is the contract; a report dump is not a backlog)', () => {
|
||||
const result = recordDoNext(SAMPLE_STATE, {
|
||||
recordDate: '2026-05-30',
|
||||
source: 'report',
|
||||
directives: ['one', 'two', 'three', 'four', 'five'],
|
||||
today: DONEXT_TODAY
|
||||
});
|
||||
assert.notEqual(result, null);
|
||||
const rows = result.content.match(/^- \[2026-05-30\] \(report\)/gm) || [];
|
||||
assert.equal(rows.length, 3, 'at most 3 rows per write');
|
||||
assert.ok(!result.content.includes('(report) four'));
|
||||
});
|
||||
|
||||
test('replaces the SAME source on the next write — directives have a lifetime, they do not accumulate', () => {
|
||||
const first = recordDoNext(SAMPLE_STATE, REPORT_OPTS);
|
||||
const second = recordDoNext(first.content, {
|
||||
recordDate: '2026-06-06',
|
||||
source: 'report',
|
||||
directives: ['Test a poll format'],
|
||||
today: DONEXT_TODAY
|
||||
});
|
||||
assert.notEqual(second, null);
|
||||
assert.ok(!second.content.includes('Double down on AI-agent posts'), 'superseded report directive must be gone');
|
||||
assert.ok(!second.content.includes('Move Tuesday posts to 08:00'), 'superseded report directive must be gone');
|
||||
assert.match(second.content, /^- \[2026-06-06\] \(report\) Test a poll format /m);
|
||||
});
|
||||
|
||||
test('leaves OTHER sources untouched when one source rewrites its slot', () => {
|
||||
const withAb = recordDoNext(SAMPLE_STATE, {
|
||||
recordDate: '2026-05-28',
|
||||
source: 'ab-test',
|
||||
directives: [{ directive: 'Adopt question hooks', evidence: 'test hook-form: B +38% comments' }],
|
||||
today: DONEXT_TODAY
|
||||
});
|
||||
const withReport = recordDoNext(withAb.content, REPORT_OPTS);
|
||||
assert.notEqual(withReport, null);
|
||||
assert.ok(withReport.content.includes('(ab-test) Adopt question hooks'), 'a report write must not evict the A/B slot');
|
||||
assert.ok(withReport.content.includes('(report) Double down on AI-agent posts'));
|
||||
});
|
||||
|
||||
test('prunes rows older than maxAgeDays regardless of source (bounded section)', () => {
|
||||
const stale = daysBeforeFixedToday(90);
|
||||
const fresh = daysBeforeFixedToday(10);
|
||||
const seeded = SAMPLE_STATE.replace(
|
||||
'## Milestone Log',
|
||||
`## Do-Next Directives\n\n- [${stale}] (48h-monitor) Old learning — evidence: post 2026-01\n- [${fresh}] (48h-monitor) Recent learning — evidence: post 2026-12\n\n## Milestone Log`
|
||||
);
|
||||
const result = recordDoNext(seeded, { ...REPORT_OPTS, recordDate: daysBeforeFixedToday(0), maxAgeDays: 60, today: FIXED_TODAY });
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(!result.content.includes('Old learning'), 'row past the age window must be dropped');
|
||||
assert.ok(result.content.includes('Recent learning'), 'row inside the age window must survive');
|
||||
});
|
||||
|
||||
test('prunes stale rows even when the write itself carries no directives', () => {
|
||||
const stale = daysBeforeFixedToday(120);
|
||||
const seeded = SAMPLE_STATE.replace(
|
||||
'## Milestone Log',
|
||||
`## Do-Next Directives\n\n- [${stale}] (report) Ancient directive — evidence: none\n\n## Milestone Log`
|
||||
);
|
||||
const result = recordDoNext(seeded, { recordDate: daysBeforeFixedToday(0), source: 'report', directives: [], maxAgeDays: 60, today: FIXED_TODAY });
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(!result.content.includes('Ancient directive'));
|
||||
});
|
||||
|
||||
test('returns null when there is nothing to record and nothing to prune', () => {
|
||||
const result = recordDoNext(SAMPLE_STATE, { recordDate: '2026-05-30', source: 'report', directives: [], today: FIXED_TODAY });
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
test('is additive — inserts last_donext_date when the field is absent', () => {
|
||||
assert.ok(!/^last_donext_date:/m.test(SAMPLE_STATE));
|
||||
const result = recordDoNext(SAMPLE_STATE, REPORT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.match(result.content, /^last_donext_date: "2026-05-30"$/m);
|
||||
});
|
||||
|
||||
test('updates an existing last_donext_date without duplicating it', () => {
|
||||
const withField = SAMPLE_STATE.replace(
|
||||
'last_post_date: "2026-04-05"',
|
||||
'last_post_date: "2026-04-05"\nlast_donext_date: null'
|
||||
);
|
||||
const result = recordDoNext(withField, { ...REPORT_OPTS, recordDate: '2026-06-06' });
|
||||
assert.notEqual(result, null);
|
||||
const hits = result.content.match(/^last_donext_date:/gm) || [];
|
||||
assert.equal(hits.length, 1, 'field must not be duplicated');
|
||||
assert.match(result.content, /^last_donext_date: "2026-06-06"$/m);
|
||||
});
|
||||
|
||||
test('no-anchor fall-through: scalar not written, not reported; section still written', () => {
|
||||
assert.ok(!/^last_post_date:/m.test(NO_ANCHOR_STATE));
|
||||
const result = recordDoNext(NO_ANCHOR_STATE, REPORT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(!/^last_donext_date:/m.test(result.content), 'scalar must not be inserted without an anchor');
|
||||
assert.ok(!result.changes.some((c) => /last_donext_date/.test(c)), 'no date-scalar change entry without an anchor');
|
||||
assert.ok(result.changes.some((c) => /^Do-Next Directives/.test(c)), 'section write must still be recorded');
|
||||
assert.ok(result.content.includes('(report) Double down on AI-agent posts'));
|
||||
});
|
||||
|
||||
test('writes into the pre-existing section (production path) without duplicating it or dropping its format comments', () => {
|
||||
assert.ok(DONEXT_TEMPLATE_STATE.includes('## Do-Next Directives'));
|
||||
const result = recordDoNext(DONEXT_TEMPLATE_STATE, REPORT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
const headings = result.content.match(/^## Do-Next Directives$/gm) || [];
|
||||
assert.equal(headings.length, 1, 'section must not be duplicated');
|
||||
assert.ok(result.content.includes('<!-- Format: - [YYYY-MM-DD] (source) directive - evidence: pointer -->'), 'template format comment survives');
|
||||
const section = result.content.slice(result.content.indexOf('## Do-Next Directives'));
|
||||
assert.ok(section.includes('(report) Double down on AI-agent posts'), 'rows must land inside the section');
|
||||
});
|
||||
|
||||
test('records $-bearing directive/evidence verbatim — special replacement patterns are not interpreted', () => {
|
||||
const result = recordDoNext(DONEXT_TEMPLATE_STATE, {
|
||||
recordDate: '2026-05-30',
|
||||
source: 'report',
|
||||
directives: [{ directive: 'Price the $&-offer at $99', evidence: 'ref $1, spend $$40' }],
|
||||
today: DONEXT_TODAY
|
||||
});
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(result.content.includes('Price the $&-offer at $99'), 'directive with $-tokens verbatim');
|
||||
assert.ok(result.content.includes('ref $1, spend $$40'), 'evidence with $-tokens verbatim');
|
||||
const headings = result.content.match(/^## Do-Next Directives$/gm) || [];
|
||||
assert.equal(headings.length, 1, 'section must not be re-injected by a $1/$& expansion');
|
||||
});
|
||||
|
||||
test('leaves existing fields and sections untouched (round-trip)', () => {
|
||||
const result = recordDoNext(SAMPLE_STATE, REPORT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.match(result.content, /^last_post_date: "2026-04-05"$/m);
|
||||
assert.match(result.content, /^follower_count: 850$/m);
|
||||
assert.ok(result.content.includes('- [2026-04-05] "AI governance is not about..."'));
|
||||
assert.ok(result.content.includes('## Milestone Log'));
|
||||
});
|
||||
|
||||
test('returns a changes array describing what changed', () => {
|
||||
const result = recordDoNext(SAMPLE_STATE, REPORT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(Array.isArray(result.changes));
|
||||
assert.ok(result.changes.some((c) => /Do-Next Directives/.test(c)));
|
||||
});
|
||||
|
||||
test('a do-next section does not break pruneContentHistory (non-R-initial section boundary)', () => {
|
||||
// pruneContentHistory captures "## Recent Posts ... (?=\n## [^R])": the do-next
|
||||
// heading must terminate that window, not extend it into the directive rows.
|
||||
const withDoNext = recordDoNext(SAMPLE_STATE, REPORT_OPTS).content;
|
||||
const pruned = pruneContentHistory(withDoNext, 30, FIXED_TODAY);
|
||||
assert.notEqual(pruned, null, 'old Recent Posts entries must still be prunable');
|
||||
assert.ok(pruned.content.includes('(report) Double down on AI-agent posts'), 'directives must survive a content-history prune');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -343,6 +343,112 @@ export function recordOutreachContact(stateContent, { contactDate, track = '', p
|
|||
return { content, changes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Record 2-3 "do next" directives deterministically — the measurement→creation
|
||||
* contract. A recommendation that only ever lands in chat dies there; a directive
|
||||
* written HERE is read by the create surfaces at their Step 0, so the next post is
|
||||
* actually shaped by the last report.
|
||||
*
|
||||
* Lifetime is REPLACE-BY-SOURCE plus an age floor, not append-forever:
|
||||
* - each writer (report / analyze / ab-test / 48h-monitor) owns its own slot, so a
|
||||
* new report supersedes the previous report's directives but never evicts an
|
||||
* A/B verdict that landed in between;
|
||||
* - rows older than `maxAgeDays` are dropped on every write regardless of source,
|
||||
* so a writer that goes quiet cannot leave stale directives steering drafts
|
||||
* forever (pruneContentHistory's bounded-section pattern);
|
||||
* - at most 3 rows per write — this is a steering signal, not a backlog.
|
||||
*
|
||||
* Additive by contract, like recordFirstHourPlan/recordOutreachContact: a missing
|
||||
* scalar is inserted, a missing section created, no existing field touched. The
|
||||
* section name is deliberately non-`R`-initial ("Do-Next …") so it falls outside
|
||||
* pruneContentHistory's `## Recent Posts … (?=\n## [^R])` capture window.
|
||||
*
|
||||
* @param {string} stateContent - Full state file content
|
||||
* @param {{ recordDate: string, source?: string, directives?: Array<string|{directive: string, evidence?: string}>, maxAgeDays?: number, today?: Date }} opts
|
||||
* @returns {{ content: string, changes: string[] } | null}
|
||||
*/
|
||||
export function recordDoNext(stateContent, { recordDate, source = 'report', directives = [], maxAgeDays = 60, today = new Date() }) {
|
||||
let content = stateContent;
|
||||
const changes = [];
|
||||
const MAX_PER_WRITE = 3;
|
||||
|
||||
const cutoff = new Date(today);
|
||||
cutoff.setDate(cutoff.getDate() - maxAgeDays);
|
||||
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
||||
|
||||
// 1. Build this write's rows (capped; string and {directive, evidence} both accepted)
|
||||
const rows = directives.slice(0, MAX_PER_WRITE).map((d) => {
|
||||
const text = (typeof d === 'string' ? d : d && d.directive) || '';
|
||||
const evidence = (typeof d === 'string' ? '' : (d && d.evidence) || '').trim();
|
||||
return `- [${recordDate}] (${source}) ${String(text).trim()} — evidence: ${evidence || '_(none)_'}`;
|
||||
}).filter((r) => !/\) +— evidence:/.test(r)); // drop rows with an empty directive
|
||||
|
||||
// 2. Rebuild the existing section body: drop this source's rows (superseded) and
|
||||
// every row past the age floor (any source). Non-row lines (format comments,
|
||||
// blank lines) are kept verbatim.
|
||||
// No /m flag — same trap pruneContentHistory documents: with /m the `$` alternative
|
||||
// in the lookahead matches the end of EVERY line, so the lazy body capture stops
|
||||
// after the first line and older rows below it are never scanned (never superseded,
|
||||
// never pruned). Without /m, `$` is end-of-string and the capture spans the section.
|
||||
const sectionRe = /(## Do-Next Directives\n\n?)([\s\S]*?)(?=\n## |$)/;
|
||||
const existing = content.match(sectionRe);
|
||||
let dropped = 0;
|
||||
|
||||
if (existing) {
|
||||
// The section's leading `<!-- … -->` format comments are documentation, not data:
|
||||
// they stay directly under the heading, above the rows (a row prepended above them
|
||||
// would push the format contract to the bottom of the section every write).
|
||||
const lines = existing[2].split('\n');
|
||||
let cut = 0;
|
||||
while (cut < lines.length && !/^- \[/.test(lines[cut]) && (lines[cut].trim() === '' || lines[cut].trim().startsWith('<!--'))) cut++;
|
||||
const preamble = lines.slice(0, cut).join('\n').replace(/\s+$/, '');
|
||||
const kept = lines.slice(cut).filter((line) => {
|
||||
if (line.trim() === '') return false; // rebuilt below, never accumulated
|
||||
const row = line.match(/^- \[(\d{4}-\d{2}-\d{2})[^\]]*\] \(([^)]*)\)/);
|
||||
if (!row) return true; // unexpected user text: keep verbatim
|
||||
if (row[2] === source) { dropped++; return false; } // superseded by this write
|
||||
if (row[1] < cutoffStr) { dropped++; return false; } // past the age floor
|
||||
return true;
|
||||
});
|
||||
|
||||
// Newest first: this write's rows go directly under the heading + comments.
|
||||
const parts = [];
|
||||
if (preamble) parts.push(preamble, '');
|
||||
parts.push(...rows, ...kept);
|
||||
const body = `${parts.join('\n')}\n`;
|
||||
// Replacement FUNCTION, not string: rows embed untrusted directive/evidence text —
|
||||
// `$&`/`$1`/`$$` must be inserted verbatim (same class as the other section writes).
|
||||
content = content.replace(sectionRe, () => `${existing[1]}${body}`);
|
||||
} else if (rows.length) {
|
||||
const trimmed = content.replace(/\s*$/, '');
|
||||
const header = '## Do-Next Directives\n\n<!-- Measurement -> next action. Written by /linkedin:report, /linkedin:analyze, /linkedin:ab-test (Adopt) and the 48h post-feedback-monitor; read by the create surfaces at Step 0. -->\n<!-- Format: - [YYYY-MM-DD] (source) directive - evidence: pointer -->\n\n';
|
||||
content = `${trimmed}\n\n${header}${rows.join('\n')}\n`;
|
||||
}
|
||||
|
||||
if (rows.length === 0 && dropped === 0) return null;
|
||||
|
||||
// 3. last_donext_date — only when something was actually recorded. Replace in place,
|
||||
// else insert after the nearest existing date anchor (additive). Report the change
|
||||
// only inside the branch that writes it: with no anchor the scalar is not inserted.
|
||||
if (rows.length) {
|
||||
if (/^last_donext_date: .*/m.test(content)) {
|
||||
content = replaceField(content, 'last_donext_date', `"${recordDate}"`);
|
||||
changes.push(`last_donext_date → ${recordDate}`);
|
||||
} else {
|
||||
const anchor = ['last_outreach_date', 'last_firsthour_date', 'last_post_date'].find((f) => new RegExp(`^${f}: .*`, 'm').test(content));
|
||||
if (anchor) {
|
||||
content = content.replace(new RegExp(`^(${anchor}: .*)$`, 'm'), (m) => `${m}\nlast_donext_date: "${recordDate}"`); // function, not string: keeps recordDate `$`-safe by construction
|
||||
changes.push(`last_donext_date → ${recordDate}`);
|
||||
}
|
||||
}
|
||||
changes.push(`Do-Next Directives ← ${rows.length} from ${source} (${recordDate})`);
|
||||
}
|
||||
if (dropped) changes.push(`Do-Next Directives: ${dropped} superseded/expired row(s) dropped`);
|
||||
|
||||
if (content === stateContent) return null;
|
||||
return { content, changes };
|
||||
}
|
||||
|
||||
/**
|
||||
* I/O wrapper: read state file, apply update function, write atomically.
|
||||
* @param {function(string): {content: string}|null} updateFn - Pure update function
|
||||
|
|
@ -405,6 +511,19 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|||
nextAction: getArg('--next') || '',
|
||||
dueDate: getArg('--due') || ''
|
||||
}));
|
||||
} else if (args.includes('--record-do-next')) {
|
||||
const getArg = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : ''; };
|
||||
// Rows are `;`-separated; each row is `directive :: evidence` (evidence optional).
|
||||
const parseDirectives = (s) => (s ? s.split(';').map((row) => {
|
||||
const [directive, ...rest] = row.split('::');
|
||||
return { directive: (directive || '').trim(), evidence: rest.join('::').trim() };
|
||||
}).filter((d) => d.directive) : []);
|
||||
writeState(content => recordDoNext(content, {
|
||||
recordDate: getArg('--date') || new Date().toISOString().slice(0, 10),
|
||||
source: getArg('--source') || 'report',
|
||||
directives: parseDirectives(getArg('--directives')),
|
||||
maxAgeDays: parseInt(getArg('--max-age') || '60', 10)
|
||||
}));
|
||||
} else {
|
||||
console.log('Usage:');
|
||||
console.log(' node state-updater.mjs --update-post --date YYYY-MM-DD --topic "topic" --hook "Hook text" --chars 1500 --format post');
|
||||
|
|
@ -412,5 +531,6 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|||
console.log(' node state-updater.mjs --update-followers --count 920 --month 2026-04');
|
||||
console.log(' node state-updater.mjs --record-firsthour --date "YYYY-MM-DD HH:MM" --topic "topic" --targets "a;b" --comments "c;d" --plan "e;f"');
|
||||
console.log(' node state-updater.mjs --record-outreach --date "YYYY-MM-DD HH:MM" --track collab --partner "@name" --stage pitched --next "follow up" --due YYYY-MM-DD');
|
||||
console.log(' node state-updater.mjs --record-do-next --date YYYY-MM-DD --source report --directives "do X :: evidence;do Y :: evidence" [--max-age 60]');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue